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,40 +0,0 @@
|
||||
//
|
||||
// SidebarContainerView.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 6/28/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SidebarContainerView: View {
|
||||
|
||||
@Environment(\.undoManager) var undoManager
|
||||
@EnvironmentObject private var sceneModel: SceneModel
|
||||
|
||||
@State var sidebarItems = [SidebarItem]()
|
||||
|
||||
var body: some View {
|
||||
SidebarView(sidebarItems: $sidebarItems)
|
||||
.modifier(SidebarToolbarModifier())
|
||||
.modifier(SidebarListStyleModifier())
|
||||
.environmentObject(sceneModel.sidebarModel)
|
||||
.onAppear {
|
||||
sceneModel.sidebarModel.undoManager = undoManager
|
||||
}
|
||||
.onReceive(sceneModel.sidebarModel.sidebarItemsPublisher!) { newItems in
|
||||
withAnimation {
|
||||
sidebarItems = newItems
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct SidebarContainerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SidebarContainerView()
|
||||
.environmentObject(SceneModel())
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
//
|
||||
// SidebarContextMenu.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 7/17/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import RSCore
|
||||
import Account
|
||||
|
||||
struct SidebarContextMenu: View {
|
||||
|
||||
@Environment(\.undoManager) var undoManager
|
||||
@Environment(\.openURL) var openURL
|
||||
@EnvironmentObject private var sidebarModel: SidebarModel
|
||||
@Binding var showInspector: Bool
|
||||
var sidebarItem: SidebarItem
|
||||
|
||||
|
||||
var body: some View {
|
||||
// MARK: Account Context Menu
|
||||
if sidebarItem.representedType == .account {
|
||||
Button {
|
||||
showInspector = true
|
||||
} label: {
|
||||
Text("Get Info")
|
||||
#if os(iOS)
|
||||
AppAssets.getInfoImage
|
||||
#endif
|
||||
}
|
||||
Button {
|
||||
sidebarModel.markAllAsReadInAccount.send(sidebarItem.represented as! Account)
|
||||
} label: {
|
||||
Text("Mark All As Read")
|
||||
#if os(iOS)
|
||||
AppAssets.markAllAsReadImage
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Pseudofeed Context Menu
|
||||
if sidebarItem.representedType == .pseudoFeed {
|
||||
Button {
|
||||
guard let feed = sidebarItem.feed else {
|
||||
return
|
||||
}
|
||||
sidebarModel.markAllAsReadInFeed.send(feed)
|
||||
} label: {
|
||||
Text("Mark All As Read")
|
||||
#if os(iOS)
|
||||
AppAssets.markAllAsReadImage
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Webfeed Context Menu
|
||||
if sidebarItem.representedType == .webFeed {
|
||||
Button {
|
||||
showInspector = true
|
||||
} label: {
|
||||
Text("Get Info")
|
||||
#if os(iOS)
|
||||
AppAssets.getInfoImage
|
||||
#endif
|
||||
}
|
||||
Button {
|
||||
guard let feed = sidebarItem.feed else {
|
||||
return
|
||||
}
|
||||
sidebarModel.markAllAsReadInFeed.send(feed)
|
||||
} label: {
|
||||
Text("Mark All As Read")
|
||||
#if os(iOS)
|
||||
AppAssets.markAllAsReadImage
|
||||
#endif
|
||||
}
|
||||
Divider()
|
||||
Button {
|
||||
guard let homepage = (sidebarItem.feed as? WebFeed)?.homePageURL,
|
||||
let url = URL(string: homepage) else {
|
||||
return
|
||||
}
|
||||
openURL(url)
|
||||
} label: {
|
||||
Text("Open Home Page")
|
||||
#if os(iOS)
|
||||
AppAssets.openInBrowserImage
|
||||
#endif
|
||||
}
|
||||
Divider()
|
||||
Button {
|
||||
guard let feedUrl = (sidebarItem.feed as? WebFeed)?.url else {
|
||||
return
|
||||
}
|
||||
#if os(macOS)
|
||||
URLPasteboardWriter.write(urlString: feedUrl, to: NSPasteboard.general)
|
||||
#else
|
||||
UIPasteboard.general.string = feedUrl
|
||||
#endif
|
||||
|
||||
} label: {
|
||||
Text("Copy Feed URL")
|
||||
#if os(iOS)
|
||||
AppAssets.copyImage
|
||||
#endif
|
||||
}
|
||||
Button {
|
||||
guard let homepage = (sidebarItem.feed as? WebFeed)?.homePageURL else {
|
||||
return
|
||||
}
|
||||
#if os(macOS)
|
||||
URLPasteboardWriter.write(urlString: homepage, to: NSPasteboard.general)
|
||||
#else
|
||||
UIPasteboard.general.string = homepage
|
||||
#endif
|
||||
} label: {
|
||||
Text("Copy Home Page URL")
|
||||
#if os(iOS)
|
||||
AppAssets.copyImage
|
||||
#endif
|
||||
}
|
||||
Divider()
|
||||
Button {
|
||||
if AppDefaults.shared.sidebarConfirmDelete == false {
|
||||
sidebarModel.deleteFromAccount.send(sidebarItem.feed!)
|
||||
} else {
|
||||
sidebarModel.sidebarItemToDelete = sidebarItem.feed!
|
||||
sidebarModel.showDeleteConfirmation = true
|
||||
}
|
||||
} label: {
|
||||
Text("Delete")
|
||||
#if os(iOS)
|
||||
AppAssets.deleteImage
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Folder Context Menu
|
||||
if sidebarItem.representedType == .folder {
|
||||
Button {
|
||||
showInspector = true
|
||||
} label: {
|
||||
Text("Get Info")
|
||||
#if os(iOS)
|
||||
AppAssets.getInfoImage
|
||||
#endif
|
||||
}
|
||||
Button {
|
||||
guard let feed = sidebarItem.feed else {
|
||||
return
|
||||
}
|
||||
sidebarModel.markAllAsReadInFeed.send(feed)
|
||||
} label: {
|
||||
Text("Mark All As Read")
|
||||
#if os(iOS)
|
||||
AppAssets.markAllAsReadImage
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
You cannot select folder level items in b4. Delete is disabled for the time being.
|
||||
*/
|
||||
/*
|
||||
Divider()
|
||||
Button {
|
||||
if AppDefaults.shared.sidebarConfirmDelete == false {
|
||||
sidebarModel.deleteFromAccount.send(sidebarItem.feed!)
|
||||
} else {
|
||||
sidebarModel.sidebarContextMenuItem = sidebarItem.feed
|
||||
sidebarModel.showDeleteConfirmation = true
|
||||
}
|
||||
} label: {
|
||||
Text("Delete")
|
||||
#if os(iOS)
|
||||
AppAssets.deleteImage
|
||||
#endif
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
//
|
||||
// SidebarExpandedContainers.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 6/30/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import Account
|
||||
|
||||
struct SidebarExpandedContainers {
|
||||
|
||||
var expandedTable = [ContainerIdentifier: Bool]()
|
||||
|
||||
var data: Data {
|
||||
get {
|
||||
let encoder = PropertyListEncoder()
|
||||
encoder.outputFormat = .binary
|
||||
return (try? encoder.encode(expandedTable)) ?? Data()
|
||||
}
|
||||
set {
|
||||
let decoder = PropertyListDecoder()
|
||||
expandedTable = (try? decoder.decode([ContainerIdentifier: Bool].self, from: newValue)) ?? [ContainerIdentifier: Bool]()
|
||||
}
|
||||
}
|
||||
|
||||
func contains(_ containerID: ContainerIdentifier) -> Bool {
|
||||
return expandedTable.keys.contains(containerID)
|
||||
}
|
||||
|
||||
subscript(_ containerID: ContainerIdentifier) -> Bool {
|
||||
get {
|
||||
if let result = expandedTable[containerID] {
|
||||
return result
|
||||
}
|
||||
switch containerID {
|
||||
case .smartFeedController, .account:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
set(newValue) {
|
||||
expandedTable[containerID] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
//
|
||||
// SidebarItem.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 6/29/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import RSCore
|
||||
import Account
|
||||
|
||||
public enum SidebarItemIdentifier: Hashable, Equatable {
|
||||
case smartFeedController
|
||||
case account(String)
|
||||
case feed(FeedIdentifier)
|
||||
}
|
||||
|
||||
public enum RepresentedType {
|
||||
case smartFeedController, webFeed, folder, pseudoFeed, account, unknown
|
||||
}
|
||||
|
||||
struct SidebarItem: Identifiable {
|
||||
|
||||
var id: SidebarItemIdentifier
|
||||
var represented: Any
|
||||
var children: [SidebarItem] = [SidebarItem]()
|
||||
|
||||
var unreadCount: Int
|
||||
var nameForDisplay: String
|
||||
|
||||
var feed: Feed? {
|
||||
represented as? Feed
|
||||
}
|
||||
|
||||
var containerID: ContainerIdentifier? {
|
||||
return (represented as? ContainerIdentifiable)?.containerID
|
||||
}
|
||||
|
||||
var representedType: RepresentedType {
|
||||
switch type(of: represented) {
|
||||
case is SmartFeedsController.Type:
|
||||
return .smartFeedController
|
||||
case is SmartFeed.Type:
|
||||
return .pseudoFeed
|
||||
case is UnreadFeed.Type:
|
||||
return .pseudoFeed
|
||||
case is WebFeed.Type:
|
||||
return .webFeed
|
||||
case is Folder.Type:
|
||||
return .folder
|
||||
case is Account.Type:
|
||||
return .account
|
||||
default:
|
||||
return .unknown
|
||||
}
|
||||
}
|
||||
|
||||
init(_ smartFeedsController: SmartFeedsController) {
|
||||
self.id = .smartFeedController
|
||||
self.represented = smartFeedsController
|
||||
self.unreadCount = 0
|
||||
self.nameForDisplay = smartFeedsController.nameForDisplay
|
||||
}
|
||||
|
||||
init(_ account: Account) {
|
||||
self.id = .account(account.accountID)
|
||||
self.represented = account
|
||||
self.unreadCount = account.unreadCount
|
||||
self.nameForDisplay = account.nameForDisplay
|
||||
}
|
||||
|
||||
init(_ feed: Feed, unreadCount: Int) {
|
||||
self.id = .feed(feed.feedID!)
|
||||
self.represented = feed
|
||||
self.unreadCount = unreadCount
|
||||
self.nameForDisplay = feed.nameForDisplay
|
||||
}
|
||||
|
||||
/// Add a sidebar item to the child list
|
||||
mutating func addChild(_ sidebarItem: SidebarItem) {
|
||||
children.append(sidebarItem)
|
||||
}
|
||||
|
||||
/// Recursively visits each sidebar item. Return true when done visiting.
|
||||
@discardableResult
|
||||
func visit(_ block: (SidebarItem) -> Bool) -> Bool {
|
||||
let stop = block(self)
|
||||
if !stop {
|
||||
for child in children {
|
||||
if child.visit(block) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return stop
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
//
|
||||
// SidebarItemView.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 6/29/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
|
||||
struct SidebarItemView: View {
|
||||
|
||||
@StateObject var feedIconImageLoader = FeedIconImageLoader()
|
||||
@EnvironmentObject private var sidebarModel: SidebarModel
|
||||
@State private var showInspector: Bool = false
|
||||
var sidebarItem: SidebarItem
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
#if os(macOS)
|
||||
HStack {
|
||||
if let image = feedIconImageLoader.image {
|
||||
IconImageView(iconImage: image)
|
||||
.frame(width: 20, height: 20, alignment: .center)
|
||||
}
|
||||
Text(verbatim: sidebarItem.nameForDisplay)
|
||||
Spacer()
|
||||
if sidebarItem.unreadCount > 0 {
|
||||
UnreadCountView(count: sidebarItem.unreadCount)
|
||||
}
|
||||
}
|
||||
#else
|
||||
HStack(alignment: .top) {
|
||||
if let image = feedIconImageLoader.image {
|
||||
IconImageView(iconImage: image)
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
Text(verbatim: sidebarItem.nameForDisplay)
|
||||
}
|
||||
Spacer()
|
||||
if sidebarItem.unreadCount > 0 {
|
||||
UnreadCountView(count: sidebarItem.unreadCount)
|
||||
}
|
||||
if sidebarItem.representedType == .webFeed || sidebarItem.representedType == .pseudoFeed {
|
||||
Spacer()
|
||||
.frame(width: 16)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.onAppear {
|
||||
if let feed = sidebarItem.feed {
|
||||
feedIconImageLoader.loadImage(for: feed)
|
||||
}
|
||||
}.contextMenu {
|
||||
SidebarContextMenu(showInspector: $showInspector, sidebarItem: sidebarItem)
|
||||
.environmentObject(sidebarModel)
|
||||
}
|
||||
.sheet(isPresented: $showInspector, onDismiss: { showInspector = false}) {
|
||||
InspectorView(sidebarItem: sidebarItem)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
//
|
||||
// SidebarListStyleModifier.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 7/6/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SidebarListStyleModifier: ViewModifier {
|
||||
|
||||
#if os(iOS)
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
#endif
|
||||
|
||||
@ViewBuilder func body(content: Content) -> some View {
|
||||
#if os(macOS)
|
||||
content
|
||||
.listStyle(SidebarListStyle())
|
||||
#else
|
||||
if horizontalSizeClass == .compact {
|
||||
content
|
||||
.listStyle(PlainListStyle())
|
||||
} else {
|
||||
content
|
||||
.listStyle(SidebarListStyle())
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,357 +0,0 @@
|
||||
//
|
||||
// SidebarModel.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 6/28/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import RSCore
|
||||
import Account
|
||||
import Articles
|
||||
|
||||
protocol SidebarModelDelegate: AnyObject {
|
||||
func unreadCount(for: Feed) -> Int
|
||||
}
|
||||
|
||||
class SidebarModel: ObservableObject, UndoableCommandRunner {
|
||||
|
||||
@Published var selectedFeedIdentifiers = Set<FeedIdentifier>()
|
||||
@Published var selectedFeedIdentifier: FeedIdentifier? = .none
|
||||
@Published var isReadFiltered = false
|
||||
@Published var expandedContainers = SidebarExpandedContainers()
|
||||
@Published var showDeleteConfirmation: Bool = false
|
||||
|
||||
weak var delegate: SidebarModelDelegate?
|
||||
|
||||
var sidebarItemsPublisher: AnyPublisher<[SidebarItem], Never>?
|
||||
var selectedFeedsPublisher: AnyPublisher<[Feed], Never>?
|
||||
|
||||
var selectNextUnread = PassthroughSubject<Void, Never>()
|
||||
var markAllAsReadInFeed = PassthroughSubject<Feed, Never>()
|
||||
var markAllAsReadInAccount = PassthroughSubject<Account, Never>()
|
||||
var deleteFromAccount = PassthroughSubject<Feed, Never>()
|
||||
|
||||
var sidebarItemToDelete: Feed?
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
var undoManager: UndoManager?
|
||||
var undoableCommands = [UndoableCommand]()
|
||||
|
||||
init(delegate: SidebarModelDelegate) {
|
||||
self.delegate = delegate
|
||||
subscribeToSelectedFeedChanges()
|
||||
subscribeToRebuildSidebarItemsEvents()
|
||||
subscribeToNextUnread()
|
||||
subscribeToMarkAllAsReadInFeed()
|
||||
subscribeToMarkAllAsReadInAccount()
|
||||
subscribeToDeleteFromAccount()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
extension SidebarModel {
|
||||
|
||||
func countOfFeedsToDelete() -> Int {
|
||||
var selectedFeeds = selectedFeedIdentifiers
|
||||
|
||||
if sidebarItemToDelete != nil {
|
||||
selectedFeeds.insert(sidebarItemToDelete!.feedID!)
|
||||
}
|
||||
|
||||
return selectedFeeds.count
|
||||
}
|
||||
|
||||
|
||||
func namesOfFeedsToDelete() -> String {
|
||||
var selectedFeeds = selectedFeedIdentifiers
|
||||
|
||||
if sidebarItemToDelete != nil {
|
||||
selectedFeeds.insert(sidebarItemToDelete!.feedID!)
|
||||
}
|
||||
|
||||
let feeds: [Feed] = selectedFeeds
|
||||
.compactMap({ AccountManager.shared.existingFeed(with: $0) })
|
||||
|
||||
return feeds
|
||||
.map({ $0.nameForDisplay })
|
||||
.joined(separator: ", ")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private extension SidebarModel {
|
||||
|
||||
// MARK: Subscriptions
|
||||
|
||||
func subscribeToSelectedFeedChanges() {
|
||||
|
||||
let selectedFeedIdentifersPublisher = $selectedFeedIdentifiers
|
||||
.map { [weak self] feedIDs -> [Feed] in
|
||||
return feedIDs.compactMap { self?.findFeed($0) }
|
||||
}
|
||||
|
||||
|
||||
let selectedFeedIdentiferPublisher = $selectedFeedIdentifier
|
||||
.compactMap { [weak self] feedID -> [Feed]? in
|
||||
if let feedID = feedID, let feed = self?.findFeed(feedID) {
|
||||
return [feed]
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
selectedFeedsPublisher = selectedFeedIdentifersPublisher
|
||||
.merge(with: selectedFeedIdentiferPublisher)
|
||||
.removeDuplicates(by: { previousFeeds, currentFeeds in
|
||||
return previousFeeds.elementsEqual(currentFeeds, by: { $0.feedID == $1.feedID })
|
||||
})
|
||||
.share()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func subscribeToRebuildSidebarItemsEvents() {
|
||||
guard let selectedFeedsPublisher = selectedFeedsPublisher else { return }
|
||||
|
||||
let chidrenDidChangePublisher = NotificationCenter.default.publisher(for: .ChildrenDidChange)
|
||||
let batchUpdateDidPerformPublisher = NotificationCenter.default.publisher(for: .BatchUpdateDidPerform)
|
||||
let displayNameDidChangePublisher = NotificationCenter.default.publisher(for: .DisplayNameDidChange)
|
||||
let accountStateDidChangePublisher = NotificationCenter.default.publisher(for: .AccountStateDidChange)
|
||||
let userDidAddAccountPublisher = NotificationCenter.default.publisher(for: .UserDidAddAccount)
|
||||
let userDidDeleteAccountPublisher = NotificationCenter.default.publisher(for: .UserDidDeleteAccount)
|
||||
let unreadCountDidInitializePublisher = NotificationCenter.default.publisher(for: .UnreadCountDidInitialize)
|
||||
let unreadCountDidChangePublisher = NotificationCenter.default.publisher(for: .UnreadCountDidChange)
|
||||
|
||||
let sidebarRebuildPublishers = chidrenDidChangePublisher.merge(with: batchUpdateDidPerformPublisher,
|
||||
displayNameDidChangePublisher,
|
||||
accountStateDidChangePublisher,
|
||||
userDidAddAccountPublisher,
|
||||
userDidDeleteAccountPublisher,
|
||||
unreadCountDidInitializePublisher,
|
||||
unreadCountDidChangePublisher)
|
||||
|
||||
let kickStarter = Notification(name: Notification.Name(rawValue: "Kick Starter"))
|
||||
|
||||
sidebarItemsPublisher = sidebarRebuildPublishers
|
||||
.prepend(kickStarter)
|
||||
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
|
||||
.combineLatest($isReadFiltered, selectedFeedsPublisher)
|
||||
.compactMap { [weak self] _, readFilter, selectedFeeds in
|
||||
self?.rebuildSidebarItems(isReadFiltered: readFilter, selectedFeeds: selectedFeeds)
|
||||
}
|
||||
.share()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func subscribeToNextUnread() {
|
||||
guard let sidebarItemsPublisher = sidebarItemsPublisher, let selectedFeedsPublisher = selectedFeedsPublisher else { return }
|
||||
|
||||
selectNextUnread
|
||||
.withLatestFrom(sidebarItemsPublisher, selectedFeedsPublisher)
|
||||
.compactMap { [weak self] (sidebarItems, selectedFeeds) in
|
||||
return self?.nextUnread(sidebarItems: sidebarItems, selectedFeeds: selectedFeeds)
|
||||
}
|
||||
.sink { [weak self] nextFeedID in
|
||||
self?.select(nextFeedID)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func subscribeToMarkAllAsReadInFeed() {
|
||||
guard let selectedFeedsPublisher = selectedFeedsPublisher else { return }
|
||||
|
||||
markAllAsReadInFeed
|
||||
.withLatestFrom(selectedFeedsPublisher, resultSelector: { givenFeed, selectedFeeds -> [Feed] in
|
||||
if selectedFeeds.contains(where: { $0.feedID == givenFeed.feedID }) {
|
||||
return selectedFeeds
|
||||
} else {
|
||||
return [givenFeed]
|
||||
}
|
||||
})
|
||||
.map { feeds in
|
||||
var articles = [Article]()
|
||||
for feed in feeds {
|
||||
articles.append(contentsOf: (try? feed.fetchUnreadArticles()) ?? Set<Article>())
|
||||
}
|
||||
return articles
|
||||
}
|
||||
.sink { [weak self] allArticles in
|
||||
self?.markAllAsRead(allArticles)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func subscribeToMarkAllAsReadInAccount() {
|
||||
markAllAsReadInAccount
|
||||
.map { account in
|
||||
var articles = [Article]()
|
||||
for feed in account.flattenedWebFeeds() {
|
||||
articles.append(contentsOf: (try? feed.fetchUnreadArticles()) ?? Set<Article>())
|
||||
}
|
||||
return articles
|
||||
}
|
||||
.sink { [weak self] articles in
|
||||
self?.markAllAsRead(articles)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func subscribeToDeleteFromAccount() {
|
||||
guard let selectedFeedsPublisher = selectedFeedsPublisher else { return }
|
||||
|
||||
deleteFromAccount
|
||||
.withLatestFrom(selectedFeedsPublisher.prepend([Feed]()), resultSelector: { givenFeed, selectedFeeds -> [Feed] in
|
||||
if selectedFeeds.contains(where: { $0.feedID == givenFeed.feedID }) {
|
||||
return selectedFeeds
|
||||
} else {
|
||||
return [givenFeed]
|
||||
}
|
||||
})
|
||||
.sink { feeds in
|
||||
for feed in feeds {
|
||||
if let webFeed = feed as? WebFeed {
|
||||
guard let account = webFeed.account,
|
||||
let containerID = account.containerID,
|
||||
let container = AccountManager.shared.existingContainer(with: containerID) else {
|
||||
return
|
||||
}
|
||||
account.removeWebFeed(webFeed, from: container, completion: { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let err):
|
||||
print(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
if let folder = feed as? Folder {
|
||||
folder.account?.removeFolder(folder) { _ in }
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
/// Marks provided artices as read.
|
||||
/// - Parameter articles: An array of `Article`s.
|
||||
/// - Warning: An `UndoManager` is created here as the `Environment`'s undo manager appears to be `nil`.
|
||||
func markAllAsRead(_ articles: [Article]) {
|
||||
guard let undoManager = undoManager,
|
||||
let markAsReadCommand = MarkStatusCommand(initialArticles: articles, markingRead: true, undoManager: undoManager) else {
|
||||
return
|
||||
}
|
||||
runCommand(markAsReadCommand)
|
||||
}
|
||||
|
||||
// MARK: Sidebar Building
|
||||
|
||||
func sort(_ folders: Set<Folder>) -> [Folder] {
|
||||
return folders.sorted(by: { $0.nameForDisplay.localizedStandardCompare($1.nameForDisplay) == .orderedAscending })
|
||||
}
|
||||
|
||||
func sort(_ feeds: Set<WebFeed>) -> [Feed] {
|
||||
return feeds.sorted(by: { $0.nameForDisplay.localizedStandardCompare($1.nameForDisplay) == .orderedAscending })
|
||||
}
|
||||
|
||||
func rebuildSidebarItems(isReadFiltered: Bool, selectedFeeds: [Feed]) -> [SidebarItem] {
|
||||
var items = [SidebarItem]()
|
||||
guard let delegate = delegate else { return items }
|
||||
|
||||
var smartFeedControllerItem = SidebarItem(SmartFeedsController.shared)
|
||||
for feed in SmartFeedsController.shared.smartFeeds {
|
||||
// It looks like SwiftUI loses its mind when the last element in a section is removed. Don't filter
|
||||
// the smartfeeds yet or we crash about everytime because Starred is almost always filtered
|
||||
// if !isReadFiltered || feed.unreadCount > 0 {
|
||||
smartFeedControllerItem.addChild(SidebarItem(feed, unreadCount: delegate.unreadCount(for: feed)))
|
||||
// }
|
||||
}
|
||||
items.append(smartFeedControllerItem)
|
||||
|
||||
let selectedFeedIDs = Set(selectedFeeds.map { $0.feedID })
|
||||
|
||||
for account in AccountManager.shared.sortedActiveAccounts {
|
||||
var accountItem = SidebarItem(account)
|
||||
|
||||
for webFeed in sort(account.topLevelWebFeeds) {
|
||||
if !isReadFiltered || !(webFeed.unreadCount < 1 && !selectedFeedIDs.contains(webFeed.feedID)) {
|
||||
accountItem.addChild(SidebarItem(webFeed, unreadCount: delegate.unreadCount(for: webFeed)))
|
||||
}
|
||||
}
|
||||
|
||||
for folder in sort(account.folders ?? Set<Folder>()) {
|
||||
if !isReadFiltered || !(folder.unreadCount < 1 && !selectedFeedIDs.contains(folder.feedID)) {
|
||||
var folderItem = SidebarItem(folder, unreadCount: delegate.unreadCount(for: folder))
|
||||
for webFeed in sort(folder.topLevelWebFeeds) {
|
||||
if !isReadFiltered || !(webFeed.unreadCount < 1 && !selectedFeedIDs.contains(webFeed.feedID)) {
|
||||
folderItem.addChild(SidebarItem(webFeed, unreadCount: delegate.unreadCount(for: webFeed)))
|
||||
}
|
||||
}
|
||||
accountItem.addChild(folderItem)
|
||||
}
|
||||
}
|
||||
|
||||
items.append(accountItem)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
// MARK:
|
||||
|
||||
func findFeed(_ feedID: FeedIdentifier) -> Feed? {
|
||||
switch feedID {
|
||||
case .smartFeed:
|
||||
return SmartFeedsController.shared.find(by: feedID)
|
||||
default:
|
||||
return AccountManager.shared.existingFeed(with: feedID)
|
||||
}
|
||||
}
|
||||
|
||||
func nextUnread(sidebarItems: [SidebarItem], selectedFeeds: [Feed]) -> FeedIdentifier? {
|
||||
guard let startFeed = selectedFeeds.first ?? sidebarItems.first?.children.first?.feed else { return nil }
|
||||
|
||||
if let feedID = nextUnread(sidebarItems: sidebarItems, startingAt: startFeed) {
|
||||
return feedID
|
||||
} else {
|
||||
return nextUnread(sidebarItems: sidebarItems, startingAt: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func nextUnread(sidebarItems: [SidebarItem], startingAt: Feed?) -> FeedIdentifier? {
|
||||
var foundStartFeed = startingAt == nil ? true : false
|
||||
var nextSidebarItem: SidebarItem? = nil
|
||||
|
||||
for section in sidebarItems {
|
||||
if nextSidebarItem == nil {
|
||||
section.visit { sidebarItem in
|
||||
if !foundStartFeed && sidebarItem.feed?.feedID == startingAt?.feedID {
|
||||
foundStartFeed = true
|
||||
return false
|
||||
}
|
||||
if foundStartFeed && sidebarItem.unreadCount > 0 {
|
||||
nextSidebarItem = sidebarItem
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nextSidebarItem?.feed?.feedID
|
||||
}
|
||||
|
||||
func select(_ feedID: FeedIdentifier) {
|
||||
selectedFeedIdentifiers = Set([feedID])
|
||||
selectedFeedIdentifier = feedID
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
//
|
||||
// SidebarToolbarModel.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Stuart Breckenridge on 4/7/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum SidebarSheets {
|
||||
case none, web, twitter, reddit, folder, settings, fixCredentials
|
||||
}
|
||||
|
||||
class SidebarToolbarModel: ObservableObject {
|
||||
|
||||
@Published var showSheet: Bool = false
|
||||
@Published var sheetToShow: SidebarSheets = .none {
|
||||
didSet {
|
||||
sheetToShow != .none ? (showSheet = true) : (showSheet = false)
|
||||
}
|
||||
}
|
||||
@Published var showAddSheet: Bool = false
|
||||
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
//
|
||||
// SidebarToolbarModifier.swift
|
||||
// Multiplatform iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 30/6/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SidebarToolbarModifier: ViewModifier {
|
||||
|
||||
@EnvironmentObject private var refreshProgress: RefreshProgressModel
|
||||
@EnvironmentObject private var defaults: AppDefaults
|
||||
@EnvironmentObject private var sidebarModel: SidebarModel
|
||||
@StateObject private var viewModel = SidebarToolbarModel()
|
||||
|
||||
@ViewBuilder func body(content: Content) -> some View {
|
||||
#if os(iOS)
|
||||
content
|
||||
.toolbar {
|
||||
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button {
|
||||
withAnimation {
|
||||
sidebarModel.isReadFiltered.toggle()
|
||||
}
|
||||
} label: {
|
||||
if sidebarModel.isReadFiltered {
|
||||
AppAssets.filterActiveImage.font(.title3)
|
||||
} else {
|
||||
AppAssets.filterInactiveImage.font(.title3)
|
||||
}
|
||||
}
|
||||
.help(sidebarModel.isReadFiltered ? "Show Read Feeds" : "Filter Read Feeds")
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .bottomBar) {
|
||||
Button {
|
||||
viewModel.sheetToShow = .settings
|
||||
} label: {
|
||||
AppAssets.settingsImage.font(.title3)
|
||||
}
|
||||
.help("Settings")
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .bottomBar) {
|
||||
Spacer()
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .bottomBar) {
|
||||
switch refreshProgress.state {
|
||||
case .refreshProgress(let progress):
|
||||
ProgressView(value: progress)
|
||||
.frame(width: 100)
|
||||
case .lastRefreshDateText(let text):
|
||||
Text(text)
|
||||
.lineLimit(1)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
case .none:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .bottomBar) {
|
||||
Spacer()
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .bottomBar, content: {
|
||||
Menu(content: {
|
||||
Button { viewModel.sheetToShow = .web } label: { Text("Add Web Feed") }
|
||||
Button { viewModel.sheetToShow = .twitter } label: { Text("Add Twitter Feed") }
|
||||
Button { viewModel.sheetToShow = .reddit } label: { Text("Add Reddit Feed") }
|
||||
Button { viewModel.sheetToShow = .folder } label: { Text("Add Folder") }
|
||||
}, label: {
|
||||
AppAssets.addMenuImage.font(.title3)
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showSheet, onDismiss: { viewModel.sheetToShow = .none }) {
|
||||
if viewModel.sheetToShow == .web {
|
||||
AddWebFeedView(isPresented: $viewModel.showSheet)
|
||||
}
|
||||
if viewModel.sheetToShow == .folder {
|
||||
AddFolderView(isPresented: $viewModel.showSheet)
|
||||
}
|
||||
if viewModel.sheetToShow == .settings {
|
||||
SettingsView()
|
||||
.preferredColorScheme(AppDefaults.userInterfaceColorScheme)
|
||||
}
|
||||
}
|
||||
#else
|
||||
content
|
||||
.toolbar {
|
||||
ToolbarItem {
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
//
|
||||
// SidebarView.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 6/29/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
|
||||
struct SidebarView: View {
|
||||
|
||||
@Binding var sidebarItems: [SidebarItem]
|
||||
|
||||
@EnvironmentObject private var refreshProgress: RefreshProgressModel
|
||||
@EnvironmentObject private var sceneModel: SceneModel
|
||||
@EnvironmentObject private var sidebarModel: SidebarModel
|
||||
|
||||
// I had to comment out SceneStorage because it blows up if used on macOS
|
||||
// @SceneStorage("expandedContainers") private var expandedContainerData = Data()
|
||||
|
||||
private let threshold: CGFloat = 80
|
||||
@State private var previousScrollOffset: CGFloat = 0
|
||||
@State private var scrollOffset: CGFloat = 0
|
||||
@State var pulling: Bool = false
|
||||
@State var refreshing: Bool = false
|
||||
|
||||
var body: some View {
|
||||
#if os(macOS)
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button (action: {
|
||||
withAnimation {
|
||||
sidebarModel.isReadFiltered.toggle()
|
||||
}
|
||||
}, label: {
|
||||
if sidebarModel.isReadFiltered {
|
||||
AppAssets.filterActiveImage
|
||||
} else {
|
||||
AppAssets.filterInactiveImage
|
||||
}
|
||||
})
|
||||
.padding(.top, 8).padding(.trailing)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.help(sidebarModel.isReadFiltered ? "Show Read Feeds" : "Filter Read Feeds")
|
||||
}
|
||||
List(selection: $sidebarModel.selectedFeedIdentifiers) {
|
||||
rows
|
||||
}
|
||||
if case .refreshProgress(let percent) = refreshProgress.state {
|
||||
HStack(alignment: .center) {
|
||||
Spacer()
|
||||
ProgressView(value: percent).frame(width: 100)
|
||||
Spacer()
|
||||
}
|
||||
.padding(8)
|
||||
.background(Color(NSColor.windowBackgroundColor))
|
||||
.frame(height: 30)
|
||||
.animation(.easeInOut(duration: 0.5))
|
||||
.transition(.move(edge: .bottom))
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $sidebarModel.showDeleteConfirmation, content: {
|
||||
Alert(title: sidebarModel.countOfFeedsToDelete() > 1 ?
|
||||
(Text("Delete multiple items?")) :
|
||||
(Text("Delete \(sidebarModel.namesOfFeedsToDelete())?")),
|
||||
message: Text("Are you sure you wish to delete \(sidebarModel.namesOfFeedsToDelete())?"),
|
||||
primaryButton: .destructive(Text("Delete"),
|
||||
action: {
|
||||
sidebarModel.deleteFromAccount.send(sidebarModel.sidebarItemToDelete!)
|
||||
sidebarModel.sidebarItemToDelete = nil
|
||||
sidebarModel.selectedFeedIdentifiers.removeAll()
|
||||
sidebarModel.showDeleteConfirmation = false
|
||||
}),
|
||||
secondaryButton: .cancel(Text("Cancel"), action: {
|
||||
sidebarModel.sidebarItemToDelete = nil
|
||||
sidebarModel.showDeleteConfirmation = false
|
||||
}))
|
||||
})
|
||||
#else
|
||||
ZStack(alignment: .top) {
|
||||
List {
|
||||
rows
|
||||
}
|
||||
.background(RefreshFixedView())
|
||||
.navigationTitle(Text("Feeds"))
|
||||
.onPreferenceChange(RefreshKeyTypes.PrefKey.self) { values in
|
||||
refreshLogic(values: values)
|
||||
}
|
||||
if pulling {
|
||||
ProgressView().offset(y: -40)
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $sidebarModel.showDeleteConfirmation, content: {
|
||||
Alert(title: sidebarModel.countOfFeedsToDelete() > 1 ?
|
||||
(Text("Delete multiple items?")) :
|
||||
(Text("Delete \(sidebarModel.namesOfFeedsToDelete())?")),
|
||||
message: Text("Are you sure you wish to delete \(sidebarModel.namesOfFeedsToDelete())?"),
|
||||
primaryButton: .destructive(Text("Delete"),
|
||||
action: {
|
||||
sidebarModel.deleteFromAccount.send(sidebarModel.sidebarItemToDelete!)
|
||||
sidebarModel.sidebarItemToDelete = nil
|
||||
sidebarModel.selectedFeedIdentifiers.removeAll()
|
||||
sidebarModel.showDeleteConfirmation = false
|
||||
}),
|
||||
secondaryButton: .cancel(Text("Cancel"), action: {
|
||||
sidebarModel.sidebarItemToDelete = nil
|
||||
sidebarModel.showDeleteConfirmation = false
|
||||
}))
|
||||
})
|
||||
#endif
|
||||
|
||||
// .onAppear {
|
||||
// expandedContainers.data = expandedContainerData
|
||||
// }
|
||||
// .onReceive(expandedContainers.objectDidChange) {
|
||||
// expandedContainerData = expandedContainers.data
|
||||
// }
|
||||
}
|
||||
|
||||
func refreshLogic(values: [RefreshKeyTypes.PrefData]) {
|
||||
DispatchQueue.main.async {
|
||||
let movingBounds = values.first { $0.vType == .movingView }?.bounds ?? .zero
|
||||
let fixedBounds = values.first { $0.vType == .fixedView }?.bounds ?? .zero
|
||||
scrollOffset = movingBounds.minY - fixedBounds.minY
|
||||
|
||||
// Crossing the threshold on the way down, we start the refresh process
|
||||
if !pulling && (scrollOffset > threshold && previousScrollOffset <= threshold) {
|
||||
pulling = true
|
||||
AccountManager.shared.refreshAll()
|
||||
}
|
||||
|
||||
// Crossing the threshold on the way UP, we end the refresh
|
||||
if pulling && previousScrollOffset > threshold && scrollOffset <= threshold {
|
||||
pulling = false
|
||||
}
|
||||
|
||||
// Update last scroll offset
|
||||
self.previousScrollOffset = self.scrollOffset
|
||||
}
|
||||
}
|
||||
|
||||
struct RefreshFixedView: View {
|
||||
var body: some View {
|
||||
GeometryReader { proxy in
|
||||
Color.clear.preference(key: RefreshKeyTypes.PrefKey.self, value: [RefreshKeyTypes.PrefData(vType: .fixedView, bounds: proxy.frame(in: .global))])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RefreshKeyTypes {
|
||||
enum ViewType: Int {
|
||||
case movingView
|
||||
case fixedView
|
||||
}
|
||||
|
||||
struct PrefData: Equatable {
|
||||
let vType: ViewType
|
||||
let bounds: CGRect
|
||||
}
|
||||
|
||||
struct PrefKey: PreferenceKey {
|
||||
static var defaultValue: [PrefData] = []
|
||||
|
||||
static func reduce(value: inout [PrefData], nextValue: () -> [PrefData]) {
|
||||
value.append(contentsOf: nextValue())
|
||||
}
|
||||
|
||||
typealias Value = [PrefData]
|
||||
}
|
||||
}
|
||||
|
||||
var rows: some View {
|
||||
ForEach(sidebarItems) { sidebarItem in
|
||||
if let containerID = sidebarItem.containerID {
|
||||
DisclosureGroup(isExpanded: $sidebarModel.expandedContainers[containerID]) {
|
||||
ForEach(sidebarItem.children) { sidebarItem in
|
||||
if let containerID = sidebarItem.containerID {
|
||||
DisclosureGroup(isExpanded: $sidebarModel.expandedContainers[containerID]) {
|
||||
ForEach(sidebarItem.children) { sidebarItem in
|
||||
SidebarItemNavigation(sidebarItem: sidebarItem)
|
||||
}
|
||||
} label: {
|
||||
SidebarItemNavigation(sidebarItem: sidebarItem)
|
||||
}
|
||||
} else {
|
||||
SidebarItemNavigation(sidebarItem: sidebarItem)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
#if os(macOS)
|
||||
SidebarItemView(sidebarItem: sidebarItem)
|
||||
.padding(.leading, 4)
|
||||
.environmentObject(sidebarModel)
|
||||
#else
|
||||
if sidebarItem.representedType == .smartFeedController {
|
||||
GeometryReader { proxy in
|
||||
SidebarItemView(sidebarItem: sidebarItem)
|
||||
.preference(key: RefreshKeyTypes.PrefKey.self, value: [RefreshKeyTypes.PrefData(vType: .movingView, bounds: proxy.frame(in: .global))])
|
||||
.environmentObject(sidebarModel)
|
||||
}
|
||||
} else {
|
||||
SidebarItemView(sidebarItem: sidebarItem)
|
||||
.environmentObject(sidebarModel)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SidebarItemNavigation: View {
|
||||
|
||||
@EnvironmentObject private var sidebarModel: SidebarModel
|
||||
var sidebarItem: SidebarItem
|
||||
|
||||
var body: some View {
|
||||
#if os(macOS)
|
||||
SidebarItemView(sidebarItem: sidebarItem)
|
||||
.tag(sidebarItem.feed!.feedID!)
|
||||
#else
|
||||
ZStack {
|
||||
SidebarItemView(sidebarItem: sidebarItem)
|
||||
NavigationLink(destination: TimelineContainerView(),
|
||||
tag: sidebarItem.feed!.feedID!,
|
||||
selection: $sidebarModel.selectedFeedIdentifier) {
|
||||
EmptyView()
|
||||
}.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
//
|
||||
// UnreadCountView.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 6/29/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct UnreadCountView: View {
|
||||
|
||||
var count: Int
|
||||
|
||||
var body: some View {
|
||||
Text(verbatim: String(count))
|
||||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
.padding(.horizontal, 7)
|
||||
.padding(.vertical, 1)
|
||||
.background(AppAssets.sidebarUnreadCountBackground)
|
||||
.foregroundColor(AppAssets.sidebarUnreadCountForeground)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
|
||||
struct UnreadCountView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
UnreadCountView(count: 123)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user