Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Commit

Permalink
Fix #4329: Add iOS widgets (#4388)
Browse files Browse the repository at this point in the history
Co-authored-by: Brandon T and Kyle
  • Loading branch information
iccub authored Nov 2, 2021
1 parent 7c05404 commit 4c29b30
Show file tree
Hide file tree
Showing 78 changed files with 5,828 additions and 137 deletions.
89 changes: 89 additions & 0 deletions BraveShared/BraveStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3088,3 +3088,92 @@ extension Strings {
public static let recentSearchScannerDescriptionBody = NSLocalizedString("RecentSearchScannerDescriptionBody", bundle: .braveShared, value: "To search by QR Code, align the QR Code in the center of the frame.", comment: "Scanning a QR Code for searching body")
public static let recentSearchClearAlertButton = NSLocalizedString("RecentSearchClearAlertButton", bundle: .braveShared, value: "Clear Recent", comment: "The button title that shows when you clear all recent searches")
}

// MARK: - Widgets
extension Strings {
public struct Widgets {
public static let noFavoritesFound = NSLocalizedString(
"widgets.noFavoritesFound",
bundle: .braveShared,
value: "Please open Brave to view your favorites here",
comment: "This shows when you add a widget but have no favorites added in your app")

public static let favoritesWidgetTitle = NSLocalizedString(
"widgets.favoritesWidgetTitle",
bundle: .braveShared,
value: "Favorites",
comment: "Title for favorites widget on 'add widget' screen.")

public static let favoritesWidgetDescription = NSLocalizedString(
"widgets.favoritesWidgetDescription",
bundle: .braveShared,
value: "Quickly access your favorite websites.",
comment: "Description for favorites widget on 'add widget' screen.")

public static let shortcutsWidgetTitle = NSLocalizedString(
"widgets.shortcutsWidgetTitle",
bundle: .braveShared,
value: "Shortcuts",
comment: "Title for shortcuts widget on 'add widget' screen.")

public static let shortcutsWidgetDescription = NSLocalizedString(
"widgets.shortcutsWidgetDescription",
bundle: .braveShared,
value: "Quick access to search the web or open web pages in Brave.",
comment: "Description for shortcuts widget on 'add widget' screen.")

public static let shortcutsNewTabButton = NSLocalizedString(
"widgets.shortcutsNewTabButton",
bundle: .braveShared,
value: "New Tab",
comment: "Button to open new browser tab.")

public static let shortcutsPrivateTabButton = NSLocalizedString(
"widgets.shortcutsPrivateTabButton",
bundle: .braveShared,
value: "Private Tab",
comment: "Button to open new private browser tab.")

public static let shortcutsPlaylistButton = NSLocalizedString(
"widgets.shortcutsPlaylistButton",
bundle: .braveShared,
value: "Playlist",
comment: "Button to open video playlist window.")

public static let shortcutsEnterURLButton = NSLocalizedString(
"widgets.shortcutsEnterURLButton",
bundle: .braveShared,
value: "Search or type a URL",
comment: "Button to the browser and enter URL or make a search query there.")

public static let shieldStatsTitle = NSLocalizedString(
"widgets.shieldStatsTitle",
bundle: .braveShared,
value: "Privacy Stats",
comment: "Title for Brave Shields widget on 'add widget' screen.")

public static let shieldStatsDescription = NSLocalizedString(
"widgets.shieldStatsDescription",
bundle: .braveShared,
value: "A summary of how Brave saves you time and protects you online.",
comment: "Description for Brave Shields widget on 'add widget' screen.")

public static let shieldStatsWidgetTitle = NSLocalizedString(
"widgets.shieldStatsWidgetTitle",
bundle: .braveShared,
value: "Privacy Stats",
comment: "Title of Brave Shields widget shown above stat numbers.")

public static let singleStatTitle = NSLocalizedString(
"widgets.singleStatTitle",
bundle: .braveShared,
value: "Privacy Stat",
comment: "Title for Brave Shields single stat widget on 'add widget' screen.")

public static let singleStatDescription = NSLocalizedString(
"widgets.singleStatDescription",
bundle: .braveShared,
value: "A summary of how Brave has protected you online.",
comment: "Description for Brave Shields single stat widget on 'add widget' screen.")
}
}
60 changes: 60 additions & 0 deletions BraveShared/Favicons/FaviconAttributes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2021 The Brave Authors. All rights reserved.
// 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

public struct FaviconAttributes: Codable {
public var image: UIImage?
public var backgroundColor: UIColor?
public var contentMode: UIView.ContentMode = .scaleAspectFit
public var includePadding: Bool = false

public init(
image: UIImage? = nil,
backgroundColor: UIColor? = nil,
contentMode: UIView.ContentMode = .scaleAspectFit,
includePadding: Bool = false
) {
self.image = image
self.backgroundColor = backgroundColor
self.contentMode = contentMode
self.includePadding = includePadding
}

// MARK: - Codable

enum CodingKeys: String, CodingKey {
case image
case backgroundColor
case contentMode
case includePadding
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(image?.sd_imageData(), forKey: .image)
var rgb: Int?
if let color = backgroundColor {
var (r, g, b): (CGFloat, CGFloat, CGFloat) = (0.0, 0.0, 0.0)
color.getRed(&r, green: &g, blue: &b, alpha: nil)
rgb = (Int(r * 255.0) << 16) | (Int(g * 255.0) << 8) | Int(b * 255.0)
}
try container.encode(rgb, forKey: .backgroundColor)
try container.encode(contentMode.rawValue, forKey: .contentMode)
try container.encode(includePadding, forKey: .includePadding)
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let imageData = try container.decode(Data?.self, forKey: .image) {
image = UIImage(data: imageData)
}
if let rgb = try container.decode(Int?.self, forKey: .backgroundColor) {
backgroundColor = UIColor(rgb: rgb)
}
contentMode = UIView.ContentMode(rawValue: try container.decode(Int.self, forKey: .contentMode)) ?? .scaleAspectFit
includePadding = try container.decode(Bool.self, forKey: .includePadding)
}
}
64 changes: 64 additions & 0 deletions BraveShared/Shields/BraveGlobalShieldStats.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,68 @@ open class BraveGlobalShieldStats {
fpProtection = Preferences.BlockStats.fingerprintingCount.value
safeBrowsing = Preferences.BlockStats.phishingCount.value
}

fileprivate let millisecondsPerItem: Int = 50
fileprivate let bytesPerItem = 30485

public var timeSaved: String {
get {
let estimatedMillisecondsSaved = (adblock + trackingProtection) * millisecondsPerItem
let hours = estimatedMillisecondsSaved < 1000 * 60 * 60 * 24
let minutes = estimatedMillisecondsSaved < 1000 * 60 * 60
let seconds = estimatedMillisecondsSaved < 1000 * 60
var counter: Double = 0
var text = ""

if seconds {
counter = ceil(Double(estimatedMillisecondsSaved / 1000))
text = Strings.shieldsTimeStatsSeconds
} else if minutes {
counter = ceil(Double(estimatedMillisecondsSaved / 1000 / 60))
text = Strings.shieldsTimeStatsMinutes
} else if hours {
counter = ceil(Double(estimatedMillisecondsSaved / 1000 / 60 / 60))
text = Strings.shieldsTimeStatsHour
} else {
counter = ceil(Double(estimatedMillisecondsSaved / 1000 / 60 / 60 / 24))
text = Strings.shieldsTimeStatsDays
}

if let counterLocaleStr = Int(counter).decimalFormattedString {
return counterLocaleStr + text
} else {
return "0" + Strings.shieldsTimeStatsSeconds // If decimalFormattedString returns nil, default to "0s"
}
}
}

public var dataSaved: String {
var estimatedDataSavedInBytes = (BraveGlobalShieldStats.shared.adblock + BraveGlobalShieldStats.shared.trackingProtection) * bytesPerItem

if estimatedDataSavedInBytes <= 0 { return "0" }
let _1MB = 1000 * 1000

// Byte formatted megabytes value can be too long to display nicely(#3274).
// As a workaround we cut fraction value from megabytes by rounding it down.
if estimatedDataSavedInBytes > _1MB {
estimatedDataSavedInBytes = (estimatedDataSavedInBytes / _1MB) * _1MB
}

let formatter = ByteCountFormatter()
formatter.allowsNonnumericFormatting = false
// Skip bytes, because it displays as `bytes` instead of `B` which is too long for our shield stat.
// This is for extra safety only, there's not many sub 1000 bytes tracker scripts in the wild.
formatter.allowedUnits = [.useKB, .useMB, .useGB, .useTB]

return formatter.string(fromByteCount: Int64(estimatedDataSavedInBytes))
}
}

private extension Int {
var decimalFormattedString: String? {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = NumberFormatter.Style.decimal
numberFormatter.locale = NSLocale.current
return numberFormatter.string(from: self as NSNumber)
}
}
59 changes: 59 additions & 0 deletions BraveShared/Widgets/FavoritesWidgetData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2020 The Brave Authors. All rights reserved.
// 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 SDWebImage
import Shared
import WidgetKit

private let log = Logger.browserLogger

public struct WidgetFavorite: Codable {
public var url: URL
public var favicon: FaviconAttributes?

public init(url: URL, favicon: FaviconAttributes) {
self.url = url
self.favicon = favicon
}
}

public class FavoritesWidgetData {
private static var widgetDataRoot: URL? {
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: AppInfo.sharedContainerIdentifier)?.appendingPathComponent("widget_data")
}

private static var widgetDataPath: URL? {
widgetDataRoot?.appendingPathComponent("favs.json")
}

public static var dataExists: Bool {
guard let url = widgetDataPath else { return false }
return FileManager.default.fileExists(atPath: url.path)
}

public static func loadWidgetData() -> [WidgetFavorite]? {
guard let dataPath = widgetDataPath else { return nil }
do {
let jsonData = try Data(contentsOf: dataPath)
return try JSONDecoder().decode([WidgetFavorite].self, from: jsonData)
} catch {
log.error("loadWidgetData error: \(error)")
return nil
}
}

public static func updateWidgetData(_ favs: [WidgetFavorite]) {
guard let rootPath = widgetDataRoot, let dataPath = widgetDataPath else { return }
do {
let widgetData = try JSONEncoder().encode(favs)
try FileManager.default.createDirectory(atPath: rootPath.path, withIntermediateDirectories: true)
try widgetData.write(to: dataPath)
WidgetCenter.shared.reloadTimelines(ofKind: "FavoritesWidget")
} catch {
log.error("updateWidgetData error: \(error)")
}
}
}
17 changes: 17 additions & 0 deletions BraveUI/Extensions/LabelExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2021 The Brave Authors. All rights reserved.
// 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 SwiftUI

extension Label where Title == Text, Icon == Image {
public init<S>(_ title: S, uiImage: UIImage) where S: StringProtocol {
self.init {
Text(title)
} icon: {
Image(uiImage: uiImage)
}
}
}
20 changes: 20 additions & 0 deletions BraveWidgets/Assets.xcassets/AccentColor.colorset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.169",
"green" : "0.329",
"red" : "0.984"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading

0 comments on commit 4c29b30

Please sign in to comment.