Merge branch 'Ranchero-Software:main' into improve-markdown

This commit is contained in:
descodess 2021-10-24 14:05:40 +02:00 committed by GitHub
commit 20f54c4725
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
333 changed files with 5019 additions and 20401 deletions

View File

@ -223,10 +223,10 @@ private extension CloudKitArticlesZone {
record[CloudKitArticle.Fields.title] = article.title
record[CloudKitArticle.Fields.contentHTML] = article.contentHTML
record[CloudKitArticle.Fields.contentText] = article.contentText
record[CloudKitArticle.Fields.url] = article.url
record[CloudKitArticle.Fields.externalURL] = article.externalURL
record[CloudKitArticle.Fields.url] = article.rawLink
record[CloudKitArticle.Fields.externalURL] = article.rawExternalLink
record[CloudKitArticle.Fields.summary] = article.summary
record[CloudKitArticle.Fields.imageURL] = article.imageURL
record[CloudKitArticle.Fields.imageURL] = article.rawImageLink
record[CloudKitArticle.Fields.datePublished] = article.datePublished
record[CloudKitArticle.Fields.dateModified] = article.dateModified

View File

@ -17,6 +17,7 @@ public enum ReadFilterType {
public protocol Feed: FeedIdentifiable, ArticleFetcher, DisplayNameProvider, UnreadCountProvider {
var account: Account? { get }
var defaultReadFilterType: ReadFilterType { get }
}

View File

@ -12,7 +12,7 @@ public protocol FeedIdentifiable {
var feedID: FeedIdentifier? { get }
}
public enum FeedIdentifier: CustomStringConvertible, Hashable {
public enum FeedIdentifier: CustomStringConvertible, Hashable, Equatable {
case smartFeed(String) // String is a unique identifier
case script(String) // String is a unique identifier
@ -80,22 +80,4 @@ public enum FeedIdentifier: CustomStringConvertible, Hashable {
}
}
// MARK: - Hashable
public func hash(into hasher: inout Hasher) {
switch self {
case .smartFeed(let id):
hasher.combine("smartFeed")
hasher.combine(id)
case .script(let id):
hasher.combine("smartFeed")
hasher.combine(id)
case .webFeed(_, let webFeedID):
hasher.combine("webFeed")
hasher.combine(webFeedID)
case .folder(_, let folderName):
hasher.combine("folder")
hasher.combine(folderName)
}
}
}

View File

@ -91,8 +91,8 @@ private extension TwitterStatus {
}
}
let offsetStartIndex = entity.startIndex - unicodeScalarOffset
let offsetEndIndex = entity.endIndex - unicodeScalarOffset
let offsetStartIndex = unicodeScalarOffset < entity.startIndex ? entity.startIndex - unicodeScalarOffset : entity.startIndex
let offsetEndIndex = unicodeScalarOffset < entity.endIndex ? entity.endIndex - unicodeScalarOffset : entity.endIndex
let entityStartIndex = text.index(text.startIndex, offsetBy: offsetStartIndex, limitedBy: text.endIndex) ?? text.startIndex
let entityEndIndex = text.index(text.startIndex, offsetBy: offsetEndIndex, limitedBy: text.endIndex) ?? text.endIndex
@ -115,7 +115,7 @@ private extension TwitterStatus {
}
if prevIndex < displayEndIndex {
html += String(text[prevIndex..<displayEndIndex])
html += String(text[prevIndex..<displayEndIndex]).replacingOccurrences(of: "\n", with: "<br>")
}
return html

View File

@ -136,8 +136,35 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
case .failure(let error):
DispatchQueue.main.async {
self.refreshProgress.clear()
let wrappedError = AccountError.wrappedError(error: error, account: account)
completion(.failure(wrappedError))
if wrappedError.isCredentialsError, let basicCredentials = try? account.retrieveCredentials(type: .readerBasic), let endpoint = account.endpointURL {
self.caller.credentials = basicCredentials
self.caller.validateCredentials(endpoint: endpoint) { result in
switch result {
case .success(let apiCredentials):
if let apiCredentials = apiCredentials {
DispatchQueue.main.async {
try? account.storeCredentials(apiCredentials)
self.caller.credentials = apiCredentials
self.refreshAll(for: account, completion: completion)
}
} else {
DispatchQueue.main.async {
completion(.failure(wrappedError))
}
}
case .failure:
DispatchQueue.main.async {
completion(.failure(wrappedError))
}
}
}
} else {
completion(.failure(wrappedError))
}
}
}

View File

@ -6,7 +6,38 @@
<description>Most recent NetNewsWire changes with links to updates.</description>
<language>en</language>
<item>
<title>NetNewsWire 6.0.3</title>
<description><![CDATA[
<p>Same as 6.0.3b2 except for the version number.</p>
]]></description>
<pubDate>Sun, 05 Sep 2021 12:20:00 -0700</pubDate>
<enclosure url="https://github.com/Ranchero-Software/NetNewsWire/releases/download/mac-6.0.3/NetNewsWire6.0.3.zip" sparkle:version="6035" sparkle:shortVersionString="6.0.3" length="10477066" type="application/zip" />
<sparkle:minimumSystemVersion>10.15.0</sparkle:minimumSystemVersion>
</item>
<item>
<title>NetNewsWire 6.0.3b2</title>
<description><![CDATA[
<p>Feedly: preserve custom feed names with Feedly when moving them between folders</p>
<p>Preferences: use full-width row style in accounts and extensions panes</p>
<p>Fixed a crashing bug triggered by running some UI code outside of main thread</p>
<p>Fixed a crashing bug that could happen when the app tries to find a feed for a website</p>
<p>Fixed a crashing bug that could happen when rendering tweets</p>
<p>Changed how images are placed in Twitter articles so that you can better see who Tweeted the image</p>
<p>Fixed bug where iCloud syncing could stop prematurely when the sync database has records not in the local database</p>
<p>Fixed bug where favicons wouldnt be found when a home page URL has non-ASCII characters</p>
<p>Fixed bug where external URLs in Feedbin feeds might be lost</p>
<p>Fixed bug where words prepended with $ wouldnt appear in Twitter feeds</p>
<p>Fixed bug where newlines would be just a space in Twitter feeds</p>
<p>Fixed bug where BazQux-synced feeds might stop updating</p>
]]></description>
<pubDate>Sun, 29 Aug 2021 15:25:00 -0700</pubDate>
<enclosure url="https://github.com/Ranchero-Software/NetNewsWire/releases/download/mac-6.0.3b2/NetNewsWire6.0.3b2.zip" sparkle:version="6034" sparkle:shortVersionString="6.0.3b2" length="10477006" type="application/zip" />
<sparkle:minimumSystemVersion>10.15.0</sparkle:minimumSystemVersion>
</item>
<item>
<title>NetNewsWire 6.0.3b1</title>
<description><![CDATA[

View File

@ -6,7 +6,35 @@
<description>Most recent NetNewsWire releases (not test builds).</description>
<language>en</language>
<item>
<item>
<title>NetNewsWire 6.0.3</title>
<description><![CDATA[
<p>Feedly: preserve custom feed names with Feedly when moving them between folders</p>
<p>Feedly: handle API change with deleting and dont show a spurious error</p>
<p>NewsBlur: dont fetch articles marked hidden by NewsBlur</p>
<p>FreshRSS: add API endpoint URL example in setup form</p>
<p>iCloud: fixed bug not retaining feeds in a folder where the folder hasnt been synced yet</p>
<p>iCloud: fixed bug where iCloud syncing could stop prematurely when the sync database has records not in the local database</p>
<p>BazQux: fixed bug where BazQux-synced feeds might stop updating</p>
<p>Feedbin: fixed bug where external URLs in Feedbin feeds might be lost</p>
<p>Twitter extension: fixed weird bug where an extra https:/ could appear in tweet text</p>
<p>Preferences: use full-width row style in accounts and extensions panes</p>
<p>Fixed a crashing bug triggered by running some UI code outside of main thread</p>
<p>Fixed a crashing bug that could happen when the app tries to find a feed for a website</p>
<p>Fixed a crashing bug that could happen when rendering tweets</p>
<p>Changed how images are placed in Twitter articles so that you can better see who Tweeted the image</p>
<p>Fixed bug where favicons wouldnt be found when a home page URL has non-ASCII characters</p>
<p>Fixed bug where words prepended with $ wouldnt appear in Twitter feeds</p>
<p>Fixed bug where newlines would be just a space in Twitter feeds</p>
<p>Feeds list: smart feeds remain visible despite Hide Read Feeds setting</p>
<p>Keyboard shortcuts: fixed regression where L key wouldnt go to next unread when feed is all read</p>
]]></description>
<pubDate>Sun, 05 Sep 2021 12:20:00 -0700</pubDate>
<enclosure url="https://github.com/Ranchero-Software/NetNewsWire/releases/download/mac-6.0.3/NetNewsWire6.0.3.zip" sparkle:version="6035" sparkle:shortVersionString="6.0.3" length="10477066" type="application/zip" />
<sparkle:minimumSystemVersion>10.15.0</sparkle:minimumSystemVersion>
</item>
<item>
<title>NetNewsWire 6.0.2</title>
<description><![CDATA[
<p>Inoreader sync: fixed (hopefully) cause of rate limit errors — now doing background sync of statuses much less often - note that this fix needs to be rolled out across all NetNewsWire users in order for it to have full effect</p>

View File

@ -19,10 +19,10 @@ public struct Article: Hashable {
public let title: String?
public let contentHTML: String?
public let contentText: String?
public let url: String?
public let externalURL: String?
public let rawLink: String? // We store raw source value, but use computed url or link other than where raw value required.
public let rawExternalLink: String? // We store raw source value, but use computed externalURL or externalLink other than where raw value required.
public let summary: String?
public let imageURL: String?
public let rawImageLink: String? // We store raw source value, but use computed imageURL or imageLink other than where raw value required.
public let datePublished: Date?
public let dateModified: Date?
public let authors: Set<Author>?
@ -35,10 +35,10 @@ public struct Article: Hashable {
self.title = title
self.contentHTML = contentHTML
self.contentText = contentText
self.url = url
self.externalURL = externalURL
self.rawLink = url
self.rawExternalLink = externalURL
self.summary = summary
self.imageURL = imageURL
self.rawImageLink = imageURL
self.datePublished = datePublished
self.dateModified = dateModified
self.authors = authors
@ -65,7 +65,7 @@ public struct Article: Hashable {
// MARK: - Equatable
static public func ==(lhs: Article, rhs: Article) -> Bool {
return lhs.articleID == rhs.articleID && lhs.accountID == rhs.accountID && lhs.webFeedID == rhs.webFeedID && lhs.uniqueID == rhs.uniqueID && lhs.title == rhs.title && lhs.contentHTML == rhs.contentHTML && lhs.contentText == rhs.contentText && lhs.url == rhs.url && lhs.externalURL == rhs.externalURL && lhs.summary == rhs.summary && lhs.imageURL == rhs.imageURL && lhs.datePublished == rhs.datePublished && lhs.dateModified == rhs.dateModified && lhs.authors == rhs.authors
return lhs.articleID == rhs.articleID && lhs.accountID == rhs.accountID && lhs.webFeedID == rhs.webFeedID && lhs.uniqueID == rhs.uniqueID && lhs.title == rhs.title && lhs.contentHTML == rhs.contentHTML && lhs.contentText == rhs.contentText && lhs.rawLink == rhs.rawLink && lhs.rawExternalLink == rhs.rawExternalLink && lhs.summary == rhs.summary && lhs.rawImageLink == rhs.rawImageLink && lhs.datePublished == rhs.datePublished && lhs.dateModified == rhs.dateModified && lhs.authors == rhs.authors
}
}

View File

@ -71,7 +71,7 @@ extension Article {
if authors.isEmpty {
return self
}
return Article(accountID: self.accountID, articleID: self.articleID, webFeedID: self.webFeedID, uniqueID: self.uniqueID, title: self.title, contentHTML: self.contentHTML, contentText: self.contentText, url: self.url, externalURL: self.externalURL, summary: self.summary, imageURL: self.imageURL, datePublished: self.datePublished, dateModified: self.dateModified, authors: authors, status: self.status)
return Article(accountID: self.accountID, articleID: self.articleID, webFeedID: self.webFeedID, uniqueID: self.uniqueID, title: self.title, contentHTML: self.contentHTML, contentText: self.contentText, url: self.rawLink, externalURL: self.rawExternalLink, summary: self.summary, imageURL: self.rawImageLink, datePublished: self.datePublished, dateModified: self.dateModified, authors: authors, status: self.status)
}
func changesFrom(_ existingArticle: Article) -> DatabaseDictionary? {
@ -87,10 +87,10 @@ extension Article {
addPossibleStringChangeWithKeyPath(\Article.title, existingArticle, DatabaseKey.title, &d)
addPossibleStringChangeWithKeyPath(\Article.contentHTML, existingArticle, DatabaseKey.contentHTML, &d)
addPossibleStringChangeWithKeyPath(\Article.contentText, existingArticle, DatabaseKey.contentText, &d)
addPossibleStringChangeWithKeyPath(\Article.url, existingArticle, DatabaseKey.url, &d)
addPossibleStringChangeWithKeyPath(\Article.externalURL, existingArticle, DatabaseKey.externalURL, &d)
addPossibleStringChangeWithKeyPath(\Article.rawLink, existingArticle, DatabaseKey.url, &d)
addPossibleStringChangeWithKeyPath(\Article.rawExternalLink, existingArticle, DatabaseKey.externalURL, &d)
addPossibleStringChangeWithKeyPath(\Article.summary, existingArticle, DatabaseKey.summary, &d)
addPossibleStringChangeWithKeyPath(\Article.imageURL, existingArticle, DatabaseKey.imageURL, &d)
addPossibleStringChangeWithKeyPath(\Article.rawImageLink, existingArticle, DatabaseKey.imageURL, &d)
// If updated versions of dates are nil, and we have existing dates, keep the existing dates.
// This is data thats good to have, and its likely that a feed removing dates is doing so in error.
@ -154,17 +154,17 @@ extension Article: DatabaseObject {
if let contentText = contentText {
d[DatabaseKey.contentText] = contentText
}
if let url = url {
d[DatabaseKey.url] = url
if let rawLink = rawLink {
d[DatabaseKey.url] = rawLink
}
if let externalURL = externalURL {
d[DatabaseKey.externalURL] = externalURL
if let rawExternalLink = rawExternalLink {
d[DatabaseKey.externalURL] = rawExternalLink
}
if let summary = summary {
d[DatabaseKey.summary] = summary
}
if let imageURL = imageURL {
d[DatabaseKey.imageURL] = imageURL
if let rawImageLink = rawImageLink {
d[DatabaseKey.imageURL] = rawImageLink
}
if let datePublished = datePublished {
d[DatabaseKey.datePublished] = datePublished

View File

@ -69,6 +69,11 @@ struct AppAssets {
return RSImage(named: "articleExtractorOn")!
}()
@available(macOS 11.0, *)
static var articleTheme: RSImage = {
return NSImage(systemSymbolName: "doc.richtext", accessibilityDescription: nil)!
}()
@available(macOS 11.0, *)
static var cleanUpImage: RSImage = {
return NSImage(systemSymbolName: "wind", accessibilityDescription: nil)!

View File

@ -16,6 +16,8 @@ enum FontSize: Int {
final class AppDefaults {
static let defaultThemeName = "Default"
static var shared = AppDefaults()
private init() {}
@ -39,6 +41,7 @@ final class AppDefaults {
static let importOPMLAccountID = "importOPMLAccountID"
static let exportOPMLAccountID = "exportOPMLAccountID"
static let defaultBrowserID = "defaultBrowserID"
static let currentThemeName = "currentThemeName"
// Hidden prefs
static let showDebugMenu = "ShowDebugMenu"
@ -209,6 +212,15 @@ final class AppDefaults {
}
}
var currentThemeName: String? {
get {
return AppDefaults.string(for: Key.currentThemeName)
}
set {
AppDefaults.setString(for: Key.currentThemeName, newValue)
}
}
var showTitleOnMainWindow: Bool {
return AppDefaults.bool(for: Key.showTitleOnMainWindow)
}
@ -311,7 +323,8 @@ final class AppDefaults {
Key.timelineGroupByFeed: false,
"NSScrollViewShouldScrollUnderTitlebar": false,
Key.refreshInterval: RefreshInterval.everyHour.rawValue,
Key.showDebugMenu: showDebugMenu]
Key.showDebugMenu: showDebugMenu,
Key.currentThemeName: Self.defaultThemeName]
UserDefaults.standard.register(defaults: defaults)

View File

@ -107,6 +107,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
private var softwareUpdater: SPUUpdater!
private var crashReporter: PLCrashReporter!
#endif
private var themeImportPath: String?
override init() {
NSWindow.allowsAutomaticWindowTabbing = false
@ -120,10 +122,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
SecretsManager.provider = Secrets()
AccountManager.shared = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!)
ArticleThemesManager.shared = ArticleThemesManager(folderPath: Platform.dataSubfolder(forApplication: nil, folderName: "Themes")!)
FeedProviderManager.shared.delegate = ExtensionPointManager.shared
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(inspectableObjectsDidChange(_:)), name: .InspectableObjectsDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(importDownloadedTheme(_:)), name: .didEndDownloadingTheme, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(themeImportError(_:)), name: .didFailToImportThemeWithError, object: nil)
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(didWakeNotification(_:)), name: NSWorkspace.didWakeNotification, object: nil)
appDelegate = self
@ -318,16 +323,24 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
AccountManager.shared.receiveRemoteNotification(userInfo: userInfo)
}
func application(_ sender: NSApplication, openFile filename: String) -> Bool {
guard filename.hasSuffix(ArticleTheme.nnwThemeSuffix) else { return false }
importTheme(filename: filename)
return true
}
func applicationWillTerminate(_ notification: Notification) {
shuttingDown = true
saveState()
ArticleThemeDownloader.shared.cleanUp()
AccountManager.shared.sendArticleStatusAll() {
self.isShutDownSyncDone = true
}
let timeout = Date().addingTimeInterval(2)
while !isShutDownSyncDone && RunLoop.current.run(mode: .default, before: .distantFuture) && timeout > Date() { }
while !isShutDownSyncDone && RunLoop.current.run(mode: .default, before: timeout) && timeout > Date() { }
}
// MARK: Notifications
@ -368,6 +381,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
@objc func didWakeNotification(_ note: Notification) {
fireOldTimers()
}
@objc func importDownloadedTheme(_ note: Notification) {
guard let userInfo = note.userInfo,
let url = userInfo["url"] as? URL else {
return
}
DispatchQueue.main.async {
self.importTheme(filename: url.path)
}
}
// MARK: Main Window
@ -758,7 +781,7 @@ extension AppDelegate {
}
private extension AppDelegate {
internal extension AppDelegate {
func fireOldTimers() {
// Its possible theres a refresh timer set to go off in the past.
@ -768,7 +791,6 @@ private extension AppDelegate {
}
func objectsForInspector() -> [Any]? {
guard let window = NSApplication.shared.mainWindow, let windowController = window.windowController as? MainWindowController else {
return nil
}
@ -781,7 +803,6 @@ private extension AppDelegate {
}
func updateSortMenuItems() {
let sortByNewestOnTop = AppDefaults.shared.timelineSortDirection == .orderedDescending
sortByNewestArticleOnTopMenuItem.state = sortByNewestOnTop ? .on : .off
sortByOldestArticleOnTopMenuItem.state = sortByNewestOnTop ? .off : .on
@ -791,6 +812,169 @@ private extension AppDelegate {
let groupByFeedEnabled = AppDefaults.shared.timelineGroupByFeed
groupArticlesByFeedMenuItem.state = groupByFeedEnabled ? .on : .off
}
func importTheme(filename: String) {
guard let window = mainWindowController?.window else { return }
do {
let theme = try ArticleTheme(path: filename)
let alert = NSAlert()
alert.alertStyle = .informational
let localizedMessageText = NSLocalizedString("Install theme “%@” by %@?", comment: "Theme message text")
alert.messageText = NSString.localizedStringWithFormat(localizedMessageText as NSString, theme.name, theme.creatorName) as String
var attrs = [NSAttributedString.Key : Any]()
attrs[.font] = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize)
attrs[.foregroundColor] = NSColor.textColor
if #available(macOS 11.0, *) {
let titleParagraphStyle = NSMutableParagraphStyle()
titleParagraphStyle.alignment = .center
attrs[.paragraphStyle] = titleParagraphStyle
}
let websiteText = NSMutableAttributedString()
websiteText.append(NSAttributedString(string: NSLocalizedString("Author's Website", comment: "Author's Website"), attributes: attrs))
if #available(macOS 11.0, *) {
websiteText.append(NSAttributedString(string: "\n"))
} else {
websiteText.append(NSAttributedString(string: " "))
}
attrs[.link] = theme.creatorHomePage
websiteText.append(NSAttributedString(string: theme.creatorHomePage, attributes: attrs))
let textViewWidth: CGFloat
if #available(macOS 11.0, *) {
textViewWidth = 200
} else {
textViewWidth = 400
}
let textView = NSTextView(frame: CGRect(x: 0, y: 0, width: textViewWidth, height: 15))
textView.isEditable = false
textView.drawsBackground = false
textView.textStorage?.setAttributedString(websiteText)
alert.accessoryView = textView
alert.addButton(withTitle: NSLocalizedString("Install Theme", comment: "Install Theme"))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel Install Theme"))
func importTheme() {
do {
try ArticleThemesManager.shared.importTheme(filename: filename)
confirmImportSuccess(themeName: theme.name)
} catch {
NSApplication.shared.presentError(error)
}
}
alert.beginSheetModal(for: window) { result in
if result == NSApplication.ModalResponse.alertFirstButtonReturn {
if ArticleThemesManager.shared.themeExists(filename: filename) {
let alert = NSAlert()
alert.alertStyle = .warning
let localizedMessageText = NSLocalizedString("The theme “%@” already exists. Overwrite it?", comment: "Overwrite theme")
alert.messageText = NSString.localizedStringWithFormat(localizedMessageText as NSString, theme.name) as String
alert.addButton(withTitle: NSLocalizedString("Overwrite", comment: "Overwrite"))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel Install Theme"))
alert.beginSheetModal(for: window) { result in
if result == NSApplication.ModalResponse.alertFirstButtonReturn {
importTheme()
}
}
} else {
importTheme()
}
}
}
} catch {
NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error" : error, "path": filename])
}
}
func confirmImportSuccess(themeName: String) {
guard let window = mainWindowController?.window else { return }
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = NSLocalizedString("Theme installed", comment: "Theme installed")
let localizedInformativeText = NSLocalizedString("The theme “%@” has been installed.", comment: "Theme installed")
alert.informativeText = NSString.localizedStringWithFormat(localizedInformativeText as NSString, themeName) as String
alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK"))
alert.beginSheetModal(for: window)
}
@objc func themeImportError(_ note: Notification) {
guard let userInfo = note.userInfo,
let error = userInfo["error"] as? Error else {
return
}
themeImportPath = userInfo["path"] as? String
var informativeText: String = ""
if let decodingError = error as? DecodingError {
switch decodingError {
case .typeMismatch(let type, _):
let localizedError = NSLocalizedString("This theme cannot be used because the the type—“%@”—is mismatched in the Info.plist", comment: "Type mismatch")
informativeText = NSString.localizedStringWithFormat(localizedError as NSString, type as! CVarArg) as String
case .valueNotFound(let value, _):
let localizedError = NSLocalizedString("This theme cannot be used because the the value—“%@”—is not found in the Info.plist.", comment: "Decoding value missing")
informativeText = NSString.localizedStringWithFormat(localizedError as NSString, value as! CVarArg) as String
case .keyNotFound(let codingKey, _):
let localizedError = NSLocalizedString("This theme cannot be used because the the key—“%@”—is not found in the Info.plist.", comment: "Decoding key missing")
informativeText = NSString.localizedStringWithFormat(localizedError as NSString, codingKey.stringValue) as String
case .dataCorrupted(let context):
guard let underlyingError = context.underlyingError as NSError?,
let debugDescription = underlyingError.userInfo["NSDebugDescription"] as? String else {
informativeText = error.localizedDescription
break
}
let localizedError = NSLocalizedString("This theme cannot be used because of data corruption in the Info.plist: %@.", comment: "Decoding key missing")
informativeText = NSString.localizedStringWithFormat(localizedError as NSString, debugDescription) as String
default:
informativeText = error.localizedDescription
}
} else {
informativeText = error.localizedDescription
}
DispatchQueue.main.async {
let alert = NSAlert()
alert.alertStyle = .warning
alert.messageText = NSLocalizedString("Theme Error", comment: "Theme download error")
alert.informativeText = informativeText
alert.addButton(withTitle: NSLocalizedString("Open Theme Folder", comment: "Open Theme Folder"))
alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK"))
let button = alert.buttons.first
button?.target = self
button?.action = #selector(self.openThemesFolder(_:))
alert.buttons[0].keyEquivalent = "\033"
alert.buttons[1].keyEquivalent = "\r"
alert.runModal()
}
}
@objc func openThemesFolder(_ sender: Any) {
if themeImportPath == nil {
let url = URL(fileURLWithPath: ArticleThemesManager.shared.folderPath)
NSWorkspace.shared.open(url)
} else {
let url = URL(fileURLWithPath: themeImportPath!)
NSWorkspace.shared.open(url.deletingLastPathComponent())
}
}
}
/*

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="19162" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17701"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19162"/>
</dependencies>
<scenes>
<!--Application-->
@ -69,24 +69,24 @@
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="File" id="bib-Uj-vzu">
<items>
<menuItem title="New Web Feed" keyEquivalent="n" id="Was-JA-tGl">
<menuItem title="New Web Feed" keyEquivalent="n" id="Was-JA-tGl">
<connections>
<action selector="showAddWebFeedWindow:" target="Ady-hI-5gd" id="LkT-kx-aCR"/>
</connections>
</menuItem>
<menuItem title="New Reddit Feed" id="n6h-Bp-CIc">
<menuItem title="New Reddit Feed" id="n6h-Bp-CIc">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="showAddRedditFeedWindow:" target="Ady-hI-5gd" id="Irh-Rw-mFK"/>
</connections>
</menuItem>
<menuItem title="New Twitter Feed" id="ki4-7l-tM6">
<menuItem title="New Twitter Feed" id="ki4-7l-tM6">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="showAddTwitterFeedWindow:" target="Ady-hI-5gd" id="dZR-aU-O52"/>
</connections>
</menuItem>
<menuItem title="New Folder" keyEquivalent="N" id="wkh-LX-Xp1">
<menuItem title="New Folder" keyEquivalent="N" id="wkh-LX-Xp1">
<connections>
<action selector="showAddFolderWindow:" target="Ady-hI-5gd" id="GIi-wc-uYk"/>
</connections>
@ -157,6 +157,18 @@
<action selector="copy:" target="Ady-hI-5gd" id="G1f-GL-Joy"/>
</connections>
</menuItem>
<menuItem title="Copy Article URL" id="qNk-By-jKp">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="copyArticleURL:" target="Ady-hI-5gd" id="A5t-x7-Mmy"/>
</connections>
</menuItem>
<menuItem title="Copy External URL" id="fOF-99-6Iv">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="copyExternalURL:" target="Ady-hI-5gd" id="MkV-Do-Bc1"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
<connections>
<action selector="paste:" target="Ady-hI-5gd" id="UvS-8e-Qdg"/>
@ -183,7 +195,7 @@
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Find" id="1b7-l0-nxx">
<items>
<menuItem title="Article Search" keyEquivalent="f" id="nB2-mv-2i5">
<menuItem title="Article Search" keyEquivalent="f" id="nB2-mv-2i5">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="moveFocusToSearchField:" target="Ady-hI-5gd" id="MhU-Pb-Po8"/>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17506"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17701"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@ -196,6 +196,24 @@
<action selector="cleanUp:" target="Oky-zY-oP4" id="UCH-DG-yk4"/>
</connections>
</toolbarItem>
<toolbarItem implicitItemIdentifier="596363B5-CE41-417C-B8AB-11CC2C99BCA5" label="Article Theme" paletteLabel="Article Theme" sizingBehavior="auto" id="3Hc-al-vK2">
<nil key="toolTip"/>
<popUpButton key="view" verticalHuggingPriority="750" id="MFT-nb-eLG">
<rect key="frame" x="0.0" y="14" width="100" height="24"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="roundTextured" title="Item 1" bezelStyle="texturedRounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" selectedItem="xAs-IL-tMv" id="ior-Gb-LTq">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="1Ac-Uq-Yeu">
<items>
<menuItem title="Item 1" state="on" id="xAs-IL-tMv"/>
<menuItem title="Item 2" id="T8e-ib-OTb"/>
<menuItem title="Item 3" id="Gfy-76-Njz"/>
</items>
</menu>
</popUpButtonCell>
</popUpButton>
</toolbarItem>
</allowedToolbarItems>
<defaultToolbarItems>
<toolbarItem reference="Skp-5r-70Q"/>
@ -221,6 +239,7 @@
</connections>
</window>
<connections>
<outlet property="articleThemePopUpButton" destination="MFT-nb-eLG" id="lHc-ej-PrT"/>
<segue destination="reS-fe-pD8" kind="relationship" relationship="window.shadowedContentViewController" id="WS2-WB-dc4"/>
</connections>
</windowController>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="18122" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="mPU-HG-I4u">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="19162" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="mPU-HG-I4u">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="18122"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19162"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@ -31,15 +31,15 @@
<scene sceneID="R4l-Wg-k7x">
<objects>
<viewController title="General" storyboardIdentifier="General" id="iuH-lz-18x" customClass="GeneralPreferencesViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="WnV-px-wCT">
<rect key="frame" x="0.0" y="0.0" width="509" height="272"/>
<view key="view" misplaced="YES" id="WnV-px-wCT">
<rect key="frame" x="0.0" y="0.0" width="509" height="339"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<customView horizontalHuggingPriority="1000" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" verticalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="Ut3-yd-q6G">
<rect key="frame" x="54" y="16" width="400" height="240"/>
<rect key="frame" x="54" y="16" width="399" height="306"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="pR2-Bf-7Fd">
<rect key="frame" x="6" y="219" width="106" height="16"/>
<rect key="frame" x="6" y="285" width="105" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Article Text Size:" id="xQu-QV-91i">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@ -47,7 +47,7 @@
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Z6O-Zt-V1g">
<rect key="frame" x="115" y="212" width="289" height="25"/>
<rect key="frame" x="114" y="278" width="289" height="25"/>
<popUpButtonCell key="cell" type="push" title="Medium" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="2" imageScaling="proportionallyDown" inset="2" selectedItem="jMV-2o-5Oh" id="6pw-Vq-tjM">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
@ -75,11 +75,36 @@
</connections>
</popUpButtonCell>
</popUpButton>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ISO-Wu-R60">
<rect key="frame" x="114" y="244" width="289" height="25"/>
<popUpButtonCell key="cell" type="push" title="Default" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="Pkl-EA-Goa" id="vN9-pm-Gls">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="Tw3-5B-F76">
<items>
<menuItem title="Default" state="on" id="Pkl-EA-Goa"/>
</items>
</menu>
</popUpButtonCell>
<connections>
<action selector="articleThemePopUpDidChange:" target="iuH-lz-18x" id="afz-Uu-a5b"/>
</connections>
</popUpButton>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1w0-nA-DEO">
<rect key="frame" x="110" y="207" width="161" height="32"/>
<buttonCell key="cell" type="push" title="Open Themes Folder" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="ySX-5i-SP1">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="showThemesFolder:" target="iuH-lz-18x" id="WEP-Fe-cAR"/>
</connections>
</button>
<box verticalHuggingPriority="750" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="Tdg-6Y-gvW">
<rect key="frame" x="0.0" y="197" width="400" height="5"/>
<rect key="frame" x="0.0" y="197" width="399" height="5"/>
</box>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Wsb-Lr-8Q7">
<rect key="frame" x="54" y="166" width="58" height="16"/>
<rect key="frame" x="53" y="166" width="58" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Browser:" id="CgU-dE-Qtb">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@ -87,7 +112,7 @@
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ci4-fW-KjU">
<rect key="frame" x="115" y="159" width="289" height="25"/>
<rect key="frame" x="114" y="159" width="289" height="25"/>
<popUpButtonCell key="cell" type="push" title="Safari" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="ObP-qK-qDJ" id="hrm-aT-Rc2" userLabel="Popup">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
@ -102,7 +127,7 @@
</connections>
</popUpButton>
<button horizontalHuggingPriority="1000" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="Ubm-Pk-l7x">
<rect key="frame" x="116" y="132" width="284" height="18"/>
<rect key="frame" x="115" y="132" width="284" height="18"/>
<buttonCell key="cell" type="check" title="Open web pages in background in browser" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="t0a-LN-rCv">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
@ -122,7 +147,7 @@
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="j0t-Wa-UTL">
<rect key="frame" x="135" y="109" width="235" height="16"/>
<rect key="frame" x="134" y="109" width="235" height="16"/>
<textFieldCell key="cell" controlSize="small" title="Press the Shift key to do the opposite." id="EMq-9M-zTJ">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
@ -130,10 +155,10 @@
</textFieldCell>
</textField>
<box verticalHuggingPriority="750" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="hQy-ng-ijd">
<rect key="frame" x="0.0" y="38" width="400" height="5"/>
<rect key="frame" x="0.0" y="38" width="399" height="5"/>
</box>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ucw-vG-yLt">
<rect key="frame" x="17" y="7" width="95" height="16"/>
<rect key="frame" x="16" y="7" width="95" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Refresh Feeds:" id="F7c-lm-g97">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@ -141,7 +166,7 @@
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SFF-mL-yc8">
<rect key="frame" x="115" y="0.0" width="289" height="25"/>
<rect key="frame" x="114" y="0.0" width="289" height="25"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="200" id="N1a-qV-4Os"/>
</constraints>
@ -176,7 +201,7 @@
</popUpButtonCell>
</popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="yrg-M3-Dbz">
<rect key="frame" x="8" y="79" width="106" height="16"/>
<rect key="frame" x="7" y="79" width="106" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Safari Extension:" id="Eth-o0-pWM">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@ -184,7 +209,7 @@
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="wtY-Zd-Ps9">
<rect key="frame" x="116" y="78" width="284" height="18"/>
<rect key="frame" x="115" y="78" width="284" height="18"/>
<buttonCell key="cell" type="radio" title="Open feeds in NetNewsWire" bezelStyle="regularSquare" imagePosition="left" alignment="left" state="on" inset="2" id="uvx-O8-HvU">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
@ -198,7 +223,7 @@
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Yrc-6Q-kx8">
<rect key="frame" x="116" y="56" width="284" height="18"/>
<rect key="frame" x="115" y="56" width="284" height="18"/>
<buttonCell key="cell" type="radio" title="Open feeds in default news reader" bezelStyle="regularSquare" imagePosition="left" alignment="left" inset="2" id="SkZ-tE-blK">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
@ -207,10 +232,17 @@
<binding destination="iuH-lz-18x" name="value" keyPath="openFeedsInDefaultNewsReader" id="QZ4-W8-rPi"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="S2Z-bG-jYk">
<rect key="frame" x="18" y="251" width="93" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Article Theme:" id="MQe-Za-N8J">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="Wsb-Lr-8Q7" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Ut3-yd-q6G" secondAttribute="leading" id="17A-5m-ZG0"/>
<constraint firstItem="Tdg-6Y-gvW" firstAttribute="top" secondItem="Z6O-Zt-V1g" secondAttribute="bottom" constant="16" id="2iQ-Xt-RLM"/>
<constraint firstItem="Z6O-Zt-V1g" firstAttribute="leading" secondItem="pR2-Bf-7Fd" secondAttribute="trailing" constant="8" symbolic="YES" id="2wM-K6-eAF"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="Ubm-Pk-l7x" secondAttribute="trailing" id="3h4-m7-pMW"/>
<constraint firstItem="Yrc-6Q-kx8" firstAttribute="top" secondItem="wtY-Zd-Ps9" secondAttribute="bottom" constant="6" symbolic="YES" id="59s-XY-cPN"/>
@ -220,15 +252,20 @@
<constraint firstItem="Ci4-fW-KjU" firstAttribute="width" secondItem="SFF-mL-yc8" secondAttribute="width" id="AE4-am-IWK"/>
<constraint firstItem="wtY-Zd-Ps9" firstAttribute="firstBaseline" secondItem="yrg-M3-Dbz" secondAttribute="baseline" id="AeO-w1-7yq"/>
<constraint firstItem="Ubm-Pk-l7x" firstAttribute="top" secondItem="Ci4-fW-KjU" secondAttribute="bottom" constant="14" id="GNx-7d-yAo"/>
<constraint firstItem="ISO-Wu-R60" firstAttribute="leading" secondItem="Z6O-Zt-V1g" secondAttribute="leading" id="GxL-2l-CYb"/>
<constraint firstItem="Z6O-Zt-V1g" firstAttribute="top" secondItem="Ut3-yd-q6G" secondAttribute="top" constant="4" id="IaE-jL-vMM"/>
<constraint firstItem="hQy-ng-ijd" firstAttribute="leading" secondItem="Ut3-yd-q6G" secondAttribute="leading" id="KEI-R5-rzD"/>
<constraint firstItem="S2Z-bG-jYk" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Ut3-yd-q6G" secondAttribute="leading" id="KQI-3T-s6M"/>
<constraint firstItem="pR2-Bf-7Fd" firstAttribute="leading" secondItem="Ut3-yd-q6G" secondAttribute="leading" constant="8" id="LRG-HZ-yxh"/>
<constraint firstAttribute="trailing" secondItem="SFF-mL-yc8" secondAttribute="trailing" id="N39-Q9-X5Q"/>
<constraint firstItem="ISO-Wu-R60" firstAttribute="trailing" secondItem="Z6O-Zt-V1g" secondAttribute="trailing" id="P3r-hD-nE8"/>
<constraint firstItem="ISO-Wu-R60" firstAttribute="leading" secondItem="S2Z-bG-jYk" secondAttribute="trailing" constant="8" symbolic="YES" id="QDj-xS-6Ox"/>
<constraint firstAttribute="trailing" secondItem="hQy-ng-ijd" secondAttribute="trailing" id="RbT-jK-fBb"/>
<constraint firstItem="Ubm-Pk-l7x" firstAttribute="width" secondItem="SFF-mL-yc8" secondAttribute="width" id="TX4-iO-J5E"/>
<constraint firstItem="j0t-Wa-UTL" firstAttribute="leading" secondItem="Ubm-Pk-l7x" secondAttribute="leading" constant="19" id="UKq-8p-lyR"/>
<constraint firstItem="j0t-Wa-UTL" firstAttribute="top" secondItem="Ubm-Pk-l7x" secondAttribute="bottom" constant="8" id="XTw-Ef-FD3"/>
<constraint firstItem="wtY-Zd-Ps9" firstAttribute="trailing" secondItem="SFF-mL-yc8" secondAttribute="trailing" id="Zkn-zv-as5"/>
<constraint firstItem="1w0-nA-DEO" firstAttribute="top" secondItem="ISO-Wu-R60" secondAttribute="bottom" constant="14" id="ZlG-V3-AAd"/>
<constraint firstItem="pR2-Bf-7Fd" firstAttribute="firstBaseline" secondItem="Z6O-Zt-V1g" secondAttribute="firstBaseline" id="aO5-iE-L7A"/>
<constraint firstAttribute="trailing" secondItem="Z6O-Zt-V1g" secondAttribute="trailing" id="aS9-KA-vSH"/>
<constraint firstItem="wtY-Zd-Ps9" firstAttribute="top" secondItem="j0t-Wa-UTL" secondAttribute="bottom" constant="14" id="aod-td-Gim"/>
@ -238,16 +275,20 @@
<constraint firstItem="Yrc-6Q-kx8" firstAttribute="trailing" secondItem="wtY-Zd-Ps9" secondAttribute="trailing" id="e6V-q6-WJq"/>
<constraint firstItem="SFF-mL-yc8" firstAttribute="top" secondItem="hQy-ng-ijd" secondAttribute="bottom" constant="16" id="eM7-OM-Qsz"/>
<constraint firstItem="Yrc-6Q-kx8" firstAttribute="leading" secondItem="wtY-Zd-Ps9" secondAttribute="leading" id="gNX-Yc-DdD"/>
<constraint firstItem="1w0-nA-DEO" firstAttribute="leading" secondItem="ISO-Wu-R60" secondAttribute="leading" id="gWR-OU-qcO"/>
<constraint firstItem="Ci4-fW-KjU" firstAttribute="top" secondItem="Tdg-6Y-gvW" secondAttribute="bottom" constant="16" id="hXl-1D-lTD"/>
<constraint firstItem="wtY-Zd-Ps9" firstAttribute="leading" secondItem="yrg-M3-Dbz" secondAttribute="trailing" constant="6" symbolic="YES" id="hpP-sx-veV"/>
<constraint firstItem="hQy-ng-ijd" firstAttribute="top" secondItem="Yrc-6Q-kx8" secondAttribute="bottom" constant="16" id="i2g-cZ-EV4"/>
<constraint firstItem="ucw-vG-yLt" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Ut3-yd-q6G" secondAttribute="leading" id="lDL-JN-ANP"/>
<constraint firstItem="Tdg-6Y-gvW" firstAttribute="top" secondItem="1w0-nA-DEO" secondAttribute="bottom" constant="14" id="lEK-yl-TCM"/>
<constraint firstItem="Z6O-Zt-V1g" firstAttribute="width" secondItem="SFF-mL-yc8" secondAttribute="width" id="noW-Jf-Xbs"/>
<constraint firstAttribute="trailing" secondItem="Tdg-6Y-gvW" secondAttribute="trailing" id="qzz-gu-8kO"/>
<constraint firstItem="Wsb-Lr-8Q7" firstAttribute="firstBaseline" secondItem="Ci4-fW-KjU" secondAttribute="firstBaseline" id="rPX-je-OG5"/>
<constraint firstItem="Ci4-fW-KjU" firstAttribute="leading" secondItem="Wsb-Lr-8Q7" secondAttribute="trailing" constant="8" symbolic="YES" id="rcx-B6-zLP"/>
<constraint firstItem="S2Z-bG-jYk" firstAttribute="firstBaseline" secondItem="ISO-Wu-R60" secondAttribute="firstBaseline" id="xt6-ua-xz8"/>
<constraint firstItem="SFF-mL-yc8" firstAttribute="leading" secondItem="ucw-vG-yLt" secondAttribute="trailing" constant="8" symbolic="YES" id="yBm-Dc-lGA"/>
<constraint firstAttribute="bottom" secondItem="SFF-mL-yc8" secondAttribute="bottom" constant="4" id="zIa-Ca-y3J"/>
<constraint firstItem="ISO-Wu-R60" firstAttribute="top" secondItem="Z6O-Zt-V1g" secondAttribute="bottom" constant="14" id="zaM-J3-VcP"/>
<constraint firstAttribute="trailing" secondItem="Ci4-fW-KjU" secondAttribute="trailing" id="zbx-Ch-NEt"/>
</constraints>
</customView>
@ -259,13 +300,14 @@
</constraints>
</view>
<connections>
<outlet property="articleThemePopup" destination="ISO-Wu-R60" id="oI3-6W-dPa"/>
<outlet property="defaultBrowserPopup" destination="Ci4-fW-KjU" id="7Nh-nU-Sbc"/>
</connections>
</viewController>
<customObject id="bSQ-tq-wd3" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
<userDefaultsController representsSharedInstance="YES" id="mAF-gO-1PI"/>
</objects>
<point key="canvasLocation" x="-568.5" y="438.5"/>
<point key="canvasLocation" x="-565.5" y="432"/>
</scene>
<!--Advanced Preferences View Controller-->
<scene sceneID="z1G-rc-sP5">
@ -445,16 +487,16 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="7UM-iq-OLB" customClass="PreferencesTableViewBackgroundView" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="44" width="180" height="243"/>
<rect key="frame" x="20" y="44" width="180" height="224"/>
<subviews>
<scrollView borderType="none" autohidesScrollers="YES" horizontalLineScroll="26" horizontalPageScroll="10" verticalLineScroll="26" verticalPageScroll="10" hasHorizontalScroller="NO" horizontalScrollElasticity="none" translatesAutoresizingMaskIntoConstraints="NO" id="PaF-du-r3c">
<rect key="frame" x="1" y="1" width="178" height="241"/>
<rect key="frame" x="1" y="1" width="178" height="222"/>
<clipView key="contentView" id="cil-Gq-akO">
<rect key="frame" x="0.0" y="0.0" width="178" height="241"/>
<rect key="frame" x="0.0" y="0.0" width="178" height="222"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" columnReordering="NO" columnSelection="YES" tableStyle="fullWidth" columnResizing="NO" multipleSelection="NO" autosaveColumns="NO" rowHeight="24" viewBased="YES" id="aTp-KR-y6b">
<rect key="frame" x="0.0" y="0.0" width="178" height="241"/>
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" tableStyle="fullWidth" columnReordering="NO" columnSelection="YES" columnResizing="NO" multipleSelection="NO" autosaveColumns="NO" rowHeight="24" viewBased="YES" id="aTp-KR-y6b">
<rect key="frame" x="0.0" y="0.0" width="178" height="222"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="3" height="2"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
@ -473,7 +515,7 @@
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="Cell" id="h2e-5a-qNO" customClass="AccountCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="11" y="1" width="155" height="17"/>
<rect key="frame" x="1" y="1" width="155" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="27f-p8-Wnt">
@ -534,7 +576,7 @@
</constraints>
</customView>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="QbY-Xt-QmB">
<rect key="frame" x="20" y="18" width="32" height="26"/>
<rect key="frame" x="20" y="19" width="32" height="26"/>
<constraints>
<constraint firstAttribute="height" constant="24" id="IQ9-li-mRa"/>
<constraint firstAttribute="width" constant="32" id="f2s-0O-ggn"/>
@ -548,7 +590,7 @@
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="9qe-g9-RR3">
<rect key="frame" x="51" y="18" width="32" height="26"/>
<rect key="frame" x="51" y="19" width="32" height="26"/>
<buttonCell key="cell" type="smallSquare" bezelStyle="smallSquare" image="NSRemoveTemplate" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" enabled="NO" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Dvx-B8-wQy">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@ -561,7 +603,7 @@
<rect key="frame" x="83" y="20" width="117" height="24"/>
</customView>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="Y7D-xQ-wep">
<rect key="frame" x="208" y="19" width="222" height="267"/>
<rect key="frame" x="208" y="20" width="222" height="248"/>
</customView>
</subviews>
<constraints>
@ -616,16 +658,16 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="pjs-G4-byk" customClass="PreferencesTableViewBackgroundView" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="43" width="180" height="243"/>
<rect key="frame" x="20" y="44" width="180" height="224"/>
<subviews>
<scrollView borderType="none" autohidesScrollers="YES" horizontalLineScroll="26" horizontalPageScroll="10" verticalLineScroll="26" verticalPageScroll="10" hasHorizontalScroller="NO" horizontalScrollElasticity="none" translatesAutoresizingMaskIntoConstraints="NO" id="29T-r2-ckC">
<rect key="frame" x="1" y="1" width="178" height="241"/>
<rect key="frame" x="1" y="1" width="178" height="222"/>
<clipView key="contentView" id="dXw-GY-TP8">
<rect key="frame" x="0.0" y="0.0" width="178" height="241"/>
<rect key="frame" x="0.0" y="0.0" width="178" height="222"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" columnReordering="NO" columnSelection="YES" tableStyle="fullWidth" columnResizing="NO" multipleSelection="NO" autosaveColumns="NO" rowHeight="24" viewBased="YES" id="dfn-Vn-oDp">
<rect key="frame" x="0.0" y="0.0" width="178" height="241"/>
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" tableStyle="fullWidth" columnReordering="NO" columnSelection="YES" columnResizing="NO" multipleSelection="NO" autosaveColumns="NO" rowHeight="24" viewBased="YES" id="dfn-Vn-oDp">
<rect key="frame" x="0.0" y="0.0" width="178" height="222"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="3" height="2"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
@ -644,7 +686,7 @@
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="Cell" id="xQs-6E-Kpy">
<rect key="frame" x="11" y="1" width="155" height="17"/>
<rect key="frame" x="1" y="1" width="155" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="kmG-vw-CbN">
@ -701,7 +743,7 @@
</constraints>
</customView>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JA2-UT-8DR">
<rect key="frame" x="20" y="18" width="32" height="26"/>
<rect key="frame" x="20" y="19" width="32" height="26"/>
<constraints>
<constraint firstAttribute="height" constant="24" id="Qnm-eZ-2KJ"/>
<constraint firstAttribute="width" constant="32" id="ZQY-kS-9lY"/>
@ -715,7 +757,7 @@
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="jfX-DL-TXs">
<rect key="frame" x="51" y="18" width="32" height="26"/>
<rect key="frame" x="51" y="19" width="32" height="26"/>
<buttonCell key="cell" type="smallSquare" bezelStyle="smallSquare" image="NSRemoveTemplate" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" enabled="NO" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="4FB-KH-Ton">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@ -725,10 +767,10 @@
</connections>
</button>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="sak-nS-Xfu" customClass="PreferencesControlsBackgroundView" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="83" y="19" width="117" height="24"/>
<rect key="frame" x="83" y="20" width="117" height="24"/>
</customView>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="N1N-pE-gBL">
<rect key="frame" x="208" y="20" width="222" height="267"/>
<rect key="frame" x="208" y="20" width="222" height="248"/>
</customView>
</subviews>
<constraints>

View File

@ -43,7 +43,17 @@ struct Browser {
/// - Note: Some browsers (specifically Chromium-derived ones) will ignore the request
/// to open in the background.
static func open(_ urlString: String, inBackground: Bool) {
if let url = URL(unicodeString: urlString) {
guard let url = URL(unicodeString: urlString), let preparedURL = url.preparedForOpeningInBrowser() else { return }
let configuration = NSWorkspace.OpenConfiguration()
configuration.requiresUniversalLinks = true
configuration.promptsUserIfNeeded = false
if inBackground {
configuration.activates = false
}
NSWorkspace.shared.open(preparedURL, configuration: configuration) { (runningApplication, error) in
guard error != nil else { return }
if let defaultBrowser = defaultBrowser {
defaultBrowser.openURL(url, inBackground: inBackground)
} else {

View File

@ -16,8 +16,8 @@ enum DetailState: Equatable {
case noSelection
case multipleSelection
case loading
case article(Article)
case extracted(Article, ExtractedArticle)
case article(Article, CGFloat?)
case extracted(Article, ExtractedArticle, CGFloat?)
}
final class DetailViewController: NSViewController, WKUIDelegate {
@ -81,13 +81,18 @@ final class DetailViewController: NSViewController, WKUIDelegate {
// MARK: - Navigation
func focus() {
guard let window = currentWebViewController.webView.window else {
return
}
window.makeFirstResponderUnlessDescendantIsFirstResponder(currentWebViewController.webView)
}
// MARK: State Restoration
func saveState(to state: inout [AnyHashable : Any]) {
currentWebViewController.saveState(to: &state)
}
}
// MARK: - DetailWebViewControllerDelegate

View File

@ -24,6 +24,12 @@ final class DetailWebViewController: NSViewController {
var state: DetailState = .noSelection {
didSet {
if state != oldValue {
switch state {
case .article(_, let scrollY), .extracted(_, _, let scrollY):
windowScrollY = scrollY
default:
break
}
reloadHTML()
}
}
@ -31,9 +37,9 @@ final class DetailWebViewController: NSViewController {
var article: Article? {
switch state {
case .article(let article):
case .article(let article, _):
return article
case .extracted(let article, _):
case .extracted(let article, _, _):
return article
default:
return nil
@ -56,10 +62,21 @@ final class DetailWebViewController: NSViewController {
private let detailIconSchemeHandler = DetailIconSchemeHandler()
private var waitingForFirstReload = false
private let keyboardDelegate = DetailKeyboardDelegate()
private var windowScrollY: CGFloat?
private var isShowingExtractedArticle: Bool {
switch state {
case .extracted(_, _, _):
return true
default:
return false
}
}
private struct MessageName {
static let mouseDidEnter = "mouseDidEnter"
static let mouseDidExit = "mouseDidExit"
static let windowDidScroll = "windowDidScroll"
}
override func loadView() {
@ -73,6 +90,7 @@ final class DetailWebViewController: NSViewController {
configuration.setURLSchemeHandler(detailIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme)
let userContentController = WKUserContentController()
userContentController.add(self, name: MessageName.windowDidScroll)
userContentController.add(self, name: MessageName.mouseDidEnter)
userContentController.add(self, name: MessageName.mouseDidExit)
configuration.userContentController = userContentController
@ -116,6 +134,7 @@ final class DetailWebViewController: NSViewController {
NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(currentArticleThemeDidChangeNotification(_:)), name: .CurrentArticleThemeDidChangeNotification, object: nil)
webView.loadFileURL(ArticleRenderer.blank.url, allowingReadAccessTo: ArticleRenderer.blank.baseURL)
}
@ -136,11 +155,14 @@ final class DetailWebViewController: NSViewController {
@objc func userDefaultsDidChange(_ note: Notification) {
if articleTextSize != AppDefaults.shared.articleTextSize {
articleTextSize = AppDefaults.shared.articleTextSize
webView.evaluateJavaScript("updateTextSize(\"\(articleTextSize.cssClass)\");")
reloadHTMLMaintainingScrollPosition()
}
}
@objc func currentArticleThemeDidChangeNotification(_ note: Notification) {
reloadHTMLMaintainingScrollPosition()
}
// MARK: Media Functions
func stopMediaPlayback() {
@ -168,6 +190,14 @@ final class DetailWebViewController: NSViewController {
override func scrollPageUp(_ sender: Any?) {
webView.scrollPageUp(sender)
}
// MARK: State Restoration
func saveState(to state: inout [AnyHashable : Any]) {
state[UserInfoKey.isShowingExtractedArticle] = isShowingExtractedArticle
state[UserInfoKey.articleWindowScrollY] = windowScrollY
}
}
// MARK: - WKScriptMessageHandler
@ -175,10 +205,11 @@ final class DetailWebViewController: NSViewController {
extension DetailWebViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == MessageName.mouseDidEnter, let link = message.body as? String {
if message.name == MessageName.windowDidScroll {
windowScrollY = message.body as? CGFloat
} else if message.name == MessageName.mouseDidEnter, let link = message.body as? String {
delegate?.mouseDidEnter(self, link: link)
}
else if message.name == MessageName.mouseDidExit {
} else if message.name == MessageName.mouseDidExit {
delegate?.mouseDidExit(self)
}
}
@ -220,6 +251,11 @@ extension DetailWebViewController: WKNavigationDelegate, WKUIDelegate {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
webView.isHidden = false
}
} else {
if let windowScrollY = windowScrollY {
webView.evaluateJavaScript("window.scrollTo(0, \(windowScrollY));")
self.windowScrollY = nil
}
}
}
@ -253,26 +289,33 @@ private extension DetailWebViewController {
webView?.evaluateJavaScript("reloadArticleImage(\"\(imageSrc)\")")
}
}
func reloadHTMLMaintainingScrollPosition() {
fetchScrollInfo() { scrollInfo in
self.windowScrollY = scrollInfo?.offsetY
self.reloadHTML()
}
}
func reloadHTML() {
delegate?.mouseDidExit(self)
let style = ArticleStylesManager.shared.currentStyle
let theme = ArticleThemesManager.shared.currentTheme
let rendering: ArticleRenderer.Rendering
switch state {
case .noSelection:
rendering = ArticleRenderer.noSelectionHTML(style: style)
rendering = ArticleRenderer.noSelectionHTML(theme: theme)
case .multipleSelection:
rendering = ArticleRenderer.multipleSelectionHTML(style: style)
rendering = ArticleRenderer.multipleSelectionHTML(theme: theme)
case .loading:
rendering = ArticleRenderer.loadingHTML(style: style)
case .article(let article):
rendering = ArticleRenderer.loadingHTML(theme: theme)
case .article(let article, _):
detailIconSchemeHandler.currentArticle = article
rendering = ArticleRenderer.articleHTML(article: article, style: style)
case .extracted(let article, let extractedArticle):
rendering = ArticleRenderer.articleHTML(article: article, theme: theme)
case .extracted(let article, let extractedArticle, _):
detailIconSchemeHandler.currentArticle = article
rendering = ArticleRenderer.articleHTML(article: article, extractedArticle: extractedArticle, style: style)
rendering = ArticleRenderer.articleHTML(article: article, extractedArticle: extractedArticle, theme: theme)
}
let substitutions = [

View File

@ -1,4 +1,9 @@
// Add the mouse listeners for the above functions
function scrollDetection() {
window.onscroll = function(event) {
window.webkit.messageHandlers.windowDidScroll.postMessage(window.scrollY);
}
}
function linkHover() {
window.onmouseover = function(event) {
var closestAnchor = event.target.closest('a')
@ -15,5 +20,6 @@ function linkHover() {
}
function postRenderProcessing() {
linkHover()
scrollDetection();
linkHover();
}

View File

@ -1,54 +0,0 @@
body {
margin-top: 20px;
margin-bottom: 64px;
padding-left: 48px;
padding-right: 48px;
font-family: -apple-system;
}
.smallText {
font-size: 14px;
}
.mediumText {
font-size: 16px;
}
.largeText {
font-size: 18px;
}
.xlargeText {
font-size: 20px;
}
.xxlargeText {
font-size: 22px;
}
:root {
color-scheme: light dark;
--accent-color: rgba(8, 106, 238, 1);
--block-quote-border-color: rgba(8, 106, 238, .50);
}
@media(prefers-color-scheme: dark) {
:root {
--accent-color: rgba(94, 158, 244, 1);
--block-quote-border-color: rgba(94, 158, 244, .50);
--header-table-border-color: rgba(255, 255, 255, 0.1);
}
}
body a, body a:visited {
color: var(--accent-color);
}
pre {
border: 1px solid var(--accent-color);
padding: 10px;
}
.nnw-overflow table {
border: 1px solid var(--accent-color);
}

View File

@ -18,7 +18,9 @@ enum TimelineSourceMode {
class MainWindowController : NSWindowController, NSUserInterfaceValidations {
private var activityManager = ActivityManager()
@IBOutlet weak var articleThemePopUpButton: NSPopUpButton?
private var activityManager = ActivityManager()
private var isShowingExtractedArticle = false
private var articleExtractor: ArticleExtractor? = nil
@ -44,6 +46,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
private var timelineContainerViewController: TimelineContainerViewController?
private var detailViewController: DetailViewController?
private var currentSearchField: NSSearchField? = nil
private let articleThemeMenuToolbarItem = NSMenuToolbarItem(itemIdentifier: .articleThemeMenu)
private var searchString: String? = nil
private var lastSentSearchString: String? = nil
private var timelineSourceMode: TimelineSourceMode = .regular {
@ -53,6 +56,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
}
}
private var searchSmartFeed: SmartFeed? = nil
private var restoreArticleWindowScrollY: CGFloat?
// MARK: - NSWindowController
@ -61,6 +65,8 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
sharingServicePickerDelegate = SharingServicePickerDelegate(self.window)
updateArticleThemeMenu()
if #available(macOS 11.0, *) {
let toolbar = NSToolbar(identifier: "MainWindowToolbar")
toolbar.allowsUserCustomization = true
@ -98,6 +104,9 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(articleThemeNamesDidChangeNotification(_:)), name: .ArticleThemeNamesDidChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(currentArticleThemeDidChangeNotification(_:)), name: .CurrentArticleThemeDidChangeNotification, object: nil)
DispatchQueue.main.async {
self.updateWindowTitle()
}
@ -150,6 +159,14 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
updateWindowTitleIfNecessary(note.object)
}
@objc func articleThemeNamesDidChangeNotification(_ note: Notification) {
updateArticleThemeMenu()
}
@objc func currentArticleThemeDidChangeNotification(_ note: Notification) {
updateArticleThemeMenu()
}
private func updateWindowTitleIfNecessary(_ noteObject: Any?) {
if let folder = currentFeedOrFolder as? Folder, let noteObject = noteObject as? Folder {
@ -188,6 +205,14 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
public func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
if item.action == #selector(copyArticleURL(_:)) {
return canCopyArticleURL()
}
if item.action == #selector(copyExternalURL(_:)) {
return canCopyExternalURL()
}
if item.action == #selector(openArticleInBrowser(_:)) {
if let item = item as? NSMenuItem, item.keyEquivalentModifierMask.contains(.shift) {
item.title = Browser.titleForOpenInBrowserInverted
@ -286,6 +311,18 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
}
@IBAction func copyArticleURL(_ sender: Any?) {
if let link = oneSelectedArticle?.preferredURL?.absoluteString {
URLPasteboardWriter.write(urlString: link, to: .general)
}
}
@IBAction func copyExternalURL(_ sender: Any?) {
if let link = oneSelectedArticle?.externalLink {
URLPasteboardWriter.write(urlString: link, to: .general)
}
}
@IBAction func openArticleInBrowser(_ sender: Any?) {
if let link = currentLink {
Browser.open(link, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
@ -374,20 +411,20 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
articleExtractor?.cancel()
articleExtractor = nil
isShowingExtractedArticle = false
detailViewController?.setState(DetailState.article(article), mode: timelineSourceMode)
detailViewController?.setState(DetailState.article(article, nil), mode: timelineSourceMode)
return
}
guard !isShowingExtractedArticle else {
isShowingExtractedArticle = false
detailViewController?.setState(DetailState.article(article), mode: timelineSourceMode)
detailViewController?.setState(DetailState.article(article, nil), mode: timelineSourceMode)
return
}
if let articleExtractor = articleExtractor, let extractedArticle = articleExtractor.article {
if currentLink == articleExtractor.articleLink {
isShowingExtractedArticle = true
let detailState = DetailState.extracted(article, extractedArticle)
let detailState = DetailState.extracted(article, extractedArticle, nil)
detailViewController?.setState(detailState, mode: timelineSourceMode)
}
} else {
@ -506,6 +543,10 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
timelineContainerViewController?.toggleReadFilter()
}
@objc func selectArticleTheme(_ menuItem: NSMenuItem) {
ArticleThemesManager.shared.currentThemeName = menuItem.title
}
}
// MARK: NSWindowDelegate
@ -579,7 +620,8 @@ extension MainWindowController: TimelineContainerViewControllerDelegate {
detailState = .loading
startArticleExtractorForCurrentLink()
} else {
detailState = .article(articles.first!)
detailState = .article(articles.first!, restoreArticleWindowScrollY)
restoreArticleWindowScrollY = nil
}
} else {
detailState = .multipleSelection
@ -679,7 +721,8 @@ extension MainWindowController: ArticleExtractorDelegate {
func articleExtractionDidComplete(extractedArticle: ExtractedArticle) {
if let article = oneSelectedArticle, articleExtractor?.state != .cancelled {
isShowingExtractedArticle = true
let detailState = DetailState.extracted(article, extractedArticle)
let detailState = DetailState.extracted(article, extractedArticle, restoreArticleWindowScrollY)
restoreArticleWindowScrollY = nil
detailViewController?.setState(detailState, mode: timelineSourceMode)
makeToolbarValidate()
}
@ -726,6 +769,7 @@ extension NSToolbarItem.Identifier {
static let readerView = NSToolbarItem.Identifier("readerView")
static let openInBrowser = NSToolbarItem.Identifier("openInBrowser")
static let share = NSToolbarItem.Identifier("share")
static let articleThemeMenu = NSToolbarItem.Identifier("articleThemeMenu")
static let cleanUp = NSToolbarItem.Identifier("cleanUp")
}
@ -795,6 +839,13 @@ extension MainWindowController: NSToolbarDelegate {
let title = NSLocalizedString("Open in Browser", comment: "Open in Browser")
return buildToolbarButton(.openInBrowser, title, AppAssets.openInBrowserImage, "openArticleInBrowser:")
case .articleThemeMenu:
articleThemeMenuToolbarItem.image = AppAssets.articleTheme
let description = NSLocalizedString("Article Theme", comment: "Article Theme")
articleThemeMenuToolbarItem.toolTip = description
articleThemeMenuToolbarItem.label = description
return articleThemeMenuToolbarItem
case .search:
let toolbarItem = NSSearchToolbarItem(itemIdentifier: .search)
let description = NSLocalizedString("Search", comment: "Search")
@ -832,6 +883,7 @@ extension MainWindowController: NSToolbarDelegate {
.readerView,
.openInBrowser,
.share,
.articleThemeMenu,
.search,
.cleanUp
]
@ -994,6 +1046,7 @@ private extension MainWindowController {
saveSplitViewState(to: &state)
sidebarViewController?.saveState(to: &state)
timelineContainerViewController?.saveState(to: &state)
detailViewController?.saveState(to: &state)
return state
}
@ -1002,11 +1055,30 @@ private extension MainWindowController {
window?.toggleFullScreen(self)
}
restoreSplitViewState(from: state)
sidebarViewController?.restoreState(from: state)
let articleWindowScrollY = state[UserInfoKey.articleWindowScrollY] as? CGFloat
restoreArticleWindowScrollY = articleWindowScrollY
timelineContainerViewController?.restoreState(from: state)
let isShowingExtractedArticle = state[UserInfoKey.isShowingExtractedArticle] as? Bool ?? false
if isShowingExtractedArticle {
restoreArticleWindowScrollY = articleWindowScrollY
startArticleExtractorForCurrentLink()
}
}
// MARK: - Command Validation
func canCopyArticleURL() -> Bool {
return currentLink != nil
}
func canCopyExternalURL() -> Bool {
return oneSelectedArticle?.externalLink != nil && oneSelectedArticle?.externalLink != currentLink
}
func canGoToNextUnread(wrappingToTop wrapping: Bool = false) -> Bool {
@ -1365,27 +1437,50 @@ private extension MainWindowController {
let menu = NSMenu()
let newWebFeedItem = NSMenuItem()
newWebFeedItem.title = NSLocalizedString("New Web Feed", comment: "New Web Feed")
newWebFeedItem.title = NSLocalizedString("New Web Feed", comment: "New Web Feed")
newWebFeedItem.action = Selector(("showAddWebFeedWindow:"))
menu.addItem(newWebFeedItem)
let newRedditFeedItem = NSMenuItem()
newRedditFeedItem.title = NSLocalizedString("New Reddit Feed", comment: "New Reddit Feed")
newRedditFeedItem.title = NSLocalizedString("New Reddit Feed", comment: "New Reddit Feed")
newRedditFeedItem.action = Selector(("showAddRedditFeedWindow:"))
menu.addItem(newRedditFeedItem)
let newTwitterFeedItem = NSMenuItem()
newTwitterFeedItem.title = NSLocalizedString("New Twitter Feed", comment: "New Twitter Feed")
newTwitterFeedItem.title = NSLocalizedString("New Twitter Feed", comment: "New Twitter Feed")
newTwitterFeedItem.action = Selector(("showAddTwitterFeedWindow:"))
menu.addItem(newTwitterFeedItem)
let newFolderFeedItem = NSMenuItem()
newFolderFeedItem.title = NSLocalizedString("New Folder", comment: "New Folder")
newFolderFeedItem.title = NSLocalizedString("New Folder", comment: "New Folder")
newFolderFeedItem.action = Selector(("showAddFolderWindow:"))
menu.addItem(newFolderFeedItem)
return menu
}
func updateArticleThemeMenu() {
let articleThemeMenu = NSMenu()
let defaultThemeItem = NSMenuItem()
defaultThemeItem.title = ArticleTheme.defaultTheme.name
defaultThemeItem.action = #selector(selectArticleTheme(_:))
defaultThemeItem.state = defaultThemeItem.title == ArticleThemesManager.shared.currentThemeName ? .on : .off
articleThemeMenu.addItem(defaultThemeItem)
articleThemeMenu.addItem(NSMenuItem.separator())
for themeName in ArticleThemesManager.shared.themeNames {
let themeItem = NSMenuItem()
themeItem.title = themeName
themeItem.action = #selector(selectArticleTheme(_:))
themeItem.state = themeItem.title == ArticleThemesManager.shared.currentThemeName ? .on : .off
articleThemeMenu.addItem(themeItem)
}
articleThemeMenuToolbarItem.menu = articleThemeMenu
articleThemePopUpButton?.menu = articleThemeMenu
}
}

View File

@ -25,7 +25,7 @@ extension Article: PasteboardWriterOwner {
static let articleUTIInternalType = NSPasteboard.PasteboardType(rawValue: articleUTIInternal)
private lazy var renderedHTML: String = {
let rendering = ArticleRenderer.articleHTML(article: article, style: ArticleStylesManager.shared.currentStyle)
let rendering = ArticleRenderer.articleHTML(article: article, theme: ArticleThemesManager.shared.currentTheme)
return rendering.html
}()
@ -87,11 +87,11 @@ private extension ArticlePasteboardWriter {
s += "\(convertedHTML)\n\n"
}
if let url = article.url {
s += "URL: \(url)\n\n"
if let link = article.link {
s += "URL: \(link)\n\n"
}
if let externalURL = article.externalURL {
s += "external URL: \(externalURL)\n\n"
if let externalLink = article.externalLink {
s += "external URL: \(externalLink)\n\n"
}
s += "Date: \(article.logicalDatePublished)\n\n"
@ -151,10 +151,10 @@ private extension ArticlePasteboardWriter {
d[Key.title] = article.title ?? nil
d[Key.contentHTML] = article.contentHTML ?? nil
d[Key.contentText] = article.contentText ?? nil
d[Key.url] = article.url ?? nil
d[Key.externalURL] = article.externalURL ?? nil
d[Key.url] = article.rawLink ?? nil
d[Key.externalURL] = article.rawExternalLink ?? nil
d[Key.summary] = article.summary ?? nil
d[Key.imageURL] = article.imageURL ?? nil
d[Key.imageURL] = article.rawImageLink ?? nil
d[Key.datePublished] = article.datePublished ?? nil
d[Key.dateModified] = article.dateModified ?? nil
d[Key.dateArrived] = article.status.dateArrived

View File

@ -90,6 +90,13 @@ extension TimelineViewController {
}
Browser.open(urlString, inBackground: false)
}
@objc func copyURLFromContextualMenu(_ sender: Any?) {
guard let menuItem = sender as? NSMenuItem, let urlString = menuItem.representedObject as? String else {
return
}
URLPasteboardWriter.write(urlString: urlString, to: .general)
}
@objc func performShareServiceFromContextualMenu(_ sender: Any?) {
guard let menuItem = sender as? NSMenuItem, let sharingCommandInfo = menuItem.representedObject as? SharingCommandInfo else {
@ -168,6 +175,12 @@ private extension TimelineViewController {
if articles.count == 1, let link = articles.first!.preferredLink {
menu.addSeparatorIfNeeded()
menu.addItem(openInBrowserMenuItem(link))
menu.addSeparatorIfNeeded()
menu.addItem(copyArticleURLMenuItem(link))
if let externalLink = articles.first?.externalLink, externalLink != link {
menu.addItem(copyExternalURLMenuItem(externalLink))
}
}
if let sharingMenu = shareMenu(for: articles) {
@ -260,6 +273,15 @@ private extension TimelineViewController {
return menuItem(NSLocalizedString("Open in Browser", comment: "Command"), #selector(openInBrowserFromContextualMenu(_:)), urlString)
}
func copyArticleURLMenuItem(_ urlString: String) -> NSMenuItem {
return menuItem(NSLocalizedString("Copy Article URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), urlString)
}
func copyExternalURLMenuItem(_ urlString: String) -> NSMenuItem {
return menuItem(NSLocalizedString("Copy External URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), urlString)
}
func menuItem(_ title: String, _ action: Selector, _ representedObject: Any) -> NSMenuItem {

View File

@ -15,7 +15,8 @@ final class GeneralPreferencesViewController: NSViewController {
private var userNotificationSettings: UNNotificationSettings?
@IBOutlet var defaultBrowserPopup: NSPopUpButton!
@IBOutlet weak var articleThemePopup: NSPopUpButton!
@IBOutlet weak var defaultBrowserPopup: NSPopUpButton!
public override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
@ -39,15 +40,32 @@ final class GeneralPreferencesViewController: NSViewController {
updateUI()
}
@objc func articleThemeNamesDidChangeNotification(_ note: Notification) {
updateArticleThemePopup()
}
// MARK: - Actions
@IBAction func showThemesFolder(_ sender: Any) {
let url = URL(fileURLWithPath: ArticleThemesManager.shared.folderPath)
NSWorkspace.shared.open(url)
}
@IBAction func articleThemePopUpDidChange(_ sender: Any) {
guard let menuItem = articleThemePopup.selectedItem else {
return
}
ArticleThemesManager.shared.currentThemeName = menuItem.title
updateArticleThemePopup()
}
@IBAction func browserPopUpDidChangeValue(_ sender: Any?) {
guard let menuItem = defaultBrowserPopup.selectedItem else {
return
}
let bundleID = menuItem.representedObject as? String
AppDefaults.shared.defaultBrowserID = bundleID
updateUI()
updateBrowserPopup()
}
}
@ -58,15 +76,29 @@ private extension GeneralPreferencesViewController {
func commonInit() {
NotificationCenter.default.addObserver(self, selector: #selector(applicationWillBecomeActive(_:)), name: NSApplication.willBecomeActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(articleThemeNamesDidChangeNotification(_:)), name: .ArticleThemeNamesDidChangeNotification, object: nil)
}
func updateUI() {
updateArticleThemePopup()
updateBrowserPopup()
}
func updateArticleThemePopup() {
let menu = articleThemePopup.menu!
menu.removeAllItems()
menu.addItem(NSMenuItem(title: ArticleTheme.defaultTheme.name, action: nil, keyEquivalent: ""))
menu.addItem(NSMenuItem.separator())
func registerAppWithBundleID(_ bundleID: String) {
NSWorkspace.shared.setDefaultAppBundleID(forURLScheme: "feed", to: bundleID)
NSWorkspace.shared.setDefaultAppBundleID(forURLScheme: "feeds", to: bundleID)
for themeName in ArticleThemesManager.shared.themeNames {
menu.addItem(NSMenuItem(title: themeName, action: nil, keyEquivalent: ""))
}
articleThemePopup.selectItem(withTitle: ArticleThemesManager.shared.currentThemeName)
if articleThemePopup.indexOfSelectedItem == -1 {
articleThemePopup.selectItem(withTitle: ArticleTheme.defaultTheme.name)
}
}
func updateBrowserPopup() {

View File

@ -25,7 +25,7 @@ NewsBlur syncing: {\field{\*\fldinst{HYPERLINK "https://twitter.com/quanganhdo"}
Under-the-hood magic and CSS stylin\'92s: {\field{\*\fldinst{HYPERLINK "https://github.com/wevah"}}{\fldrslt Nate Weaver}}\
Newsfoot (JS footnote displayer): {\field{\*\fldinst{HYPERLINK "https://github.com/brehaut/"}}{\fldrslt Andrew Brehaut}}\
Help book: {\field{\*\fldinst{HYPERLINK "https://nostodnayr.net/"}}{\fldrslt Ryan Dotson}}\
And featuring contributions from {\field{\*\fldinst{HYPERLINK "https://github.com/danielpunkass"}}{\fldrslt Daniel Jalkut}}, {\field{\*\fldinst{HYPERLINK "https://rhonabwy.com/"}}{\fldrslt Joe Heck}}, {\field{\*\fldinst{HYPERLINK "https://github.com/olofhellman"}}{\fldrslt Olof Hellman}}, {\field{\*\fldinst{HYPERLINK "https://blog.rizwan.dev/"}}{\fldrslt Rizwan Mohamed Ibrahim}}, {\field{\*\fldinst{HYPERLINK "https://stuartbreckenridge.com/"}}{\fldrslt Stuart Breckenridge}}, {\field{\*\fldinst{HYPERLINK "https://twitter.com/philviso"}}{\fldrslt Phil Viso}}, and {\field{\*\fldinst{HYPERLINK "https://github.com/Ranchero-Software/NetNewsWire/graphs/contributors"}}{\fldrslt many more}}!\
And featuring contributions from {\field{\*\fldinst{HYPERLINK "https://github.com/danielpunkass"}}{\fldrslt Daniel Jalkut}}, {\field{\*\fldinst{HYPERLINK "https://rhonabwy.com/"}}{\fldrslt Joe Heck}}, {\field{\*\fldinst{HYPERLINK "https://github.com/olofhellman"}}{\fldrslt Olof Hellman}}, {\field{\*\fldinst{HYPERLINK "https://blog.rizwan.dev/"}}{\fldrslt Rizwan Mohamed Ibrahim}}, {\field{\*\fldinst{HYPERLINK "https://mynameisstuart.com/"}}{\fldrslt Stuart Breckenridge}}, {\field{\*\fldinst{HYPERLINK "https://twitter.com/philviso"}}{\fldrslt Phil Viso}}, and {\field{\*\fldinst{HYPERLINK "https://github.com/Ranchero-Software/NetNewsWire/graphs/contributors"}}{\fldrslt many more}}!\
\
\pard\pardeftab720\sa60\partightenfactor0
@ -49,4 +49,4 @@ And featuring contributions from {\field{\*\fldinst{HYPERLINK "https://github.co
\pard\pardeftab720\li360\sa60\partightenfactor0
\f1\b0 \cf2 NetNewsWire 6 is dedicated to everyone working to save democracy around the world.\
}
}

View File

@ -31,6 +31,7 @@
<string>RSS Feed</string>
<key>CFBundleURLSchemes</key>
<array>
<string>netnewswire</string>
<string>feed</string>
<string>feeds</string>
<string>x-netnewswire-feed</string>
@ -74,5 +75,46 @@
<string>https://ranchero.com/downloads/netnewswire-release.xml</string>
<key>UserAgent</key>
<string>NetNewsWire (RSS Reader; https://netnewswire.com/)</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>nnwtheme</string>
</array>
<key>CFBundleTypeName</key>
<string>NetNewsWire Theme</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSItemContentTypes</key>
<array>
<string>com.ranchero.netnewswire.theme</string>
</array>
<key>LSTypeIsPackage</key>
<true/>
</dict>
</array>
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>com.apple.package</string>
</array>
<key>UTTypeDescription</key>
<string>NetNewsWire Theme</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>com.ranchero.netnewswire.theme</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>nnwtheme</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>

View File

@ -18,6 +18,7 @@
import Foundation
import Articles
import Zip
protocol AppDelegateAppleEvents {
func installAppleEventHandlers()
@ -44,6 +45,34 @@ extension AppDelegate : AppDelegateAppleEvents {
return
}
// Handle themes
if urlString.hasPrefix("netnewswire://theme") {
guard let comps = URLComponents(string: urlString),
let queryItems = comps.queryItems,
let themeURLString = queryItems.first(where: { $0.name == "url" })?.value else {
return
}
if let themeURL = URL(string: themeURLString) {
let request = URLRequest(url: themeURL)
let task = URLSession.shared.downloadTask(with: request) { location, response, error in
guard let location = location else {
return
}
do {
try ArticleThemeDownloader.shared.handleFile(at: location)
} catch {
NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error])
}
}
task.resume()
}
return
}
// Special case URL with specific scheme handler x-netnewswire-feed: intended to ensure we open
// it regardless of which news reader may be set as the default
let nnwScheme = "x-netnewswire-feed:"

View File

@ -57,17 +57,17 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
@objc(url)
var url:String? {
return article.url ?? article.externalURL
return article.preferredLink
}
@objc(permalink)
var permalink:String? {
return article.url
return article.link
}
@objc(externalUrl)
var externalUrl:String? {
return article.externalURL
return article.externalLink
}
@objc(title)
@ -132,7 +132,7 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
@objc(imageURL)
var imageURL:String {
return article.imageURL ?? ""
return article.imageLink ?? ""
}
@objc(authors)

View File

@ -1,167 +0,0 @@
//
// FixAccountCredentialView.swift
// NetNewsWire
//
// Created by Stuart Breckenridge on 24/7/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
struct FixAccountCredentialView: View {
let accountSyncError: AccountSyncError
@Environment(\.presentationMode) var presentationMode
@StateObject private var editModel = EditAccountCredentialsModel()
var body: some View {
#if os(macOS)
MacForm
.onAppear {
editModel.retrieveCredentials(accountSyncError.account)
}
.onChange(of: editModel.accountCredentialsWereUpdated) { value in
if value == true {
presentationMode.wrappedValue.dismiss()
}
}
.alert(isPresented: $editModel.showError) {
Alert(title: Text("Error Adding Account"),
message: Text(editModel.error.description),
dismissButton: .default(Text("Dismiss"),
action: {
editModel.error = .none
}))
}
.frame(idealWidth: 300, idealHeight: 200, alignment: .top)
.padding()
#else
iOSForm
.onAppear {
editModel.retrieveCredentials(accountSyncError.account)
}
.onChange(of: editModel.accountCredentialsWereUpdated) { value in
if value == true {
presentationMode.wrappedValue.dismiss()
}
}
.alert(isPresented: $editModel.showError) {
Alert(title: Text("Error Adding Account"),
message: Text(editModel.error.description),
dismissButton: .default(Text("Dismiss"),
action: {
editModel.error = .none
}))
}
#endif
}
var MacForm: some View {
Form {
header
HStack(alignment: .center) {
VStack(alignment: .trailing, spacing: 12) {
Text("Username: ")
Text("Password: ")
if accountSyncError.account.type == .freshRSS {
Text("API URL: ")
}
}.frame(width: 75)
VStack(alignment: .leading, spacing: 12) {
accountFields
}
}
.textFieldStyle(RoundedBorderTextFieldStyle())
Spacer()
HStack{
if editModel.accountIsUpdatingCredentials {
ProgressView("Updating")
}
Spacer()
cancelButton
updateButton
}
}.frame(height: 220)
}
#if os(iOS)
var iOSForm: some View {
NavigationView {
List {
Section(header: header, content: {
accountFields
})
}
.listStyle(InsetGroupedListStyle())
.navigationBarItems(
leading:
cancelButton
, trailing:
HStack {
if editModel.accountIsUpdatingCredentials {
ProgressView()
.frame(width: 20 , height: 20)
.padding(.horizontal, 4)
}
updateButton
}
)
}
}
#endif
var header: some View {
HStack {
Spacer()
VStack {
Image(rsImage: accountSyncError.account.smallIcon!.image)
.resizable()
.frame(width: 30, height: 30)
Text(accountSyncError.account.nameForDisplay)
Text(accountSyncError.error.localizedDescription)
.multilineTextAlignment(.center)
.lineLimit(3)
.padding(.top, 4)
}
Spacer()
}.padding()
}
@ViewBuilder
var accountFields: some View {
TextField("Username", text: $editModel.userName)
SecureField("Password", text: $editModel.password)
if accountSyncError.account.type == .freshRSS {
TextField("API URL", text: $editModel.apiUrl)
}
}
@ViewBuilder
var updateButton: some View {
if accountSyncError.account.type != .freshRSS {
Button("Update", action: {
editModel.updateAccountCredentials(accountSyncError.account)
}).disabled(editModel.userName.count == 0 || editModel.password.count == 0)
} else {
Button("Update", action: {
editModel.updateAccountCredentials(accountSyncError.account)
}).disabled(editModel.userName.count == 0 || editModel.password.count == 0 || editModel.apiUrl.count == 0)
}
}
var cancelButton: some View {
Button("Cancel", action: {
presentationMode.wrappedValue.dismiss()
})
}
}

View File

@ -1,68 +0,0 @@
//
// AddAccountSignUp.swift
// NetNewsWire
//
// Created by Stuart Breckenridge on 06/12/2020.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import Account
#if os(iOS)
import UIKit
#endif
/// Helper functions common to most account services.
protocol AddAccountSignUp {
func presentSignUpOption(_ accountType: AccountType)
}
extension AddAccountSignUp {
func presentSignUpOption(_ accountType: AccountType) {
#if os(macOS)
switch accountType {
case .bazQux:
NSWorkspace.shared.open(URL(string: "https://bazqux.com")!)
case .feedbin:
NSWorkspace.shared.open(URL(string: "https://feedbin.com/signup")!)
case .feedly:
NSWorkspace.shared.open(URL(string: "https://feedly.com")!)
case .feedWrangler:
NSWorkspace.shared.open(URL(string: "https://feedwrangler.net/users/new")!)
case .freshRSS:
NSWorkspace.shared.open(URL(string: "https://freshrss.org")!)
case .inoreader:
NSWorkspace.shared.open(URL(string: "https://www.inoreader.com")!)
case .newsBlur:
NSWorkspace.shared.open(URL(string: "https://newsblur.com")!)
case .theOldReader:
NSWorkspace.shared.open(URL(string: "https://theoldreader.com")!)
default:
return
}
#else
switch accountType {
case .bazQux:
UIApplication.shared.open(URL(string: "https://bazqux.com")!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly : false], completionHandler: nil)
case .feedbin:
UIApplication.shared.open(URL(string: "https://feedbin.com/signup")!, options: [:], completionHandler: nil)
case .feedly:
UIApplication.shared.open(URL(string: "https://feedly.com")!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly : false], completionHandler: nil)
case .feedWrangler:
UIApplication.shared.open(URL(string: "https://feedwrangler.net/users/new")!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly : false], completionHandler: nil)
case .freshRSS:
UIApplication.shared.open(URL(string: "https://freshrss.org")!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly : false], completionHandler: nil)
case .inoreader:
UIApplication.shared.open(URL(string: "https://www.inoreader.com")!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly : false], completionHandler: nil)
case .newsBlur:
UIApplication.shared.open(URL(string: "https://newsblur.com")!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly : false], completionHandler: nil)
case .theOldReader:
UIApplication.shared.open(URL(string: "https://theoldreader.com")!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly : false], completionHandler: nil)
default:
return
}
#endif
}
}

View File

@ -1,70 +0,0 @@
//
// AddFeedWranglerViewModel.swift
// Multiplatform macOS
//
// Created by Stuart Breckenridge on 05/12/2020.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
import RSCore
import RSWeb
import Secrets
class AddFeedWranglerViewModel: ObservableObject, AddAccountSignUp {
@Published var isAuthenticating: Bool = false
@Published var accountUpdateError: AccountUpdateErrors = .none
@Published var showError: Bool = false
@Published var username: String = ""
@Published var password: String = ""
@Published var canDismiss: Bool = false
@Published var showPassword: Bool = false
func authenticateFeedWrangler() {
isAuthenticating = true
let credentials = Credentials(type: .feedWranglerBasic, username: username, secret: password)
Account.validateCredentials(type: .feedWrangler, credentials: credentials) { result in
self.isAuthenticating = false
switch result {
case .success(let validatedCredentials):
guard let validatedCredentials = validatedCredentials else {
self.accountUpdateError = .invalidUsernamePassword
self.showError = true
return
}
let account = AccountManager.shared.createAccount(type: .feedWrangler)
do {
try account.removeCredentials(type: .feedWranglerBasic)
try account.removeCredentials(type: .feedWranglerToken)
try account.storeCredentials(credentials)
try account.storeCredentials(validatedCredentials)
self.canDismiss = true
account.refreshAll(completion: { result in
switch result {
case .success:
break
case .failure(let error):
self.accountUpdateError = .other(error: error)
self.showError = true
}
})
} catch {
self.accountUpdateError = .keyChainError
self.showError = true
}
case .failure:
self.accountUpdateError = .networkError
self.showError = true
}
}
}
}

View File

@ -1,68 +0,0 @@
//
// AddFeedbinViewModel.swift
// Multiplatform macOS
//
// Created by Stuart Breckenridge on 05/12/2020.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
import RSCore
import RSWeb
import Secrets
class AddFeedbinViewModel: ObservableObject, AddAccountSignUp {
@Published var isAuthenticating: Bool = false
@Published var accountUpdateError: AccountUpdateErrors = .none
@Published var showError: Bool = false
@Published var username: String = ""
@Published var password: String = ""
@Published var canDismiss: Bool = false
@Published var showPassword: Bool = false
func authenticateFeedbin() {
isAuthenticating = true
let credentials = Credentials(type: .basic, username: username, secret: password)
Account.validateCredentials(type: .feedbin, credentials: credentials) { result in
self.isAuthenticating = false
switch result {
case .success(let validatedCredentials):
guard let validatedCredentials = validatedCredentials else {
self.accountUpdateError = .invalidUsernamePassword
self.showError = true
return
}
let account = AccountManager.shared.createAccount(type: .feedbin)
do {
try account.removeCredentials(type: .basic)
try account.storeCredentials(validatedCredentials)
self.isAuthenticating = false
self.canDismiss = true
account.refreshAll(completion: { result in
switch result {
case .success:
break
case .failure(let error):
self.accountUpdateError = .other(error: error)
self.showError = true
}
})
} catch {
self.accountUpdateError = .keyChainError
self.showError = true
}
case .failure:
self.accountUpdateError = .networkError
self.showError = true
}
}
}
}

View File

@ -1,67 +0,0 @@
//
// AddFeedlyViewModel.swift
// Multiplatform macOS
//
// Created by Stuart Breckenridge on 05/12/2020.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
import RSCore
import RSWeb
import Secrets
class AddFeedlyViewModel: ObservableObject, OAuthAccountAuthorizationOperationDelegate, AddAccountSignUp {
@Published var isAuthenticating: Bool = false
@Published var accountUpdateError: AccountUpdateErrors = .none
@Published var showError: Bool = false
@Published var username: String = ""
@Published var password: String = ""
func authenticateFeedly() {
isAuthenticating = true
let addAccount = OAuthAccountAuthorizationOperation(accountType: .feedly)
addAccount.delegate = self
#if os(macOS)
addAccount.presentationAnchor = NSApplication.shared.windows.last
#else
addAccount.presentationAnchor = UIApplication.shared.windows.last
#endif
MainThreadOperationQueue.shared.add(addAccount)
}
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account) {
isAuthenticating = false
// macOS only: `ASWebAuthenticationSession` leaves the browser in the foreground.
// Ensure the app is in the foreground so the user can see their Feedly account load.
#if os(macOS)
NSApplication.shared.activate(ignoringOtherApps: true)
#endif
account.refreshAll { [weak self] result in
switch result {
case .success:
break
case .failure(let error):
self?.accountUpdateError = .other(error: error)
self?.showError = true
}
}
}
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) {
isAuthenticating = false
// macOS only: `ASWebAuthenticationSession` leaves the browser in the foreground.
// Ensure the app is in the foreground so the user can see the error.
#if os(macOS)
NSApplication.shared.activate(ignoringOtherApps: true)
#endif
accountUpdateError = .other(error: error)
showError = true
}
}

View File

@ -1,71 +0,0 @@
//
// AddNewsBlurViewModel.swift
// Multiplatform macOS
//
// Created by Stuart Breckenridge on 05/12/2020.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
import RSCore
import RSWeb
import Secrets
class AddNewsBlurViewModel: ObservableObject, AddAccountSignUp {
@Published var isAuthenticating: Bool = false
@Published var accountUpdateError: AccountUpdateErrors = .none
@Published var showError: Bool = false
@Published var username: String = ""
@Published var password: String = ""
@Published var canDismiss: Bool = false
@Published var showPassword: Bool = false
func authenticateNewsBlur() {
isAuthenticating = true
let credentials = Credentials(type: .newsBlurBasic, username: username, secret: password)
Account.validateCredentials(type: .newsBlur, credentials: credentials) { result in
self.isAuthenticating = false
switch result {
case .success(let validatedCredentials):
guard let validatedCredentials = validatedCredentials else {
self.accountUpdateError = .invalidUsernamePassword
self.showError = true
return
}
let account = AccountManager.shared.createAccount(type: .newsBlur)
do {
try account.removeCredentials(type: .newsBlurBasic)
try account.removeCredentials(type: .newsBlurSessionId)
try account.storeCredentials(credentials)
try account.storeCredentials(validatedCredentials)
self.canDismiss = true
account.refreshAll(completion: { result in
switch result {
case .success:
break
case .failure(let error):
self.accountUpdateError = .other(error: error)
self.showError = true
}
})
} catch {
self.accountUpdateError = .keyChainError
self.showError = true
}
case .failure:
self.accountUpdateError = .networkError
self.showError = true
}
}
}
}

View File

@ -1,122 +0,0 @@
//
// AddReaderAPIViewModel.swift
// Multiplatform macOS
//
// Created by Stuart Breckenridge on 05/12/2020.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
import RSCore
import RSWeb
import Secrets
class AddReaderAPIViewModel: ObservableObject, AddAccountSignUp {
@Published var isAuthenticating: Bool = false
@Published var accountUpdateError: AccountUpdateErrors = .none
@Published var showError: Bool = false
@Published var username: String = ""
@Published var password: String = ""
@Published var apiUrl: String = ""
@Published var canDismiss: Bool = false
@Published var showPassword: Bool = false
func authenticateReaderAccount(_ accountType: AccountType) {
isAuthenticating = true
let credentials = Credentials(type: .readerBasic, username: username, secret: password)
if accountType == .freshRSS {
Account.validateCredentials(type: accountType, credentials: credentials, endpoint: URL(string: apiUrl)!) { result in
self.isAuthenticating = false
switch result {
case .success(let validatedCredentials):
guard let validatedCredentials = validatedCredentials else {
self.accountUpdateError = .invalidUsernamePassword
self.showError = true
return
}
let account = AccountManager.shared.createAccount(type: .freshRSS)
do {
try account.removeCredentials(type: .readerBasic)
try account.removeCredentials(type: .readerAPIKey)
try account.storeCredentials(credentials)
try account.storeCredentials(validatedCredentials)
self.canDismiss = true
account.refreshAll(completion: { result in
switch result {
case .success:
break
case .failure(let error):
self.accountUpdateError = .other(error: error)
self.showError = true
}
})
} catch {
self.accountUpdateError = .keyChainError
self.showError = true
}
case .failure:
self.accountUpdateError = .networkError
self.showError = true
}
}
}
else {
Account.validateCredentials(type: accountType, credentials: credentials) { result in
self.isAuthenticating = false
switch result {
case .success(let validatedCredentials):
guard let validatedCredentials = validatedCredentials else {
self.accountUpdateError = .invalidUsernamePassword
self.showError = true
return
}
let account = AccountManager.shared.createAccount(type: .freshRSS)
do {
try account.removeCredentials(type: .readerBasic)
try account.removeCredentials(type: .readerAPIKey)
try account.storeCredentials(credentials)
try account.storeCredentials(validatedCredentials)
self.canDismiss = true
account.refreshAll(completion: { result in
switch result {
case .success:
break
case .failure(let error):
self.accountUpdateError = .other(error: error)
self.showError = true
}
})
} catch {
self.accountUpdateError = .keyChainError
self.showError = true
}
case .failure:
self.accountUpdateError = .networkError
self.showError = true
}
}
}
}
}

View File

@ -1,134 +0,0 @@
//
// AddCloudKitAccountView.swift
// Multiplatform macOS
//
// Created by Stuart Breckenridge on 03/12/2020.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
struct AddCloudKitAccountView: View {
@Environment (\.presentationMode) var presentationMode
var body: some View {
#if os(macOS)
macBody
#else
NavigationView {
iosBody
}
#endif
}
#if os(iOS)
var iosBody: some View {
List {
Section(header: formHeader, footer: formFooter, content: {
Button(action: {
_ = AccountManager.shared.createAccount(type: .cloudKit)
presentationMode.wrappedValue.dismiss()
}, label: {
HStack {
Spacer()
Text("Add Account")
Spacer()
}
}).disabled(AccountManager.shared.activeAccounts.filter({ $0.type == .cloudKit }).count > 0)
})
}.navigationBarItems(leading:
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
})
)
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(Text(AccountType.cloudKit.localizedAccountName()))
.listStyle(InsetGroupedListStyle())
}
#endif
#if os(macOS)
var macBody: some View {
VStack {
HStack(spacing: 16) {
VStack(alignment: .leading) {
AccountType.cloudKit.image()
.resizable()
.frame(width: 50, height: 50)
Spacer()
}
VStack(alignment: .leading, spacing: 8) {
Text("Sign in to your iCloud account.")
.font(.headline)
Text("This account syncs across your Mac and iOS devices using your iCloud account.")
.foregroundColor(.secondary)
.font(.callout)
.lineLimit(2)
.padding(.top, 4)
Spacer()
HStack(spacing: 8) {
Spacer()
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
.frame(width: 60)
}).keyboardShortcut(.cancelAction)
Button(action: {
_ = AccountManager.shared.createAccount(type: .cloudKit)
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Create")
.frame(width: 60)
})
.keyboardShortcut(.defaultAction)
.disabled(AccountManager.shared.activeAccounts.filter({ $0.type == .cloudKit }).count > 0)
}
}
}
}
.padding()
.frame(minWidth: 400, maxWidth: 400, maxHeight: 150)
}
#endif
var formHeader: some View {
HStack {
Spacer()
VStack(alignment: .center) {
AccountType.cloudKit.image()
.resizable()
.frame(width: 50, height: 50)
}
Spacer()
}.padding(.vertical)
}
var formFooter: some View {
HStack {
Spacer()
VStack(spacing: 8) {
Text("This account syncs across your Mac and iOS devices using your iCloud account.").foregroundColor(.secondary)
}
.multilineTextAlignment(.center)
.font(.caption)
Spacer()
}.padding(.vertical)
}
}
struct AddCloudKitAccountView_Previews: PreviewProvider {
static var previews: some View {
AddCloudKitAccountView()
}
}

View File

@ -1,216 +0,0 @@
//
// AddFeedWranglerAccountView.swift
// Multiplatform macOS
//
// Created by Stuart Breckenridge on 03/12/2020.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
import RSCore
import RSWeb
import Secrets
struct AddFeedWranglerAccountView: View {
@Environment (\.presentationMode) var presentationMode
@StateObject private var model = AddFeedWranglerViewModel()
var body: some View {
#if os(macOS)
macBody
#else
NavigationView {
iosBody
}
#endif
}
#if os(iOS)
var iosBody: some View {
List {
Section(header: formHeader, content: {
TextField("Email", text: $model.username)
if model.showPassword == false {
ZStack {
HStack {
SecureField("Password", text: $model.password)
Spacer()
Image(systemName: "eye.fill")
.foregroundColor(.accentColor)
.onTapGesture {
model.showPassword = true
}
}
}
}
else {
ZStack {
HStack {
TextField("Password", text: $model.password)
Spacer()
Image(systemName: "eye.slash.fill")
.foregroundColor(.accentColor)
.onTapGesture {
model.showPassword = false
}
}
}
}
})
Section(footer: formFooter, content: {
Button(action: {
model.authenticateFeedWrangler()
}, label: {
HStack {
Spacer()
Text("Add Account")
Spacer()
}
}).disabled(model.username.isEmpty || model.password.isEmpty)
})
}
.navigationBarItems(leading:
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
}))
.listStyle(InsetGroupedListStyle())
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(Text("Feed Wrangler"))
.alert(isPresented: $model.showError, content: {
Alert(title: Text("Sign In Error"), message: Text(model.accountUpdateError.description), dismissButton: .cancel(Text("Dismiss")))
})
.onReceive(model.$canDismiss, perform: { value in
if value == true {
presentationMode.wrappedValue.dismiss()
}
})
}
#endif
#if os(macOS)
var macBody: some View {
VStack {
HStack(spacing: 16) {
VStack(alignment: .leading) {
AccountType.feedWrangler.image()
.resizable()
.frame(width: 50, height: 50)
Spacer()
}
VStack(alignment: .leading, spacing: 8) {
Text("Sign in to your Feed Wrangler account.")
.font(.headline)
HStack {
Text("Dont have a Feed Wrangler account?")
.font(.callout)
Button(action: {
model.presentSignUpOption(.feedWrangler)
}, label: {
Text("Sign up here.").font(.callout)
}).buttonStyle(LinkButtonStyle())
}
HStack {
VStack(alignment: .trailing, spacing: 14) {
Text("Email")
Text("Password")
}
VStack(spacing: 8) {
TextField("me@email.com", text: $model.username)
SecureField("•••••••••••", text: $model.password)
}
}
Text("Your username and password will be encrypted and stored in Keychain.")
.foregroundColor(.secondary)
.font(.callout)
.lineLimit(2)
.padding(.top, 4)
Spacer()
HStack(spacing: 8) {
Spacer()
ProgressView()
.scaleEffect(CGSize(width: 0.5, height: 0.5))
.hidden(!model.isAuthenticating)
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
.frame(width: 60)
}).keyboardShortcut(.cancelAction)
Button(action: {
model.authenticateFeedWrangler()
}, label: {
Text("Sign In")
.frame(width: 60)
})
.keyboardShortcut(.defaultAction)
.disabled(model.username.isEmpty || model.password.isEmpty)
}
}
}
}
.padding()
.frame(minWidth: 400, maxWidth: 400, minHeight: 230, maxHeight: 260)
.textFieldStyle(RoundedBorderTextFieldStyle())
.alert(isPresented: $model.showError, content: {
Alert(title: Text("Sign In Error"), message: Text(model.accountUpdateError.description), dismissButton: .cancel())
})
.onReceive(model.$canDismiss, perform: { value in
if value == true {
presentationMode.wrappedValue.dismiss()
}
})
}
#endif
var formHeader: some View {
HStack {
Spacer()
VStack(alignment: .center) {
AccountType.feedWrangler.image()
.resizable()
.frame(width: 50, height: 50)
}
Spacer()
}.padding(.vertical)
}
var formFooter: some View {
HStack {
Spacer()
VStack(spacing: 8) {
Text("Sign in to your Feed Wrangler account and sync your feeds across your devices. Your username and password and password will be encrypted and stored in Keychain.").foregroundColor(.secondary)
Text("Dont have a Feed Wrangler account?").foregroundColor(.secondary)
Button(action: {
model.presentSignUpOption(.feedWrangler)
}, label: {
Text("Sign Up Here").foregroundColor(.blue).multilineTextAlignment(.center)
})
ProgressView().hidden(!model.isAuthenticating)
}
.multilineTextAlignment(.center)
.font(.caption2)
Spacer()
}.padding(.vertical)
}
}
struct AddFeedWranglerAccountView_Previews: PreviewProvider {
static var previews: some View {
AddFeedWranglerAccountView()
}
}

View File

@ -1,214 +0,0 @@
//
// AddFeedbinAccountView.swift
// Multiplatform macOS
//
// Created by Stuart Breckenridge on 02/12/2020.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
import RSCore
import RSWeb
import Secrets
struct AddFeedbinAccountView: View {
@Environment (\.presentationMode) var presentationMode
@StateObject private var model = AddFeedbinViewModel()
var body: some View {
#if os(macOS)
macBody
#else
NavigationView {
iosBody
}
#endif
}
#if os(iOS)
var iosBody: some View {
List {
Section(header: formHeader, content: {
TextField("Email", text: $model.username)
if model.showPassword == false {
ZStack {
HStack {
SecureField("Password", text: $model.password)
Spacer()
Image(systemName: "eye.fill")
.foregroundColor(.accentColor)
.onTapGesture {
model.showPassword = true
}
}
}
}
else {
ZStack {
HStack {
TextField("Password", text: $model.password)
Spacer()
Image(systemName: "eye.slash.fill")
.foregroundColor(.accentColor)
.onTapGesture {
model.showPassword = false
}
}
}
}
})
Section(footer: formFooter, content: {
Button(action: {
model.authenticateFeedbin()
}, label: {
HStack {
Spacer()
Text("Add Account")
Spacer()
}
}).disabled(model.username.isEmpty || model.password.isEmpty)
})
}
.navigationBarItems(leading:
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
}))
.listStyle(InsetGroupedListStyle())
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(Text("Feedbin"))
.alert(isPresented: $model.showError, content: {
Alert(title: Text("Sign In Error"), message: Text(model.accountUpdateError.description), dismissButton: .cancel(Text("Dismiss")))
})
.onReceive(model.$canDismiss, perform: { value in
if value == true {
presentationMode.wrappedValue.dismiss()
}
})
}
#endif
#if os(macOS)
var macBody: some View {
VStack {
HStack(spacing: 16) {
VStack(alignment: .leading) {
AccountType.feedbin.image()
.frame(width: 50, height: 50)
Spacer()
}
VStack(alignment: .leading, spacing: 8) {
Text("Sign in to your Feedbin account.")
.font(.headline)
HStack {
Text("Dont have a Feedbin account?")
.font(.callout)
Button(action: {
model.presentSignUpOption(.feedbin)
}, label: {
Text("Sign up here.").font(.callout)
}).buttonStyle(LinkButtonStyle())
}
HStack {
VStack(alignment: .trailing, spacing: 14) {
Text("Email")
Text("Password")
}
VStack(spacing: 8) {
TextField("me@email.com", text: $model.username)
SecureField("•••••••••••", text: $model.password)
}
}
Text("Your username and password will be encrypted and stored in Keychain.")
.foregroundColor(.secondary)
.font(.callout)
.lineLimit(2)
.padding(.top, 4)
Spacer()
HStack(spacing: 8) {
Spacer()
ProgressView()
.scaleEffect(CGSize(width: 0.5, height: 0.5))
.hidden(!model.isAuthenticating)
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
.frame(width: 60)
}).keyboardShortcut(.cancelAction)
Button(action: {
model.authenticateFeedbin()
}, label: {
Text("Sign In")
.frame(width: 60)
})
.keyboardShortcut(.defaultAction)
.disabled(model.username.isEmpty || model.password.isEmpty)
}
}
}
}
.padding()
.frame(minWidth: 400, maxWidth: 400, minHeight: 230, maxHeight: 260)
.textFieldStyle(RoundedBorderTextFieldStyle())
.alert(isPresented: $model.showError, content: {
Alert(title: Text("Sign In Error"), message: Text(model.accountUpdateError.description), dismissButton: .cancel())
})
.onReceive(model.$canDismiss, perform: { value in
if value == true {
presentationMode.wrappedValue.dismiss()
}
})
}
#endif
var formHeader: some View {
HStack {
Spacer()
VStack(alignment: .center) {
AccountType.feedbin.image()
.resizable()
.frame(width: 50, height: 50)
}
Spacer()
}.padding(.vertical)
}
var formFooter: some View {
HStack {
Spacer()
VStack(spacing: 8) {
Text("Sign in to your Feedbin account and sync your feeds across your devices. Your username and password and password will be encrypted and stored in Keychain.").foregroundColor(.secondary)
Text("Dont have a Feedbin account?").foregroundColor(.secondary)
Button(action: {
model.presentSignUpOption(.feedbin)
}, label: {
Text("Sign Up Here").foregroundColor(.blue).multilineTextAlignment(.center)
})
ProgressView().hidden(!model.isAuthenticating)
}
.multilineTextAlignment(.center)
.font(.caption2)
Spacer()
}.padding(.vertical)
}
}
struct AddFeedbinAccountView_Previews: PreviewProvider {
static var previews: some View {
AddFeedbinAccountView()
}
}

View File

@ -1,149 +0,0 @@
//
// AddFeedlyAccountView.swift
// Multiplatform macOS
//
// Created by Stuart Breckenridge on 05/12/2020.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
import RSCore
import RSWeb
import Secrets
struct AddFeedlyAccountView: View {
@Environment (\.presentationMode) var presentationMode
@StateObject private var model = AddFeedlyViewModel()
var body: some View {
#if os(macOS)
macBody
#else
NavigationView {
iosBody
}
#endif
}
#if os(iOS)
var iosBody: some View {
List {
Section(header: formHeader, footer: formFooter, content: {
Button(action: {
model.authenticateFeedly()
}, label: {
HStack {
Spacer()
Text("Add Account")
Spacer()
}
})
})
}.navigationBarItems(leading:
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
})
)
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(Text(AccountType.feedly.localizedAccountName()))
.listStyle(InsetGroupedListStyle())
}
#endif
#if os(macOS)
var macBody: some View {
VStack {
HStack(spacing: 16) {
VStack(alignment: .leading) {
AccountType.feedly.image()
.resizable()
.frame(width: 50, height: 50)
Spacer()
}
VStack(alignment: .leading, spacing: 8) {
Text("Sign in to your Feedly account.")
.font(.headline)
HStack {
Text("Dont have a Feedly account?")
.font(.callout)
Button(action: {
model.presentSignUpOption(.feedly)
}, label: {
Text("Sign up here.").font(.callout)
}).buttonStyle(LinkButtonStyle())
}
Spacer()
HStack(spacing: 8) {
Spacer()
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
.frame(width: 60)
}).keyboardShortcut(.cancelAction)
Button(action: {
model.authenticateFeedly()
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Sign In")
.frame(width: 60)
})
.keyboardShortcut(.defaultAction)
.disabled(AccountManager.shared.activeAccounts.filter({ $0.type == .cloudKit }).count > 0)
}
}
}
}
.padding()
.frame(minWidth: 400, maxWidth: 400, maxHeight: 150)
.alert(isPresented: $model.showError, content: {
Alert(title: Text("Sign In Error"), message: Text(model.accountUpdateError.description), dismissButton: .cancel())
})
}
#endif
var formHeader: some View {
HStack {
Spacer()
VStack(alignment: .center) {
AccountType.feedly.image()
.resizable()
.frame(width: 50, height: 50)
}
Spacer()
}.padding(.vertical)
}
var formFooter: some View {
HStack {
Spacer()
VStack(spacing: 8) {
Text("Sign in to your Feedly account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDont have an Feedly account?").foregroundColor(.secondary)
Button(action: {
model.presentSignUpOption(.feedly)
}, label: {
Text("Sign Up Here").foregroundColor(.blue).multilineTextAlignment(.center)
})
}
.multilineTextAlignment(.center)
.font(.caption)
Spacer()
}.padding(.vertical)
}
}
struct AddFeedlyAccountView_Previews: PreviewProvider {
static var previews: some View {
AddFeedlyAccountView()
}
}

View File

@ -1,140 +0,0 @@
//
// AddLocalAccountView.swift
// Multiplatform macOS
//
// Created by Stuart Breckenridge on 02/12/2020.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
import RSCore
struct AddLocalAccountView: View {
@State private var newAccountName: String = ""
@Environment (\.presentationMode) var presentationMode
var body: some View {
#if os(macOS)
macBody
#else
NavigationView {
iosBody
}
#endif
}
#if os(iOS)
var iosBody: some View {
List {
Section(header: formHeader, content: {
TextField("Account Name", text: $newAccountName)
})
Section(footer: formFooter, content: {
Button(action: {
let newAccount = AccountManager.shared.createAccount(type: .onMyMac)
newAccount.name = newAccountName
presentationMode.wrappedValue.dismiss()
}, label: {
HStack {
Spacer()
Text("Add Account")
Spacer()
}
})
})
}.navigationBarItems(leading:
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
})
)
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(Text(AccountType.onMyMac.localizedAccountName()))
.listStyle(InsetGroupedListStyle())
}
#endif
#if os(macOS)
var macBody: some View {
VStack {
HStack(spacing: 16) {
VStack(alignment: .leading) {
AccountType.onMyMac.image()
.resizable()
.frame(width: 50, height: 50)
Spacer()
}
VStack(alignment: .leading, spacing: 8) {
Text("Create a local account on your Mac.")
.font(.headline)
Text("Local accounts store their data on your Mac. They do not sync across your devices.")
.font(.callout)
.foregroundColor(.secondary)
HStack {
Text("Name: ")
TextField("Account Name", text: $newAccountName)
}.padding(.top, 8)
Spacer()
HStack(spacing: 8) {
Spacer()
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
.frame(width: 60)
}).keyboardShortcut(.cancelAction)
Button(action: {
let newAccount = AccountManager.shared.createAccount(type: .onMyMac)
newAccount.name = newAccountName
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Create")
.frame(width: 60)
}).keyboardShortcut(.defaultAction)
}
}
}
}
.padding()
.frame(minWidth: 400, maxWidth: 400, minHeight: 230, maxHeight: 260)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
#endif
var formHeader: some View {
HStack {
Spacer()
VStack(alignment: .center) {
AccountType.onMyMac.image()
.resizable()
.frame(width: 50, height: 50)
}
Spacer()
}.padding(.vertical)
}
var formFooter: some View {
HStack {
Spacer()
VStack(spacing: 8) {
Text("Local accounts do not sync your feeds across devices.").foregroundColor(.secondary)
}
.multilineTextAlignment(.center)
.font(.caption)
Spacer()
}.padding(.vertical)
}
}
struct AddLocalAccount_Previews: PreviewProvider {
static var previews: some View {
AddLocalAccountView()
}
}

View File

@ -1,212 +0,0 @@
//
// AddNewsBlurAccountView.swift
// Multiplatform macOS
//
// Created by Stuart Breckenridge on 03/12/2020.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
import RSCore
import RSWeb
import Secrets
struct AddNewsBlurAccountView: View {
@Environment (\.presentationMode) var presentationMode
@StateObject private var model = AddNewsBlurViewModel()
var body: some View {
#if os(macOS)
macBody
#else
NavigationView {
iosBody
}
#endif
}
#if os(iOS)
var iosBody: some View {
List {
Section(header: formHeader, content: {
TextField("Email", text: $model.username)
if model.showPassword == false {
ZStack {
HStack {
SecureField("Password", text: $model.password)
Spacer()
Image(systemName: "eye.fill")
.foregroundColor(.accentColor)
.onTapGesture {
model.showPassword = true
}
}
}
}
else {
ZStack {
HStack {
TextField("Password", text: $model.password)
Spacer()
Image(systemName: "eye.slash.fill")
.foregroundColor(.accentColor)
.onTapGesture {
model.showPassword = false
}
}
}
}
})
Section(footer: formFooter, content: {
Button(action: {
model.authenticateNewsBlur()
}, label: {
HStack {
Spacer()
Text("Add Account")
Spacer()
}
}).disabled(model.username.isEmpty || model.password.isEmpty)
})
}
.navigationBarItems(leading:
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
}))
.listStyle(InsetGroupedListStyle())
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(Text("NewsBlur"))
.alert(isPresented: $model.showError, content: {
Alert(title: Text("Sign In Error"), message: Text(model.accountUpdateError.description), dismissButton: .cancel(Text("Dismiss")))
})
.onReceive(model.$canDismiss, perform: { value in
if value == true {
presentationMode.wrappedValue.dismiss()
}
})
}
#endif
#if os(macOS)
var macBody: some View {
VStack {
HStack(spacing: 16) {
VStack(alignment: .leading) {
AccountType.newsBlur.image()
.frame(width: 50, height: 50)
Spacer()
}
VStack(alignment: .leading, spacing: 8) {
Text("Sign in to your NewsBlur account.")
.font(.headline)
HStack {
Text("Dont have a NewsBlur account?")
.font(.callout)
Button(action: {
model.presentSignUpOption(.newsBlur)
}, label: {
Text("Sign up here.").font(.callout)
}).buttonStyle(LinkButtonStyle())
}
HStack {
VStack(alignment: .trailing, spacing: 14) {
Text("Email")
Text("Password")
}
VStack(spacing: 8) {
TextField("me@email.com", text: $model.username)
SecureField("•••••••••••", text: $model.password)
}
}
Text("Your username and password will be encrypted and stored in Keychain.")
.foregroundColor(.secondary)
.font(.callout)
.lineLimit(2)
.padding(.top, 4)
Spacer()
HStack(spacing: 8) {
Spacer()
ProgressView()
.scaleEffect(CGSize(width: 0.5, height: 0.5))
.hidden(!model.isAuthenticating)
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
.frame(width: 60)
}).keyboardShortcut(.cancelAction)
Button(action: {
model.authenticateNewsBlur()
}, label: {
Text("Sign In")
.frame(width: 60)
})
.keyboardShortcut(.defaultAction)
.disabled(model.username.isEmpty || model.password.isEmpty)
}
}
}
}
.padding()
.frame(minWidth: 400, maxWidth: 400, minHeight: 230, maxHeight: 260)
.textFieldStyle(RoundedBorderTextFieldStyle())
.alert(isPresented: $model.showError, content: {
Alert(title: Text("Sign In Error"), message: Text(model.accountUpdateError.description), dismissButton: .cancel())
})
.onReceive(model.$canDismiss, perform: { value in
if value == true {
presentationMode.wrappedValue.dismiss()
}
})
}
#endif
var formHeader: some View {
HStack {
Spacer()
VStack(alignment: .center) {
AccountType.newsBlur.image()
.resizable()
.frame(width: 50, height: 50)
}
Spacer()
}.padding(.vertical)
}
var formFooter: some View {
HStack {
Spacer()
VStack(spacing: 8) {
Text("Sign in to your NewsBlur account and sync your feeds across your devices. Your username and password and password will be encrypted and stored in Keychain.").foregroundColor(.secondary)
Text("Dont have a NewsBlur account?").foregroundColor(.secondary)
Button(action: {
model.presentSignUpOption(.newsBlur)
}, label: {
Text("Sign Up Here").foregroundColor(.blue).multilineTextAlignment(.center)
})
ProgressView().hidden(!model.isAuthenticating)
}
.multilineTextAlignment(.center)
.font(.caption2)
Spacer()
}.padding(.vertical)
}
}
struct AddNewsBlurAccountView_Previews: PreviewProvider {
static var previews: some View {
AddNewsBlurAccountView()
}
}

View File

@ -1,247 +0,0 @@
//
// AddReaderAPIAccountView.swift
// Multiplatform macOS
//
// Created by Stuart Breckenridge on 03/12/2020.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
import RSCore
import RSWeb
import Secrets
struct AddReaderAPIAccountView: View {
@Environment (\.presentationMode) var presentationMode
@StateObject private var model = AddReaderAPIViewModel()
public var accountType: AccountType
var body: some View {
#if os(macOS)
macBody
#else
NavigationView {
iosBody
}
#endif
}
#if os(iOS)
var iosBody: some View {
List {
Section(header: formHeader, content: {
TextField("Email", text: $model.username)
if model.showPassword == false {
ZStack {
HStack {
SecureField("Password", text: $model.password)
Spacer()
Image(systemName: "eye.fill")
.foregroundColor(.accentColor)
.onTapGesture {
model.showPassword = true
}
}
}
}
else {
ZStack {
HStack {
TextField("Password", text: $model.password)
Spacer()
Image(systemName: "eye.slash.fill")
.foregroundColor(.accentColor)
.onTapGesture {
model.showPassword = false
}
}
}
}
if accountType == .freshRSS {
TextField("API URL", text: $model.apiUrl)
}
})
Section(footer: formFooter, content: {
Button(action: {
model.authenticateReaderAccount(accountType)
}, label: {
HStack {
Spacer()
Text("Add Account")
Spacer()
}
}).disabled(createDisabled())
})
}
.navigationBarItems(leading:
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
}))
.listStyle(InsetGroupedListStyle())
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(Text(accountType.localizedAccountName()))
.alert(isPresented: $model.showError, content: {
Alert(title: Text("Sign In Error"), message: Text(model.accountUpdateError.description), dismissButton: .cancel(Text("Dismiss")))
})
.onReceive(model.$canDismiss, perform: { value in
if value == true {
presentationMode.wrappedValue.dismiss()
}
})
}
#endif
#if os(macOS)
var macBody: some View {
VStack {
HStack(spacing: 16) {
VStack(alignment: .leading) {
accountType.image()
.resizable()
.frame(width: 50, height: 50)
Spacer()
}
VStack(alignment: .leading, spacing: 8) {
Text("Sign in to your \(accountType.localizedAccountName()) account.")
.font(.headline)
HStack {
if accountType == .freshRSS {
Text("Dont have a \(accountType.localizedAccountName()) instance?")
.font(.callout)
} else {
Text("Dont have an \(accountType.localizedAccountName()) account?")
.font(.callout)
}
Button(action: {
model.presentSignUpOption(accountType)
}, label: {
Text(accountType == .freshRSS ? "Find out more." : "Sign up here.").font(.callout)
}).buttonStyle(LinkButtonStyle())
}
HStack {
VStack(alignment: .trailing, spacing: 14) {
Text("Email")
Text("Password")
if accountType == .freshRSS {
Text("API URL")
}
}
VStack(spacing: 8) {
TextField("me@email.com", text: $model.username)
SecureField("•••••••••••", text: $model.password)
if accountType == .freshRSS {
TextField("https://myfreshrss.rocks", text: $model.apiUrl)
}
}
}
Text("Your username and password will be encrypted and stored in Keychain.")
.foregroundColor(.secondary)
.font(.callout)
.lineLimit(2)
.padding(.top, 4)
Spacer()
HStack(spacing: 8) {
Spacer()
ProgressView()
.scaleEffect(CGSize(width: 0.5, height: 0.5))
.hidden(!model.isAuthenticating)
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
.frame(width: 60)
}).keyboardShortcut(.cancelAction)
Button(action: {
model.authenticateReaderAccount(accountType)
}, label: {
Text("Sign In")
.frame(width: 60)
})
.keyboardShortcut(.defaultAction)
.disabled(createDisabled())
}
}
}
}
.padding()
.frame(width: 400, height: height())
.textFieldStyle(RoundedBorderTextFieldStyle())
.alert(isPresented: $model.showError, content: {
Alert(title: Text("Sign In Error"), message: Text(model.accountUpdateError.description), dismissButton: .cancel())
})
.onReceive(model.$canDismiss, perform: { value in
if value == true {
presentationMode.wrappedValue.dismiss()
}
})
}
#endif
func createDisabled() -> Bool {
if accountType == .freshRSS {
return model.username.isEmpty || model.password.isEmpty || !model.apiUrl.mayBeURL
}
return model.username.isEmpty || model.password.isEmpty
}
func height() -> CGFloat {
if accountType == .freshRSS {
return 260
}
return 230
}
var formHeader: some View {
HStack {
Spacer()
VStack(alignment: .center) {
accountType.image()
.resizable()
.frame(width: 50, height: 50)
}
Spacer()
}.padding(.vertical)
}
var formFooter: some View {
HStack {
Spacer()
VStack(spacing: 8) {
Text("Sign in to your \(accountType.localizedAccountName()) account and sync your feeds across your devices. Your username and password and password will be encrypted and stored in Keychain.").foregroundColor(.secondary)
Text("Dont have a \(accountType.localizedAccountName()) instance?").foregroundColor(.secondary)
Button(action: {
model.presentSignUpOption(accountType)
}, label: {
Text("Sign Up Here").foregroundColor(.blue).multilineTextAlignment(.center)
})
ProgressView().hidden(!model.isAuthenticating)
}
.multilineTextAlignment(.center)
.font(.caption2)
Spacer()
}.padding(.vertical)
}
}
struct AddReaderAPIAccountView_Previews: PreviewProvider {
static var previews: some View {
AddReaderAPIAccountView(accountType: .freshRSS)
//AddReaderAPIAccountView(accountType: .inoreader)
}
}

View File

@ -1,51 +0,0 @@
//
// AddFolderModel.swift
// NetNewsWire
//
// Created by Alex Faber on 04/07/2020.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import Account
import RSCore
import SwiftUI
class AddFolderModel: ObservableObject {
@Published var shouldDismiss: Bool = false
@Published var folderName: String = ""
@Published var selectedAccountIndex: Int = 0
@Published var accounts: [Account] = []
@Published var showError: Bool = false
@Published var showProgressIndicator: Bool = false
init() {
for account in
AccountManager.shared.sortedActiveAccounts{
accounts.append(account)
}
}
func addFolder() {
let account = accounts[selectedAccountIndex]
showProgressIndicator = true
account.addFolder(folderName){ result in
self.showProgressIndicator = false
switch result {
case .success(_):
self.shouldDismiss = true
case .failure(let error):
print("Error")
print(error)
}
}
}
}

View File

@ -1,125 +0,0 @@
//
// AddFolderView.swift
// NetNewsWire
//
// Created by Alex Faber on 04/07/2020.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
import RSCore
struct AddFolderView: View {
@ObservedObject private var viewModel = AddFolderModel()
@Binding var isPresented: Bool
var body: some View {
#if os(iOS)
iosForm
.onReceive(viewModel.$shouldDismiss, perform: {
dismiss in
if dismiss == true {
isPresented = false
}
})
#else
macForm
.onReceive(viewModel.$shouldDismiss, perform: { dismiss in
if dismiss == true {
isPresented = false
}
})
#endif
}
#if os(iOS)
var iosForm: some View {
NavigationView {
Form {
Section {
TextField("Name", text: $viewModel.folderName)
}
Section {
accountPicker
}
}
.navigationTitle("Add Folder")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(
leading:Button("Cancel", action: {
isPresented = false
}
)
.help("Cancel Adding Folder"),
trailing:Button("Add", action: {
viewModel.addFolder()
}
)
.disabled(viewModel.folderName.isEmpty)
.help("Save Adding Folder")
)
}
}
#endif
#if os(macOS)
var macForm: some View {
Form {
HStack {
Spacer()
Image(rsImage: AppAssets.faviconTemplateImage)
.resizable()
.renderingMode(.template)
.frame(width: 30, height: 30)
Text("Add a Folder")
.font(.title)
Spacer()
}
LazyVGrid(columns: [GridItem(.fixed(75), spacing: 10, alignment: .trailing),GridItem(.fixed(400), spacing: 0, alignment: .leading) ], alignment: .leading, spacing: 10, pinnedViews: [], content:{
Text("Name:").bold()
TextField("Name", text: $viewModel.folderName)
.textFieldStyle(RoundedBorderTextFieldStyle())
.help("The name of the folder you want to create")
Text("Account:").bold()
accountPicker
.help("Pick the account you want to create a folder in.")
})
buttonStack
}
.frame(maxWidth: 485)
.padding(12)
}
#endif
var accountPicker: some View {
Picker("Account:", selection: $viewModel.selectedAccountIndex, content: {
ForEach(0..<viewModel.accounts.count, id: \.self, content: { index in
Text("\(viewModel.accounts[index].account?.nameForDisplay ?? "")").tag(index)
})
})
}
var buttonStack: some View {
HStack {
if viewModel.showProgressIndicator == true {
ProgressView()
.frame(width: 25, height: 25)
.help("Adding Folder")
}
Spacer()
Button("Cancel", action: {
isPresented = false
})
.help("Cancel Adding Folder")
Button("Add", action: {
viewModel.addFolder()
})
.disabled(viewModel.folderName.isEmpty)
.help("Add Folder")
}
}
}

View File

@ -1,144 +0,0 @@
//
// AddWebFeedModel.swift
// NetNewsWire
//
// Created by Stuart Breckenridge on 4/7/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import Account
import RSCore
import SwiftUI
enum AddWebFeedError: LocalizedError {
case none, alreadySubscribed, initialDownload, noFeeds
var errorDescription: String? {
switch self {
case .alreadySubscribed:
return NSLocalizedString("Cant add this feed because youve already subscribed to it.", comment: "Feed finder")
case .initialDownload:
return NSLocalizedString("Cant add this feed because of a download error.", comment: "Feed finder")
case .noFeeds:
return NSLocalizedString("Cant add a feed because no feed was found.", comment: "Feed finder")
default:
return nil
}
}
}
class AddWebFeedModel: ObservableObject {
@Published var shouldDismiss: Bool = false
@Published var providedURL: String = ""
@Published var providedName: String = ""
@Published var selectedFolderIndex: Int = 0
@Published var addFeedError: AddWebFeedError? {
didSet {
addFeedError != AddWebFeedError.none ? (showError = true) : (showError = false)
}
}
@Published var showError: Bool = false
@Published var containers: [Container] = []
@Published var showProgressIndicator: Bool = false
init() {
for account in AccountManager.shared.sortedActiveAccounts {
containers.append(account)
if let sortedFolders = account.sortedFolders {
containers.append(contentsOf: sortedFolders)
}
}
}
func pasteUrlFromPasteboard() {
guard let stringFromPasteboard = urlStringFromPasteboard, stringFromPasteboard.mayBeURL else {
return
}
providedURL = stringFromPasteboard
}
#if os(macOS)
var urlStringFromPasteboard: String? {
if let urlString = NSPasteboard.urlString(from: NSPasteboard.general) {
return urlString.normalizedURL
}
return nil
}
#else
var urlStringFromPasteboard: String? {
if let urlString = UIPasteboard.general.url?.absoluteString {
return urlString.normalizedURL
}
return nil
}
#endif
struct AccountAndFolderSpecifier {
let account: Account
let folder: Folder?
}
func accountAndFolderFromContainer(_ container: Container) -> AccountAndFolderSpecifier? {
if let account = container as? Account {
return AccountAndFolderSpecifier(account: account, folder: nil)
}
if let folder = container as? Folder, let account = folder.account {
return AccountAndFolderSpecifier(account: account, folder: folder)
}
return nil
}
func addWebFeed() {
if let account = accountAndFolderFromContainer(containers[selectedFolderIndex])?.account {
showProgressIndicator = true
let normalizedURLString = providedURL.normalizedURL
guard !normalizedURLString.isEmpty, let url = URL(string: normalizedURLString) else {
showProgressIndicator = false
return
}
let container = containers[selectedFolderIndex]
if account.hasWebFeed(withURL: normalizedURLString) {
addFeedError = .alreadySubscribed
showProgressIndicator = false
return
}
account.createWebFeed(url: url.absoluteString, name: providedName, container: container, validateFeed: true, completion: { [weak self] result in
self?.showProgressIndicator = false
switch result {
case .success(let feed):
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.webFeed: feed])
self?.shouldDismiss = true
case .failure(let error):
switch error {
case AccountError.createErrorAlreadySubscribed:
self?.addFeedError = .alreadySubscribed
return
case AccountError.createErrorNotFound:
self?.addFeedError = .noFeeds
return
default:
print("Error")
}
}
})
}
}
func smallIconImage(for container: Container) -> RSImage? {
if let smallIconProvider = container as? SmallIconProvider {
return smallIconProvider.smallIcon?.image
}
return nil
}
}

View File

@ -1,209 +0,0 @@
//
// AddWebFeedView.swift
// NetNewsWire
//
// Created by Stuart Breckenridge on 3/7/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
import RSCore
struct AddWebFeedView: View {
@StateObject private var viewModel = AddWebFeedModel()
@Binding var isPresented: Bool
var body: some View {
#if os(iOS)
iosForm
.onAppear {
viewModel.pasteUrlFromPasteboard()
}
.onReceive(viewModel.$shouldDismiss, perform: { dismiss in
if dismiss == true {
isPresented = false
}
})
#else
macForm
.onAppear {
viewModel.pasteUrlFromPasteboard()
}.alert(isPresented: $viewModel.showError) {
Alert(title: Text("Oops"),
message: Text(viewModel.addFeedError!.localizedDescription),
dismissButton: Alert.Button.cancel({
viewModel.addFeedError = AddWebFeedError.none
}))
}
.onChange(of: viewModel.shouldDismiss, perform: { dismiss in
if dismiss == true {
isPresented = false
}
})
#endif
}
#if os(macOS)
var macForm: some View {
Form {
HStack {
Spacer()
Image(rsImage: AppAssets.faviconTemplateImage)
.resizable()
.renderingMode(.template)
.frame(width: 30, height: 30)
Text("Add a Web Feed")
.font(.title)
Spacer()
}.padding()
LazyVGrid(columns: [GridItem(.fixed(75), spacing: 10, alignment: .trailing),GridItem(.fixed(400), spacing: 0, alignment: .leading) ], alignment: .leading, spacing: 10, pinnedViews: [], content:{
Text("URL:").bold()
urlTextField
.textFieldStyle(RoundedBorderTextFieldStyle())
.help("The URL of the feed you want to add.")
Text("Name:").bold()
providedNameTextField
.textFieldStyle(RoundedBorderTextFieldStyle())
.help("The name of the feed. (Optional.)")
Text("Folder:").bold()
folderPicker
.help("Pick the folder you want to add the feed to.")
})
buttonStack
}
.frame(maxWidth: 485)
.padding(12)
}
#endif
#if os(iOS)
var iosForm: some View {
NavigationView {
List {
urlTextField
providedNameTextField
folderPicker
}
.listStyle(InsetGroupedListStyle())
.navigationBarTitle("Add Web Feed")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading:
Button("Cancel", action: {
isPresented = false
})
.help("Cancel Add Feed")
, trailing:
HStack(spacing: 12) {
if viewModel.showProgressIndicator == true {
ProgressView()
}
Button("Add", action: {
viewModel.addWebFeed()
})
.disabled(!viewModel.providedURL.mayBeURL)
.help("Add Feed")
}
)
}
}
#endif
@ViewBuilder var urlTextField: some View {
#if os(iOS)
TextField("URL", text: $viewModel.providedURL)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
#else
TextField("URL", text: $viewModel.providedURL)
.disableAutocorrection(true)
#endif
}
var providedNameTextField: some View {
TextField("Title (Optional)", text: $viewModel.providedName)
}
@ViewBuilder var folderPicker: some View {
#if os(iOS)
Picker("Folder", selection: $viewModel.selectedFolderIndex, content: {
ForEach(0..<viewModel.containers.count, id: \.self, content: { position in
if let containerName = (viewModel.containers[position] as? DisplayNameProvider)?.nameForDisplay {
if viewModel.containers[position] is Folder {
HStack(alignment: .top) {
if let image = viewModel.smallIconImage(for: viewModel.containers[position]) {
Image(rsImage: image)
.foregroundColor(Color("AccentColor"))
}
Text("\(containerName)")
.tag(position)
}.padding(.leading, 16)
} else {
HStack(alignment: .top) {
if let image = viewModel.smallIconImage(for: viewModel.containers[position]) {
Image(rsImage: image)
.foregroundColor(Color("AccentColor"))
}
Text(containerName)
.tag(position)
}
}
}
})
})
#else
Picker("", selection: $viewModel.selectedFolderIndex, content: {
ForEach(0..<viewModel.containers.count, id: \.self, content: { index in
if let containerName = (viewModel.containers[index] as? DisplayNameProvider)?.nameForDisplay {
if viewModel.containers[index] is Folder {
HStack {
if let image = viewModel.smallIconImage(for: viewModel.containers[index]) {
Image(rsImage: image)
}
Text("\(containerName)")
}
.padding(.leading, 2)
.tag(index)
} else {
Text(containerName)
.padding(.leading, 2)
.tag(index)
}
}
})
})
.padding(.leading, -8)
#endif
}
var buttonStack: some View {
HStack {
if viewModel.showProgressIndicator == true {
ProgressView()
.frame(width: 25, height: 25)
.help("Adding Feed")
}
Spacer()
Button("Cancel", action: {
isPresented = false
})
.help("Cancel Add Feed")
Button("Add", action: {
viewModel.addWebFeed()
})
.disabled(!viewModel.providedURL.mayBeURL)
.help("Add Feed")
}
.padding(.trailing, 2)
}
}

View File

@ -1,362 +0,0 @@
//
// AppAssets.swift
// NetNewsWire
//
// Created by Maurice Parker on 6/27/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
import RSCore
import Account
struct AppAssets {
#if os(iOS)
static var accentColor: UIColor! = {
return UIColor(named: "AccentColor")
}()
#endif
static var accountBazQux: RSImage! = {
return RSImage(named: "accountBazQux")
}()
static var accountLocalMacImage: RSImage! = {
return RSImage(named: "accountLocalMac")
}()
static var accountLocalPadImage: RSImage = {
return RSImage(named: "accountLocalPad")!
}()
static var accountLocalPhoneImage: RSImage = {
return RSImage(named: "accountLocalPhone")!
}()
static var accountCloudKitImage: RSImage = {
return RSImage(named: "accountCloudKit")!
}()
static var accountFeedbinImage: RSImage = {
return RSImage(named: "accountFeedbin")!
}()
static var accountFeedlyImage: RSImage = {
return RSImage(named: "accountFeedly")!
}()
static var accountFeedWranglerImage: RSImage = {
return RSImage(named: "accountFeedWrangler")!
}()
static var accountFreshRSSImage: RSImage = {
return RSImage(named: "accountFreshRSS")!
}()
static var accountInoreader: RSImage! = {
return RSImage(named: "accountInoreader")
}()
static var accountNewsBlurImage: RSImage = {
return RSImage(named: "accountNewsBlur")!
}()
static var accountTheOldReader: RSImage! = {
return RSImage(named: "accountTheOldReader")
}()
static var addMenuImage: Image = {
return Image(systemName: "plus")
}()
static var articleExtractorError: Image = {
return Image("ArticleExtractorError")
}()
static var articleExtractorOff: Image = {
return Image(systemName: "doc.plaintext")
}()
static var articleExtractorOn: Image = {
return Image("ArticleExtractorOn")
}()
static var checkmarkImage: Image = {
return Image(systemName: "checkmark")
}()
static var copyImage: Image = {
return Image(systemName: "doc.on.doc")
}()
static var deleteImage: Image = {
return Image(systemName: "trash")
}()
static var extensionPointMarsEdit: RSImage = {
return RSImage(named: "ExtensionPointMarsEdit")!
}()
static var extensionPointMicroblog: RSImage = {
return RSImage(named: "ExtensionPointMicroblog")!
}()
static var extensionPointReddit: RSImage = {
return RSImage(named: "ExtensionPointReddit")!
}()
static var extensionPointTwitter: RSImage = {
return RSImage(named: "ExtensionPointTwitter")!
}()
static var faviconTemplateImage: RSImage = {
return RSImage(named: "FaviconTemplateImage")!
}()
static var filterInactiveImage: Image = {
return Image(systemName: "line.horizontal.3.decrease.circle")
}()
static var filterActiveImage: Image = {
return Image(systemName: "line.horizontal.3.decrease.circle.fill")
}()
static var getInfoImage: Image = {
return Image(systemName: "info.circle")
}()
#if os(macOS)
static var nsIconBackgroundColor: NSColor = {
return NSColor(named: "IconBackgroundColor")!
}()
#endif
#if os(iOS)
static var uiIconBackgroundColor: UIColor = {
return UIColor(named: "IconBackgroundColor")!
}()
#endif
static var iconBackgroundColor: Color = {
return Color("IconBackgroundColor")
}()
static var markBelowAsReadImage: Image = {
return Image(systemName: "arrowtriangle.down.circle")
}()
static var markAboveAsReadImage: Image = {
return Image(systemName: "arrowtriangle.up.circle")
}()
static var nextArticleImage: Image = {
return Image(systemName: "chevron.down")
}()
static var prevArticleImage: Image = {
return Image(systemName: "chevron.up")
}()
static var renameImage: Image = {
return Image(systemName: "textformat")
}()
static var settingsImage: Image = {
return Image(systemName: "gear")
}()
static var masterFolderImage: IconImage {
#if os(macOS)
let image = NSImage(systemSymbolName: "folder.fill", accessibilityDescription: nil)!
let coloredImage = image.tinted(with: NSColor(named: "AccentColor")!)
return IconImage(coloredImage, isSymbol: true)
#endif
#if os(iOS)
let image = UIImage(systemName: "folder.fill")!
let coloredImage = image.tinted(color: UIColor(named: "AccentColor")!)!
return IconImage(coloredImage, isSymbol: true)
#endif
}
static var markAllAsReadImage: Image = {
return Image("MarkAllAsRead")
}()
static var markAllAsReadImagePNG: Image = {
return Image("MarkAllAsReadPNG")
}()
static var nextUnreadArticleImage: Image = {
return Image(systemName: "chevron.down.circle")
}()
static var openInBrowserImage: Image = {
return Image(systemName: "safari")
}()
static var readClosedImage: Image = {
return Image(systemName: "largecircle.fill.circle")
}()
static var readOpenImage: Image = {
return Image(systemName: "circle")
}()
static var refreshImage: Image = {
return Image(systemName: "arrow.clockwise")
}()
static var searchFeedImage: IconImage = {
#if os(macOS)
return IconImage(NSImage(systemSymbolName: "magnifyingglass", accessibilityDescription: nil)!, isSymbol: true)
#endif
#if os(iOS)
return IconImage(UIImage(systemName: "magnifyingglass")!, isSymbol: true)
#endif
}()
static var sidebarUnreadCountBackground: Color = {
return Color("SidebarUnreadCountBackground")
}()
static var sidebarUnreadCountForeground: Color = {
return Color("SidebarUnreadCountForeground")
}()
static var shareImage: Image = {
Image(systemName: "square.and.arrow.up")
}()
static var starClosedImage: Image = {
return Image(systemName: "star.fill")
}()
static var starOpenImage: Image = {
return Image(systemName: "star")
}()
static var smartFeedImage: RSImage = {
#if os(macOS)
return NSImage(systemSymbolName: "gear", accessibilityDescription: nil)!
#endif
#if os(iOS)
return UIImage(systemName: "gear")!
#endif
}()
static var starredFeedImage: IconImage = {
#if os(macOS)
let image = NSImage(systemSymbolName: "star.fill", accessibilityDescription: nil)!
let coloredImage = image.tinted(with: NSColor(named: "StarColor")!)
return IconImage(coloredImage, isSymbol: true)
#endif
#if os(iOS)
let image = UIImage(systemName: "star.fill")!
let coloredImage = image.tinted(color: UIColor(named: "StarColor")!)!
return IconImage(coloredImage, isSymbol: true)
#endif
}()
static var timelineStarred: Image = {
#if os(macOS)
let image = NSImage(systemSymbolName: "star.fill", accessibilityDescription: nil)!
let coloredImage = image.tinted(with: NSColor(named: "StarColor")!)
return Image(nsImage: coloredImage)
#endif
#if os(iOS)
let image = UIImage(systemName: "star.fill")!
let coloredImage = image.tinted(color: UIColor(named: "StarColor")!)!
return Image(uiImage: coloredImage)
#endif
}()
static var timelineUnread: Image {
#if os(macOS)
let image = NSImage(systemSymbolName: "circle.fill", accessibilityDescription: nil)!
let coloredImage = image.tinted(with: NSColor.controlAccentColor)
return Image(nsImage: coloredImage)
#endif
#if os(iOS)
let image = UIImage(systemName: "circle.fill")!
let coloredImage = image.tinted(color: UIColor(named: "AccentColor")!)!
return Image(uiImage: coloredImage)
#endif
}
static var timelineUnreadSelected: Image {
return Image(systemName: "circle.fill")
}
static var todayFeedImage: IconImage = {
#if os(macOS)
let image = NSImage(systemSymbolName: "sun.max.fill", accessibilityDescription: nil)!
let coloredImage = image.tinted(with: .orange)
return IconImage(coloredImage, isSymbol: true)
#endif
#if os(iOS)
let image = UIImage(systemName: "sun.max.fill")!
let coloredImage = image.tinted(color: .orange)!
return IconImage(coloredImage, isSymbol: true)
#endif
}()
static var unreadFeedImage: IconImage {
#if os(macOS)
let image = NSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: nil)!
let coloredImage = image.tinted(with: NSColor(named: "AccentColor")!)
return IconImage(coloredImage, isSymbol: true)
#endif
#if os(iOS)
let image = UIImage(systemName: "largecircle.fill.circle")!
let coloredImage = image.tinted(color: UIColor(named: "AccentColor")!)!
return IconImage(coloredImage, isSymbol: true)
#endif
}
static var sidebarToggleImage: Image {
return Image(systemName: "sidebar.left")
}
#if os(macOS)
static var webStatusBarBackground: NSColor = {
return NSColor(named: "WebStatusBarBackground")!
}()
#endif
static func image(for accountType: AccountType) -> RSImage? {
switch accountType {
case .onMyMac:
#if os(macOS)
return AppAssets.accountLocalMacImage
#endif
#if os(iOS)
if UIDevice.current.userInterfaceIdiom == .pad {
return AppAssets.accountLocalPadImage
} else {
return AppAssets.accountLocalPhoneImage
}
#endif
case .bazQux:
return AppAssets.accountBazQux
case .cloudKit:
return AppAssets.accountCloudKitImage
case .feedbin:
return AppAssets.accountFeedbinImage
case .feedly:
return AppAssets.accountFeedlyImage
case .feedWrangler:
return AppAssets.accountFeedWranglerImage
case .freshRSS:
return AppAssets.accountFreshRSSImage
case .newsBlur:
return AppAssets.accountNewsBlurImage
case .inoreader:
return AppAssets.accountInoreader
case .theOldReader:
return AppAssets.accountTheOldReader
}
}
}

View File

@ -1,353 +0,0 @@
//
// AppDefaults.swift
// NetNewsWire
//
// Created by Stuart Breckenridge on 1/7/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import SwiftUI
enum UserInterfaceColorPalette: Int, CustomStringConvertible, CaseIterable {
case automatic = 0
case light = 1
case dark = 2
var description: String {
switch self {
case .automatic:
return NSLocalizedString("Automatic", comment: "Automatic")
case .light:
return NSLocalizedString("Light", comment: "Light")
case .dark:
return NSLocalizedString("Dark", comment: "Dark")
}
}
}
final class AppDefaults: ObservableObject {
#if os(macOS)
static let store: UserDefaults = UserDefaults.standard
#endif
#if os(iOS)
static let store: UserDefaults = {
let appIdentifierPrefix = Bundle.main.object(forInfoDictionaryKey: "AppIdentifierPrefix") as! String
let suiteName = "\(appIdentifierPrefix)group.\(Bundle.main.bundleIdentifier!)"
return UserDefaults.init(suiteName: suiteName)!
}()
#endif
public static let shared = AppDefaults()
private init() {}
struct Key {
// Shared Defaults
static let refreshInterval = "refreshInterval"
static let hideDockUnreadCount = "JustinMillerHideDockUnreadCount"
static let activeExtensionPointIDs = "activeExtensionPointIDs"
static let lastImageCacheFlushDate = "lastImageCacheFlushDate"
static let firstRunDate = "firstRunDate"
static let lastRefresh = "lastRefresh"
static let addWebFeedAccountID = "addWebFeedAccountID"
static let addWebFeedFolderName = "addWebFeedFolderName"
static let addFolderAccountID = "addFolderAccountID"
static let userInterfaceColorPalette = "userInterfaceColorPalette"
static let timelineSortDirection = "timelineSortDirection"
static let timelineGroupByFeed = "timelineGroupByFeed"
static let timelineIconDimensions = "timelineIconDimensions"
static let timelineNumberOfLines = "timelineNumberOfLines"
// Sidebar Defaults
static let sidebarConfirmDelete = "sidebarConfirmDelete"
// iOS Defaults
static let refreshClearsReadArticles = "refreshClearsReadArticles"
static let articleFullscreenAvailable = "articleFullscreenAvailable"
static let articleFullscreenEnabled = "articleFullscreenEnabled"
static let confirmMarkAllAsRead = "confirmMarkAllAsRead"
// macOS Defaults
static let articleTextSize = "articleTextSize"
static let openInBrowserInBackground = "openInBrowserInBackground"
static let defaultBrowserID = "defaultBrowserID"
static let subscribeToFeedsInDefaultBrowser = "subscribeToFeedsInDefaultBrowser"
static let checkForUpdatesAutomatically = "checkForUpdatesAutomatically"
static let downloadTestBuilds = "downloadTestBuild"
static let sendCrashLogs = "sendCrashLogs"
// Hidden macOS Defaults
static let showDebugMenu = "ShowDebugMenu"
static let timelineShowsSeparators = "CorreiaSeparators"
static let showTitleOnMainWindow = "KafasisTitleMode"
#if !MAC_APP_STORE
static let webInspectorEnabled = "WebInspectorEnabled"
static let webInspectorStartsAttached = "__WebInspectorPageGroupLevel1__.WebKit2InspectorStartsAttached"
#endif
}
// MARK: Development Builds
let isDeveloperBuild: Bool = {
if let dev = Bundle.main.object(forInfoDictionaryKey: "DeveloperEntitlements") as? String, dev == "-dev" {
return true
}
return false
}()
// MARK: First Run Details
var firstRunDate: Date? {
set {
AppDefaults.store.setValue(newValue, forKey: Key.firstRunDate)
objectWillChange.send()
}
get {
AppDefaults.store.object(forKey: Key.firstRunDate) as? Date
}
}
// MARK: Refresh Interval
@AppStorage(wrappedValue: 4, Key.refreshInterval, store: store) var interval: Int {
didSet {
objectWillChange.send()
}
}
var refreshInterval: RefreshInterval {
RefreshInterval(rawValue: interval) ?? RefreshInterval.everyHour
}
// MARK: Dock Badge
@AppStorage(wrappedValue: false, Key.hideDockUnreadCount, store: store) var hideDockUnreadCount {
didSet {
objectWillChange.send()
}
}
// MARK: Color Palette
var userInterfaceColorPalette: UserInterfaceColorPalette {
get {
if let palette = UserInterfaceColorPalette(rawValue: AppDefaults.store.integer(forKey: Key.userInterfaceColorPalette)) {
return palette
}
return .automatic
}
set {
AppDefaults.store.set(newValue.rawValue, forKey: Key.userInterfaceColorPalette)
#if os(macOS)
self.objectWillChange.send()
#else
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
self.objectWillChange.send()
})
#endif
}
}
static var userInterfaceColorScheme: ColorScheme? {
switch AppDefaults.shared.userInterfaceColorPalette {
case .light:
return ColorScheme.light
case .dark:
return ColorScheme.dark
default:
return nil
}
}
// MARK: Feeds & Folders
@AppStorage(Key.addWebFeedAccountID, store: store) var addWebFeedAccountID: String?
@AppStorage(Key.addWebFeedFolderName, store: store) var addWebFeedFolderName: String?
@AppStorage(Key.addFolderAccountID, store: store) var addFolderAccountID: String?
@AppStorage(wrappedValue: false, Key.confirmMarkAllAsRead, store: store) var confirmMarkAllAsRead: Bool
// MARK: Extension Points
var activeExtensionPointIDs: [[AnyHashable : AnyHashable]]? {
get {
return AppDefaults.store.object(forKey: Key.activeExtensionPointIDs) as? [[AnyHashable : AnyHashable]]
}
set {
UserDefaults.standard.set(newValue, forKey: Key.activeExtensionPointIDs)
objectWillChange.send()
}
}
// MARK: Image Cache
var lastImageCacheFlushDate: Date? {
set {
AppDefaults.store.setValue(newValue, forKey: Key.lastImageCacheFlushDate)
objectWillChange.send()
}
get {
AppDefaults.store.object(forKey: Key.lastImageCacheFlushDate) as? Date
}
}
// MARK: Timeline
@AppStorage(wrappedValue: false, Key.timelineGroupByFeed, store: store) var timelineGroupByFeed: Bool {
didSet {
objectWillChange.send()
}
}
@AppStorage(wrappedValue: 2.0, Key.timelineNumberOfLines, store: store) var timelineNumberOfLines: Double {
didSet {
objectWillChange.send()
}
}
@AppStorage(wrappedValue: 40.0, Key.timelineIconDimensions, store: store) var timelineIconDimensions: Double {
didSet {
objectWillChange.send()
}
}
/// Set to `true` to sort oldest to newest, `false` for newest to oldest. Default is `false`.
@AppStorage(wrappedValue: false, Key.timelineSortDirection, store: store) var timelineSortDirection: Bool {
didSet {
objectWillChange.send()
}
}
// MARK: Sidebar
@AppStorage(wrappedValue: true, Key.sidebarConfirmDelete, store: store) var sidebarConfirmDelete: Bool {
didSet {
objectWillChange.send()
}
}
// MARK: Refresh
@AppStorage(wrappedValue: false, Key.refreshClearsReadArticles, store: store) var refreshClearsReadArticles: Bool
// MARK: Articles
@AppStorage(wrappedValue: false, Key.articleFullscreenAvailable, store: store) var articleFullscreenAvailable: Bool
@AppStorage(wrappedValue: false, Key.articleFullscreenEnabled, store: store) var articleFullscreenEnabled: Bool
@AppStorage(wrappedValue: 3, Key.articleTextSize, store: store) var articleTextSizeTag: Int {
didSet {
objectWillChange.send()
}
}
var articleTextSize: ArticleTextSize {
ArticleTextSize(rawValue: articleTextSizeTag) ?? ArticleTextSize.large
}
// MARK: Refresh
var lastRefresh: Date? {
set {
AppDefaults.store.setValue(newValue, forKey: Key.lastRefresh)
objectWillChange.send()
}
get {
AppDefaults.store.object(forKey: Key.lastRefresh) as? Date
}
}
// MARK: Window State
@AppStorage(wrappedValue: false, Key.openInBrowserInBackground, store: store) var openInBrowserInBackground: Bool {
didSet {
objectWillChange.send()
}
}
@AppStorage(Key.defaultBrowserID, store: store) var defaultBrowserID: String? {
didSet {
objectWillChange.send()
}
}
@AppStorage(wrappedValue: false, Key.subscribeToFeedsInDefaultBrowser, store: store) var subscribeToFeedsInDefaultBrowser: Bool {
didSet {
objectWillChange.send()
}
}
@AppStorage(Key.showTitleOnMainWindow, store: store) var showTitleOnMainWindow: Bool? {
didSet {
objectWillChange.send()
}
}
@AppStorage(wrappedValue: false, Key.showDebugMenu, store: store) var showDebugMenu: Bool {
didSet {
objectWillChange.send()
}
}
@AppStorage(wrappedValue: false, Key.timelineShowsSeparators, store: store) var timelineShowsSeparators: Bool {
didSet {
objectWillChange.send()
}
}
#if !MAC_APP_STORE
@AppStorage(wrappedValue: false, Key.webInspectorEnabled, store: store) var webInspectorEnabled: Bool {
didSet {
objectWillChange.send()
}
}
@AppStorage(wrappedValue: false, Key.webInspectorStartsAttached, store: store) var webInspectorStartsAttached: Bool {
didSet {
objectWillChange.send()
}
}
#endif
@AppStorage(wrappedValue: true, Key.checkForUpdatesAutomatically, store: store) var checkForUpdatesAutomatically: Bool {
didSet {
objectWillChange.send()
}
}
@AppStorage(wrappedValue: false, Key.downloadTestBuilds, store: store) var downloadTestBuilds: Bool {
didSet {
objectWillChange.send()
}
}
@AppStorage(wrappedValue: true, Key.sendCrashLogs, store: store) var sendCrashLogs: Bool {
didSet {
objectWillChange.send()
}
}
static func registerDefaults() {
let defaults: [String : Any] = [Key.userInterfaceColorPalette: UserInterfaceColorPalette.automatic.rawValue,
Key.timelineGroupByFeed: false,
Key.refreshClearsReadArticles: false,
Key.timelineNumberOfLines: 2,
Key.timelineIconDimensions: 40,
Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue,
Key.articleFullscreenAvailable: false,
Key.articleFullscreenEnabled: false,
Key.confirmMarkAllAsRead: true,
"NSScrollViewShouldScrollUnderTitlebar": false,
Key.refreshInterval: RefreshInterval.everyHour.rawValue]
AppDefaults.store.register(defaults: defaults)
}
}
extension AppDefaults {
func isFirstRun() -> Bool {
if let _ = AppDefaults.store.object(forKey: Key.firstRunDate) as? Date {
return false
}
firstRunDate = Date()
return true
}
}

View File

@ -1,19 +0,0 @@
//
// ArticleContainerView.swift
// NetNewsWire
//
// Created by Maurice Parker on 7/2/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
import Articles
struct ArticleContainerView: View {
var body: some View {
ArticleView()
.modifier(ArticleToolbarModifier())
}
}

View File

@ -1,16 +0,0 @@
//
// ArticleExtractorButtonState.swift
// Multiplatform iOS
//
// Created by Maurice Parker on 7/6/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
enum ArticleExtractorButtonState {
case error
case animated
case on
case off
}

View File

@ -1,60 +0,0 @@
//
// ArticleIconSchemeHandler.swift
// Multiplatform iOS
//
// Created by Maurice Parker on 7/6/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import WebKit
import Articles
class ArticleIconSchemeHandler: NSObject, WKURLSchemeHandler {
weak var sceneModel: SceneModel?
init(sceneModel: SceneModel) {
self.sceneModel = sceneModel
}
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
guard let url = urlSchemeTask.request.url, let sceneModel = sceneModel else {
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
return
}
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return
}
let articleID = components.path
guard let iconImage = sceneModel.articleFor(articleID)?.iconImage() else {
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
return
}
let iconView = IconView(frame: CGRect(x: 0, y: 0, width: 48, height: 48))
iconView.iconImage = iconImage
let renderedImage = iconView.asImage()
guard let data = renderedImage.dataRepresentation() else {
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
return
}
let headerFields = ["Cache-Control": "no-cache"]
if let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: headerFields) {
urlSchemeTask.didReceive(response)
urlSchemeTask.didReceive(data)
urlSchemeTask.didFinish()
}
}
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
urlSchemeTask.didFailWithError(URLError(.unknown))
}
}

View File

@ -1,120 +0,0 @@
//
// ArticleToolbarModifier.swift
// NetNewsWire
//
// Created by Maurice Parker on 7/5/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
struct ArticleToolbarModifier: ViewModifier {
@EnvironmentObject private var sceneModel: SceneModel
@State private var showActivityView = false
func body(content: Content) -> some View {
content
.toolbar {
#if os(iOS)
ToolbarItem(placement: .primaryAction) {
HStack(spacing: 20) {
Button {
} label: {
AppAssets.prevArticleImage
.font(.title3)
}
.help("Previouse Unread")
Button {
} label: {
AppAssets.nextArticleImage.font(.title3)
}
.help("Next Unread")
}
}
ToolbarItem(placement: .bottomBar) {
Button {
sceneModel.toggleReadStatusForSelectedArticles()
} label: {
if sceneModel.readButtonState == true {
AppAssets.readClosedImage
} else {
AppAssets.readOpenImage
}
}
.disabled(sceneModel.readButtonState == nil)
.help(sceneModel.readButtonState ?? false ? "Mark as Unread" : "Mark as Read")
}
ToolbarItem(placement: .bottomBar) {
Spacer()
}
ToolbarItem(placement: .bottomBar) {
Button {
sceneModel.toggleStarredStatusForSelectedArticles()
} label: {
if sceneModel.starButtonState ?? false {
AppAssets.starClosedImage
} else {
AppAssets.starOpenImage
}
}
.disabled(sceneModel.starButtonState == nil)
.help(sceneModel.starButtonState ?? false ? "Mark as Unstarred" : "Mark as Starred")
}
ToolbarItem(placement: .bottomBar) {
Spacer()
}
ToolbarItem(placement: .bottomBar) {
Button {
sceneModel.goToNextUnread()
} label: {
AppAssets.nextUnreadArticleImage.font(.title3)
}
.disabled(sceneModel.nextUnreadButtonState == nil)
.help("Next Unread")
}
ToolbarItem(placement: .bottomBar) {
Spacer()
}
ToolbarItem(placement: .bottomBar) {
Button {
} label: {
AppAssets.articleExtractorOff
.font(.title3)
}
.disabled(sceneModel.extractorButtonState == nil)
.help("Reader View")
}
ToolbarItem(placement: .bottomBar) {
Spacer()
}
ToolbarItem(placement: .bottomBar) {
Button {
showActivityView.toggle()
} label: {
AppAssets.shareImage.font(.title3)
}
.disabled(sceneModel.shareButtonState == nil)
.help("Share")
.sheet(isPresented: $showActivityView) {
if let article = sceneModel.selectedArticles.first, let url = article.preferredURL {
ActivityViewController(title: article.title, url: url)
}
}
}
#endif
}
}
}

View File

@ -1,132 +0,0 @@
//
// PreloadedWebView.swift
// Multiplatform iOS
//
// Created by Maurice Parker on 7/6/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import WebKit
import RSWeb
class PreloadedWebView: WKWebView {
private var isReady: Bool = false
private var readyCompletion: (() -> Void)?
init(articleIconSchemeHandler: ArticleIconSchemeHandler) {
let preferences = WKPreferences()
preferences.javaScriptCanOpenWindowsAutomatically = false
let configuration = WKWebViewConfiguration()
configuration.preferences = preferences
configuration.setValue(true, forKey: "allowUniversalAccessFromFileURLs")
#if os(iOS)
configuration.allowsInlineMediaPlayback = true
#endif
configuration.mediaTypesRequiringUserActionForPlayback = .audio
configuration.setURLSchemeHandler(articleIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme)
super.init(frame: .zero, configuration: configuration)
if let userAgent = UserAgent.fromInfoPlist() {
customUserAgent = userAgent
}
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
func preload() {
navigationDelegate = self
loadFileURL(ArticleRenderer.blank.url, allowingReadAccessTo: ArticleRenderer.blank.baseURL)
}
func ready(completion: @escaping () -> Void) {
if isReady {
completeRequest(completion: completion)
} else {
readyCompletion = completion
}
}
#if os(macOS)
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
// Theres no API for affecting a WKWebViews contextual menu.
// (WebView had API for this.)
//
// This a minor hack. It hides unwanted menu items.
// The menu item identifiers are not documented anywhere;
// they could change, and this code would need updating.
for menuItem in menu.items {
if shouldHideMenuItem(menuItem) {
menuItem.isHidden = true
}
}
super.willOpenMenu(menu, with: event)
}
#endif
}
// MARK: WKScriptMessageHandler
extension PreloadedWebView: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
isReady = true
if let completion = readyCompletion {
completeRequest(completion: completion)
readyCompletion = nil
}
}
}
// MARK: Private
private extension PreloadedWebView {
func completeRequest(completion: @escaping () -> Void) {
isReady = false
navigationDelegate = nil
completion()
}
}
#if os(macOS)
private extension NSUserInterfaceItemIdentifier {
static let DetailMenuItemIdentifierReload = NSUserInterfaceItemIdentifier(rawValue: "WKMenuItemIdentifierReload")
static let DetailMenuItemIdentifierOpenLink = NSUserInterfaceItemIdentifier(rawValue: "WKMenuItemIdentifierOpenLink")
}
private extension PreloadedWebView {
static let menuItemIdentifiersToHide: [NSUserInterfaceItemIdentifier] = [.DetailMenuItemIdentifierReload, .DetailMenuItemIdentifierOpenLink]
static let menuItemIdentifierMatchStrings = ["newwindow", "download"]
func shouldHideMenuItem(_ menuItem: NSMenuItem) -> Bool {
guard let identifier = menuItem.identifier else {
return false
}
if PreloadedWebView.menuItemIdentifiersToHide.contains(identifier) {
return true
}
let lowerIdentifier = identifier.rawValue.lowercased()
for matchString in PreloadedWebView.menuItemIdentifierMatchStrings {
if lowerIdentifier.contains(matchString) {
return true
}
}
return false
}
}
#endif

View File

@ -1,103 +0,0 @@
//
// WebViewProvider.swift
// Multiplatform iOS
//
// Created by Maurice Parker on 7/6/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import RSCore
import WebKit
/// WKWebView has an awful behavior of a flash to white on first load when in dark mode.
/// Keep a queue of WebViews where we've already done a trivial load so that by the time we need them in the UI, they're past the flash-to-shite part of their lifecycle.
class WebViewProvider: NSObject {
private let articleIconSchemeHandler: ArticleIconSchemeHandler
private let operationQueue = MainThreadOperationQueue()
private var queue = NSMutableArray()
init(articleIconSchemeHandler: ArticleIconSchemeHandler) {
self.articleIconSchemeHandler = articleIconSchemeHandler
super.init()
replenishQueueIfNeeded()
}
func replenishQueueIfNeeded() {
operationQueue.add(WebViewProviderReplenishQueueOperation(queue: queue, articleIconSchemeHandler: articleIconSchemeHandler))
}
func dequeueWebView(completion: @escaping (PreloadedWebView) -> ()) {
operationQueue.add(WebViewProviderDequeueOperation(queue: queue, articleIconSchemeHandler: articleIconSchemeHandler, completion: completion))
operationQueue.add(WebViewProviderReplenishQueueOperation(queue: queue, articleIconSchemeHandler: articleIconSchemeHandler))
}
}
class WebViewProviderReplenishQueueOperation: MainThreadOperation {
// MainThreadOperation
public var isCanceled = false
public var id: Int?
public weak var operationDelegate: MainThreadOperationDelegate?
public var name: String? = "WebViewProviderReplenishQueueOperation"
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
private let minimumQueueDepth = 3
private var queue: NSMutableArray
private var articleIconSchemeHandler: ArticleIconSchemeHandler
init(queue: NSMutableArray, articleIconSchemeHandler: ArticleIconSchemeHandler) {
self.queue = queue
self.articleIconSchemeHandler = articleIconSchemeHandler
}
func run() {
while queue.count < minimumQueueDepth {
let webView = PreloadedWebView(articleIconSchemeHandler: articleIconSchemeHandler)
webView.preload()
queue.insert(webView, at: 0)
}
self.operationDelegate?.operationDidComplete(self)
}
}
class WebViewProviderDequeueOperation: MainThreadOperation {
// MainThreadOperation
public var isCanceled = false
public var id: Int?
public weak var operationDelegate: MainThreadOperationDelegate?
public var name: String? = "WebViewProviderFlushQueueOperation"
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
private var queue: NSMutableArray
private var articleIconSchemeHandler: ArticleIconSchemeHandler
private var completion: (PreloadedWebView) -> ()
init(queue: NSMutableArray, articleIconSchemeHandler: ArticleIconSchemeHandler, completion: @escaping (PreloadedWebView) -> ()) {
self.queue = queue
self.articleIconSchemeHandler = articleIconSchemeHandler
self.completion = completion
}
func run() {
if let webView = queue.lastObject as? PreloadedWebView {
self.completion(webView)
self.queue.remove(webView)
self.operationDelegate?.operationDidComplete(self)
return
}
assertionFailure("Creating PreloadedWebView in \(#function); queue has run dry.")
let webView = PreloadedWebView(articleIconSchemeHandler: articleIconSchemeHandler)
webView.preload()
self.completion(webView)
self.operationDelegate?.operationDidComplete(self)
}
}

View File

@ -1,25 +0,0 @@
//
// WrapperScriptMessageHandler.swift
// Multiplatform iOS
//
// Created by Maurice Parker on 7/6/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import WebKit
class WrapperScriptMessageHandler: NSObject, WKScriptMessageHandler {
// We need to wrap a message handler to prevent a circlular reference
private weak var handler: WKScriptMessageHandler?
init(_ handler: WKScriptMessageHandler) {
self.handler = handler
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
handler?.userContentController(userContentController, didReceive: message)
}
}

View File

@ -1,11 +0,0 @@
<html>
<head>
<style>
:root {
color-scheme: light dark;
}
</style>
</head>
<body>
</body>
</html>

View File

@ -1,498 +0,0 @@
var activeImageViewer = null;
class ImageViewer {
constructor(img) {
this.img = img;
this.loadingInterval = null;
this.activityIndicator = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjAiIHdpZHRoPSI2NHB4IiBoZWlnaHQ9IjY0cHgiIHZpZXdCb3g9IjAgMCAxMjggMTI4IiB4bWw6c3BhY2U9InByZXNlcnZlIj48Zz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiMwMDAwMDAiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiNjY2NjY2MiIHRyYW5zZm9ybT0icm90YXRlKDMwIDY0IDY0KSIvPjxwYXRoIGQ9Ik01OS42IDBoOHY0MGgtOFYweiIgZmlsbD0iI2NjY2NjYyIgdHJhbnNmb3JtPSJyb3RhdGUoNjAgNjQgNjQpIi8+PHBhdGggZD0iTTU5LjYgMGg4djQwaC04VjB6IiBmaWxsPSIjY2NjY2NjIiB0cmFuc2Zvcm09InJvdGF0ZSg5MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiNjY2NjY2MiIHRyYW5zZm9ybT0icm90YXRlKDEyMCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiNiMmIyYjIiIHRyYW5zZm9ybT0icm90YXRlKDE1MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM5OTk5OTkiIHRyYW5zZm9ybT0icm90YXRlKDE4MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM3ZjdmN2YiIHRyYW5zZm9ybT0icm90YXRlKDIxMCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM2NjY2NjYiIHRyYW5zZm9ybT0icm90YXRlKDI0MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM0YzRjNGMiIHRyYW5zZm9ybT0icm90YXRlKDI3MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiMzMzMzMzMiIHRyYW5zZm9ybT0icm90YXRlKDMwMCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiMxOTE5MTkiIHRyYW5zZm9ybT0icm90YXRlKDMzMCA2NCA2NCkiLz48YW5pbWF0ZVRyYW5zZm9ybSBhdHRyaWJ1dGVOYW1lPSJ0cmFuc2Zvcm0iIHR5cGU9InJvdGF0ZSIgdmFsdWVzPSIwIDY0IDY0OzMwIDY0IDY0OzYwIDY0IDY0OzkwIDY0IDY0OzEyMCA2NCA2NDsxNTAgNjQgNjQ7MTgwIDY0IDY0OzIxMCA2NCA2NDsyNDAgNjQgNjQ7MjcwIDY0IDY0OzMwMCA2NCA2NDszMzAgNjQgNjQiIGNhbGNNb2RlPSJkaXNjcmV0ZSIgZHVyPSIxMDgwbXMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIj48L2FuaW1hdGVUcmFuc2Zvcm0+PC9nPjwvc3ZnPg==";
}
isLoaded() {
return this.img.classList.contains("nnwLoaded");
}
clicked() {
this.showLoadingIndicator();
if (this.isLoaded()) {
this.showViewer();
} else {
var callback = () => {
if (this.isLoaded()) {
clearInterval(this.loadingInterval);
this.showViewer();
}
}
this.loadingInterval = setInterval(callback, 100);
}
}
cancel() {
clearInterval(this.loadingInterval);
this.hideLoadingIndicator();
}
showViewer() {
this.hideLoadingIndicator();
var canvas = document.createElement("canvas");
var pixelRatio = window.devicePixelRatio;
do {
canvas.width = this.img.naturalWidth * pixelRatio;
canvas.height = this.img.naturalHeight * pixelRatio;
pixelRatio--;
} while (pixelRatio > 0 && canvas.width * canvas.height > 16777216)
canvas.getContext("2d").drawImage(this.img, 0, 0, canvas.width, canvas.height);
const rect = this.img.getBoundingClientRect();
const message = {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
imageTitle: this.img.title,
imageURL: canvas.toDataURL(),
};
var jsonMessage = JSON.stringify(message);
window.webkit.messageHandlers.imageWasClicked.postMessage(jsonMessage);
}
hideImage() {
this.img.style.opacity = 0;
}
showImage() {
this.img.style.opacity = 1
}
showLoadingIndicator() {
var wrapper = document.createElement("div");
wrapper.classList.add("activityIndicatorWrap");
this.img.parentNode.insertBefore(wrapper, this.img);
wrapper.appendChild(this.img);
var activityIndicatorImg = document.createElement("img");
activityIndicatorImg.classList.add("activityIndicator");
activityIndicatorImg.style.opacity = 0;
activityIndicatorImg.src = this.activityIndicator;
wrapper.appendChild(activityIndicatorImg);
activityIndicatorImg.style.opacity = 1;
}
hideLoadingIndicator() {
var wrapper = this.img.parentNode;
if (wrapper.classList.contains("activityIndicatorWrap")) {
var wrapperParent = wrapper.parentNode;
wrapperParent.insertBefore(this.img, wrapper);
wrapperParent.removeChild(wrapper);
}
}
static init() {
cancelImageLoad();
// keep track of when an image has finished downloading for ImageViewer
document.querySelectorAll("img").forEach(element => {
element.onload = function() {
this.classList.add("nnwLoaded");
}
});
// Add the click listener for images
window.onclick = function(event) {
if (event.target.matches("img") && !event.target.classList.contains("nnw-nozoom")) {
if (activeImageViewer && activeImageViewer.img === event.target) {
cancelImageLoad();
} else {
cancelImageLoad();
activeImageViewer = new ImageViewer(event.target);
activeImageViewer.clicked();
}
}
}
}
}
function cancelImageLoad() {
if (activeImageViewer) {
activeImageViewer.cancel();
activeImageViewer = null;
}
}
function hideClickedImage() {
if (activeImageViewer) {
activeImageViewer.hideImage();
}
}
// Used to animate the transition from a fullscreen image
function showClickedImage() {
if (activeImageViewer) {
activeImageViewer.showImage();
}
window.webkit.messageHandlers.imageWasShown.postMessage("");
}
function showFeedInspectorSetup() {
document.getElementById("nnwImageIcon").onclick = function(event) {
window.webkit.messageHandlers.showFeedInspector.postMessage("");
}
}
function linkHover() {
window.onmouseover = function(event) {
var closestAnchor = event.target.closest('a')
if (closestAnchor) {
window.webkit.messageHandlers.mouseDidEnter.postMessage(closestAnchor.href);
}
}
window.onmouseout = function(event) {
var closestAnchor = event.target.closest('a')
if (closestAnchor) {
window.webkit.messageHandlers.mouseDidExit.postMessage(closestAnchor.href);
}
}
}
function postRenderProcessing() {
ImageViewer.init();
showFeedInspectorSetup();
linkHover();
}
function makeHighlightRect({left, top, width, height}, offsetTop=0, offsetLeft=0) {
const overlay = document.createElement('a');
Object.assign(overlay.style, {
position: 'absolute',
left: `${Math.floor(left + offsetLeft)}px`,
top: `${Math.floor(top + offsetTop)}px`,
width: `${Math.ceil(width)}px`,
height: `${Math.ceil(height)}px`,
backgroundColor: 'rgba(200, 220, 10, 0.4)',
pointerEvents: 'none'
});
return overlay;
}
function clearHighlightRects() {
let container = document.getElementById('nnw:highlightContainer')
if (container) container.remove();
}
function highlightRects(rects, clearOldRects=true, makeHighlightRect=makeHighlightRect) {
const article = document.querySelector('article');
let container = document.getElementById('nnw:highlightContainer');
article.style.position = 'relative';
if (container && clearOldRects)
container.remove();
container = document.createElement('div');
container.id = 'nnw:highlightContainer';
article.appendChild(container);
const {top, left} = article.getBoundingClientRect();
return Array.from(rects, rect =>
container.appendChild(makeHighlightRect(rect, -top, -left))
);
}
FinderResult = class {
constructor(result) {
Object.assign(this, result);
}
range() {
const range = document.createRange();
range.setStart(this.node, this.offset);
range.setEnd(this.node, this.offsetEnd);
return range;
}
bounds() {
return this.range().getBoundingClientRect();
}
rects() {
return this.range().getClientRects();
}
highlight({clearOldRects=true, fn=makeHighlightRect} = {}) {
highlightRects(this.rects(), clearOldRects, fn);
}
scrollTo() {
scrollToRect(this.bounds(), this.node);
}
toJSON() {
return {
rects: Array.from(this.rects()),
bounds: this.bounds(),
index: this.index,
matchGroups: this.match
};
}
toJSONString() {
return JSON.stringify(this.toJSON());
}
}
Finder = class {
constructor(pattern, options) {
if (!pattern.global) {
pattern = new RegExp(pattern, 'g');
}
this.pattern = pattern;
this.lastResult = null;
this._nodeMatches = [];
this.options = {
rootSelector: '.articleBody',
startNode: null,
startOffset: null,
}
this.resultIndex = -1
Object.assign(this.options, options);
this.walker = document.createTreeWalker(this.root, NodeFilter.SHOW_TEXT);
}
get root() {
return document.querySelector(this.options.rootSelector)
}
get count() {
const node = this.walker.currentNode;
const index = this.resultIndex;
this.reset();
let result, count = 0;
while ((result = this.next())) ++count;
this.resultIndex = index;
this.walker.currentNode = node;
return count;
}
reset() {
this.walker.currentNode = this.options.startNode || this.root;
this.resultIndex = -1;
}
[Symbol.iterator]() {
return this;
}
next({wrap = false} = {}) {
const { startNode } = this.options;
const { pattern, walker } = this;
let { node, matchIndex = -1 } = this.lastResult || { node: startNode };
while (true) {
if (!node)
node = walker.nextNode();
if (!node) {
if (!wrap || this.resultIndex < 0) break;
this.reset();
continue;
}
let nextIndex = matchIndex + 1;
let matches = this._nodeMatches;
if (!matches.length) {
matches = Array.from(node.textContent.matchAll(pattern));
nextIndex = 0;
}
if (matches[nextIndex]) {
this._nodeMatches = matches;
const m = matches[nextIndex];
this.lastResult = new FinderResult({
node,
offset: m.index,
offsetEnd: m.index + m[0].length,
text: m[0],
match: m,
matchIndex: nextIndex,
index: ++this.resultIndex,
});
return { value: this.lastResult, done: false };
}
this._nodeMatches = [];
node = null;
}
return { value: undefined, done: true };
}
/// TODO Call when the search text changes
retry() {
if (this.lastResult) {
this.lastResult.offsetEnd = this.lastResult.offset;
}
}
toJSON() {
const results = Array.from(this);
}
}
function scrollParent(node) {
let elt = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
while (elt) {
if (elt.scrollHeight > elt.clientHeight)
return elt;
elt = elt.parentElement;
}
}
function scrollToRect({top, height}, node, pad=20, padBottom=60) {
const scrollToTop = top - pad;
let scrollBy = scrollToTop;
if (scrollToTop >= 0) {
const visible = window.visualViewport;
const scrollToBottom = top + height + padBottom - visible.height;
// The top of the rect is already in the viewport
if (scrollToBottom <= 0 || scrollToTop === 0)
// Don't need to scroll up--or can't
return;
scrollBy = Math.min(scrollToBottom, scrollBy);
}
scrollParent(node).scrollBy({ top: scrollBy });
}
function withEncodedArg(fn) {
return function(encodedData, ...rest) {
const data = encodedData && JSON.parse(atob(encodedData));
return fn(data, ...rest);
}
}
function escapeRegex(s) {
return s.replace(/[.?*+^$\\()[\]{}]/g, '\\$&');
}
class FindState {
constructor(options) {
let { text, caseSensitive, regex } = options;
if (!regex)
text = escapeRegex(text);
const finder = new Finder(new RegExp(text, caseSensitive ? 'g' : 'ig'));
this.results = Array.from(finder);
this.index = -1;
this.options = options;
}
get selected() {
return this.index > -1 ? this.results[this.index] : null;
}
toJSON() {
return {
index: this.index > -1 ? this.index : null,
results: this.results,
count: this.results.length
};
}
selectNext(step=1) {
const index = this.index + step;
const result = this.results[index];
if (result) {
this.index = index;
result.highlight();
result.scrollTo();
}
return result;
}
selectPrevious() {
return this.selectNext(-1);
}
}
CurrentFindState = null;
const ExcludeKeys = new Set(['top', 'right', 'bottom', 'left']);
updateFind = withEncodedArg(options => {
// TODO Start at the current result position
// TODO Introduce slight delay, cap the number of results, and report results asynchronously
let newFindState;
if (!options || !options.text) {
clearHighlightRects();
return
}
try {
newFindState = new FindState(options);
} catch (err) {
clearHighlightRects();
throw err;
}
if (newFindState.results.length) {
let selected = CurrentFindState && CurrentFindState.selected;
let selectIndex = 0;
if (selected) {
let {node: currentNode, offset: currentOffset} = selected;
selectIndex = newFindState.results.findIndex(r => {
if (r.node === currentNode) {
return r.offset >= currentOffset;
}
let relation = currentNode.compareDocumentPosition(r.node);
return Boolean(relation & Node.DOCUMENT_POSITION_FOLLOWING);
});
}
newFindState.selectNext(selectIndex+1);
} else {
clearHighlightRects();
}
CurrentFindState = newFindState;
return btoa(JSON.stringify(CurrentFindState, (k, v) => (ExcludeKeys.has(k) ? undefined : v)));
});
selectNextResult = withEncodedArg(options => {
if (CurrentFindState)
CurrentFindState.selectNext();
});
selectPreviousResult = withEncodedArg(options => {
if (CurrentFindState)
CurrentFindState.selectPrevious();
});
function endFind() {
clearHighlightRects()
CurrentFindState = null;
}

View File

@ -1,21 +0,0 @@
<html dir="auto">
<head>
<title>[[title]]</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
[[style]]
</style>
<script src="main.js"></script>
<script src="main_multiplatform.js"></script>
<script src="newsfoot.js" async="async"></script>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function(event) {
processPage();
})
</script>
<base href="[[baseURL]]">
</head>
<body>
[[body]]
</body>
</html>

View File

@ -1,65 +0,0 @@
body {
margin-top: 3px;
margin-bottom: 20px;
padding-left: 20px;
padding-right: 20px;
word-break: break-word;
-webkit-hyphens: auto;
-webkit-text-size-adjust: none;
}
:root {
color-scheme: light dark;
font: -apple-system-body;
font-family: -apple-system;
font-size: [[font-size]]px;
--accent-color: rgba([[accent-r]], [[accent-g]], [[accent-b]], .75);
--block-quote-border-color: rgba([[accent-r]], [[accent-g]], [[accent-b]], .50);
}
@media(prefers-color-scheme: dark) {
:root {
--accent-color: rgba([[accent-r]], [[accent-g]], [[accent-b]], .75);
--block-quote-border-color: rgba([[accent-r]], [[accent-g]], [[accent-b]], .50);
--header-table-border-color: rgba(255, 255, 255, 0.2);
}
}
body a, body a:link, body a:visited {
color: var(--accent-color);
}
body .header {
font: -apple-system-body;
font-size: [[font-size]]px;
}
body .header a:link, body .header a:visited {
color: var(--accent-color);
}
.avatar img {
border-radius: 4px;
}
pre {
border: 1px solid var(--accent-color);
padding: 5px;
}
.nnw-overflow table {
border: 1px solid var(--accent-color);
}
.activityIndicatorWrap {
position: relative;
}
.activityIndicator {
z-index: 1;
width: 64px;
height: 64px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

View File

@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.933",
"green" : "0.416",
"red" : "0.031"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.957",
"green" : "0.620",
"red" : "0.369"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,176 +0,0 @@
{
"images" : [
{
"filename" : "icon-40.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "icon-60.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "icon-58.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "icon-87.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "icon-80.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "icon-120.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "icon-121.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "icon-180.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "icon-20.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "icon-41.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "icon-29.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "icon-59.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "icon-42.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "icon-81.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "icon-76.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "icon-152.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "icon-167.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "icon-1024.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
},
{
"filename" : "Icon-MacOS-16x16@1x.png.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "Icon-MacOS-16x16@2x.png.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "Icon-MacOS-32x32@1x.png.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "Icon-MacOS-32x32@2x.png.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "Icon-MacOS-128x128@1x.png.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "Icon-MacOS-128x128@2x.png.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "Icon-MacOS-256x256@1x.png.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "Icon-MacOS-256x256@2x.png.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "Icon-MacOS-512x512@1x.png.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "Icon-MacOS-512x512@2x.png.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 674 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 689 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "ArticleExtractorError.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -1,12 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"symbols" : [
{
"filename" : "doc.plaintext.on.svg",
"idiom" : "universal"
}
]
}

View File

@ -1,218 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="3300px" height="2200px" viewBox="0 0 3300 2200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<title>Untitled</title>
<desc>Created with Sketch.</desc>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="doc.plaintext.on">
<g id="Notes">
<rect id="artboard" fill="#FFFFFF" fill-rule="nonzero" x="0" y="0" width="3300" height="2200"></rect>
<line x1="263" y1="292" x2="3036" y2="292" id="Path" stroke="#000000" stroke-width="0.5"></line>
<text id="Weight/Scale-Variations" fill="#000000" font-family="Helvetica-Bold, Helvetica" font-size="13" font-weight="bold">
<tspan x="263" y="322">Weight/Scale Variations</tspan>
</text>
<text id="Ultralight" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="533.711" y="322">Ultralight</tspan>
</text>
<text id="Thin" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="843.422" y="322">Thin</tspan>
</text>
<text id="Light" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="1138.63" y="322">Light</tspan>
</text>
<text id="Regular" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="1426.84" y="322">Regular</tspan>
</text>
<text id="Medium" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="1723.06" y="322">Medium</tspan>
</text>
<text id="Semibold" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="2015.77" y="322">Semibold</tspan>
</text>
<text id="Bold" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="2326.48" y="322">Bold</tspan>
</text>
<text id="Heavy" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="2618.19" y="322">Heavy</tspan>
</text>
<text id="Black" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="2917.4" y="322">Black</tspan>
</text>
<line x1="263" y1="1903" x2="3036" y2="1903" id="Path" stroke="#000000" stroke-width="0.5"></line>
<g id="Group" transform="translate(264.000000, 1918.000000)" fill="#000000" fill-rule="nonzero">
<path d="M8.24805,15.830078 C12.5547,15.830078 16.1387,12.25586 16.1387,7.94922 C16.1387,3.6426 12.5449,0.0684 8.23828,0.0684 C3.94141,0.0684 0.36719,3.6426 0.36719,7.94922 C0.36719,12.25586 3.95117,15.830078 8.24805,15.830078 Z M8.24805,14.345703 C4.70312,14.345703 1.87109,11.50391 1.87109,7.94922 C1.87109,4.3945 4.69336,1.5527 8.23828,1.5527 C11.793,1.5527 14.6348,4.3945 14.6445252,7.94922 C14.6543,11.50391 11.8027,14.345703 8.24805,14.345703 Z M8.22852,11.57227 C8.69727,11.57227 8.9707,11.25977 8.9707,10.74219 L8.9707,8.68164 L11.1973,8.68164 C11.6953,8.68164 12.0371,8.42773 12.0371,7.95898 C12.0371,7.48047 11.7148,7.2168 11.1973,7.2168 L8.9707,7.2168 L8.9707,4.9902 C8.9707,4.4727 8.69727,4.1504 8.22852,4.1504 C7.75977,4.1504 7.50586,4.4922 7.50586,4.9902 L7.50586,7.2168 L5.29883,7.2168 C4.78125,7.2168 4.44922,7.48047 4.44922,7.95898 C4.44922,8.42773 4.80078,8.68164 5.29883,8.68164 L7.50586,8.68164 L7.50586,10.74219 C7.50586,11.24023 7.75977,11.57227 8.22852,11.57227 Z" id="Shape"></path>
</g>
<g id="Group" transform="translate(282.506000, 1915.000000)" fill="#000000" fill-rule="nonzero">
<path d="M10.709,20.91016 C16.1582,20.91016 20.6699,16.39844 20.6699,10.94922 C20.6699,5.5098 16.1484,0.9883 10.6992,0.9883 C5.25977,0.9883 0.74805,5.5098 0.74805,10.94922 C0.74805,16.39844 5.26953,20.91016 10.709,20.91016 Z M10.709,19.25 C6.09961,19.25 2.41797,15.55859 2.41797,10.94922 C2.41797,6.3496 6.08984,2.6484 10.6992,2.6484 C15.3086,2.6484 19,6.3496 19.009819,10.94922 C19.0195,15.55859 15.3184,19.25 10.709,19.25 Z M10.6895,15.58789 C11.207,15.58789 11.5195,15.22656 11.5195,14.66016 L11.5195,11.76953 L14.5762,11.76953 C15.123,11.76953 15.5039,11.48633 15.5039,10.96875 C15.5039,10.44141 15.1426,10.13867 14.5762,10.13867 L11.5195,10.13867 L11.5195,7.0723 C11.5195,6.4961 11.207,6.1445 10.6895,6.1445 C10.1719,6.1445 9.8789,6.5156 9.8789,7.0723 L9.8789,10.13867 L6.83203,10.13867 C6.26562,10.13867 5.89453,10.44141 5.89453,10.96875 C5.89453,11.48633 6.28516,11.76953 6.83203,11.76953 L9.8789,11.76953 L9.8789,14.66016 C9.8789,15.20703 10.1719,15.58789 10.6895,15.58789 Z" id="Shape"></path>
</g>
<g id="Group" transform="translate(306.924000, 1913.000000)" fill="#000000" fill-rule="nonzero">
<path d="M12.9707,25.67383 C19.9336,25.67383 25.6953,19.921875 25.6953,12.95898 C25.6953,5.9961 19.9238,0.2441 12.9609,0.2441 C6.00781,0.2441 0.25586,5.9961 0.25586,12.95898 C0.25586,19.921875 6.01758,25.67383 12.9707,25.67383 Z M12.9707,23.85742 C6.93555,23.85742 2.08203,18.99414 2.08203,12.95898 C2.08203,6.9238 6.92578,2.0605 12.9609,2.0605 C19.0059,2.0605 23.8594,6.9238 23.8691148,12.95898 C23.8789,18.99414 19.0156,23.85742 12.9707,23.85742 Z M12.9512,18.93555 C13.5176,18.93555 13.8691,18.54492 13.8691,17.93945 L13.8691,13.86719 L18.1074,13.86719 C18.6934,13.86719 19.1133,13.53516 19.1133,12.97852 C19.1133,12.40234 18.7227,12.06055 18.1074,12.06055 L13.8691,12.06055 L13.8691,7.8125 C13.8691,7.1973 13.5176,6.8066 12.9512,6.8066 C12.3848,6.8066 12.0625,7.2168 12.0625,7.8125 L12.0625,12.06055 L7.83398,12.06055 C7.21875,12.06055 6.80859,12.40234 6.80859,12.97852 C6.80859,13.53516 7.23828,13.86719 7.83398,13.86719 L12.0625,13.86719 L12.0625,17.93945 C12.0625,18.52539 12.3848,18.93555 12.9512,18.93555 Z" id="Shape"></path>
</g>
<text id="Design-Variations" fill="#000000" font-family="Helvetica-Bold, Helvetica" font-size="13" font-weight="bold">
<tspan x="263" y="1953">Design Variations</tspan>
</text>
<text id="Symbols-are-supported-in-up-to-nine-weights-and-three-scales." fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="263" y="1971">Symbols are supported in up to nine weights and three scales.</tspan>
</text>
<text id="For-optimal-layout-with-text-and-other-symbols,-vertically-align" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="263" y="1989">For optimal layout with text and other symbols, vertically align</tspan>
</text>
<text id="symbols-with-the-adjacent-text." fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="263" y="2007">symbols with the adjacent text.</tspan>
</text>
<rect id="Rectangle" fill="#00AEEF" fill-rule="nonzero" opacity="0.4" x="776" y="1919" width="3" height="14"></rect>
<g id="Group" transform="translate(779.000000, 1918.000000)" fill="#000000" fill-rule="nonzero">
<path d="M10.5273,15 L12.373,15 L7.17773,0.9082 L5.43945,0.9082 L0.244141,15 L2.08984,15 L3.50586,10.9668 L9.11133,10.9668 L10.5273,15 Z M6.2793,3.0469 L6.33789,3.0469 L8.59375,9.47266 L4.02344,9.47266 L6.2793,3.0469 Z" id="Shape"></path>
</g>
<rect id="Rectangle" fill="#00AEEF" fill-rule="nonzero" opacity="0.4" x="791.617" y="1919" width="3" height="14"></rect>
<text id="Margins" fill="#000000" font-family="Helvetica-Bold, Helvetica" font-size="13" font-weight="bold">
<tspan x="776" y="1953">Margins</tspan>
</text>
<text id="Leading-and-trailing-margins-on-the-left-and-right-side-of-each-symbol" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="776" y="1971">Leading and trailing margins on the left and right side of each symbol</tspan>
</text>
<text id="can-be-adjusted-by-modifying-the-width-of-the-blue-rectangles." fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="776" y="1989">can be adjusted by modifying the width of the blue rectangles.</tspan>
</text>
<text id="Modifications-are-automatically-applied-proportionally-to-all" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="776" y="2007">Modifications are automatically applied proportionally to all</tspan>
</text>
<text id="scales-and-weights." fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="776" y="2025">scales and weights.</tspan>
</text>
<g id="Group" transform="translate(1291.000000, 1915.000000)" fill="#000000" fill-rule="nonzero">
<path d="M0.83203,21.11523 L2.375,22.6582 C3.22461,23.48828 4.19141,23.41992 5.06055,22.46289 L15.2754,11.33984 C15.7051,11.63281 16.0957,11.62305 16.5645,11.52539 L17.6094,11.31055 L18.3027,12.00391 L18.2539,12.52148 C18.1855,13.04883 18.3516,13.46875 18.8496,13.9668 L19.6602,14.77734 C20.168,15.28516 20.8223,15.31445 21.3008,14.83594 L24.5527,11.58398 C25.0312,11.10547 25.0117,10.45117 24.5039,9.94336 L23.6836,9.12305 C23.1855,8.625 22.7754,8.44922 22.2383,8.52734 L21.7109,8.58594 L21.0566,7.9219 L21.3398,6.7793 C21.4863,6.2129 21.3398,5.7441 20.7148,5.1387 L18.3027,2.7461 C14.7578,-0.7793 10.2266,-0.6719 7.11133,2.4629 C6.69141,2.8926 6.64258,3.4785 6.91602,3.9082 C7.15039,4.2793 7.62891,4.5039 8.2734,4.3379 C9.7871,3.957 11.3008,4.0742 12.7852,5.0801 L12.1602,6.6621 C11.9258,7.248 11.9453,7.7266 12.1797,8.16602 L1.01758,18.439453 C0.08008,19.30859 -0.02734,20.25586 0.83203,21.11523 Z M8.6738,2.8535 C11.3398,0.8613 14.6504,1.1738 17.0527,3.5859 L19.6797,6.1934 C19.9141,6.4277 19.9434,6.6133 19.8848,6.9062 L19.5039,8.46875 L21.0762,10.04102 L22.043,9.95312 C22.3262,9.92383 22.4141,9.94336 22.6387,10.16797 L23.2637,10.79297 L20.5098,13.53711 L19.8848,12.92188 C19.6602,12.69727 19.6406,12.59961 19.6699,12.31641 L19.7578,11.35938 L18.1953,9.79688 L16.5742,10.10938 C16.291,10.16797 16.1445,10.16797 15.9102,9.92383 L13.7324,7.7461 C13.5078,7.5117 13.4785,7.375 13.6055,7.0527 L14.5527,4.7773 C12.9512,3.2441 10.8418,2.3945 8.8008,3.0488 C8.7129,3.0781 8.6445,3.0586 8.6152,3.0195 C8.5859,2.9707 8.5859,2.9219 8.6738,2.8535 Z M2.10156,20.41211 C1.61328,19.91406 1.78906,19.61133 2.12109,19.30859 L13.0781,9.19141 L14.3086,10.42188 L4.15234,21.34961 C3.84961,21.68164 3.46875,21.7793 3.06836,21.37891 L2.10156,20.41211 Z" id="Shape"></path>
</g>
<text id="Exporting" fill="#000000" font-family="Helvetica-Bold, Helvetica" font-size="13" font-weight="bold">
<tspan x="1289" y="1953">Exporting</tspan>
</text>
<text id="Symbols-should-be-outlined-when-exporting-to-ensure-the" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="1289" y="1971">Symbols should be outlined when exporting to ensure the</tspan>
</text>
<text id="design-is-preserved-when-submitting-to-Xcode." fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="1289" y="1989">design is preserved when submitting to Xcode.</tspan>
</text>
<text id="template-version" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="2952" y="1933">Template v.1.0</tspan>
</text>
<text id="Generated-from-doc.plaintext" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="2868" y="1951">Generated from doc.plaintext</tspan>
</text>
<text id="Typeset-at-100-points" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="2912" y="1969">Typeset at 100 points</tspan>
</text>
<text id="Small" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="263" y="726">Small</tspan>
</text>
<text id="Medium" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="263" y="1156">Medium</tspan>
</text>
<text id="Large" fill="#000000" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="263" y="1586">Large</tspan>
</text>
</g>
<g id="Guides" transform="translate(263.000000, 625.000000)">
<g id="H-reference" transform="translate(76.000000, 0.000000)" fill="#27AAE1" fill-rule="nonzero">
<path d="M54.9316,71 L57.666,71 L30.5664,0.541 L28.0762,0.541 L0.976562,71 L3.66211,71 L12.9395,46.5371 L45.7031,46.5371 L54.9316,71 Z M29.1992,3.9102 L29.4434,3.9102 L44.8242,44.291 L13.8184,44.291 L29.1992,3.9102 Z" id="Shape"></path>
</g>
<line x1="0" y1="71" x2="2773" y2="71" id="Baseline-S" stroke="#27AAE1" stroke-width="0.577"></line>
<line x1="0" y1="0.541" x2="2773" y2="0.541" id="Capline-S" stroke="#27AAE1" stroke-width="0.577"></line>
<g id="H-reference" transform="translate(76.000000, 430.000000)" fill="#27AAE1" fill-rule="nonzero">
<path d="M54.9316,71 L57.666,71 L30.5664,0.541 L28.0762,0.541 L0.976562,71 L3.66211,71 L12.9395,46.5371 L45.7031,46.5371 L54.9316,71 Z M29.1992,3.9102 L29.4434,3.9102 L44.8242,44.291 L13.8184,44.291 L29.1992,3.9102 Z" id="Shape"></path>
</g>
<line x1="0" y1="501" x2="2773" y2="501" id="Baseline-M" stroke="#27AAE1" stroke-width="0.577"></line>
<line x1="0" y1="430.54" x2="2773" y2="430.54" id="Capline-M" stroke="#27AAE1" stroke-width="0.577"></line>
<g id="H-reference" transform="translate(76.000000, 860.000000)" fill="#27AAE1" fill-rule="nonzero">
<path d="M54.9316,71 L57.666,71 L30.5664,0.541 L28.0762,0.541 L0.976562,71 L3.66211,71 L12.9395,46.5371 L45.7031,46.5371 L54.9316,71 Z M29.1992,3.9102 L29.4434,3.9102 L44.8242,44.291 L13.8184,44.291 L29.1992,3.9102 Z" id="Shape"></path>
</g>
<line x1="0" y1="931" x2="2773" y2="931" id="Baseline-L" stroke="#27AAE1" stroke-width="0.577"></line>
<line x1="0" y1="860.54" x2="2773" y2="860.54" id="Capline-L" stroke="#27AAE1" stroke-width="0.577"></line>
<rect id="left-margin" fill="#00AEEF" fill-rule="nonzero" opacity="0.4" x="1133.33" y="405.79" width="12.4512" height="119.336"></rect>
<rect id="right-margin" fill="#00AEEF" fill-rule="nonzero" opacity="0.4" x="1227.91" y="405.79" width="12.4512" height="119.336"></rect>
</g>
<g id="Symbols" transform="translate(509.000000, 617.000000)" fill="#000000" fill-rule="nonzero">
<g id="Black-L" transform="translate(2366.650000, 833.000000)">
<path d="M91.617,0.385 C106.803,0.385 115.445,9.0273 115.445,24.2617 L115.445,24.2617 L115.445,117.377 C115.445,132.6113 106.803,141.2539 91.617,141.2539 L91.617,141.2539 L23.8926,141.2539 C8.707,141.2539 0.0645,132.6113 0.0645,117.377 L0.0645,117.377 L0.0645,24.2617 C0.0645,9.0273 8.707,0.385 23.8926,0.385 L23.8926,0.385 Z M58.6582,76.166 L37.7109,76.166 C35.0254,76.166 33.1211,78.2168 33.1211,80.7559 C33.1211,83.2461 35.0254,85.1992 37.7109,85.1992 L58.6582,85.1992 C61.2461,85.1992 63.1992,83.2461 63.1992,80.7559 C63.1992,78.2168 61.2461,76.166 58.6582,76.166 Z M77.8965,56.1953 L37.7109,56.1953 C35.0254,56.1953 33.1211,58.0996 33.1211,60.5898 C33.1211,63.1777 35.0254,65.1797 37.7109,65.1797 L77.8965,65.1797 C80.4844,65.1797 82.3887,63.1777 82.3887,60.5898 C82.3887,58.0996 80.4844,56.1953 77.8965,56.1953 Z M77.8965,36.1758 L37.7109,36.1758 C35.0254,36.1758 33.1211,38.0801 33.1211,40.6191 C33.1211,43.1582 35.0254,45.1602 37.7109,45.1602 L77.8965,45.1602 C80.4844,45.1602 82.3887,43.1582 82.3887,40.6191 C82.3887,38.0801 80.4844,36.1758 77.8965,36.1758 Z" id="Shape"></path>
</g>
<g id="Heavy-L" transform="translate(2071.180000, 834.000000)">
<path d="M90.348,0.117 C104.898,0.117 112.955,8.2715 112.955,22.8711 L112.955,22.8711 L112.955,116.7676 C112.955,131.416 104.898,139.5215 90.348,139.5215 L90.348,139.5215 L22.7207,139.5215 C8.1211,139.5215 0.0645,131.416 0.0645,116.7676 L0.0645,116.7676 L0.0645,22.8711 C0.0645,8.2715 8.1699,0.117 22.7207,0.117 L22.7207,0.117 Z M56.998,75.0684 L35.416,75.0684 C32.9258,75.0684 31.1191,76.9238 31.1191,79.3652 C31.1191,81.709 32.9258,83.5156 35.416,83.5156 L56.998,83.5156 C59.3906,83.5156 61.2461,81.709 61.2461,79.3652 C61.2461,76.9238 59.3906,75.0684 56.998,75.0684 Z M77.6523,54.5605 L35.416,54.5605 C32.9258,54.5605 31.1191,56.3672 31.1191,58.7109 C31.1191,61.1523 32.9258,63.0566 35.416,63.0566 L77.6523,63.0566 C80.0938,63.0566 81.9004,61.1523 81.9004,58.7109 C81.9004,56.3672 80.0938,54.5605 77.6523,54.5605 Z M77.6523,34.0527 L35.416,34.0527 C32.9258,34.0527 31.1191,35.9082 31.1191,38.252 C31.1191,40.6445 32.9258,42.5488 35.416,42.5488 L77.6523,42.5488 C80.0938,42.5488 81.9004,40.6445 81.9004,38.252 C81.9004,35.9082 80.0938,34.0527 77.6523,34.0527 Z" id="Shape"></path>
</g>
<g id="Bold-L" transform="translate(1775.860000, 834.000000)">
<path d="M88.883,0.947 C102.701,0.947 110.172,8.5156 110.172,22.4316 L110.172,22.4316 L110.172,117.2559 C110.172,131.1719 102.701,138.6914 88.883,138.6914 L88.883,138.6914 L21.4023,138.6914 C7.5352,138.6914 0.0645,131.1719 0.0645,117.2559 L0.0645,117.2559 L0.0645,22.4316 C0.0645,8.5156 7.584,0.947 21.4023,0.947 L21.4023,0.947 Z M55.0938,74.9219 L32.877,74.9219 C30.582,74.9219 28.873,76.6797 28.873,78.9258 C28.873,81.0742 30.582,82.7832 32.877,82.7832 L55.0938,82.7832 C57.3398,82.7832 59.0488,81.0742 59.0488,78.9258 C59.0488,76.6797 57.3398,74.9219 55.0938,74.9219 Z M77.4082,53.877 L32.877,53.877 C30.582,53.877 28.873,55.5859 28.873,57.7344 C28.873,59.9805 30.582,61.7383 32.877,61.7383 L77.4082,61.7383 C79.7031,61.7383 81.3633,59.9805 81.3633,57.7344 C81.3633,55.5859 79.7031,53.877 77.4082,53.877 Z M77.4082,32.832 L32.877,32.832 C30.582,32.832 28.873,34.541 28.873,36.7383 C28.873,38.9844 30.582,40.7422 32.877,40.7422 L77.4082,40.7422 C79.7031,40.7422 81.3633,38.9844 81.3633,36.7383 C81.3633,34.541 79.7031,32.832 77.4082,32.832 Z" id="Shape"></path>
</g>
<g id="Semibold-L" transform="translate(1480.100000, 835.000000)">
<path d="M87.857,0.533 C101.188,0.533 108.268,7.6621 108.268,21.0898 L108.268,21.0898 L108.268,116.5488 C108.268,130.0254 101.188,137.1055 87.857,137.1055 L87.857,137.1055 L20.4746,137.1055 C7.0957,137.1055 0.0645,130.0254 0.0645,116.5488 L0.0645,116.5488 L0.0645,21.0898 C0.0645,7.6621 7.1445,0.533 20.4746,0.533 L20.4746,0.533 Z M53.7754,73.7754 L31.1191,73.7754 C28.9219,73.7754 27.3105,75.4844 27.3105,77.584 C27.3105,79.6836 28.9219,81.2949 31.1191,81.2949 L53.7754,81.2949 C55.9238,81.2949 57.5352,79.6836 57.5352,77.584 C57.5352,75.4844 55.9238,73.7754 53.7754,73.7754 Z M77.2617,52.3887 L31.1191,52.3887 C28.9219,52.3887 27.3105,54 27.3105,56.0508 C27.3105,58.1992 28.9219,59.8594 31.1191,59.8594 L77.2617,59.8594 C79.4102,59.8594 81.0215,58.1992 81.0215,56.0508 C81.0215,54 79.4102,52.3887 77.2617,52.3887 Z M77.2617,31.002 L31.1191,31.002 C28.9219,31.002 27.3105,32.6133 27.3105,34.6641 C27.3105,36.8125 28.9219,38.4727 31.1191,38.4727 L77.2617,38.4727 C79.4102,38.4727 81.0215,36.8125 81.0215,34.6641 C81.0215,32.6133 79.4102,31.002 77.2617,31.002 Z" id="Shape"></path>
</g>
<g id="Medium-L" transform="translate(1184.120000, 835.000000)">
<path d="M87.076,0.973 C100.064,0.973 106.803,7.8086 106.803,20.8945 L106.803,20.8945 L106.803,116.793 C106.803,129.9277 100.064,136.666 87.076,136.666 L87.076,136.666 L19.791,136.666 C6.8027,136.666 0.0645,129.9277 0.0645,116.793 L0.0645,116.793 L0.0645,20.8945 C0.0645,7.8086 6.8516,0.973 19.791,0.973 L19.791,0.973 Z M52.7988,73.7266 L29.8008,73.7266 C27.7012,73.7266 26.1387,75.3379 26.1387,77.3398 C26.1387,79.3418 27.7012,80.9043 29.8008,80.9043 L52.7988,80.9043 C54.8496,80.9043 56.4121,79.3418 56.4121,77.3398 C56.4121,75.3379 54.8496,73.7266 52.7988,73.7266 Z M77.1152,52.0469 L29.8008,52.0469 C27.7012,52.0469 26.1387,53.5605 26.1387,55.5625 C26.1387,57.6133 27.7012,59.2246 29.8008,59.2246 L77.1152,59.2246 C79.166,59.2246 80.7285,57.6133 80.7285,55.5625 C80.7285,53.5605 79.166,52.0469 77.1152,52.0469 Z M77.1152,30.3184 L29.8008,30.3184 C27.7012,30.3184 26.1387,31.8809 26.1387,33.8828 C26.1387,35.9336 27.7012,37.4961 29.8008,37.4961 L77.1152,37.4961 C79.166,37.4961 80.7285,35.9336 80.7285,33.8828 C80.7285,31.8809 79.166,30.3184 77.1152,30.3184 Z" id="Shape"></path>
</g>
<g id="Regular-L" transform="translate(888.360000, 836.000000)">
<path d="M86.1,0.559 C98.6,0.559 104.898,6.9551 104.898,19.5527 L104.898,19.5527 L104.898,116.1348 C104.898,128.7324 98.6,135.1289 86.1,135.1289 L86.1,135.1289 L18.8633,135.1289 C6.3633,135.1289 0.0645,128.7324 0.0645,116.1348 L0.0645,116.1348 L0.0645,19.5527 C0.0645,6.9551 6.4609,0.559 18.8633,0.559 L18.8633,0.559 Z M51.4805,72.6289 L28.0918,72.6289 C26.0898,72.6289 24.625,74.1426 24.625,76.0469 C24.625,77.9023 26.0898,79.3672 28.0918,79.3672 L51.4805,79.3672 C53.4336,79.3672 54.8984,77.9023 54.8984,76.0469 C54.8984,74.1426 53.4336,72.6289 51.4805,72.6289 Z M76.9199,50.5586 L28.0918,50.5586 C26.0898,50.5586 24.625,52.0234 24.625,53.8789 C24.625,55.832 26.0898,57.3457 28.0918,57.3457 L76.9199,57.3457 C78.873,57.3457 80.3379,55.832 80.3379,53.8789 C80.3379,52.0234 78.873,50.5586 76.9199,50.5586 Z M76.9199,28.4883 L28.0918,28.4883 C26.0898,28.4883 24.625,29.9531 24.625,31.8086 C24.625,33.7617 26.0898,35.2754 28.0918,35.2754 L76.9199,35.2754 C78.873,35.2754 80.3379,33.7617 80.3379,31.8086 C80.3379,29.9531 78.873,28.4883 76.9199,28.4883 Z" id="Shape"></path>
</g>
<g id="Light-L" transform="translate(592.430000, 836.000000)">
<path d="M85.514,0.949 C97.281,0.949 103.336,7.0527 103.336,18.918 L103.336,18.918 L103.336,116.8184 C103.336,128.6836 97.281,134.7871 85.514,134.7871 L85.514,134.7871 L17.9355,134.7871 C6.1191,134.7871 0.0645,128.6836 0.0645,116.8184 L0.0645,116.8184 L0.0645,18.918 C0.0645,7.0527 6.2168,0.949 17.9355,0.949 L17.9355,0.949 Z M51.041,72.1895 L26.5781,72.1895 C24.9668,72.1895 23.7461,73.4102 23.7461,74.9238 C23.7461,76.4375 24.9668,77.6094 26.5781,77.6094 L51.041,77.6094 C52.6035,77.6094 53.7754,76.4375 53.7754,74.9238 C53.7754,73.4102 52.6035,72.1895 51.041,72.1895 Z M76.8711,50.0215 L26.5781,50.0215 C24.9668,50.0215 23.7461,51.1934 23.7461,52.707 C23.7461,54.2207 24.9668,55.4414 26.5781,55.4414 L76.8711,55.4414 C78.4824,55.4414 79.6543,54.2207 79.6543,52.707 C79.6543,51.1934 78.4824,50.0215 76.8711,50.0215 Z M76.8711,27.8535 L26.5781,27.8535 C24.9668,27.8535 23.7461,29.0254 23.7461,30.5391 C23.7461,32.0527 24.9668,33.2734 26.5781,33.2734 L76.8711,33.2734 C78.4824,33.2734 79.6543,32.0527 79.6543,30.5391 C79.6543,29.0254 78.4824,27.8535 76.8711,27.8535 Z" id="Shape"></path>
</g>
<g id="Thin-L" transform="translate(296.747000, 837.000000)">
<path d="M84.732,0.438 C95.572,0.438 101.285,6.1504 101.285,17.0391 L101.285,17.0391 L101.285,116.7461 C101.285,127.6348 95.572,133.3477 84.732,133.3477 L84.732,133.3477 L16.666,133.3477 C5.7773,133.3477 0.0645,127.6348 0.0645,116.7461 L0.0645,116.7461 L0.0645,17.0391 C0.0645,6.1504 5.9238,0.438 16.666,0.438 L16.666,0.438 Z M50.4062,70.6035 L24.5762,70.6035 C23.4531,70.6035 22.623,71.4336 22.623,72.4102 C22.623,73.4355 23.4531,74.2656 24.5762,74.2656 L50.4062,74.2656 C51.4805,74.2656 52.3105,73.4355 52.3105,72.4102 C52.3105,71.4336 51.4805,70.6035 50.4062,70.6035 Z M76.8223,48.2891 L24.5762,48.2891 C23.4531,48.2891 22.623,49.0703 22.623,50.0957 C22.623,51.1211 23.4531,51.9512 24.5762,51.9512 L76.8223,51.9512 C77.8965,51.9512 78.7266,51.1211 78.7266,50.0957 C78.7266,49.0703 77.8965,48.2891 76.8223,48.2891 Z M76.8223,25.9258 L24.5762,25.9258 C23.4531,25.9258 22.623,26.7559 22.623,27.7812 C22.623,28.8066 23.4531,29.6367 24.5762,29.6367 L76.8223,29.6367 C77.8965,29.6367 78.7266,28.8066 78.7266,27.7812 C78.7266,26.7559 77.8965,25.9258 76.8223,25.9258 Z" id="Shape"></path>
</g>
<g id="Ultralight-L" transform="translate(0.549000, 837.000000)">
<path d="M84.293,0.682 C94.693,0.682 100.26,6.1992 100.26,16.5508 L100.26,16.5508 L100.26,117.2344 C100.26,127.5859 94.693,133.1523 84.293,133.1523 L84.293,133.1523 L15.9824,133.1523 C5.6309,133.1523 0.0645,127.5859 0.0645,117.2344 L0.0645,117.2344 L0.0645,16.5508 C0.0645,6.1992 5.7773,0.682 15.9824,0.682 L15.9824,0.682 Z M50.1133,70.2617 L23.5508,70.2617 C22.6719,70.2617 22.0859,70.8965 22.0859,71.6777 C22.0859,72.4102 22.6719,73.0449 23.5508,73.0449 L50.1133,73.0449 C50.9434,73.0449 51.5781,72.4102 51.5781,71.6777 C51.5781,70.8965 50.9434,70.2617 50.1133,70.2617 Z M76.7734,47.8984 L23.5508,47.8984 C22.6719,47.8984 22.0859,48.5332 22.0859,49.2656 C22.0859,50.0469 22.6719,50.6816 23.5508,50.6816 L76.7734,50.6816 C77.6035,50.6816 78.2383,50.0469 78.2383,49.2656 C78.2383,48.5332 77.6035,47.8984 76.7734,47.8984 Z M76.7734,25.4863 L23.5508,25.4863 C22.6719,25.4863 22.0859,26.1211 22.0859,26.9023 C22.0859,27.6348 22.6719,28.2695 23.5508,28.2695 L76.7734,28.2695 C77.6035,28.2695 78.2383,27.6348 78.2383,26.9023 C78.2383,26.1211 77.6035,25.4863 76.7734,25.4863 Z" id="Shape"></path>
</g>
<g id="Black-M" transform="translate(2377.980000, 418.000000)">
<path d="M72.8633,0.375 C85.1191,0.375 92.395,7.6504 92.395,19.9062 L92.395,19.9062 L92.395,91.683594 C92.395,103.9395 85.1191,111.2148 72.8633,111.2148 L72.8633,111.2148 L19.9824,111.2148 C7.7266,111.2148 0.4512,103.9395 0.4512,91.683594 L0.4512,91.683594 L0.4512,19.9062 C0.4512,7.6504 7.7266,0.375 19.9824,0.375 L19.9824,0.375 Z M47.9121,59.8965 L31.8477,59.8965 C29.7969,59.8965 28.2832,61.459 28.2832,63.4609 C28.2832,65.4141 29.7969,66.8789 31.8477,66.8789 L47.9121,66.8789 C49.9141,66.8789 51.4277,65.4141 51.4277,63.4609 C51.4277,61.459 49.9141,59.8965 47.9121,59.8965 Z M61.0469,45.0039 L31.8477,45.0039 C29.7969,45.0039 28.2832,46.5176 28.2832,48.4219 C28.2832,50.4238 29.7969,51.9863 31.8477,51.9863 L61.0469,51.9863 C63.0488,51.9863 64.5625,50.4238 64.5625,48.4219 C64.5625,46.5176 63.0488,45.0039 61.0469,45.0039 Z M61.0469,30.1113 L31.8477,30.1113 C29.7969,30.1113 28.2832,31.5762 28.2832,33.5293 C28.2832,35.5312 29.7969,37.0938 31.8477,37.0938 L61.0469,37.0938 C63.0488,37.0938 64.5625,35.5312 64.5625,33.5293 C64.5625,31.5762 63.0488,30.1113 61.0469,30.1113 Z" id="Shape"></path>
</g>
<g id="Heavy-M" transform="translate(2082.410000, 419.000000)">
<path d="M71.5938,0.1074 C83.3125,0.1074 90.1,6.8945 90.1,18.6621 L90.1,18.6621 L90.1,90.976562 C90.1,102.7441 83.3125,109.4824 71.5938,109.4824 L71.5938,109.4824 L18.957,109.4824 C7.1895,109.4824 0.4512,102.7441 0.4512,90.976562 L0.4512,90.976562 L0.4512,18.6621 C0.4512,6.8945 7.1895,0.1074 18.957,0.1074 L18.957,0.1074 Z M46.2031,58.8965 L29.7969,58.8965 C27.7949,58.8965 26.3301,60.4102 26.3301,62.3145 C26.3301,64.1699 27.7949,65.6348 29.7969,65.6348 L46.2031,65.6348 C48.1074,65.6348 49.5723,64.1699 49.5723,62.3145 C49.5723,60.4102 48.1074,58.8965 46.2031,58.8965 Z M60.8027,43.4668 L29.7969,43.4668 C27.7949,43.4668 26.3301,44.9316 26.3301,46.7871 C26.3301,48.6914 27.7949,50.2051 29.7969,50.2051 L60.8027,50.2051 C62.7559,50.2051 64.1719,48.6914 64.1719,46.7871 C64.1719,44.9316 62.7559,43.4668 60.8027,43.4668 Z M60.8027,28.0371 L29.7969,28.0371 C27.7949,28.0371 26.3301,29.502 26.3301,31.3574 C26.3301,33.3105 27.7949,34.8242 29.7969,34.8242 L60.8027,34.8242 C62.7559,34.8242 64.1719,33.3105 64.1719,31.3574 C64.1719,29.502 62.7559,28.0371 60.8027,28.0371 Z" id="Shape"></path>
</g>
<g id="Bold-M" transform="translate(1787.000000, 419.000000)">
<path d="M70.1777,0.9375 C81.3105,0.9375 87.5117,7.1387 87.5117,18.3691 L87.5117,18.3691 L87.5117,91.26953 C87.5117,102.5 81.3105,108.7012 70.1777,108.7012 L70.1777,108.7012 L17.7852,108.7012 C6.6035,108.7012 0.4512,102.5 0.4512,91.26953 L0.4512,91.26953 L0.4512,18.3691 C0.4512,7.1387 6.6035,0.9375 17.7852,0.9375 L17.7852,0.9375 Z M44.25,58.8477 L27.4531,58.8477 C25.5488,58.8477 24.1328,60.3125 24.1328,62.168 C24.1328,63.9258 25.5488,65.3418 27.4531,65.3418 L44.25,65.3418 C46.1055,65.3418 47.4727,63.9258 47.4727,62.168 C47.4727,60.3125 46.1055,58.8477 44.25,58.8477 Z M60.5586,42.8809 L27.4531,42.8809 C25.5488,42.8809 24.1328,44.248 24.1328,46.0547 C24.1328,47.9102 25.5488,49.375 27.4531,49.375 L60.5586,49.375 C62.4141,49.375 63.8301,47.9102 63.8301,46.0547 C63.8301,44.248 62.4141,42.8809 60.5586,42.8809 Z M60.5586,26.9141 L27.4531,26.9141 C25.5488,26.9141 24.1328,28.2812 24.1328,30.0879 C24.1328,31.9434 25.5488,33.4082 27.4531,33.4082 L60.5586,33.4082 C62.4141,33.4082 63.8301,31.9434 63.8301,30.0879 C63.8301,28.2812 62.4141,26.9141 60.5586,26.9141 Z" id="Shape"></path>
</g>
<g id="Semibold-M" transform="translate(1491.190000, 420.000000)">
<path d="M69.1523,0.4746 C79.9434,0.4746 85.7051,6.334 85.7051,17.1738 L85.7051,17.1738 L85.7051,90.46484 C85.7051,101.3535 79.9434,107.1152 69.1523,107.1152 L69.1523,107.1152 L17.0039,107.1152 C6.2129,107.1152 0.4512,101.3535 0.4512,90.46484 L0.4512,90.46484 L0.4512,17.1738 C0.4512,6.334 6.2129,0.4746 17.0039,0.4746 L17.0039,0.4746 Z M42.8828,57.7988 L25.8418,57.7988 C23.9863,57.7988 22.6191,59.2148 22.6191,61.0215 C22.6191,62.7793 23.9863,64.1465 25.8418,64.1465 L42.8828,64.1465 C44.6895,64.1465 46.0566,62.7793 46.0566,61.0215 C46.0566,59.2148 44.6895,57.7988 42.8828,57.7988 Z M60.3633,41.4414 L25.8418,41.4414 C23.9863,41.4414 22.6191,42.8086 22.6191,44.5664 C22.6191,46.373 23.9863,47.7891 25.8418,47.7891 L60.3633,47.7891 C62.1699,47.7891 63.5371,46.373 63.5371,44.5664 C63.5371,42.8086 62.1699,41.4414 60.3633,41.4414 Z M60.3633,25.084 L25.8418,25.084 C23.9863,25.084 22.6191,26.4512 22.6191,28.209 C22.6191,30.0156 23.9863,31.4316 25.8418,31.4316 L60.3633,31.4316 C62.1699,31.4316 63.5371,30.0156 63.5371,28.209 C63.5371,26.4512 62.1699,25.084 60.3633,25.084 Z" id="Shape"></path>
</g>
<g id="Medium-M" transform="translate(1195.160000, 420.000000)">
<path d="M68.4199,0.9141 C78.918,0.9141 84.3379,6.4316 84.3379,16.9785 L84.3379,16.9785 L84.3379,90.61133 C84.3379,101.207 78.918,106.7246 68.4199,106.7246 L68.4199,106.7246 L16.3691,106.7246 C5.9199,106.7246 0.4512,101.207 0.4512,90.61133 L0.4512,90.61133 L0.4512,16.9785 C0.4512,6.4316 5.9199,0.9141 16.3691,0.9141 L16.3691,0.9141 Z M41.9062,57.7988 L24.6211,57.7988 C22.8145,57.7988 21.4473,59.166 21.4473,60.9238 C21.4473,62.6328 22.8145,64 24.6211,64 L41.9062,64 C43.6641,64 44.9824,62.6328 44.9824,60.9238 C44.9824,59.166 43.6641,57.7988 41.9062,57.7988 Z M60.2168,41.1484 L24.6211,41.1484 C22.8145,41.1484 21.4473,42.4668 21.4473,44.1758 C21.4473,45.9336 22.8145,47.3496 24.6211,47.3496 L60.2168,47.3496 C62.0234,47.3496 63.3418,45.9336 63.3418,44.1758 C63.3418,42.4668 62.0234,41.1484 60.2168,41.1484 Z M60.2168,24.498 L24.6211,24.498 C22.8145,24.498 21.4473,25.8164 21.4473,27.5254 C21.4473,29.2832 22.8145,30.6992 24.6211,30.6992 L60.2168,30.6992 C62.0234,30.6992 63.3418,29.2832 63.3418,27.5254 C63.3418,25.8164 62.0234,24.498 60.2168,24.498 Z" id="Shape"></path>
</g>
<g id="Regular-M" transform="translate(899.330000, 421.000000)">
<path d="M67.4434,0.4512 C77.5508,0.4512 82.5801,5.627 82.5801,15.7832 L82.5801,15.7832 L82.5801,89.85547 C82.5801,100.0605 77.5508,105.1387 67.4434,105.1387 L67.4434,105.1387 L15.5879,105.1387 C5.4805,105.1387 0.4512,100.0605 0.4512,89.85547 L0.4512,89.85547 L0.4512,15.7832 C0.4512,5.627 5.5293,0.4512 15.5879,0.4512 L15.5879,0.4512 Z M40.5391,56.7988 L23.0098,56.7988 C21.252,56.7988 19.9824,58.1172 19.9824,59.8262 C19.9824,61.4863 21.252,62.8047 23.0098,62.8047 L40.5391,62.8047 C42.2969,62.8047 43.5664,61.4863 43.5664,59.8262 C43.5664,58.1172 42.2969,56.7988 40.5391,56.7988 Z M60.0703,39.7578 L23.0098,39.7578 C21.252,39.7578 19.9824,41.0273 19.9824,42.6875 C19.9824,44.3965 21.252,45.7637 23.0098,45.7637 L60.0703,45.7637 C61.7793,45.7637 63.0488,44.3965 63.0488,42.6875 C63.0488,41.0273 61.7793,39.7578 60.0703,39.7578 Z M60.0703,22.7168 L23.0098,22.7168 C21.252,22.7168 19.9824,23.9863 19.9824,25.6465 C19.9824,27.3555 21.252,28.7227 23.0098,28.7227 L60.0703,28.7227 C61.7793,28.7227 63.0488,27.3555 63.0488,25.6465 C63.0488,23.9863 61.7793,22.7168 60.0703,22.7168 Z" id="Shape"></path>
</g>
<g id="Light-M" transform="translate(603.330000, 421.000000)">
<path d="M66.9062,0.793 C76.3789,0.793 81.1641,5.6758 81.1641,15.1973 L81.1641,15.1973 L81.1641,90.49023 C81.1641,100.0117 76.3789,104.8457 66.9062,104.8457 L66.9062,104.8457 L14.709,104.8457 C5.2852,104.8457 0.4512,100.0117 0.4512,90.49023 L0.4512,90.49023 L0.4512,15.1973 C0.4512,5.6758 5.334,0.793 14.709,0.793 L14.709,0.793 Z M40.1484,56.3594 L21.6426,56.3594 C20.2266,56.3594 19.2012,57.4336 19.2012,58.8008 C19.2012,60.168 20.2266,61.1934 21.6426,61.1934 L40.1484,61.1934 C41.5156,61.1934 42.5898,60.168 42.5898,58.8008 C42.5898,57.4336 41.5156,56.3594 40.1484,56.3594 Z M60.0215,39.2695 L21.6426,39.2695 C20.2266,39.2695 19.2012,40.2949 19.2012,41.6133 C19.2012,42.9805 20.2266,44.0547 21.6426,44.0547 L60.0215,44.0547 C61.3887,44.0547 62.4629,42.9805 62.4629,41.6133 C62.4629,40.2949 61.3887,39.2695 60.0215,39.2695 Z M60.0215,22.1309 L21.6426,22.1309 C20.2266,22.1309 19.2012,23.1562 19.2012,24.4746 C19.2012,25.8418 20.2266,26.916 21.6426,26.916 L60.0215,26.916 C61.3887,26.916 62.4629,25.8418 62.4629,24.4746 C62.4629,23.1562 61.3887,22.1309 60.0215,22.1309 Z" id="Shape"></path>
</g>
<g id="Thin-M" transform="translate(307.518000, 422.000000)">
<path d="M66.2227,0.2324 C74.8164,0.2324 79.3574,4.7734 79.3574,13.416 L79.3574,13.416 L79.3574,90.32031 C79.3574,98.9629 74.8164,103.4551 66.2227,103.4551 L66.2227,103.4551 L13.5859,103.4551 C4.9434,103.4551 0.4512,98.9629 0.4512,90.32031 L0.4512,90.32031 L0.4512,13.416 C0.4512,4.7734 5.041,0.2324 13.5859,0.2324 L13.5859,0.2324 Z M39.6113,54.8711 L19.8848,54.8711 C18.9082,54.8711 18.1758,55.6035 18.1758,56.4824 C18.1758,57.3613 18.9082,58.0938 19.8848,58.0938 L39.6113,58.0938 C40.5391,58.0938 41.2715,57.3613 41.2715,56.4824 C41.2715,55.6035 40.5391,54.8711 39.6113,54.8711 Z M59.9238,37.5859 L19.8848,37.5859 C18.9082,37.5859 18.1758,38.3184 18.1758,39.1973 C18.1758,40.0762 18.9082,40.8086 19.8848,40.8086 L59.9238,40.8086 C60.9004,40.8086 61.6328,40.0762 61.6328,39.1973 C61.6328,38.3184 60.9004,37.5859 59.9238,37.5859 Z M59.9238,20.3496 L19.8848,20.3496 C18.9082,20.3496 18.1758,21.0332 18.1758,21.9121 C18.1758,22.8398 18.9082,23.5723 19.8848,23.5723 L59.9238,23.5723 C60.9004,23.5723 61.6328,22.8398 61.6328,21.9121 C61.6328,21.0332 60.9004,20.3496 59.9238,20.3496 Z" id="Shape"></path>
</g>
<g id="Ultralight-M" transform="translate(11.295000, 422.000000)">
<path d="M65.8809,0.4766 C74.0352,0.4766 78.3809,4.8223 78.3809,13.0254 L78.3809,13.0254 L78.3809,90.71094 C78.3809,98.9141 74.0352,103.2598 65.8809,103.2598 L65.8809,103.2598 L13,103.2598 C4.7969,103.2598 0.4512,98.9141 0.4512,90.71094 L0.4512,90.71094 L0.4512,13.0254 C0.4512,4.8223 4.9434,0.4766 13,0.4766 L13,0.4766 Z M39.3184,54.5781 L18.957,54.5781 C18.2246,54.5781 17.6875,55.1152 17.6875,55.7988 C17.6875,56.4824 18.2246,57.0195 18.957,57.0195 L39.3184,57.0195 C40.0508,57.0195 40.5879,56.4824 40.5879,55.7988 C40.5879,55.1152 40.0508,54.5781 39.3184,54.5781 Z M59.9238,37.2441 L18.957,37.2441 C18.2246,37.2441 17.6875,37.7812 17.6875,38.4648 C17.6875,39.1484 18.2246,39.6855 18.957,39.6855 L59.9238,39.6855 C60.6562,39.6855 61.1934,39.1484 61.1934,38.4648 C61.1934,37.7812 60.6562,37.2441 59.9238,37.2441 Z M59.9238,19.9102 L18.957,19.9102 C18.2246,19.9102 17.6875,20.4473 17.6875,21.1309 C17.6875,21.8145 18.2246,22.3516 18.957,22.3516 L59.9238,22.3516 C60.6562,22.3516 61.1934,21.8145 61.1934,21.1309 C61.1934,20.4473 60.6562,19.9102 59.9238,19.9102 Z" id="Shape"></path>
</g>
<g id="Black-S" transform="translate(2387.700000, 0.000000)">
<path d="M57.4062,0.4355 C66.9766,0.4355 72.6406,6.0996 72.6406,15.6699 L72.6406,15.6699 L72.6406,71.91992 C72.6406,81.49023 66.9766,87.1543 57.4062,87.1543 L57.4062,87.1543 L16,87.1543 C6.4297,87.1543 0.76562,81.49023 0.76562,71.91992 L0.76562,71.91992 L0.76562,15.6699 C0.76562,6.0996 6.4297,0.4355 16,0.4355 L16,0.4355 Z M37.8262,47.0176 L25.2285,47.0176 C23.6172,47.0176 22.4453,48.2383 22.4453,49.8008 C22.4453,51.3145 23.6172,52.4863 25.2285,52.4863 L37.8262,52.4863 C39.3887,52.4863 40.5605,51.3145 40.5605,49.8008 C40.5605,48.2383 39.3887,47.0176 37.8262,47.0176 Z M48.2266,35.2988 L25.2285,35.2988 C23.6172,35.2988 22.4453,36.4707 22.4453,37.9844 C22.4453,39.5469 23.6172,40.7676 25.2285,40.7676 L48.2266,40.7676 C49.7891,40.7676 50.9609,39.5469 50.9609,37.9844 C50.9609,36.4707 49.7891,35.2988 48.2266,35.2988 Z M48.2266,23.5801 L25.2285,23.5801 C23.6172,23.5801 22.4453,24.752 22.4453,26.2656 C22.4453,27.8281 23.6172,29.0488 25.2285,29.0488 L48.2266,29.0488 C49.7891,29.0488 50.9609,27.8281 50.9609,26.2656 C50.9609,24.752 49.7891,23.5801 48.2266,23.5801 Z" id="Shape"></path>
</g>
<g id="Heavy-S" transform="translate(2091.790000, 0.000000)">
<path d="M56.4785,0.9727 C65.7559,0.9727 71.0293,6.2461 71.0293,15.5234 L71.0293,15.5234 L71.0293,72.06641 C71.0293,81.34375 65.7559,86.61719 56.4785,86.61719 L56.4785,86.61719 L15.3164,86.61719 C6.0391,86.61719 0.76562,81.34375 0.76562,72.06641 L0.76562,72.06641 L0.76562,15.5234 C0.76562,6.2461 6.0391,0.9727 15.3164,0.9727 L15.3164,0.9727 Z M36.5566,47.0176 L23.7637,47.0176 C22.2012,47.0176 21.0293,48.2383 21.0293,49.8008 C21.0293,51.3145 22.2012,52.4863 23.7637,52.4863 L36.5566,52.4863 C38.0703,52.4863 39.2422,51.3145 39.2422,49.8008 C39.2422,48.2383 38.0703,47.0176 36.5566,47.0176 Z M48.0312,34.957 L23.7637,34.957 C22.2012,34.957 21.0293,36.1289 21.0293,37.5938 C21.0293,39.1562 22.2012,40.377 23.7637,40.377 L48.0312,40.377 C49.5938,40.377 50.7656,39.1562 50.7656,37.5938 C50.7656,36.1289 49.5938,34.957 48.0312,34.957 Z M48.0312,22.8477 L23.7637,22.8477 C22.2012,22.8477 21.0293,24.0195 21.0293,25.5332 C21.0293,27.0469 22.2012,28.2676 23.7637,28.2676 L48.0312,28.2676 C49.5938,28.2676 50.7656,27.0469 50.7656,25.5332 C50.7656,24.0195 49.5938,22.8477 48.0312,22.8477 Z" id="Shape"></path>
</g>
<g id="Bold-S" transform="translate(1795.980000, 1.000000)">
<path d="M55.4531,0.5586 C64.3398,0.5586 69.2227,5.4414 69.2227,14.377 L69.2227,14.377 L69.2227,71.21289 C69.2227,80.14844 64.3398,85.08008 55.4531,85.08008 L55.4531,85.08008 L14.5352,85.08008 C5.6484,85.08008 0.76562,80.14844 0.76562,71.21289 L0.76562,71.21289 L0.76562,14.377 C0.76562,5.4414 5.6484,0.5586 14.5352,0.5586 L14.5352,0.5586 Z M35.0918,46.0664 L22.1523,46.0664 C20.5898,46.0664 19.418,47.2383 19.418,48.8008 C19.418,50.2656 20.5898,51.4375 22.1523,51.4375 L35.0918,51.4375 C36.6543,51.4375 37.7773,50.2656 37.7773,48.8008 C37.7773,47.2383 36.6543,46.0664 35.0918,46.0664 Z M47.8359,33.5664 L22.1523,33.5664 C20.5898,33.5664 19.418,34.6895 19.418,36.2031 C19.418,37.7168 20.5898,38.9375 22.1523,38.9375 L47.8359,38.9375 C49.3984,38.9375 50.5703,37.7168 50.5703,36.2031 C50.5703,34.6895 49.3984,33.5664 47.8359,33.5664 Z M47.8359,21.0664 L22.1523,21.0664 C20.5898,21.0664 19.418,22.1895 19.418,23.7031 C19.418,25.2168 20.5898,26.4375 22.1523,26.4375 L47.8359,26.4375 C49.3984,26.4375 50.5703,25.2168 50.5703,23.7031 C50.5703,22.1895 49.3984,21.0664 47.8359,21.0664 Z" id="Shape"></path>
</g>
<g id="Semibold-S" transform="translate(1499.910000, 1.000000)">
<path d="M54.7695,0.9492 C63.4121,0.9492 67.9531,5.5879 67.9531,14.2793 L67.9531,14.2793 L67.9531,71.31055 C67.9531,80.05078 63.4121,84.64062 54.7695,84.64062 L54.7695,84.64062 L13.998,84.64062 C5.3555,84.64062 0.76562,80.05078 0.76562,71.31055 L0.76562,71.31055 L0.76562,14.2793 C0.76562,5.5879 5.3555,0.9492 13.998,0.9492 L13.998,0.9492 Z M34.1152,46.0664 L21.0781,46.0664 C19.4668,46.0664 18.3438,47.2871 18.3438,48.8008 C18.3438,50.2656 19.4668,51.4375 21.0781,51.4375 L34.1152,51.4375 C35.6289,51.4375 36.8008,50.2656 36.8008,48.8008 C36.8008,47.2871 35.6289,46.0664 34.1152,46.0664 Z M47.7383,33.2734 L21.0781,33.2734 C19.4668,33.2734 18.3438,34.4453 18.3438,35.9102 C18.3438,37.4238 19.4668,38.6445 21.0781,38.6445 L47.7383,38.6445 C49.252,38.6445 50.375,37.4238 50.375,35.9102 C50.375,34.4453 49.252,33.2734 47.7383,33.2734 Z M47.7383,20.4805 L21.0781,20.4805 C19.4668,20.4805 18.3438,21.6523 18.3438,23.1172 C18.3438,24.6309 19.4668,25.8516 21.0781,25.8516 L47.7383,25.8516 C49.252,25.8516 50.375,24.6309 50.375,23.1172 C50.375,21.6523 49.252,20.4805 47.7383,20.4805 Z" id="Shape"></path>
</g>
<g id="Medium-S" transform="translate(1203.660000, 2.000000)">
<path d="M54.2324,0.2422 C62.6797,0.2422 67.0254,4.6855 67.0254,13.1816 L67.0254,13.1816 L67.0254,70.4082 C67.0254,78.95312 62.6797,83.34766 54.2324,83.34766 L54.2324,83.34766 L13.5586,83.34766 C5.1113,83.34766 0.76562,78.95312 0.76562,70.4082 L0.76562,70.4082 L0.76562,13.1816 C0.76562,4.6855 5.1113,0.2422 13.5586,0.2422 L13.5586,0.2422 Z M33.3828,45.0664 L20.1992,45.0664 C18.6367,45.0664 17.5137,46.2871 17.5137,47.8008 C17.5137,49.2656 18.6367,50.3887 20.1992,50.3887 L33.3828,50.3887 C34.8965,50.3887 36.0195,49.2656 36.0195,47.8008 C36.0195,46.2871 34.8965,45.0664 33.3828,45.0664 Z M47.6406,32.0781 L20.1992,32.0781 C18.6367,32.0781 17.5137,33.2012 17.5137,34.666 C17.5137,36.2285 18.6367,37.4004 20.1992,37.4004 L47.6406,37.4004 C49.1543,37.4004 50.2773,36.2285 50.2773,34.666 C50.2773,33.2012 49.1543,32.0781 47.6406,32.0781 Z M47.6406,19.041 L20.1992,19.041 C18.6367,19.041 17.5137,20.2129 17.5137,21.6777 C17.5137,23.1914 18.6367,24.3633 20.1992,24.3633 L47.6406,24.3633 C49.1543,24.3633 50.2773,23.1914 50.2773,21.6777 C50.2773,20.2129 49.1543,19.041 47.6406,19.041 Z" id="Shape"></path>
</g>
<g id="Regular-S" transform="translate(907.560000, 2.000000)">
<path d="M53.5,0.6328 C61.7031,0.6328 65.8047,4.7832 65.8047,13.084 L65.8047,13.084 L65.8047,70.50586 C65.8047,78.85547 61.7031,82.95703 53.5,82.95703 L53.5,82.95703 L13.0215,82.95703 C4.8184,82.95703 0.76562,78.85547 0.76562,70.50586 L0.76562,70.50586 L0.76562,13.084 C0.76562,4.7832 4.8184,0.6328 13.0215,0.6328 L13.0215,0.6328 Z M32.4062,45.1152 L19.125,45.1152 C17.5625,45.1152 16.4395,46.2871 16.4395,47.8008 C16.4395,49.2656 17.5625,50.3887 19.125,50.3887 L32.4062,50.3887 C33.9199,50.3887 35.043,49.2656 35.043,47.8008 C35.043,46.2871 33.9199,45.1152 32.4062,45.1152 Z M47.4941,31.7852 L19.125,31.7852 C17.5625,31.7852 16.4395,32.957 16.4395,34.4219 C16.4395,35.8867 17.5625,37.1074 19.125,37.1074 L47.4941,37.1074 C49.0078,37.1074 50.1309,35.8867 50.1309,34.4219 C50.1309,32.957 49.0078,31.7852 47.4941,31.7852 Z M47.4941,18.5039 L19.125,18.5039 C17.5625,18.5039 16.4395,19.627 16.4395,21.0918 C16.4395,22.6055 17.5625,23.7773 19.125,23.7773 L47.4941,23.7773 C49.0078,23.7773 50.1309,22.6055 50.1309,21.0918 C50.1309,19.627 49.0078,18.5039 47.4941,18.5039 Z" id="Shape"></path>
</g>
<g id="Light-S" transform="translate(611.480000, 2.000000)">
<path d="M53.0605,0.9258 C60.6777,0.9258 64.5352,4.832 64.5352,12.5469 L64.5352,12.5469 L64.5352,71.04297 C64.5352,78.80664 60.6777,82.71289 53.0605,82.71289 L53.0605,82.71289 L12.2891,82.71289 C4.623,82.71289 0.76562,78.80664 0.76562,71.04297 L0.76562,71.04297 L0.76562,12.5469 C0.76562,4.832 4.6719,0.9258 12.2891,0.9258 L12.2891,0.9258 Z M32.0156,44.7734 L17.9043,44.7734 C16.6348,44.7734 15.7559,45.7012 15.7559,46.873 C15.7559,48.0449 16.6348,48.9727 17.9043,48.9727 L32.0156,48.9727 C33.2363,48.9727 34.1641,48.0449 34.1641,46.873 C34.1641,45.7012 33.2363,44.7734 32.0156,44.7734 Z M47.4453,31.3457 L17.9043,31.3457 C16.6348,31.3457 15.7559,32.2734 15.7559,33.4453 C15.7559,34.666 16.6348,35.5938 17.9043,35.5938 L47.4453,35.5938 C48.666,35.5938 49.5938,34.666 49.5938,33.4453 C49.5938,32.2734 48.666,31.3457 47.4453,31.3457 Z M47.4453,17.9668 L17.9043,17.9668 C16.6348,17.9668 15.7559,18.8945 15.7559,20.0664 C15.7559,21.2871 16.6348,22.2148 17.9043,22.2148 L47.4453,22.2148 C48.666,22.2148 49.5938,21.2871 49.5938,20.0664 C49.5938,18.8945 48.666,17.9668 47.4453,17.9668 Z" id="Shape"></path>
</g>
<g id="Thin-S" transform="translate(315.578000, 3.000000)">
<path d="M52.4258,0.3164 C59.3594,0.3164 62.9238,3.9297 62.9238,10.8633 L62.9238,10.8633 L62.9238,70.77539 C62.9238,77.75781 59.3594,81.37109 52.4258,81.37109 L52.4258,81.37109 L11.2637,81.37109 C4.3789,81.37109 0.76562,77.75781 0.76562,70.77539 L0.76562,70.77539 L0.76562,10.8633 C0.76562,3.9297 4.4277,0.3164 11.2637,0.3164 L11.2637,0.3164 Z M31.5273,43.2852 L16.3418,43.2852 C15.4629,43.2852 14.877,43.9199 14.877,44.7012 C14.877,45.4824 15.4629,46.1172 16.3418,46.1172 L31.5273,46.1172 C32.3574,46.1172 32.9922,45.4824 32.9922,44.7012 C32.9922,43.9199 32.3574,43.2852 31.5273,43.2852 Z M47.3965,29.8086 L16.3418,29.8086 C15.4629,29.8086 14.877,30.3945 14.877,31.1758 C14.877,32.0059 15.4629,32.6406 16.3418,32.6406 L47.3965,32.6406 C48.2266,32.6406 48.8613,32.0059 48.8613,31.1758 C48.8613,30.3945 48.2266,29.8086 47.3965,29.8086 Z M47.3965,16.2832 L16.3418,16.2832 C15.4629,16.2832 14.877,16.918 14.877,17.6992 C14.877,18.4805 15.4629,19.1152 16.3418,19.1152 L47.3965,19.1152 C48.2266,19.1152 48.8613,18.4805 48.8613,17.6992 C48.8613,16.918 48.2266,16.2832 47.3965,16.2832 Z" id="Shape"></path>
</g>
<g id="Ultralight-S" transform="translate(19.281000, 3.000000)">
<path d="M52.084,0.5117 C58.627,0.5117 62.0938,3.9785 62.0938,10.5215 L62.0938,10.5215 L62.0938,71.16602 C62.0938,77.70898 58.627,81.17578 52.084,81.17578 L52.084,81.17578 L10.7266,81.17578 C4.2324,81.17578 0.76562,77.70898 0.76562,71.16602 L0.76562,71.16602 L0.76562,10.5215 C0.76562,3.9785 4.3301,0.5117 10.7266,0.5117 L10.7266,0.5117 Z M31.2832,43.041 L15.5117,43.041 C14.877,43.041 14.3887,43.5293 14.3887,44.1152 C14.3887,44.7012 14.877,45.1406 15.5117,45.1406 L31.2832,45.1406 C31.918,45.1406 32.4062,44.7012 32.4062,44.1152 C32.4062,43.5293 31.918,43.041 31.2832,43.041 Z M47.3477,29.4668 L15.5117,29.4668 C14.877,29.4668 14.3887,29.9551 14.3887,30.541 C14.3887,31.127 14.877,31.6152 15.5117,31.6152 L47.3477,31.6152 C47.9824,31.6152 48.4707,31.127 48.4707,30.541 C48.4707,29.9551 47.9824,29.4668 47.3477,29.4668 Z M47.3477,15.9414 L15.5117,15.9414 C14.877,15.9414 14.3887,16.3809 14.3887,16.9668 C14.3887,17.5527 14.877,18.041 15.5117,18.041 L47.3477,18.041 C47.9824,18.041 48.4707,17.5527 48.4707,16.9668 C48.4707,16.3809 47.9824,15.9414 47.3477,15.9414 Z" id="Shape"></path>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 47 KiB

View File

@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "MarsEditOfficial.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "micro-dot-blog.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -1,25 +0,0 @@
{
"images" : [
{
"filename" : "reddit-light.pdf",
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "reddit-dark.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Some files were not shown because too many files have changed in this diff Show More