Remove Multiplatform targets

This commit is contained in:
Maurice Parker
2021-10-15 17:23:40 -05:00
parent 3d5bdb44fb
commit 23fe288fe9
239 changed files with 0 additions and 18881 deletions

View File

@@ -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())
}
}

View File

@@ -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
}
*/
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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
}
}
}

View File

@@ -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)
}
}