mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Remove Multiplatform targets
This commit is contained in:
@@ -1,285 +0,0 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// Multiplatform macOS
|
||||
//
|
||||
// Created by Maurice Parker on 6/28/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import os.log
|
||||
import UserNotifications
|
||||
import Articles
|
||||
import RSWeb
|
||||
import Account
|
||||
import RSCore
|
||||
import Secrets
|
||||
|
||||
// If we're not going to import Sparkle, provide dummy protocols to make it easy
|
||||
// for AppDelegate to comply
|
||||
#if MAC_APP_STORE || TEST
|
||||
protocol SPUStandardUserDriverDelegate {}
|
||||
protocol SPUUpdaterDelegate {}
|
||||
#else
|
||||
import Sparkle
|
||||
#endif
|
||||
|
||||
var appDelegate: AppDelegate!
|
||||
|
||||
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate, UnreadCountProvider, SPUStandardUserDriverDelegate, SPUUpdaterDelegate
|
||||
{
|
||||
|
||||
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
|
||||
|
||||
var userNotificationManager: UserNotificationManager!
|
||||
var faviconDownloader: FaviconDownloader!
|
||||
var imageDownloader: ImageDownloader!
|
||||
var authorAvatarDownloader: AuthorAvatarDownloader!
|
||||
var webFeedIconDownloader: WebFeedIconDownloader!
|
||||
|
||||
var refreshTimer: AccountRefreshTimer?
|
||||
var syncTimer: ArticleStatusSyncTimer?
|
||||
|
||||
var shuttingDown = false {
|
||||
didSet {
|
||||
if shuttingDown {
|
||||
refreshTimer?.shuttingDown = shuttingDown
|
||||
refreshTimer?.invalidate()
|
||||
syncTimer?.shuttingDown = shuttingDown
|
||||
syncTimer?.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var unreadCount = 0 {
|
||||
didSet {
|
||||
if unreadCount != oldValue {
|
||||
CoalescingQueue.standard.add(self, #selector(updateDockBadge))
|
||||
postUnreadCountDidChangeNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var appName: String!
|
||||
|
||||
private let appMovementMonitor = RSAppMovementMonitor()
|
||||
#if !MAC_APP_STORE && !TEST
|
||||
var softwareUpdater: SPUUpdater!
|
||||
#endif
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
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)
|
||||
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(didWakeNotification(_:)), name: NSWorkspace.didWakeNotification, object: nil)
|
||||
|
||||
appDelegate = self
|
||||
}
|
||||
|
||||
// MARK: - NSApplicationDelegate
|
||||
|
||||
func applicationWillFinishLaunching(_ notification: Notification) {
|
||||
// TODO: add Apple Events back in
|
||||
// installAppleEventHandlers()
|
||||
|
||||
CacheCleaner.purgeIfNecessary()
|
||||
|
||||
// Try to establish a cache in the Caches folder, but if it fails for some reason fall back to a temporary dir
|
||||
let cacheFolder: String
|
||||
if let userCacheFolder = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).path {
|
||||
cacheFolder = userCacheFolder
|
||||
}
|
||||
else {
|
||||
let bundleIdentifier = (Bundle.main.infoDictionary!["CFBundleIdentifier"]! as! String)
|
||||
cacheFolder = (NSTemporaryDirectory() as NSString).appendingPathComponent(bundleIdentifier)
|
||||
}
|
||||
|
||||
let faviconsFolder = (cacheFolder as NSString).appendingPathComponent("Favicons")
|
||||
let faviconsFolderURL = URL(fileURLWithPath: faviconsFolder)
|
||||
try! FileManager.default.createDirectory(at: faviconsFolderURL, withIntermediateDirectories: true, attributes: nil)
|
||||
faviconDownloader = FaviconDownloader(folder: faviconsFolder)
|
||||
|
||||
let imagesFolder = (cacheFolder as NSString).appendingPathComponent("Images")
|
||||
let imagesFolderURL = URL(fileURLWithPath: imagesFolder)
|
||||
try! FileManager.default.createDirectory(at: imagesFolderURL, withIntermediateDirectories: true, attributes: nil)
|
||||
imageDownloader = ImageDownloader(folder: imagesFolder)
|
||||
|
||||
authorAvatarDownloader = AuthorAvatarDownloader(imageDownloader: imageDownloader)
|
||||
webFeedIconDownloader = WebFeedIconDownloader(imageDownloader: imageDownloader, folder: cacheFolder)
|
||||
|
||||
appName = (Bundle.main.infoDictionary!["CFBundleExecutable"]! as! String)
|
||||
}
|
||||
|
||||
func applicationDidFinishLaunching(_ note: Notification) {
|
||||
|
||||
#if MAC_APP_STORE || TEST
|
||||
checkForUpdatesMenuItem.isHidden = true
|
||||
#else
|
||||
// Initialize Sparkle...
|
||||
let hostBundle = Bundle.main
|
||||
let updateDriver = SPUStandardUserDriver(hostBundle: hostBundle, delegate: self)
|
||||
self.softwareUpdater = SPUUpdater(hostBundle: hostBundle, applicationBundle: hostBundle, userDriver: updateDriver, delegate: self)
|
||||
|
||||
do {
|
||||
try self.softwareUpdater.start()
|
||||
}
|
||||
catch {
|
||||
NSLog("Failed to start software updater with error: \(error)")
|
||||
}
|
||||
#endif
|
||||
|
||||
AppDefaults.registerDefaults()
|
||||
let isFirstRun = AppDefaults.shared.isFirstRun()
|
||||
if isFirstRun {
|
||||
os_log(.debug, log: log, "Is first run.")
|
||||
}
|
||||
let localAccount = AccountManager.shared.defaultAccount
|
||||
|
||||
if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() {
|
||||
DefaultFeedsImporter.importDefaultFeeds(account: localAccount)
|
||||
}
|
||||
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(webFeedSettingDidChange(_:)), name: .WebFeedSettingDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.unreadCount = AccountManager.shared.unreadCount
|
||||
}
|
||||
|
||||
refreshTimer = AccountRefreshTimer()
|
||||
syncTimer = ArticleStatusSyncTimer()
|
||||
|
||||
NSApplication.shared.registerForRemoteNotifications()
|
||||
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
userNotificationManager = UserNotificationManager()
|
||||
|
||||
// TODO: Add a debug menu
|
||||
// if AppDefaults.showDebugMenu {
|
||||
// refreshTimer!.update()
|
||||
// syncTimer!.update()
|
||||
//
|
||||
// // The Web Inspector uses SPI and can never appear in a MAC_APP_STORE build.
|
||||
// #if MAC_APP_STORE
|
||||
// let debugMenu = debugMenuItem.submenu!
|
||||
// let toggleWebInspectorItemIndex = debugMenu.indexOfItem(withTarget: self, andAction: #selector(toggleWebInspectorEnabled(_:)))
|
||||
// if toggleWebInspectorItemIndex != -1 {
|
||||
// debugMenu.removeItem(at: toggleWebInspectorItemIndex)
|
||||
// }
|
||||
// #endif
|
||||
// } else {
|
||||
// debugMenuItem.menu?.removeItem(debugMenuItem)
|
||||
// DispatchQueue.main.async {
|
||||
// self.refreshTimer!.timedRefresh(nil)
|
||||
// self.syncTimer!.timedRefresh(nil)
|
||||
// }
|
||||
// }
|
||||
|
||||
// TODO: Add back in crash reporter
|
||||
// #if !MAC_APP_STORE
|
||||
// DispatchQueue.main.async {
|
||||
// CrashReporter.check(appName: "NetNewsWire")
|
||||
// }
|
||||
// #endif
|
||||
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ notification: Notification) {
|
||||
fireOldTimers()
|
||||
}
|
||||
|
||||
func applicationDidResignActive(_ notification: Notification) {
|
||||
ArticleStringFormatter.emptyCaches()
|
||||
}
|
||||
|
||||
func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) {
|
||||
AccountManager.shared.receiveRemoteNotification(userInfo: userInfo)
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ notification: Notification) {
|
||||
shuttingDown = true
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
@objc func unreadCountDidChange(_ note: Notification) {
|
||||
if note.object is AccountManager {
|
||||
unreadCount = AccountManager.shared.unreadCount
|
||||
}
|
||||
}
|
||||
|
||||
@objc func webFeedSettingDidChange(_ note: Notification) {
|
||||
guard let feed = note.object as? WebFeed, let key = note.userInfo?[WebFeed.WebFeedSettingUserInfoKey] as? String else {
|
||||
return
|
||||
}
|
||||
if key == WebFeed.WebFeedSettingKey.homePageURL || key == WebFeed.WebFeedSettingKey.faviconURL {
|
||||
let _ = faviconDownloader.favicon(for: feed)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func userDefaultsDidChange(_ note: Notification) {
|
||||
refreshTimer?.update()
|
||||
updateDockBadge()
|
||||
}
|
||||
|
||||
@objc func didWakeNotification(_ note: Notification) {
|
||||
fireOldTimers()
|
||||
}
|
||||
|
||||
// MARK: UNUserNotificationCenterDelegate
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
completionHandler([.banner, .badge, .sound])
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
// TODO: Add back in Notification handling
|
||||
// mainWindowController?.handle(response)
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
// MARK: - Dock Badge
|
||||
@objc func updateDockBadge() {
|
||||
let label = unreadCount > 0 && !AppDefaults.shared.hideDockUnreadCount ? "\(unreadCount)" : ""
|
||||
NSApplication.shared.dockTile.badgeLabel = label
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension AppDelegate {
|
||||
|
||||
func fireOldTimers() {
|
||||
// It’s possible there’s a refresh timer set to go off in the past.
|
||||
// In that case, refresh now and update the timer.
|
||||
refreshTimer?.fireOldTimer()
|
||||
syncTimer?.fireOldTimer()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
the ScriptingAppDelegate protocol exposes a narrow set of accessors with
|
||||
internal visibility which are very similar to some private vars.
|
||||
|
||||
These would be unnecessary if the similar accessors were marked internal rather than private,
|
||||
but for now, we'll keep the stratification of visibility
|
||||
*/
|
||||
//extension AppDelegate : ScriptingAppDelegate {
|
||||
//
|
||||
// internal var scriptingMainWindowController: ScriptingMainWindowController? {
|
||||
// return mainWindowController
|
||||
// }
|
||||
//
|
||||
// internal var scriptingCurrentArticle: Article? {
|
||||
// return self.scriptingMainWindowController?.scriptingCurrentArticle
|
||||
// }
|
||||
//
|
||||
// internal var scriptingSelectedArticles: [Article] {
|
||||
// return self.scriptingMainWindowController?.scriptingSelectedArticles ?? []
|
||||
// }
|
||||
//}
|
||||
@@ -1,25 +0,0 @@
|
||||
//
|
||||
// ArticleView.swift
|
||||
// Multiplatform macOS
|
||||
//
|
||||
// Created by Maurice Parker on 7/8/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Articles
|
||||
|
||||
struct ArticleView: NSViewControllerRepresentable {
|
||||
|
||||
@EnvironmentObject private var sceneModel: SceneModel
|
||||
|
||||
func makeNSViewController(context: Context) -> WebViewController {
|
||||
let controller = WebViewController()
|
||||
controller.sceneModel = sceneModel
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateNSViewController(_ controller: WebViewController, context: Context) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
//
|
||||
// IconView.swift
|
||||
// Multiplatform macOS
|
||||
//
|
||||
// Created by Maurice Parker on 7/7/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
|
||||
final class IconView: NSView {
|
||||
|
||||
var iconImage: IconImage? = nil {
|
||||
didSet {
|
||||
if iconImage !== oldValue {
|
||||
imageView.image = iconImage?.image
|
||||
|
||||
if NSApplication.shared.effectiveAppearance.isDarkMode {
|
||||
if self.iconImage?.isDark ?? false {
|
||||
self.isDiscernable = false
|
||||
} else {
|
||||
self.isDiscernable = true
|
||||
}
|
||||
} else {
|
||||
if self.iconImage?.isBright ?? false {
|
||||
self.isDiscernable = false
|
||||
} else {
|
||||
self.isDiscernable = true
|
||||
}
|
||||
}
|
||||
|
||||
needsDisplay = true
|
||||
needsLayout = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isDiscernable = true
|
||||
|
||||
override var isFlipped: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
private let imageView: NSImageView = {
|
||||
let imageView = NSImageView(frame: NSRect.zero)
|
||||
imageView.animates = false
|
||||
imageView.imageAlignment = .alignCenter
|
||||
imageView.imageScaling = .scaleProportionallyUpOrDown
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private var hasExposedVerticalBackground: Bool {
|
||||
return imageView.frame.size.height < bounds.size.height
|
||||
}
|
||||
|
||||
override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
convenience init() {
|
||||
self.init(frame: NSRect.zero)
|
||||
}
|
||||
|
||||
override func viewDidMoveToSuperview() {
|
||||
needsLayout = true
|
||||
needsDisplay = true
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
resizeSubviews(withOldSize: NSZeroSize)
|
||||
}
|
||||
|
||||
override func resizeSubviews(withOldSize oldSize: NSSize) {
|
||||
imageView.setFrame(ifNotEqualTo: rectForImageView())
|
||||
}
|
||||
|
||||
override func draw(_ dirtyRect: NSRect) {
|
||||
guard hasExposedVerticalBackground || !isDiscernable else {
|
||||
return
|
||||
}
|
||||
|
||||
let color = AppAssets.nsIconBackgroundColor
|
||||
color.set()
|
||||
dirtyRect.fill()
|
||||
}
|
||||
}
|
||||
|
||||
private extension IconView {
|
||||
|
||||
func commonInit() {
|
||||
addSubview(imageView)
|
||||
wantsLayer = true
|
||||
layer?.cornerRadius = 4.0
|
||||
}
|
||||
|
||||
func rectForImageView() -> NSRect {
|
||||
guard !(iconImage?.isSymbol ?? false) else {
|
||||
return NSMakeRect(0.0, 0.0, bounds.size.width, bounds.size.height)
|
||||
}
|
||||
|
||||
guard let image = iconImage?.image else {
|
||||
return NSRect.zero
|
||||
}
|
||||
|
||||
let imageSize = image.size
|
||||
let viewSize = bounds.size
|
||||
if imageSize.height == imageSize.width {
|
||||
if imageSize.height >= viewSize.height {
|
||||
return NSMakeRect(0.0, 0.0, viewSize.width, viewSize.height)
|
||||
}
|
||||
let offset = floor((viewSize.height - imageSize.height) / 2.0)
|
||||
return NSMakeRect(offset, offset, imageSize.width, imageSize.height)
|
||||
}
|
||||
else if imageSize.height > imageSize.width {
|
||||
let factor = viewSize.height / imageSize.height
|
||||
let width = imageSize.width * factor
|
||||
let originX = floor((viewSize.width - width) / 2.0)
|
||||
return NSMakeRect(originX, 0.0, width, viewSize.height)
|
||||
}
|
||||
|
||||
// Wider than tall: imageSize.width > imageSize.height
|
||||
let factor = viewSize.width / imageSize.width
|
||||
let height = imageSize.height * factor
|
||||
let originY = floor((viewSize.height - height) / 2.0)
|
||||
return NSMakeRect(0.0, originY, viewSize.width, height)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
//
|
||||
// SharingServiceDelegate.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 9/7/18.
|
||||
// Copyright © 2018 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
|
||||
@objc final class SharingServiceDelegate: NSObject, NSSharingServiceDelegate {
|
||||
|
||||
weak var window: NSWindow?
|
||||
|
||||
init(_ window: NSWindow?) {
|
||||
self.window = window
|
||||
}
|
||||
|
||||
func sharingService(_ sharingService: NSSharingService, willShareItems items: [Any]) {
|
||||
sharingService.subject = items
|
||||
.compactMap { item in
|
||||
let writer = item as? ArticlePasteboardWriter
|
||||
return writer?.article.title
|
||||
}
|
||||
.joined(separator: ", ")
|
||||
}
|
||||
|
||||
func sharingService(_ sharingService: NSSharingService, sourceWindowForShareItems items: [Any], sharingContentScope: UnsafeMutablePointer<NSSharingService.SharingContentScope>) -> NSWindow? {
|
||||
return window
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
//
|
||||
// SharingServicePickerDelegate.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Brent Simmons on 2/17/18.
|
||||
// Copyright © 2018 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import RSCore
|
||||
|
||||
@objc final class SharingServicePickerDelegate: NSObject, NSSharingServicePickerDelegate {
|
||||
|
||||
private let sharingServiceDelegate: SharingServiceDelegate
|
||||
private let completion: (() -> Void)?
|
||||
|
||||
init(_ window: NSWindow?, completion: (() -> Void)?) {
|
||||
self.sharingServiceDelegate = SharingServiceDelegate(window)
|
||||
self.completion = completion
|
||||
}
|
||||
|
||||
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, sharingServicesForItems items: [Any], proposedSharingServices proposedServices: [NSSharingService]) -> [NSSharingService] {
|
||||
let filteredServices = proposedServices.filter { $0.menuItemTitle != "NetNewsWire" }
|
||||
return filteredServices + SharingServicePickerDelegate.customSharingServices(for: items)
|
||||
}
|
||||
|
||||
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, delegateFor sharingService: NSSharingService) -> NSSharingServiceDelegate? {
|
||||
return sharingServiceDelegate
|
||||
}
|
||||
|
||||
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, didChoose service: NSSharingService?) {
|
||||
completion?()
|
||||
}
|
||||
|
||||
static func customSharingServices(for items: [Any]) -> [NSSharingService] {
|
||||
let customServices = ExtensionPointManager.shared.activeSendToCommands.compactMap { (sendToCommand) -> NSSharingService? in
|
||||
|
||||
guard let object = items.first else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard sendToCommand.canSendObject(object, selectedText: nil) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let image = sendToCommand.image ?? NSImage()
|
||||
return NSSharingService(title: sendToCommand.title, image: image, alternateImage: nil) {
|
||||
sendToCommand.sendObject(object, selectedText: nil)
|
||||
}
|
||||
}
|
||||
return customServices
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
//
|
||||
// SharingServiceView.swift
|
||||
// Multiplatform macOS
|
||||
//
|
||||
// Created by Maurice Parker on 7/14/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AppKit
|
||||
import Articles
|
||||
|
||||
class SharingServiceController: NSViewController {
|
||||
|
||||
var sharingServicePickerDelegate: SharingServicePickerDelegate? = nil
|
||||
var articles = [Article]()
|
||||
var completion: (() -> Void)? = nil
|
||||
|
||||
override func loadView() {
|
||||
view = NSView()
|
||||
}
|
||||
|
||||
override func viewDidAppear() {
|
||||
guard let anchor = view.superview?.superview else { return }
|
||||
|
||||
sharingServicePickerDelegate = SharingServicePickerDelegate(self.view.window, completion: completion)
|
||||
|
||||
let sortedArticles = articles.sortedByDate(.orderedAscending)
|
||||
let items = sortedArticles.map { ArticlePasteboardWriter(article: $0) }
|
||||
|
||||
let sharingServicePicker = NSSharingServicePicker(items: items)
|
||||
sharingServicePicker.delegate = sharingServicePickerDelegate
|
||||
|
||||
sharingServicePicker.show(relativeTo: anchor.bounds, of: anchor, preferredEdge: .minY)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct SharingServiceView: NSViewControllerRepresentable {
|
||||
|
||||
var articles: [Article]
|
||||
@Binding var showing: Bool
|
||||
|
||||
func makeNSViewController(context: Context) -> SharingServiceController {
|
||||
let controller = SharingServiceController()
|
||||
controller.articles = articles
|
||||
controller.completion = {
|
||||
showing = false
|
||||
}
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateNSViewController(_ nsViewController: SharingServiceController, context: Context) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
//
|
||||
// WebStatusBarView.swift
|
||||
// Multiplatform macOS
|
||||
//
|
||||
// Created by Maurice Parker on 7/8/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import Articles
|
||||
|
||||
final class WebStatusBarView: NSView {
|
||||
|
||||
var urlLabel = NSTextField(labelWithString: "")
|
||||
|
||||
var mouseoverLink: String? {
|
||||
didSet {
|
||||
updateLinkForDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
private var linkForDisplay: String? {
|
||||
didSet {
|
||||
needsLayout = true
|
||||
if let link = linkForDisplay {
|
||||
urlLabel.stringValue = link
|
||||
self.isHidden = false
|
||||
}
|
||||
else {
|
||||
urlLabel.stringValue = ""
|
||||
self.isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var didConfigureLayerRadius = false
|
||||
|
||||
override var isOpaque: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
override var isFlipped: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override var wantsUpdateLayer: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
override func updateLayer() {
|
||||
guard let layer = layer else {
|
||||
return
|
||||
}
|
||||
if !didConfigureLayerRadius {
|
||||
layer.cornerRadius = 4.0
|
||||
didConfigureLayerRadius = true
|
||||
}
|
||||
|
||||
layer.backgroundColor = AppAssets.webStatusBarBackground.cgColor
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private extension WebStatusBarView {
|
||||
|
||||
func commonInit() {
|
||||
self.isHidden = true
|
||||
urlLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
urlLabel.lineBreakMode = .byTruncatingMiddle
|
||||
urlLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
|
||||
addSubview(urlLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
leadingAnchor.constraint(equalTo: urlLabel.leadingAnchor, constant: -6),
|
||||
trailingAnchor.constraint(equalTo: urlLabel.trailingAnchor, constant: 6),
|
||||
centerYAnchor.constraint(equalTo: urlLabel.centerYAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
func updateLinkForDisplay() {
|
||||
if let mouseoverLink = mouseoverLink, !mouseoverLink.isEmpty {
|
||||
linkForDisplay = mouseoverLink.strippingHTTPOrHTTPSScheme
|
||||
}
|
||||
else {
|
||||
linkForDisplay = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,373 +0,0 @@
|
||||
//
|
||||
// WebViewController.swift
|
||||
// Multiplatform macOS
|
||||
//
|
||||
// Created by Maurice Parker on 7/8/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import Combine
|
||||
import RSCore
|
||||
import Articles
|
||||
|
||||
protocol WebViewControllerDelegate: AnyObject {
|
||||
func webViewController(_: WebViewController, articleExtractorButtonStateDidUpdate: ArticleExtractorButtonState)
|
||||
}
|
||||
|
||||
class WebViewController: NSViewController {
|
||||
|
||||
private struct MessageName {
|
||||
static let imageWasClicked = "imageWasClicked"
|
||||
static let imageWasShown = "imageWasShown"
|
||||
static let mouseDidEnter = "mouseDidEnter"
|
||||
static let mouseDidExit = "mouseDidExit"
|
||||
static let showFeedInspector = "showFeedInspector"
|
||||
}
|
||||
|
||||
var statusBarView: WebStatusBarView!
|
||||
|
||||
private var webView: PreloadedWebView?
|
||||
|
||||
private var articleExtractor: ArticleExtractor? = nil
|
||||
var extractedArticle: ExtractedArticle?
|
||||
var isShowingExtractedArticle = false
|
||||
|
||||
var articleExtractorButtonState: ArticleExtractorButtonState = .off {
|
||||
didSet {
|
||||
delegate?.webViewController(self, articleExtractorButtonStateDidUpdate: articleExtractorButtonState)
|
||||
}
|
||||
}
|
||||
|
||||
var sceneModel: SceneModel?
|
||||
weak var delegate: WebViewControllerDelegate?
|
||||
|
||||
var articles: [Article]? {
|
||||
didSet {
|
||||
if oldValue != articles {
|
||||
loadWebView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
override func loadView() {
|
||||
view = NSView()
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
|
||||
|
||||
statusBarView = WebStatusBarView()
|
||||
statusBarView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.view.addSubview(statusBarView)
|
||||
NSLayoutConstraint.activate([
|
||||
self.view.leadingAnchor.constraint(equalTo: statusBarView.leadingAnchor, constant: -6),
|
||||
self.view.trailingAnchor.constraint(greaterThanOrEqualTo: statusBarView.trailingAnchor, constant: 6),
|
||||
self.view.bottomAnchor.constraint(equalTo: statusBarView.bottomAnchor, constant: 2),
|
||||
statusBarView.heightAnchor.constraint(equalToConstant: 20)
|
||||
])
|
||||
|
||||
sceneModel?.timelineModel.selectedArticlesPublisher?.sink { [weak self] articles in
|
||||
self?.articles = articles
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
||||
reloadArticleImage()
|
||||
}
|
||||
|
||||
@objc func avatarDidBecomeAvailable(_ note: Notification) {
|
||||
reloadArticleImage()
|
||||
}
|
||||
|
||||
@objc func faviconDidBecomeAvailable(_ note: Notification) {
|
||||
reloadArticleImage()
|
||||
}
|
||||
|
||||
// MARK: API
|
||||
|
||||
func focus() {
|
||||
webView?.becomeFirstResponder()
|
||||
}
|
||||
|
||||
func canScrollDown(_ completion: @escaping (Bool) -> Void) {
|
||||
fetchScrollInfo { (scrollInfo) in
|
||||
completion(scrollInfo?.canScrollDown ?? false)
|
||||
}
|
||||
}
|
||||
|
||||
override func scrollPageDown(_ sender: Any?) {
|
||||
webView?.scrollPageDown(sender)
|
||||
}
|
||||
|
||||
func toggleArticleExtractor() {
|
||||
|
||||
guard let article = articles?.first else {
|
||||
return
|
||||
}
|
||||
|
||||
guard articleExtractor?.state != .processing else {
|
||||
stopArticleExtractor()
|
||||
loadWebView()
|
||||
return
|
||||
}
|
||||
|
||||
guard !isShowingExtractedArticle else {
|
||||
isShowingExtractedArticle = false
|
||||
loadWebView()
|
||||
articleExtractorButtonState = .off
|
||||
return
|
||||
}
|
||||
|
||||
if let articleExtractor = articleExtractor {
|
||||
if article.preferredLink == articleExtractor.articleLink {
|
||||
isShowingExtractedArticle = true
|
||||
loadWebView()
|
||||
articleExtractorButtonState = .on
|
||||
}
|
||||
} else {
|
||||
startArticleExtractor()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func stopArticleExtractorIfProcessing() {
|
||||
if articleExtractor?.state == .processing {
|
||||
stopArticleExtractor()
|
||||
}
|
||||
}
|
||||
|
||||
func stopWebViewActivity() {
|
||||
if let webView = webView {
|
||||
stopMediaPlayback(webView)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: ArticleExtractorDelegate
|
||||
|
||||
extension WebViewController: ArticleExtractorDelegate {
|
||||
|
||||
func articleExtractionDidFail(with: Error) {
|
||||
stopArticleExtractor()
|
||||
articleExtractorButtonState = .error
|
||||
loadWebView()
|
||||
}
|
||||
|
||||
func articleExtractionDidComplete(extractedArticle: ExtractedArticle) {
|
||||
if articleExtractor?.state != .cancelled {
|
||||
self.extractedArticle = extractedArticle
|
||||
isShowingExtractedArticle = true
|
||||
loadWebView()
|
||||
articleExtractorButtonState = .on
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: WKScriptMessageHandler
|
||||
|
||||
extension WebViewController: WKScriptMessageHandler {
|
||||
|
||||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
switch message.name {
|
||||
case MessageName.imageWasShown:
|
||||
return
|
||||
case MessageName.imageWasClicked:
|
||||
return
|
||||
case MessageName.mouseDidEnter:
|
||||
if let link = message.body as? String {
|
||||
statusBarView.mouseoverLink = link
|
||||
}
|
||||
case MessageName.mouseDidExit:
|
||||
statusBarView.mouseoverLink = nil
|
||||
case MessageName.showFeedInspector:
|
||||
return
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension WebViewController: WKNavigationDelegate {
|
||||
|
||||
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||
if navigationAction.navigationType == .linkActivated {
|
||||
if let url = navigationAction.request.url {
|
||||
let flags = navigationAction.modifierFlags
|
||||
let invert = flags.contains(.shift) || flags.contains(.command)
|
||||
Browser.open(url.absoluteString, invertPreference: invert)
|
||||
}
|
||||
decisionHandler(.cancel)
|
||||
return
|
||||
}
|
||||
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private extension WebViewController {
|
||||
|
||||
func loadWebView() {
|
||||
if let webView = webView {
|
||||
self.renderPage(webView)
|
||||
return
|
||||
}
|
||||
|
||||
sceneModel?.webViewProvider?.dequeueWebView() { webView in
|
||||
|
||||
webView.ready {
|
||||
|
||||
// Add the webview
|
||||
self.webView = webView
|
||||
|
||||
webView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.view.addSubview(webView, positioned: .below, relativeTo: self.statusBarView)
|
||||
NSLayoutConstraint.activate([
|
||||
self.view.leadingAnchor.constraint(equalTo: webView.leadingAnchor),
|
||||
self.view.trailingAnchor.constraint(equalTo: webView.trailingAnchor),
|
||||
self.view.topAnchor.constraint(equalTo: webView.topAnchor),
|
||||
self.view.bottomAnchor.constraint(equalTo: webView.bottomAnchor)
|
||||
])
|
||||
|
||||
webView.navigationDelegate = self
|
||||
|
||||
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasClicked)
|
||||
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasShown)
|
||||
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.mouseDidEnter)
|
||||
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.mouseDidExit)
|
||||
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.showFeedInspector)
|
||||
|
||||
self.renderPage(webView)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func renderPage(_ webView: PreloadedWebView) {
|
||||
let theme = ArticleThemesManager.shared.currentTheme
|
||||
let rendering: ArticleRenderer.Rendering
|
||||
|
||||
if articles?.count ?? 0 > 1 {
|
||||
rendering = ArticleRenderer.multipleSelectionHTML(theme: theme)
|
||||
} else if let articleExtractor = articleExtractor, articleExtractor.state == .processing {
|
||||
rendering = ArticleRenderer.loadingHTML(theme: theme)
|
||||
} else if let articleExtractor = articleExtractor, articleExtractor.state == .failedToParse, let article = articles?.first {
|
||||
rendering = ArticleRenderer.articleHTML(article: article, theme: theme)
|
||||
} else if let article = articles?.first, let extractedArticle = extractedArticle {
|
||||
if isShowingExtractedArticle {
|
||||
rendering = ArticleRenderer.articleHTML(article: article, extractedArticle: extractedArticle, theme: theme)
|
||||
} else {
|
||||
rendering = ArticleRenderer.articleHTML(article: article, theme: theme)
|
||||
}
|
||||
} else if let article = articles?.first {
|
||||
rendering = ArticleRenderer.articleHTML(article: article, theme: theme)
|
||||
} else {
|
||||
rendering = ArticleRenderer.noSelectionHTML(theme: theme)
|
||||
}
|
||||
|
||||
let substitutions = [
|
||||
"title": rendering.title,
|
||||
"baseURL": rendering.baseURL,
|
||||
"style": rendering.style,
|
||||
"body": rendering.html
|
||||
]
|
||||
|
||||
let html = try! MacroProcessor.renderedText(withTemplate: ArticleRenderer.page.html, substitutions: substitutions)
|
||||
webView.loadHTMLString(html, baseURL: ArticleRenderer.page.baseURL)
|
||||
|
||||
}
|
||||
|
||||
func fetchScrollInfo(_ completion: @escaping (ScrollInfo?) -> Void) {
|
||||
guard let webView = webView else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let javascriptString = "var x = {contentHeight: document.body.scrollHeight, offsetY: window.pageYOffset}; x"
|
||||
|
||||
webView.evaluateJavaScript(javascriptString) { (info, error) in
|
||||
guard let info = info as? [String: Any] else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
guard let contentHeight = info["contentHeight"] as? CGFloat, let offsetY = info["offsetY"] as? CGFloat else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let scrollInfo = ScrollInfo(contentHeight: contentHeight, viewHeight: webView.frame.height, offsetY: offsetY)
|
||||
completion(scrollInfo)
|
||||
}
|
||||
}
|
||||
|
||||
func startArticleExtractor() {
|
||||
if let link = articles?.first?.preferredLink, let extractor = ArticleExtractor(link) {
|
||||
extractor.delegate = self
|
||||
extractor.process()
|
||||
articleExtractor = extractor
|
||||
articleExtractorButtonState = .animated
|
||||
}
|
||||
}
|
||||
|
||||
func stopArticleExtractor() {
|
||||
articleExtractor?.cancel()
|
||||
articleExtractor = nil
|
||||
isShowingExtractedArticle = false
|
||||
articleExtractorButtonState = .off
|
||||
}
|
||||
|
||||
func reloadArticleImage() {
|
||||
guard let article = articles?.first else { return }
|
||||
|
||||
var components = URLComponents()
|
||||
components.scheme = ArticleRenderer.imageIconScheme
|
||||
components.path = article.articleID
|
||||
|
||||
if let imageSrc = components.string {
|
||||
webView?.evaluateJavaScript("reloadArticleImage(\"\(imageSrc)\")")
|
||||
}
|
||||
}
|
||||
|
||||
func stopMediaPlayback(_ webView: WKWebView) {
|
||||
webView.evaluateJavaScript("stopMediaPlayback();")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - ScrollInfo
|
||||
|
||||
private struct ScrollInfo {
|
||||
|
||||
let contentHeight: CGFloat
|
||||
let viewHeight: CGFloat
|
||||
let offsetY: CGFloat
|
||||
let canScrollDown: Bool
|
||||
let canScrollUp: Bool
|
||||
|
||||
init(contentHeight: CGFloat, viewHeight: CGFloat, offsetY: CGFloat) {
|
||||
self.contentHeight = contentHeight
|
||||
self.viewHeight = viewHeight
|
||||
self.offsetY = offsetY
|
||||
|
||||
self.canScrollDown = viewHeight + offsetY < contentHeight
|
||||
self.canScrollUp = offsetY > 0.1
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
//
|
||||
// Browser.swift
|
||||
// Evergren
|
||||
//
|
||||
// Created by Brent Simmons on 2/23/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RSWeb
|
||||
|
||||
struct Browser {
|
||||
|
||||
/// The user-specified default browser for opening web pages.
|
||||
///
|
||||
/// The user-assigned default browser, or `nil` if none was assigned
|
||||
/// (i.e., the system default should be used).
|
||||
static var defaultBrowser: MacWebBrowser? {
|
||||
if let bundleID = AppDefaults.shared.defaultBrowserID, let browser = MacWebBrowser(bundleIdentifier: bundleID) {
|
||||
return browser
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
/// Opens a URL in the default browser.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - urlString: The URL to open.
|
||||
/// - invert: Whether to invert the "open in background in browser" preference
|
||||
static func open(_ urlString: String, invertPreference invert: Bool = false) {
|
||||
// Opens according to prefs.
|
||||
open(urlString, inBackground: invert ? !AppDefaults.shared.openInBrowserInBackground : AppDefaults.shared.openInBrowserInBackground)
|
||||
}
|
||||
|
||||
|
||||
/// Opens a URL in the default browser.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - urlString: The URL to open.
|
||||
/// - inBackground: Whether to open the URL in the background or not.
|
||||
/// - 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(string: urlString) {
|
||||
if let defaultBrowser = defaultBrowser {
|
||||
defaultBrowser.openURL(url, inBackground: inBackground)
|
||||
} else {
|
||||
MacWebBrowser.openURL(url, inBackground: inBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Browser {
|
||||
|
||||
static var titleForOpenInBrowserInverted: String {
|
||||
let openInBackgroundPref = AppDefaults.shared.openInBrowserInBackground
|
||||
|
||||
return openInBackgroundPref ?
|
||||
NSLocalizedString("Open in Browser in Foreground", comment: "Open in Browser in Foreground menu item title") :
|
||||
NSLocalizedString("Open in Browser in Background", comment: "Open in Browser in Background menu item title")
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2002-2021 Ranchero Software. All rights reserved.</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UserAgent</key>
|
||||
<string>NetNewsWire (RSS Reader; https://netnewswire.com/)</string>
|
||||
<key>OrganizationIdentifier</key>
|
||||
<string>$(ORGANIZATION_IDENTIFIER)</string>
|
||||
<key>DeveloperEntitlements</key>
|
||||
<string>$(DEVELOPER_ENTITLEMENTS)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>RSS Feed</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>feed</string>
|
||||
<string>feeds</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>SUFeedURL</key>
|
||||
<string>https://ranchero.com/downloads/netnewswire-release.xml</string>
|
||||
<key>FeedURLForTestBuilds</key>
|
||||
<string>https://ranchero.com/downloads/netnewswire-beta.xml</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,49 +0,0 @@
|
||||
//
|
||||
// MacSearchField.swift
|
||||
// Multiplatform macOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 29/6/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
|
||||
final class MacSearchField: NSViewRepresentable {
|
||||
|
||||
typealias NSViewType = NSSearchField
|
||||
|
||||
|
||||
func makeNSView(context: Context) -> NSSearchField {
|
||||
let searchField = NSSearchField()
|
||||
searchField.delegate = context.coordinator
|
||||
return searchField
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: NSSearchField, context: Context) {
|
||||
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, NSSearchFieldDelegate {
|
||||
var parent: MacSearchField
|
||||
|
||||
init(_ parent: MacSearchField) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func searchFieldDidStartSearching(_ sender: NSSearchField) {
|
||||
//
|
||||
}
|
||||
|
||||
func searchFieldDidEndSearching(_ sender: NSSearchField) {
|
||||
//
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
//
|
||||
// MacPreferencesView.swift
|
||||
// macOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 27/6/20.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum MacPreferencePane: Int, CaseIterable {
|
||||
case general = 1
|
||||
case accounts = 2
|
||||
case viewing = 3
|
||||
case advanced = 4
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .general:
|
||||
return "General"
|
||||
case .accounts:
|
||||
return "Accounts"
|
||||
case .viewing:
|
||||
return "Appearance"
|
||||
case .advanced:
|
||||
return "Advanced"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
//
|
||||
// AccountsPreferencesModel.swift
|
||||
// Multiplatform macOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 13/7/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Account
|
||||
import Combine
|
||||
|
||||
public enum AccountConfigurationSheets: Equatable {
|
||||
case addAccountPicker, addSelectedAccount(AccountType), credentials, none
|
||||
|
||||
public static func == (lhs: AccountConfigurationSheets, rhs: AccountConfigurationSheets) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (let .addSelectedAccount(lhsType), let .addSelectedAccount(rhsType)):
|
||||
return lhsType == rhsType
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class AccountsPreferencesModel: ObservableObject {
|
||||
|
||||
// Selected Account
|
||||
public private(set) var account: Account?
|
||||
|
||||
// All Accounts
|
||||
@Published var sortedAccounts: [Account] = []
|
||||
@Published var selectedConfiguredAccountID: String? = AccountManager.shared.defaultAccount.accountID {
|
||||
didSet {
|
||||
if let accountID = selectedConfiguredAccountID {
|
||||
account = sortedAccounts.first(where: { $0.accountID == accountID })
|
||||
accountIsActive = account?.isActive ?? false
|
||||
accountName = account?.name ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@Published var showAddAccountView: Bool = false
|
||||
var selectedAccountIsDefault: Bool {
|
||||
guard let selected = selectedConfiguredAccountID else {
|
||||
return true
|
||||
}
|
||||
if selected == AccountManager.shared.defaultAccount.accountID {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Edit Account
|
||||
@Published var accountIsActive: Bool = false {
|
||||
didSet {
|
||||
account?.isActive = accountIsActive
|
||||
}
|
||||
}
|
||||
@Published var accountName: String = "" {
|
||||
didSet {
|
||||
account?.name = accountName
|
||||
}
|
||||
}
|
||||
|
||||
// Sheets
|
||||
@Published var showSheet: Bool = false
|
||||
@Published var sheetToShow: AccountConfigurationSheets = .none {
|
||||
didSet {
|
||||
if sheetToShow == .none { showSheet = false } else { showSheet = true }
|
||||
}
|
||||
}
|
||||
@Published var showDeleteConfirmation: Bool = false
|
||||
|
||||
// Subscriptions
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init() {
|
||||
sortedAccounts = AccountManager.shared.sortedAccounts
|
||||
|
||||
NotificationCenter.default.publisher(for: .UserDidAddAccount).sink { [weak self] _ in
|
||||
self?.sortedAccounts = AccountManager.shared.sortedAccounts
|
||||
}.store(in: &cancellables)
|
||||
|
||||
NotificationCenter.default.publisher(for: .UserDidDeleteAccount).sink { [weak self] _ in
|
||||
self?.selectedConfiguredAccountID = nil
|
||||
self?.sortedAccounts = AccountManager.shared.sortedAccounts
|
||||
self?.selectedConfiguredAccountID = AccountManager.shared.defaultAccount.accountID
|
||||
}.store(in: &cancellables)
|
||||
|
||||
NotificationCenter.default.publisher(for: .AccountStateDidChange).sink { [weak self] notification in
|
||||
guard let account = notification.object as? Account else {
|
||||
return
|
||||
}
|
||||
if account.accountID == self?.account?.accountID {
|
||||
self?.account = account
|
||||
self?.accountIsActive = account.isActive
|
||||
self?.accountName = account.name ?? ""
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
//
|
||||
// AccountsPreferencesView.swift
|
||||
// macOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 27/6/20.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
|
||||
|
||||
struct AccountsPreferencesView: View {
|
||||
|
||||
@StateObject var viewModel = AccountsPreferencesModel()
|
||||
@State private var hoverOnAdd: Bool = false
|
||||
@State private var hoverOnRemove: Bool = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
listOfAccounts
|
||||
|
||||
AccountDetailView(viewModel: viewModel)
|
||||
.frame(height: 300, alignment: .leading)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showSheet,
|
||||
onDismiss: { viewModel.sheetToShow = .none },
|
||||
content: {
|
||||
switch viewModel.sheetToShow {
|
||||
case .addAccountPicker:
|
||||
AddAccountView(accountToAdd: $viewModel.sheetToShow)
|
||||
case .credentials:
|
||||
EditAccountCredentialsView(viewModel: viewModel)
|
||||
case .none:
|
||||
EmptyView()
|
||||
case .addSelectedAccount(let type):
|
||||
switch type {
|
||||
case .onMyMac:
|
||||
AddLocalAccountView()
|
||||
case .feedbin:
|
||||
AddFeedbinAccountView()
|
||||
case .cloudKit:
|
||||
AddCloudKitAccountView()
|
||||
case .feedWrangler:
|
||||
AddFeedWranglerAccountView()
|
||||
case .newsBlur:
|
||||
AddNewsBlurAccountView()
|
||||
case .feedly:
|
||||
AddFeedlyAccountView()
|
||||
default:
|
||||
AddReaderAPIAccountView(accountType: type)
|
||||
}
|
||||
}
|
||||
})
|
||||
.alert(isPresented: $viewModel.showDeleteConfirmation, content: {
|
||||
Alert(title: Text("Delete \(viewModel.account!.nameForDisplay)?"),
|
||||
message: Text("Are you sure you want to delete the account \"\(viewModel.account!.nameForDisplay)\"? This can not be undone."),
|
||||
primaryButton: .destructive(Text("Delete"), action: {
|
||||
AccountManager.shared.deleteAccount(viewModel.account!)
|
||||
viewModel.showDeleteConfirmation = false
|
||||
}),
|
||||
secondaryButton: .cancel({
|
||||
viewModel.showDeleteConfirmation = false
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
var listOfAccounts: some View {
|
||||
VStack(alignment: .leading) {
|
||||
List(viewModel.sortedAccounts, id: \.accountID, selection: $viewModel.selectedConfiguredAccountID) {
|
||||
ConfiguredAccountRow(account: $0)
|
||||
.id($0.accountID)
|
||||
}.overlay(
|
||||
Group {
|
||||
bottomButtonStack
|
||||
}, alignment: .bottom)
|
||||
}
|
||||
.frame(width: 160, height: 300, alignment: .leading)
|
||||
.border(Color.gray, width: 1)
|
||||
}
|
||||
|
||||
var bottomButtonStack: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Divider()
|
||||
HStack(alignment: .center, spacing: 4) {
|
||||
Button(action: {
|
||||
viewModel.sheetToShow = .addAccountPicker
|
||||
}, label: {
|
||||
Image(systemName: "plus")
|
||||
.font(.title)
|
||||
.frame(width: 30, height: 30)
|
||||
.overlay(RoundedRectangle(cornerRadius: 4, style: .continuous)
|
||||
.foregroundColor(hoverOnAdd ? Color.gray.opacity(0.1) : Color.clear))
|
||||
.padding(4)
|
||||
})
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
.onHover { hovering in
|
||||
hoverOnAdd = hovering
|
||||
}
|
||||
.help("Add Account")
|
||||
|
||||
Button(action: {
|
||||
viewModel.showDeleteConfirmation = true
|
||||
}, label: {
|
||||
Image(systemName: "minus")
|
||||
.font(.title)
|
||||
.frame(width: 30, height: 30)
|
||||
.overlay(RoundedRectangle(cornerRadius: 4, style: .continuous)
|
||||
.foregroundColor(hoverOnRemove ? Color.gray.opacity(0.1) : Color.clear))
|
||||
.padding(4)
|
||||
})
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
.onHover { hovering in
|
||||
hoverOnRemove = hovering
|
||||
}
|
||||
.disabled(viewModel.selectedAccountIsDefault)
|
||||
.help("Delete Account")
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.background(Color.init(.windowBackgroundColor))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
//
|
||||
// AddAccountView.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Stuart Breckenridge on 28/10/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
|
||||
enum AddAccountSections: Int, CaseIterable {
|
||||
case local = 0
|
||||
case icloud
|
||||
case web
|
||||
case selfhosted
|
||||
case allOrdered
|
||||
|
||||
var sectionHeader: String {
|
||||
switch self {
|
||||
case .local:
|
||||
return NSLocalizedString("Local", comment: "Local Account")
|
||||
case .icloud:
|
||||
return NSLocalizedString("iCloud", comment: "iCloud Account")
|
||||
case .web:
|
||||
return NSLocalizedString("Web", comment: "Web Account")
|
||||
case .selfhosted:
|
||||
return NSLocalizedString("Self-hosted", comment: "Self hosted Account")
|
||||
case .allOrdered:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
var sectionFooter: String {
|
||||
switch self {
|
||||
case .local:
|
||||
return NSLocalizedString("Local accounts do not sync feeds across devices.", comment: "Local Account")
|
||||
case .icloud:
|
||||
return NSLocalizedString("Your iCloud account syncs your feeds across your Mac and iOS devices.", comment: "iCloud Account")
|
||||
case .web:
|
||||
return NSLocalizedString("Web accounts sync your feeds across all your devices.", comment: "Web Account")
|
||||
case .selfhosted:
|
||||
return NSLocalizedString("Self-hosted accounts sync your feeds across all your devices.", comment: "Self hosted Account")
|
||||
case .allOrdered:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
var sectionContent: [AccountType] {
|
||||
switch self {
|
||||
case .local:
|
||||
return [.onMyMac]
|
||||
case .icloud:
|
||||
return [.cloudKit]
|
||||
case .web:
|
||||
#if DEBUG
|
||||
return [.bazQux, .feedbin, .feedly, .feedWrangler, .inoreader, .newsBlur, .theOldReader]
|
||||
#else
|
||||
return [.bazQux, .feedbin, .feedly, .feedWrangler, .inoreader, .newsBlur, .theOldReader]
|
||||
#endif
|
||||
case .selfhosted:
|
||||
return [.freshRSS]
|
||||
case .allOrdered:
|
||||
return AddAccountSections.local.sectionContent +
|
||||
AddAccountSections.icloud.sectionContent +
|
||||
AddAccountSections.web.sectionContent +
|
||||
AddAccountSections.selfhosted.sectionContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AddAccountView: View {
|
||||
|
||||
@State private var selectedAccount: AccountType = .onMyMac
|
||||
@Binding public var accountToAdd: AccountConfigurationSheets
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Choose an account type to add...")
|
||||
.font(.headline)
|
||||
.padding()
|
||||
|
||||
localAccount
|
||||
icloudAccount
|
||||
webAccounts
|
||||
selfhostedAccounts
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Spacer()
|
||||
if #available(OSX 11.0, *) {
|
||||
Button(action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}, label: {
|
||||
Text("Cancel")
|
||||
.frame(width: 80)
|
||||
})
|
||||
.help("Cancel")
|
||||
.keyboardShortcut(.cancelAction)
|
||||
|
||||
} else {
|
||||
Button(action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}, label: {
|
||||
Text("Cancel")
|
||||
.frame(width: 80)
|
||||
})
|
||||
.accessibility(label: Text("Add Account"))
|
||||
}
|
||||
if #available(OSX 11.0, *) {
|
||||
Button(action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
|
||||
accountToAdd = AccountConfigurationSheets.addSelectedAccount(selectedAccount)
|
||||
})
|
||||
}, label: {
|
||||
Text("Continue")
|
||||
.frame(width: 80)
|
||||
})
|
||||
.help("Add Account")
|
||||
.keyboardShortcut(.defaultAction)
|
||||
|
||||
} else {
|
||||
Button(action: {
|
||||
accountToAdd = AccountConfigurationSheets.addSelectedAccount(selectedAccount)
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
|
||||
}, label: {
|
||||
Text("Continue")
|
||||
.frame(width: 80)
|
||||
})
|
||||
}
|
||||
}
|
||||
.padding(.top, 12)
|
||||
.padding(.bottom, 4)
|
||||
}
|
||||
.pickerStyle(RadioGroupPickerStyle())
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(width: 420)
|
||||
.padding()
|
||||
}
|
||||
|
||||
var localAccount: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Local")
|
||||
.font(.headline)
|
||||
.padding(.horizontal)
|
||||
|
||||
Picker(selection: $selectedAccount, label: Text(""), content: {
|
||||
ForEach(AddAccountSections.local.sectionContent, id: \.self, content: { account in
|
||||
HStack(alignment: .center) {
|
||||
account.image()
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 25, height: 25, alignment: .center)
|
||||
.padding(.leading, 4)
|
||||
Text(account.localizedAccountName())
|
||||
}
|
||||
.tag(account)
|
||||
})
|
||||
})
|
||||
.pickerStyle(RadioGroupPickerStyle())
|
||||
.offset(x: 7.5, y: 0)
|
||||
|
||||
Text(AddAccountSections.local.sectionFooter).foregroundColor(.gray)
|
||||
.font(.caption)
|
||||
.padding(.horizontal)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var icloudAccount: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("iCloud")
|
||||
.font(.headline)
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 8)
|
||||
|
||||
Picker(selection: $selectedAccount, label: Text(""), content: {
|
||||
ForEach(AddAccountSections.icloud.sectionContent, id: \.self, content: { account in
|
||||
HStack(alignment: .center) {
|
||||
account.image()
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 25, height: 25, alignment: .center)
|
||||
.padding(.leading, 4)
|
||||
|
||||
Text(account.localizedAccountName())
|
||||
}
|
||||
.tag(account)
|
||||
})
|
||||
})
|
||||
.offset(x: 7.5, y: 0)
|
||||
.disabled(isCloudInUse())
|
||||
|
||||
Text(AddAccountSections.icloud.sectionFooter).foregroundColor(.gray)
|
||||
.font(.caption)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
|
||||
var webAccounts: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Web")
|
||||
.font(.headline)
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 8)
|
||||
|
||||
Picker(selection: $selectedAccount, label: Text(""), content: {
|
||||
ForEach(AddAccountSections.web.sectionContent, id: \.self, content: { account in
|
||||
|
||||
HStack(alignment: .center) {
|
||||
account.image()
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 25, height: 25, alignment: .center)
|
||||
.padding(.leading, 4)
|
||||
|
||||
Text(account.localizedAccountName())
|
||||
}
|
||||
.tag(account)
|
||||
|
||||
})
|
||||
})
|
||||
.offset(x: 7.5, y: 0)
|
||||
|
||||
Text(AddAccountSections.web.sectionFooter).foregroundColor(.gray)
|
||||
.font(.caption)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
|
||||
var selfhostedAccounts: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Self-hosted")
|
||||
.font(.headline)
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 8)
|
||||
|
||||
Picker(selection: $selectedAccount, label: Text(""), content: {
|
||||
ForEach(AddAccountSections.selfhosted.sectionContent, id: \.self, content: { account in
|
||||
HStack(alignment: .center) {
|
||||
account.image()
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 25, height: 25, alignment: .center)
|
||||
.padding(.leading, 4)
|
||||
|
||||
Text(account.localizedAccountName())
|
||||
}.tag(account)
|
||||
})
|
||||
})
|
||||
.offset(x: 7.5, y: 0)
|
||||
|
||||
Text(AddAccountSections.selfhosted.sectionFooter).foregroundColor(.gray)
|
||||
.font(.caption)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
|
||||
private func isCloudInUse() -> Bool {
|
||||
AccountManager.shared.accounts.contains(where: { $0.type == .cloudKit })
|
||||
}
|
||||
|
||||
private func isRestricted(_ accountType: AccountType) -> Bool {
|
||||
if AppDefaults.shared.isDeveloperBuild && (accountType == .feedly || accountType == .feedWrangler || accountType == .inoreader) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
//
|
||||
// ConfiguredAccountRow.swift
|
||||
// Multiplatform macOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 13/7/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
|
||||
struct ConfiguredAccountRow: View {
|
||||
|
||||
var account: Account
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center) {
|
||||
if let img = account.smallIcon?.image {
|
||||
Image(rsImage: img)
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
}
|
||||
Text(account.nameForDisplay)
|
||||
}.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
//
|
||||
// AccountDetailView.swift
|
||||
// Multiplatform macOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 14/7/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
import Combine
|
||||
|
||||
struct AccountDetailView: View {
|
||||
|
||||
@ObservedObject var viewModel: AccountsPreferencesModel
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 8, style: .circular)
|
||||
.foregroundColor(Color.secondary.opacity(0.1))
|
||||
.padding(.top, 8)
|
||||
|
||||
VStack {
|
||||
editAccountHeader
|
||||
if viewModel.account != nil {
|
||||
editAccountForm
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var editAccountHeader: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("Account Information", action: {})
|
||||
Spacer()
|
||||
}
|
||||
.padding([.leading, .trailing, .bottom], 4)
|
||||
}
|
||||
|
||||
var editAccountForm: some View {
|
||||
Form(content: {
|
||||
HStack(alignment: .top) {
|
||||
Text("Type: ")
|
||||
.frame(width: 50)
|
||||
VStack(alignment: .leading) {
|
||||
Text(viewModel.account!.defaultName)
|
||||
Toggle("Active", isOn: $viewModel.accountIsActive)
|
||||
}
|
||||
}
|
||||
HStack(alignment: .top) {
|
||||
Text("Name: ")
|
||||
.frame(width: 50)
|
||||
VStack(alignment: .leading) {
|
||||
TextField(viewModel.account!.name ?? "", text: $viewModel.accountName)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
Text("The name appears in the sidebar. It can be anything you want. You can even use emoji. 🎸")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
if viewModel.account?.type != .onMyMac {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("Credentials", action: {
|
||||
viewModel.sheetToShow = .credentials
|
||||
})
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
})
|
||||
.padding()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct AccountDetailView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AccountDetailView(viewModel: AccountsPreferencesModel())
|
||||
}
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
//
|
||||
// EditAccountCredentialsModel.swift
|
||||
// Multiplatform macOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 14/7/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Account
|
||||
import Secrets
|
||||
import RSCore
|
||||
|
||||
class EditAccountCredentialsModel: ObservableObject {
|
||||
|
||||
@Published var userName: String = ""
|
||||
@Published var password: String = ""
|
||||
@Published var apiUrl: String = ""
|
||||
@Published var accountIsUpdatingCredentials: Bool = false
|
||||
@Published var accountCredentialsWereUpdated: Bool = false
|
||||
@Published var error: AccountUpdateErrors = .none {
|
||||
didSet {
|
||||
if error == .none {
|
||||
showError = false
|
||||
} else {
|
||||
showError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@Published var showError: Bool = false
|
||||
|
||||
func updateAccountCredentials(_ account: Account) {
|
||||
switch account.type {
|
||||
case .onMyMac:
|
||||
return
|
||||
case .feedbin:
|
||||
updateFeedbin(account)
|
||||
case .cloudKit:
|
||||
return
|
||||
case .feedWrangler:
|
||||
updateFeedWrangler(account)
|
||||
case .feedly:
|
||||
updateFeedly(account)
|
||||
case .freshRSS:
|
||||
updateReaderAccount(account)
|
||||
case .newsBlur:
|
||||
updateNewsblur(account)
|
||||
case .inoreader:
|
||||
updateReaderAccount(account)
|
||||
case .bazQux:
|
||||
updateReaderAccount(account)
|
||||
case .theOldReader:
|
||||
updateReaderAccount(account)
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveCredentials(_ account: Account) {
|
||||
switch account.type {
|
||||
case .feedbin:
|
||||
let credentials = try? account.retrieveCredentials(type: .basic)
|
||||
userName = credentials?.username ?? ""
|
||||
case .feedWrangler:
|
||||
let credentials = try? account.retrieveCredentials(type: .feedWranglerBasic)
|
||||
userName = credentials?.username ?? ""
|
||||
case .feedly:
|
||||
return
|
||||
case .freshRSS:
|
||||
let credentials = try? account.retrieveCredentials(type: .readerBasic)
|
||||
userName = credentials?.username ?? ""
|
||||
case .newsBlur:
|
||||
let credentials = try? account.retrieveCredentials(type: .newsBlurBasic)
|
||||
userName = credentials?.username ?? ""
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK:- Update API
|
||||
extension EditAccountCredentialsModel {
|
||||
|
||||
func updateFeedbin(_ account: Account) {
|
||||
accountIsUpdatingCredentials = true
|
||||
let credentials = Credentials(type: .basic, username: userName, secret: password)
|
||||
|
||||
Account.validateCredentials(type: .feedbin, credentials: credentials) { [weak self] result in
|
||||
|
||||
guard let self = self else { return }
|
||||
|
||||
self.accountIsUpdatingCredentials = false
|
||||
|
||||
switch result {
|
||||
case .success(let validatedCredentials):
|
||||
|
||||
guard let validatedCredentials = validatedCredentials else {
|
||||
self.error = .invalidUsernamePassword
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try account.removeCredentials(type: .basic)
|
||||
try account.storeCredentials(validatedCredentials)
|
||||
self.accountCredentialsWereUpdated = true
|
||||
account.refreshAll(completion: { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
self.error = .other(error: error)
|
||||
}
|
||||
})
|
||||
|
||||
} catch {
|
||||
self.error = .keyChainError
|
||||
}
|
||||
|
||||
case .failure:
|
||||
self.error = .networkError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateFeedWrangler(_ account: Account) {
|
||||
accountIsUpdatingCredentials = true
|
||||
let credentials = Credentials(type: .feedWranglerBasic, username: userName, secret: password)
|
||||
|
||||
Account.validateCredentials(type: .feedWrangler, credentials: credentials) { [weak self] result in
|
||||
|
||||
guard let self = self else { return }
|
||||
|
||||
self.accountIsUpdatingCredentials = false
|
||||
|
||||
switch result {
|
||||
case .success(let validatedCredentials):
|
||||
|
||||
guard let validatedCredentials = validatedCredentials else {
|
||||
self.error = .invalidUsernamePassword
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try account.removeCredentials(type: .feedWranglerBasic)
|
||||
try account.removeCredentials(type: .feedWranglerToken)
|
||||
try account.storeCredentials(credentials)
|
||||
try account.storeCredentials(validatedCredentials)
|
||||
self.accountCredentialsWereUpdated = true
|
||||
account.refreshAll(completion: { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
self.error = .other(error: error)
|
||||
}
|
||||
})
|
||||
|
||||
} catch {
|
||||
self.error = .keyChainError
|
||||
}
|
||||
|
||||
case .failure:
|
||||
self.error = .networkError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateFeedly(_ account: Account) {
|
||||
accountIsUpdatingCredentials = true
|
||||
let updateAccount = OAuthAccountAuthorizationOperation(accountType: .feedly)
|
||||
updateAccount.delegate = self
|
||||
#if os(macOS)
|
||||
updateAccount.presentationAnchor = NSApplication.shared.windows.last
|
||||
#endif
|
||||
MainThreadOperationQueue.shared.add(updateAccount)
|
||||
}
|
||||
|
||||
func updateReaderAccount(_ account: Account) {
|
||||
accountIsUpdatingCredentials = true
|
||||
let credentials = Credentials(type: .readerBasic, username: userName, secret: password)
|
||||
|
||||
Account.validateCredentials(type: account.type, credentials: credentials) { [weak self] result in
|
||||
|
||||
guard let self = self else { return }
|
||||
|
||||
self.accountIsUpdatingCredentials = false
|
||||
|
||||
switch result {
|
||||
case .success(let validatedCredentials):
|
||||
|
||||
guard let validatedCredentials = validatedCredentials else {
|
||||
self.error = .invalidUsernamePassword
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try account.removeCredentials(type: .readerBasic)
|
||||
try account.removeCredentials(type: .readerAPIKey)
|
||||
try account.storeCredentials(credentials)
|
||||
try account.storeCredentials(validatedCredentials)
|
||||
self.accountCredentialsWereUpdated = true
|
||||
account.refreshAll(completion: { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
self.error = .other(error: error)
|
||||
}
|
||||
})
|
||||
|
||||
} catch {
|
||||
self.error = .keyChainError
|
||||
}
|
||||
|
||||
case .failure:
|
||||
self.error = .networkError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNewsblur(_ account: Account) {
|
||||
accountIsUpdatingCredentials = true
|
||||
let credentials = Credentials(type: .newsBlurBasic, username: userName, secret: password)
|
||||
|
||||
Account.validateCredentials(type: .newsBlur, credentials: credentials) { [weak self] result in
|
||||
|
||||
guard let self = self else { return }
|
||||
|
||||
self.accountIsUpdatingCredentials = false
|
||||
|
||||
switch result {
|
||||
case .success(let validatedCredentials):
|
||||
|
||||
guard let validatedCredentials = validatedCredentials else {
|
||||
self.error = .invalidUsernamePassword
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try account.removeCredentials(type: .newsBlurBasic)
|
||||
try account.removeCredentials(type: .newsBlurSessionId)
|
||||
try account.storeCredentials(credentials)
|
||||
try account.storeCredentials(validatedCredentials)
|
||||
self.accountCredentialsWereUpdated = true
|
||||
account.refreshAll(completion: { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
self.error = .other(error: error)
|
||||
}
|
||||
})
|
||||
|
||||
} catch {
|
||||
self.error = .keyChainError
|
||||
}
|
||||
|
||||
case .failure:
|
||||
self.error = .networkError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK:- OAuthAccountAuthorizationOperationDelegate
|
||||
extension EditAccountCredentialsModel: OAuthAccountAuthorizationOperationDelegate {
|
||||
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account) {
|
||||
accountIsUpdatingCredentials = false
|
||||
accountCredentialsWereUpdated = true
|
||||
account.refreshAll { [weak self] result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
self?.error = .other(error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) {
|
||||
accountIsUpdatingCredentials = false
|
||||
self.error = .other(error: error)
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
//
|
||||
// EditAccountCredentialsView.swift
|
||||
// Multiplatform macOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 14/7/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Secrets
|
||||
|
||||
struct EditAccountCredentialsView: View {
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@StateObject private var editModel = EditAccountCredentialsModel()
|
||||
@ObservedObject var viewModel: AccountsPreferencesModel
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(rsImage: viewModel.account!.smallIcon!.image)
|
||||
.resizable()
|
||||
.frame(width: 30, height: 30)
|
||||
Text(viewModel.account?.nameForDisplay ?? "")
|
||||
Spacer()
|
||||
}.padding()
|
||||
|
||||
HStack(alignment: .center) {
|
||||
VStack(alignment: .trailing, spacing: 12) {
|
||||
Text("Username: ")
|
||||
Text("Password: ")
|
||||
if viewModel.account?.type == .freshRSS {
|
||||
Text("API URL: ")
|
||||
}
|
||||
}.frame(width: 75)
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
TextField("Username", text: $editModel.userName)
|
||||
SecureField("Password", text: $editModel.password)
|
||||
if viewModel.account?.type == .freshRSS {
|
||||
TextField("API URL", text: $editModel.apiUrl)
|
||||
}
|
||||
}
|
||||
}.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
|
||||
Spacer()
|
||||
HStack{
|
||||
if editModel.accountIsUpdatingCredentials {
|
||||
ProgressView("Updating")
|
||||
}
|
||||
Spacer()
|
||||
Button("Cancel", action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
})
|
||||
if viewModel.account?.type != .freshRSS {
|
||||
Button("Update", action: {
|
||||
editModel.updateAccountCredentials(viewModel.account!)
|
||||
}).disabled(editModel.userName.count == 0 || editModel.password.count == 0)
|
||||
} else {
|
||||
Button("Update", action: {
|
||||
editModel.updateAccountCredentials(viewModel.account!)
|
||||
}).disabled(editModel.userName.count == 0 || editModel.password.count == 0 || editModel.apiUrl.count == 0)
|
||||
}
|
||||
|
||||
}
|
||||
}.onAppear {
|
||||
editModel.retrieveCredentials(viewModel.account!)
|
||||
}
|
||||
.onChange(of: editModel.accountCredentialsWereUpdated) { value in
|
||||
if value == true {
|
||||
viewModel.sheetToShow = .none
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
struct EditAccountCredentials_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
EditAccountCredentialsView(viewModel: AccountsPreferencesModel())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
//
|
||||
// AccountUpdateErrors.swift
|
||||
// Multiplatform macOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 14/7/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum AccountUpdateErrors: CustomStringConvertible {
|
||||
case invalidUsernamePassword, invalidUsernamePasswordAPI, networkError, keyChainError, other(error: Error) , none
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .invalidUsernamePassword:
|
||||
return NSLocalizedString("Invalid email or password combination.", comment: "Invalid email/password combination.")
|
||||
case .invalidUsernamePasswordAPI:
|
||||
return NSLocalizedString("Invalid email, password, or API URL combination.", comment: "Invalid email/password/API combination.")
|
||||
case .networkError:
|
||||
return NSLocalizedString("Network Error. Please try later.", comment: "Network Error. Please try later.")
|
||||
case .keyChainError:
|
||||
return NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error")
|
||||
case .other(let error):
|
||||
return NSLocalizedString(error.localizedDescription, comment: "Other add account error")
|
||||
default:
|
||||
return NSLocalizedString("N/A", comment: "N/A")
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: AccountUpdateErrors, rhs: AccountUpdateErrors) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.other(let lhsError), .other(let rhsError)):
|
||||
return lhsError.localizedDescription == rhsError.localizedDescription
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
//
|
||||
// AdvancedPreferencesModel.swift
|
||||
// Multiplatform macOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 16/7/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class AdvancedPreferencesModel: ObservableObject {
|
||||
|
||||
let releaseBuildsURL = Bundle.main.infoDictionary!["SUFeedURL"]! as! String
|
||||
let testBuildsURL = Bundle.main.infoDictionary!["FeedURLForTestBuilds"]! as! String
|
||||
let appcastDefaultsKey = "SUFeedURL"
|
||||
|
||||
init() {
|
||||
if AppDefaults.shared.downloadTestBuilds == false {
|
||||
AppDefaults.store.setValue(releaseBuildsURL, forKey: appcastDefaultsKey)
|
||||
} else {
|
||||
AppDefaults.store.setValue(testBuildsURL, forKey: appcastDefaultsKey)
|
||||
}
|
||||
}
|
||||
|
||||
func updateAppcast() {
|
||||
if AppDefaults.shared.downloadTestBuilds == false {
|
||||
AppDefaults.store.setValue(releaseBuildsURL, forKey: appcastDefaultsKey)
|
||||
} else {
|
||||
AppDefaults.store.setValue(testBuildsURL, forKey: appcastDefaultsKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
//
|
||||
// AdvancedPreferencesView.swift
|
||||
// macOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 27/6/20.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AdvancedPreferencesView: View {
|
||||
|
||||
@StateObject private var preferences = AppDefaults.shared
|
||||
@StateObject private var viewModel = AdvancedPreferencesModel()
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Toggle("Check for app updates automatically", isOn: $preferences.checkForUpdatesAutomatically)
|
||||
Toggle("Download Test Builds", isOn: $preferences.downloadTestBuilds)
|
||||
Text("If you’re not sure, don’t enable test builds. Test builds may have bugs, which may include crashing bugs and data loss.")
|
||||
.foregroundColor(.secondary)
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("Check for Updates") {
|
||||
appDelegate.softwareUpdater.checkForUpdates()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
Toggle("Send Crash Logs Automatically", isOn: $preferences.sendCrashLogs)
|
||||
Divider()
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("Privacy Policy", action: {
|
||||
NSWorkspace.shared.open(URL(string: "https://netnewswire.com/privacypolicy")!)
|
||||
})
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.onChange(of: preferences.downloadTestBuilds, perform: { _ in
|
||||
viewModel.updateAppcast()
|
||||
})
|
||||
.frame(width: 400, alignment: .center)
|
||||
.lineLimit(3)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
//
|
||||
// GeneralPreferencesModel.swift
|
||||
// Multiplatform macOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 12/7/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
|
||||
class GeneralPreferencesModel: ObservableObject {
|
||||
|
||||
@Published var rssReaders = [RSSReader]()
|
||||
@Published var readerSelection: Int = 0 {
|
||||
willSet {
|
||||
if newValue != readerSelection {
|
||||
registerAppWithBundleID(rssReaders[newValue].bundleID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let readerInfo = RSSReaderInfo()
|
||||
|
||||
init() {
|
||||
prepareRSSReaders()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK:- RSS Readers
|
||||
|
||||
private extension GeneralPreferencesModel {
|
||||
|
||||
func prepareRSSReaders() {
|
||||
|
||||
// Populate rssReaders
|
||||
var thisApp = RSSReader(bundleID: Bundle.main.bundleIdentifier!)
|
||||
thisApp?.nameMinusAppSuffix.append(" (this app—multiplatform)")
|
||||
|
||||
let otherRSSReaders = readerInfo.rssReaders.filter { $0.bundleID != Bundle.main.bundleIdentifier! }.sorted(by: { $0.nameMinusAppSuffix < $1.nameMinusAppSuffix })
|
||||
rssReaders.append(thisApp!)
|
||||
rssReaders.append(contentsOf: otherRSSReaders)
|
||||
|
||||
if readerInfo.defaultRSSReaderBundleID != nil {
|
||||
let defaultReader = rssReaders.filter({ $0.bundleID == readerInfo.defaultRSSReaderBundleID })
|
||||
if defaultReader.count == 1 {
|
||||
let reader = defaultReader[0]
|
||||
readerSelection = rssReaders.firstIndex(of: reader)!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func registerAppWithBundleID(_ bundleID: String) {
|
||||
NSWorkspace.shared.setDefaultAppBundleID(forURLScheme: "feed", to: bundleID)
|
||||
NSWorkspace.shared.setDefaultAppBundleID(forURLScheme: "feeds", to: bundleID)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: - RSSReaderInfo
|
||||
|
||||
struct RSSReaderInfo {
|
||||
|
||||
var defaultRSSReaderBundleID: String? {
|
||||
NSWorkspace.shared.defaultAppBundleID(forURLScheme: RSSReaderInfo.feedURLScheme)
|
||||
}
|
||||
let rssReaders: Set<RSSReader>
|
||||
static let feedURLScheme = "feed:"
|
||||
|
||||
init() {
|
||||
self.rssReaders = RSSReaderInfo.fetchRSSReaders()
|
||||
}
|
||||
|
||||
static func fetchRSSReaders() -> Set<RSSReader> {
|
||||
let rssReaderBundleIDs = NSWorkspace.shared.bundleIDsForApps(forURLScheme: feedURLScheme)
|
||||
|
||||
var rssReaders = Set<RSSReader>()
|
||||
rssReaderBundleIDs.forEach { (bundleID) in
|
||||
if let reader = RSSReader(bundleID: bundleID) {
|
||||
rssReaders.insert(reader)
|
||||
}
|
||||
}
|
||||
return rssReaders
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - RSSReader
|
||||
|
||||
struct RSSReader: Hashable {
|
||||
|
||||
let bundleID: String
|
||||
let name: String
|
||||
var nameMinusAppSuffix: String
|
||||
let path: String
|
||||
|
||||
init?(bundleID: String) {
|
||||
guard let path = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.path = path.path
|
||||
self.bundleID = bundleID
|
||||
|
||||
let name = (self.path as NSString).lastPathComponent
|
||||
self.name = name
|
||||
if name.hasSuffix(".app") {
|
||||
self.nameMinusAppSuffix = name.stripping(suffix: ".app")
|
||||
}
|
||||
else {
|
||||
self.nameMinusAppSuffix = name
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Hashable
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(bundleID)
|
||||
}
|
||||
|
||||
// MARK: - Equatable
|
||||
|
||||
static func ==(lhs: RSSReader, rhs: RSSReader) -> Bool {
|
||||
return lhs.bundleID == rhs.bundleID
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
//
|
||||
// GeneralPreferencesView.swift
|
||||
// macOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 27/6/20.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct GeneralPreferencesView: View {
|
||||
|
||||
@StateObject private var defaults = AppDefaults.shared
|
||||
@StateObject private var preferences = GeneralPreferencesModel()
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Picker("Refresh feeds:",
|
||||
selection: $defaults.interval,
|
||||
content: {
|
||||
ForEach(RefreshInterval.allCases, content: { interval in
|
||||
Text(interval.description())
|
||||
.tag(interval.rawValue)
|
||||
})
|
||||
})
|
||||
|
||||
Picker("Default RSS reader:", selection: $preferences.readerSelection, content: {
|
||||
ForEach(0..<preferences.rssReaders.count, content: { index in
|
||||
if index > 0 && preferences.rssReaders[index].nameMinusAppSuffix.contains("NetNewsWire") {
|
||||
Text(preferences.rssReaders[index].nameMinusAppSuffix.appending(" (old version)"))
|
||||
|
||||
} else {
|
||||
Text(preferences.rssReaders[index].nameMinusAppSuffix)
|
||||
.tag(index)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Toggle("Confirm when deleting feeds and folders", isOn: $defaults.sidebarConfirmDelete)
|
||||
|
||||
Toggle("Open webpages in background in browser", isOn: $defaults.openInBrowserInBackground)
|
||||
Toggle("Hide Unread Count in Dock", isOn: $defaults.hideDockUnreadCount)
|
||||
|
||||
Picker("Safari Extension:",
|
||||
selection: $defaults.subscribeToFeedsInDefaultBrowser,
|
||||
content: {
|
||||
Text("Open feeds in NetNewsWire").tag(false)
|
||||
Text("Open feeds in default news reader").tag(true)
|
||||
}).pickerStyle(RadioGroupPickerStyle())
|
||||
}
|
||||
.frame(width: 400, alignment: .center)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
//
|
||||
// LayoutPreferencesView.swift
|
||||
// Multiplatform macOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 17/7/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LayoutPreferencesView: View {
|
||||
|
||||
@StateObject private var defaults = AppDefaults.shared
|
||||
private let colorPalettes = UserInterfaceColorPalette.allCases
|
||||
private let sampleTitle = "Lorem dolor sed viverra ipsum. Gravida rutrum quisque non tellus. Rutrum tellus pellentesque eu tincidunt tortor. Sed blandit libero volutpat sed cras ornare. Et netus et malesuada fames ac. Ultrices eros in cursus turpis massa tincidunt dui ut ornare. Lacus sed viverra tellus in. Sollicitudin ac orci phasellus egestas. Purus in mollis nunc sed. Sollicitudin ac orci phasellus egestas tellus rutrum tellus pellentesque. Interdum consectetur libero id faucibus nisl tincidunt eget."
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack {
|
||||
Form {
|
||||
Picker("Appearance", selection: $defaults.userInterfaceColorPalette, content: {
|
||||
ForEach(colorPalettes, id: \.self, content: {
|
||||
Text($0.description)
|
||||
})
|
||||
})
|
||||
|
||||
Divider()
|
||||
|
||||
Text("Timeline: ")
|
||||
Picker("Number of Lines", selection: $defaults.timelineNumberOfLines, content: {
|
||||
ForEach(1..<6, content: { i in
|
||||
Text(String(i))
|
||||
.tag(Double(i))
|
||||
})
|
||||
}).padding(.leading, 16)
|
||||
Slider(value: $defaults.timelineIconDimensions, in: 20...60, step: 10, minimumValueLabel: Text("Small"), maximumValueLabel: Text("Large"), label: {
|
||||
Text("Icon size")
|
||||
}).padding(.leading, 16)
|
||||
|
||||
}
|
||||
|
||||
timelineRowPreview
|
||||
.frame(width: 300)
|
||||
.padding()
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
||||
.stroke(Color.gray, lineWidth: 1)
|
||||
)
|
||||
.animation(.default)
|
||||
|
||||
Text("PREVIEW")
|
||||
.font(.caption)
|
||||
.tracking(0.3)
|
||||
Spacer()
|
||||
|
||||
}.frame(width: 400, height: 300, alignment: .center)
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
var timelineRowPreview: some View {
|
||||
HStack(alignment: .top) {
|
||||
Image(systemName: "circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 10, height: 10, alignment: .top)
|
||||
.foregroundColor(.accentColor)
|
||||
|
||||
Image(systemName: "paperplane.circle")
|
||||
.resizable()
|
||||
.frame(width: CGFloat(defaults.timelineIconDimensions), height: CGFloat(defaults.timelineIconDimensions), alignment: .top)
|
||||
.foregroundColor(.accentColor)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(sampleTitle)
|
||||
.font(.headline)
|
||||
.lineLimit(Int(defaults.timelineNumberOfLines))
|
||||
HStack {
|
||||
Text("Feed Name")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.footnote)
|
||||
Spacer()
|
||||
Text("10:31")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
struct SwiftUIView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
LayoutPreferencesView()
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<false/>
|
||||
<key>com.apple.security.automation.apple-events</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.temporary-exception.apple-events</key>
|
||||
<array>
|
||||
<string>com.red-sweater.marsedit4</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.$(ORGANIZATION_IDENTIFIER).NetNewsWire</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
<string>CloudKit</string>
|
||||
</array>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<false/>
|
||||
<key>com.apple.security.automation.apple-events</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.temporary-exception.apple-events</key>
|
||||
<array>
|
||||
<string>com.red-sweater.marsedit4</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
Reference in New Issue
Block a user