Implement Timeline multiselect
This commit is contained in:
parent
cf79d3f508
commit
184ef57576
@ -12,10 +12,10 @@ import Articles
|
|||||||
struct ArticleContainerView: View {
|
struct ArticleContainerView: View {
|
||||||
|
|
||||||
@EnvironmentObject private var sceneModel: SceneModel
|
@EnvironmentObject private var sceneModel: SceneModel
|
||||||
var article: Article
|
var articles: [Article]
|
||||||
|
|
||||||
@ViewBuilder var body: some View {
|
@ViewBuilder var body: some View {
|
||||||
ArticleView(sceneModel: sceneModel, article: article)
|
ArticleView(sceneModel: sceneModel, articles: articles)
|
||||||
.modifier(ArticleToolbarModifier())
|
.modifier(ArticleToolbarModifier())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
//
|
|
||||||
// ArticleManager.swift
|
|
||||||
// NetNewsWire
|
|
||||||
//
|
|
||||||
// Created by Maurice Parker on 7/9/20.
|
|
||||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Articles
|
|
||||||
|
|
||||||
protocol ArticleManager: class {
|
|
||||||
var currentArticle: Article? { get }
|
|
||||||
}
|
|
@ -33,7 +33,7 @@ struct ArticleToolbarModifier: ViewModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItem(placement: .bottomBar) {
|
ToolbarItem(placement: .bottomBar) {
|
||||||
Button(action: { sceneModel.toggleReadForCurrentArticle() }, label: {
|
Button(action: { }, label: {
|
||||||
if sceneModel.readButtonState == .on {
|
if sceneModel.readButtonState == .on {
|
||||||
AppAssets.readClosedImage
|
AppAssets.readClosedImage
|
||||||
} else {
|
} else {
|
||||||
@ -49,7 +49,7 @@ struct ArticleToolbarModifier: ViewModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItem(placement: .bottomBar) {
|
ToolbarItem(placement: .bottomBar) {
|
||||||
Button(action: { sceneModel.toggleStarForCurrentArticle() }, label: {
|
Button(action: { }, label: {
|
||||||
if sceneModel.starButtonState == .on {
|
if sceneModel.starButtonState == .on {
|
||||||
AppAssets.starClosedImage
|
AppAssets.starClosedImage
|
||||||
} else {
|
} else {
|
||||||
|
@ -21,23 +21,17 @@ final class SceneModel: ObservableObject {
|
|||||||
private var refreshProgressModel: RefreshProgressModel? = nil
|
private var refreshProgressModel: RefreshProgressModel? = nil
|
||||||
private var articleIconSchemeHandler: ArticleIconSchemeHandler? = nil
|
private var articleIconSchemeHandler: ArticleIconSchemeHandler? = nil
|
||||||
|
|
||||||
var webViewProvider: WebViewProvider? = nil
|
private(set) var webViewProvider: WebViewProvider? = nil
|
||||||
|
private(set) var sidebarModel = SidebarModel()
|
||||||
var undoManager: UndoManager?
|
private(set) var timelineModel = TimelineModel()
|
||||||
var undoableCommands = [UndoableCommand]()
|
|
||||||
|
|
||||||
var sidebarModel: SidebarModel?
|
|
||||||
var timelineModel: TimelineModel?
|
|
||||||
var articleManager: ArticleManager?
|
|
||||||
|
|
||||||
var currentArticle: Article? {
|
|
||||||
return articleManager?.currentArticle
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Initialization API
|
// MARK: Initialization API
|
||||||
|
|
||||||
/// Prepares the SceneModel to be used in the views
|
/// Prepares the SceneModel to be used in the views
|
||||||
func startup() {
|
func startup() {
|
||||||
|
sidebarModel.delegate = self
|
||||||
|
timelineModel.delegate = self
|
||||||
|
|
||||||
self.refreshProgressModel = RefreshProgressModel()
|
self.refreshProgressModel = RefreshProgressModel()
|
||||||
self.refreshProgressModel!.$state.assign(to: self.$refreshProgressState)
|
self.refreshProgressModel!.$state.assign(to: self.$refreshProgressState)
|
||||||
|
|
||||||
@ -48,58 +42,20 @@ final class SceneModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Article Management API
|
// MARK: Article Management API
|
||||||
|
|
||||||
/// Toggles the read indicator for the currently viewable article
|
|
||||||
func toggleReadForCurrentArticle() {
|
|
||||||
if let article = articleManager?.currentArticle {
|
|
||||||
toggleRead(article)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Toggles the read indicator for the given article
|
|
||||||
func toggleRead(_ article: Article) {
|
|
||||||
guard !article.status.read || article.isAvailableToMarkUnread else { return }
|
|
||||||
markArticles([article], statusKey: .read, flag: !article.status.read)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Toggles the star indicator for the currently viewable article
|
|
||||||
func toggleStarForCurrentArticle() {
|
|
||||||
if let article = articleManager?.currentArticle {
|
|
||||||
toggleStar(article)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Toggles the star indicator for the given article
|
|
||||||
func toggleStar(_ article: Article) {
|
|
||||||
markArticles([article], statusKey: .starred, flag: !article.status.starred)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieves the article before the given article in the Timeline
|
/// Retrieves the article before the given article in the Timeline
|
||||||
func findPrevArticle(_ article: Article) -> Article? {
|
func findPrevArticle(_ article: Article) -> Article? {
|
||||||
return timelineModel?.findPrevArticle(article)
|
return timelineModel.findPrevArticle(article)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves the article after the given article in the Timeline
|
/// Retrieves the article after the given article in the Timeline
|
||||||
func findNextArticle(_ article: Article) -> Article? {
|
func findNextArticle(_ article: Article) -> Article? {
|
||||||
return timelineModel?.findNextArticle(article)
|
return timelineModel.findNextArticle(article)
|
||||||
}
|
|
||||||
|
|
||||||
/// Marks the article as read and selects it in the Timeline. Don't call until after the ArticleManager article has been set.
|
|
||||||
func updateArticleSelection() {
|
|
||||||
guard let article = currentArticle else { return }
|
|
||||||
|
|
||||||
timelineModel?.selectArticle(article)
|
|
||||||
|
|
||||||
if article.status.read {
|
|
||||||
updateArticleState()
|
|
||||||
} else {
|
|
||||||
markArticles([article], statusKey: .read, flag: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the article with the given articleID
|
/// Returns the article with the given articleID
|
||||||
func articleFor(_ articleID: String) -> Article? {
|
func articleFor(_ articleID: String) -> Article? {
|
||||||
return timelineModel?.articleFor(articleID)
|
return timelineModel.articleFor(articleID)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -124,19 +80,6 @@ extension SceneModel: TimelineModelDelegate {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: UndoableCommandRunner
|
|
||||||
|
|
||||||
extension SceneModel: UndoableCommandRunner {
|
|
||||||
|
|
||||||
func markArticlesWithUndo(_ articles: [Article], statusKey: ArticleStatus.Key, flag: Bool) {
|
|
||||||
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articles, statusKey: statusKey, flag: flag, undoManager: undoManager) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
runCommand(markReadCommand)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
private extension SceneModel {
|
private extension SceneModel {
|
||||||
@ -144,30 +87,39 @@ private extension SceneModel {
|
|||||||
// MARK: Notifications
|
// MARK: Notifications
|
||||||
|
|
||||||
@objc func statusesDidChange(_ note: Notification) {
|
@objc func statusesDidChange(_ note: Notification) {
|
||||||
guard let article = currentArticle, let articleIDs = note.userInfo?[Account.UserInfoKey.articleIDs] as? Set<String> else {
|
guard let articleIDs = note.userInfo?[Account.UserInfoKey.articleIDs] as? Set<String> else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if articleIDs.contains(article.articleID) {
|
let selectedArticleIDs = timelineModel.selectedArticles.map { $0.articleID }
|
||||||
|
if !articleIDs.intersection(selectedArticleIDs).isEmpty {
|
||||||
updateArticleState()
|
updateArticleState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: State Updates
|
// MARK: Button State Updates
|
||||||
|
|
||||||
func updateArticleState() {
|
func updateArticleState() {
|
||||||
guard let article = currentArticle else {
|
let articles = timelineModel.selectedArticles
|
||||||
|
|
||||||
|
guard !articles.isEmpty else {
|
||||||
readButtonState = nil
|
readButtonState = nil
|
||||||
starButtonState = nil
|
starButtonState = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if article.isAvailableToMarkUnread {
|
if articles.anyArticleIsUnread() {
|
||||||
readButtonState = article.status.read ? .off : .on
|
readButtonState = .on
|
||||||
|
} else if articles.anyArticleIsReadAndCanMarkUnread() {
|
||||||
|
readButtonState = .off
|
||||||
} else {
|
} else {
|
||||||
readButtonState = nil
|
readButtonState = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
starButtonState = article.status.starred ? .on : .off
|
if articles.anyArticleIsUnstarred() {
|
||||||
|
starButtonState = .on
|
||||||
|
} else {
|
||||||
|
starButtonState = .off
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,6 @@ import SwiftUI
|
|||||||
|
|
||||||
struct SceneNavigationView: View {
|
struct SceneNavigationView: View {
|
||||||
|
|
||||||
@Environment(\.undoManager) var undoManager
|
|
||||||
@StateObject private var sceneModel = SceneModel()
|
@StateObject private var sceneModel = SceneModel()
|
||||||
@State private var showSheet: Bool = false
|
@State private var showSheet: Bool = false
|
||||||
@State private var sheetToShow: ToolbarSheets = .none
|
@State private var sheetToShow: ToolbarSheets = .none
|
||||||
@ -49,7 +48,6 @@ struct SceneNavigationView: View {
|
|||||||
}
|
}
|
||||||
.environmentObject(sceneModel)
|
.environmentObject(sceneModel)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
sceneModel.undoManager = undoManager
|
|
||||||
sceneModel.startup()
|
sceneModel.startup()
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showSheet, onDismiss: { sheetToShow = .none }) {
|
.sheet(isPresented: $showSheet, onDismiss: { sheetToShow = .none }) {
|
||||||
@ -100,7 +98,7 @@ struct SceneNavigationView: View {
|
|||||||
}).help("Go to Next Unread").padding(.trailing, 40)
|
}).help("Go to Next Unread").padding(.trailing, 40)
|
||||||
}
|
}
|
||||||
ToolbarItem {
|
ToolbarItem {
|
||||||
Button(action: { sceneModel.toggleReadForCurrentArticle() }, label: {
|
Button(action: { }, label: {
|
||||||
if sceneModel.readButtonState == .on {
|
if sceneModel.readButtonState == .on {
|
||||||
AppAssets.readClosedImage
|
AppAssets.readClosedImage
|
||||||
} else {
|
} else {
|
||||||
@ -111,7 +109,7 @@ struct SceneNavigationView: View {
|
|||||||
.help(sceneModel.readButtonState == .on ? "Mark as Unread" : "Mark as Read")
|
.help(sceneModel.readButtonState == .on ? "Mark as Unread" : "Mark as Read")
|
||||||
}
|
}
|
||||||
ToolbarItem {
|
ToolbarItem {
|
||||||
Button(action: { sceneModel.toggleStarForCurrentArticle() }, label: {
|
Button(action: { }, label: {
|
||||||
if sceneModel.starButtonState == .on {
|
if sceneModel.starButtonState == .on {
|
||||||
AppAssets.starClosedImage
|
AppAssets.starClosedImage
|
||||||
} else {
|
} else {
|
||||||
|
@ -10,8 +10,8 @@ import SwiftUI
|
|||||||
|
|
||||||
struct SidebarContainerView: View {
|
struct SidebarContainerView: View {
|
||||||
|
|
||||||
|
@Environment(\.undoManager) var undoManager
|
||||||
@EnvironmentObject private var sceneModel: SceneModel
|
@EnvironmentObject private var sceneModel: SceneModel
|
||||||
@StateObject private var sidebarModel = SidebarModel()
|
|
||||||
|
|
||||||
@State private var showSettings: Bool = false
|
@State private var showSettings: Bool = false
|
||||||
|
|
||||||
@ -19,12 +19,11 @@ struct SidebarContainerView: View {
|
|||||||
SidebarView()
|
SidebarView()
|
||||||
.modifier(SidebarToolbarModifier())
|
.modifier(SidebarToolbarModifier())
|
||||||
.modifier(SidebarListStyleModifier())
|
.modifier(SidebarListStyleModifier())
|
||||||
.environmentObject(sidebarModel)
|
.environmentObject(sceneModel.sidebarModel)
|
||||||
.navigationTitle(Text("Feeds"))
|
.navigationTitle(Text("Feeds"))
|
||||||
.onAppear {
|
.onAppear {
|
||||||
sceneModel.sidebarModel = sidebarModel
|
sceneModel.sidebarModel.undoManager = undoManager
|
||||||
sidebarModel.delegate = sceneModel
|
sceneModel.sidebarModel.rebuildSidebarItems()
|
||||||
sidebarModel.rebuildSidebarItems()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ protocol SidebarModelDelegate: class {
|
|||||||
func unreadCount(for: Feed) -> Int
|
func unreadCount(for: Feed) -> Int
|
||||||
}
|
}
|
||||||
|
|
||||||
class SidebarModel: ObservableObject {
|
class SidebarModel: ObservableObject, UndoableCommandRunner {
|
||||||
|
|
||||||
weak var delegate: SidebarModelDelegate?
|
weak var delegate: SidebarModelDelegate?
|
||||||
|
|
||||||
@ -27,6 +27,9 @@ class SidebarModel: ObservableObject {
|
|||||||
private var selectedFeedIdentifiersCancellable: AnyCancellable?
|
private var selectedFeedIdentifiersCancellable: AnyCancellable?
|
||||||
private var selectedFeedIdentifierCancellable: AnyCancellable?
|
private var selectedFeedIdentifierCancellable: AnyCancellable?
|
||||||
|
|
||||||
|
var undoManager: UndoManager?
|
||||||
|
var undoableCommands = [UndoableCommand]()
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidInitialize(_:)), name: .UnreadCountDidInitialize, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidInitialize(_:)), name: .UnreadCountDidInitialize, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(containerChildrenDidChange(_:)), name: .ChildrenDidChange, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(containerChildrenDidChange(_:)), name: .ChildrenDidChange, object: nil)
|
||||||
@ -49,7 +52,6 @@ class SidebarModel: ObservableObject {
|
|||||||
self.selectedFeeds = [feed]
|
self.selectedFeeds = [feed]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: API
|
// MARK: API
|
||||||
|
@ -17,8 +17,7 @@ struct SidebarView: View {
|
|||||||
@EnvironmentObject private var sidebarModel: SidebarModel
|
@EnvironmentObject private var sidebarModel: SidebarModel
|
||||||
@State var navigate = false
|
@State var navigate = false
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder var body: some View {
|
||||||
var body: some View {
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
ZStack {
|
ZStack {
|
||||||
NavigationLink(destination: TimelineContainerView(feeds: sidebarModel.selectedFeeds), isActive: $navigate) {
|
NavigationLink(destination: TimelineContainerView(feeds: sidebarModel.selectedFeeds), isActive: $navigate) {
|
||||||
|
@ -11,20 +11,19 @@ import Account
|
|||||||
|
|
||||||
struct TimelineContainerView: View {
|
struct TimelineContainerView: View {
|
||||||
|
|
||||||
|
@Environment(\.undoManager) var undoManager
|
||||||
@EnvironmentObject private var sceneModel: SceneModel
|
@EnvironmentObject private var sceneModel: SceneModel
|
||||||
@StateObject private var timelineModel = TimelineModel()
|
|
||||||
var feeds: [Feed]? = nil
|
var feeds: [Feed]? = nil
|
||||||
|
|
||||||
@ViewBuilder var body: some View {
|
@ViewBuilder var body: some View {
|
||||||
if let feeds = feeds {
|
if let feeds = feeds {
|
||||||
TimelineView()
|
TimelineView()
|
||||||
.modifier(TimelineTitleModifier(title: timelineModel.nameForDisplay))
|
.modifier(TimelineTitleModifier(title: sceneModel.timelineModel.nameForDisplay))
|
||||||
.modifier(TimelineToolbarModifier())
|
.modifier(TimelineToolbarModifier())
|
||||||
.environmentObject(timelineModel)
|
.environmentObject(sceneModel.timelineModel)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
sceneModel.timelineModel = timelineModel
|
sceneModel.timelineModel.undoManager = undoManager
|
||||||
timelineModel.delegate = sceneModel
|
sceneModel.timelineModel.rebuildTimelineItems(feeds: feeds)
|
||||||
timelineModel.rebuildTimelineItems(feeds: feeds)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Combine
|
||||||
import RSCore
|
import RSCore
|
||||||
import Account
|
import Account
|
||||||
import Articles
|
import Articles
|
||||||
@ -15,13 +16,22 @@ protocol TimelineModelDelegate: class {
|
|||||||
func timelineRequestedWebFeedSelection(_: TimelineModel, webFeed: WebFeed)
|
func timelineRequestedWebFeedSelection(_: TimelineModel, webFeed: WebFeed)
|
||||||
}
|
}
|
||||||
|
|
||||||
class TimelineModel: ObservableObject {
|
class TimelineModel: ObservableObject, UndoableCommandRunner {
|
||||||
|
|
||||||
weak var delegate: TimelineModelDelegate?
|
weak var delegate: TimelineModelDelegate?
|
||||||
|
|
||||||
@Published var nameForDisplay = ""
|
@Published var nameForDisplay = ""
|
||||||
@Published var timelineItems = [TimelineItem]()
|
@Published var timelineItems = [TimelineItem]()
|
||||||
|
@Published var selectedArticleIDs = Set<String>()
|
||||||
|
@Published var selectedArticleID: String? = .none
|
||||||
|
@Published var selectedArticles = [Article]()
|
||||||
|
|
||||||
|
var undoManager: UndoManager?
|
||||||
|
var undoableCommands = [UndoableCommand]()
|
||||||
|
|
||||||
|
private var selectedArticleIDsCancellable: AnyCancellable?
|
||||||
|
private var selectedArticleIDCancellable: AnyCancellable?
|
||||||
|
|
||||||
private var fetchSerialNumber = 0
|
private var fetchSerialNumber = 0
|
||||||
private let fetchRequestQueue = FetchRequestQueue()
|
private let fetchRequestQueue = FetchRequestQueue()
|
||||||
private var exceptionArticleFetcher: ArticleFetcher?
|
private var exceptionArticleFetcher: ArticleFetcher?
|
||||||
@ -60,6 +70,20 @@ class TimelineModel: ObservableObject {
|
|||||||
|
|
||||||
init() {
|
init() {
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
|
||||||
|
|
||||||
|
// TODO: This should be rewritten to use Combine correctly
|
||||||
|
selectedArticleIDsCancellable = $selectedArticleIDs.sink { [weak self] articleIDs in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.selectedArticles = articleIDs.compactMap { self.idToArticleDictionary[$0] }
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This should be rewritten to use Combine correctly
|
||||||
|
selectedArticleIDCancellable = $selectedArticleID.sink { [weak self] articleID in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if let articleID = articleID, let article = self.idToArticleDictionary[articleID] {
|
||||||
|
self.selectedArticles = [article]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: API
|
// MARK: API
|
||||||
@ -73,14 +97,6 @@ class TimelineModel: ObservableObject {
|
|||||||
fetchAndReplaceArticlesAsync(feeds: feeds)
|
fetchAndReplaceArticlesAsync(feeds: feeds)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Replace this with ScrollViewReader if we have to keep it
|
|
||||||
func loadMoreTimelineItemsIfNecessary(_ timelineItem: TimelineItem) {
|
|
||||||
let thresholdIndex = timelineItems.index(timelineItems.endIndex, offsetBy: -10)
|
|
||||||
if timelineItems.firstIndex(where: { $0.id == timelineItem.id }) == thresholdIndex {
|
|
||||||
nextBatch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func articleFor(_ articleID: String) -> Article? {
|
func articleFor(_ articleID: String) -> Article? {
|
||||||
return idToArticleDictionary[articleID]
|
return idToArticleDictionary[articleID]
|
||||||
}
|
}
|
||||||
@ -182,18 +198,9 @@ private extension TimelineModel {
|
|||||||
|
|
||||||
func replaceArticles(with unsortedArticles: Set<Article>) {
|
func replaceArticles(with unsortedArticles: Set<Article>) {
|
||||||
articles = Array(unsortedArticles).sortedByDate(sortDirection ? .orderedDescending : .orderedAscending, groupByFeed: groupByFeed)
|
articles = Array(unsortedArticles).sortedByDate(sortDirection ? .orderedDescending : .orderedAscending, groupByFeed: groupByFeed)
|
||||||
timelineItems = [TimelineItem]()
|
timelineItems = articles.map { TimelineItem(article: $0) }
|
||||||
nextBatch()
|
|
||||||
// TODO: Update unread counts and other item done in didSet on AppKit
|
// TODO: Update unread counts and other item done in didSet on AppKit
|
||||||
}
|
}
|
||||||
|
|
||||||
func nextBatch() {
|
|
||||||
let rangeEndIndex = timelineItems.endIndex + 50 > articles.endIndex ? articles.endIndex : timelineItems.endIndex + 50
|
|
||||||
let range = timelineItems.endIndex..<rangeEndIndex
|
|
||||||
for i in range {
|
|
||||||
timelineItems.append(TimelineItem(article: articles[i]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Notifications
|
// MARK: - Notifications
|
||||||
|
|
||||||
|
@ -11,25 +11,41 @@ import SwiftUI
|
|||||||
struct TimelineView: View {
|
struct TimelineView: View {
|
||||||
|
|
||||||
@EnvironmentObject private var timelineModel: TimelineModel
|
@EnvironmentObject private var timelineModel: TimelineModel
|
||||||
|
@State var navigate = false
|
||||||
|
|
||||||
var body: some View {
|
@ViewBuilder var body: some View {
|
||||||
List(timelineModel.timelineItems) { timelineItem in
|
#if os(macOS)
|
||||||
ZStack {
|
ZStack {
|
||||||
TimelineItemView(timelineItem: timelineItem)
|
NavigationLink(destination: ArticleContainerView(articles: timelineModel.selectedArticles), isActive: $navigate) {
|
||||||
.onAppear {
|
EmptyView()
|
||||||
timelineModel.loadMoreTimelineItemsIfNecessary(timelineItem)
|
}.hidden()
|
||||||
}
|
List(timelineModel.timelineItems, selection: $timelineModel.selectedArticleIDs) { timelineItem in
|
||||||
NavigationLink(destination: (ArticleContainerView(article: timelineItem.article))) {
|
buildTimelineItemNavigation(timelineItem)
|
||||||
EmptyView()
|
|
||||||
}.buttonStyle(PlainButtonStyle())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: timelineModel.selectedArticleIDs) { value in
|
||||||
|
navigate = !timelineModel.selectedArticleIDs.isEmpty
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
List(timelineModel.timelineItems) { timelineItem in
|
||||||
|
buildTimelineItemNavigation(timelineItem)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// var body: some View {
|
func buildTimelineItemNavigation(_ timelineItem: TimelineItem) -> some View {
|
||||||
// List(timelineModel.timelineItems) { timelineItem in
|
#if os(macOS)
|
||||||
// TimelineItemView(timelineItem: timelineItem)
|
return TimelineItemView(timelineItem: timelineItem) //.tag(timelineItem.article.articleID)
|
||||||
// }
|
#else
|
||||||
// }
|
return ZStack {
|
||||||
|
TimelineItemView(timelineItem: timelineItem)
|
||||||
|
NavigationLink(destination: ArticleContainerView(articles: timelineModel.selectedArticles),
|
||||||
|
tag: timelineItem.article.articleID,
|
||||||
|
selection: $timelineModel.selectedArticleID) {
|
||||||
|
EmptyView()
|
||||||
|
}.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,21 +9,15 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Articles
|
import Articles
|
||||||
|
|
||||||
final class ArticleView: UIViewControllerRepresentable {
|
struct ArticleView: UIViewControllerRepresentable {
|
||||||
|
|
||||||
var sceneModel: SceneModel
|
var sceneModel: SceneModel
|
||||||
var article: Article
|
var articles: [Article]
|
||||||
|
|
||||||
init(sceneModel: SceneModel, article: Article) {
|
|
||||||
self.sceneModel = sceneModel
|
|
||||||
self.article = article
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeUIViewController(context: Context) -> ArticleViewController {
|
func makeUIViewController(context: Context) -> ArticleViewController {
|
||||||
let controller = ArticleViewController()
|
let controller = ArticleViewController()
|
||||||
sceneModel.articleManager = controller
|
|
||||||
controller.sceneModel = sceneModel
|
controller.sceneModel = sceneModel
|
||||||
controller.currentArticle = article
|
controller.articles = articles
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ import Account
|
|||||||
import Articles
|
import Articles
|
||||||
import SafariServices
|
import SafariServices
|
||||||
|
|
||||||
class ArticleViewController: UIViewController, ArticleManager {
|
class ArticleViewController: UIViewController {
|
||||||
|
|
||||||
weak var sceneModel: SceneModel?
|
weak var sceneModel: SceneModel?
|
||||||
|
|
||||||
@ -22,6 +22,12 @@ class ArticleViewController: UIViewController, ArticleManager {
|
|||||||
return pageViewController?.viewControllers?.first as? WebViewController
|
return pageViewController?.viewControllers?.first as? WebViewController
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var articles: [Article]? {
|
||||||
|
didSet {
|
||||||
|
currentArticle = articles?.first
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var currentArticle: Article? {
|
var currentArticle: Article? {
|
||||||
didSet {
|
didSet {
|
||||||
if let controller = currentWebViewController, controller.article != currentArticle {
|
if let controller = currentWebViewController, controller.article != currentArticle {
|
||||||
@ -54,8 +60,6 @@ class ArticleViewController: UIViewController, ArticleManager {
|
|||||||
|
|
||||||
let controller = createWebViewController(currentArticle, updateView: true)
|
let controller = createWebViewController(currentArticle, updateView: true)
|
||||||
self.pageViewController.setViewControllers([controller], direction: .forward, animated: false, completion: nil)
|
self.pageViewController.setViewControllers([controller], direction: .forward, animated: false, completion: nil)
|
||||||
|
|
||||||
sceneModel?.updateArticleSelection()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: API
|
// MARK: API
|
||||||
@ -123,7 +127,6 @@ extension ArticleViewController: UIPageViewControllerDelegate {
|
|||||||
guard finished, completed else { return }
|
guard finished, completed else { return }
|
||||||
// guard let article = currentWebViewController?.article else { return }
|
// guard let article = currentWebViewController?.article else { return }
|
||||||
|
|
||||||
sceneModel?.updateArticleSelection()
|
|
||||||
// articleExtractorButton.buttonState = currentWebViewController?.articleExtractorButtonState ?? .off
|
// articleExtractorButton.buttonState = currentWebViewController?.articleExtractorButtonState ?? .off
|
||||||
|
|
||||||
previousViewControllers.compactMap({ $0 as? WebViewController }).forEach({ $0.stopWebViewActivity() })
|
previousViewControllers.compactMap({ $0 as? WebViewController }).forEach({ $0.stopWebViewActivity() })
|
||||||
|
@ -12,18 +12,12 @@ import Articles
|
|||||||
struct ArticleView: NSViewControllerRepresentable {
|
struct ArticleView: NSViewControllerRepresentable {
|
||||||
|
|
||||||
var sceneModel: SceneModel
|
var sceneModel: SceneModel
|
||||||
var article: Article
|
var articles: [Article]
|
||||||
|
|
||||||
init(sceneModel: SceneModel, article: Article) {
|
|
||||||
self.sceneModel = sceneModel
|
|
||||||
self.article = article
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeNSViewController(context: Context) -> WebViewController {
|
func makeNSViewController(context: Context) -> WebViewController {
|
||||||
let controller = WebViewController()
|
let controller = WebViewController()
|
||||||
sceneModel.articleManager = controller
|
|
||||||
controller.sceneModel = sceneModel
|
controller.sceneModel = sceneModel
|
||||||
controller.currentArticle = article
|
controller.articles = articles
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ protocol WebViewControllerDelegate: class {
|
|||||||
func webViewController(_: WebViewController, articleExtractorButtonStateDidUpdate: ArticleExtractorButtonState)
|
func webViewController(_: WebViewController, articleExtractorButtonStateDidUpdate: ArticleExtractorButtonState)
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebViewController: NSViewController, ArticleManager {
|
class WebViewController: NSViewController {
|
||||||
|
|
||||||
private struct MessageName {
|
private struct MessageName {
|
||||||
static let imageWasClicked = "imageWasClicked"
|
static let imageWasClicked = "imageWasClicked"
|
||||||
@ -49,7 +49,7 @@ class WebViewController: NSViewController, ArticleManager {
|
|||||||
var sceneModel: SceneModel?
|
var sceneModel: SceneModel?
|
||||||
weak var delegate: WebViewControllerDelegate?
|
weak var delegate: WebViewControllerDelegate?
|
||||||
|
|
||||||
var currentArticle: Article?
|
var articles: [Article]?
|
||||||
|
|
||||||
override func loadView() {
|
override func loadView() {
|
||||||
view = NSView()
|
view = NSView()
|
||||||
@ -75,8 +75,6 @@ class WebViewController: NSViewController, ArticleManager {
|
|||||||
])
|
])
|
||||||
|
|
||||||
loadWebView()
|
loadWebView()
|
||||||
|
|
||||||
sceneModel?.updateArticleSelection()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Notifications
|
// MARK: Notifications
|
||||||
@ -119,7 +117,7 @@ class WebViewController: NSViewController, ArticleManager {
|
|||||||
|
|
||||||
func toggleArticleExtractor() {
|
func toggleArticleExtractor() {
|
||||||
|
|
||||||
guard let article = currentArticle else {
|
guard let article = articles?.first else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,17 +245,19 @@ private extension WebViewController {
|
|||||||
let style = ArticleStylesManager.shared.currentStyle
|
let style = ArticleStylesManager.shared.currentStyle
|
||||||
let rendering: ArticleRenderer.Rendering
|
let rendering: ArticleRenderer.Rendering
|
||||||
|
|
||||||
if let articleExtractor = articleExtractor, articleExtractor.state == .processing {
|
if articles?.count ?? 0 > 1 {
|
||||||
|
rendering = ArticleRenderer.multipleSelectionHTML(style: style)
|
||||||
|
} else if let articleExtractor = articleExtractor, articleExtractor.state == .processing {
|
||||||
rendering = ArticleRenderer.loadingHTML(style: style)
|
rendering = ArticleRenderer.loadingHTML(style: style)
|
||||||
} else if let articleExtractor = articleExtractor, articleExtractor.state == .failedToParse, let article = currentArticle {
|
} else if let articleExtractor = articleExtractor, articleExtractor.state == .failedToParse, let article = articles?.first {
|
||||||
rendering = ArticleRenderer.articleHTML(article: article, style: style)
|
rendering = ArticleRenderer.articleHTML(article: article, style: style)
|
||||||
} else if let article = currentArticle, let extractedArticle = extractedArticle {
|
} else if let article = articles?.first, let extractedArticle = extractedArticle {
|
||||||
if isShowingExtractedArticle {
|
if isShowingExtractedArticle {
|
||||||
rendering = ArticleRenderer.articleHTML(article: article, extractedArticle: extractedArticle, style: style)
|
rendering = ArticleRenderer.articleHTML(article: article, extractedArticle: extractedArticle, style: style)
|
||||||
} else {
|
} else {
|
||||||
rendering = ArticleRenderer.articleHTML(article: article, style: style)
|
rendering = ArticleRenderer.articleHTML(article: article, style: style)
|
||||||
}
|
}
|
||||||
} else if let article = currentArticle {
|
} else if let article = articles?.first {
|
||||||
rendering = ArticleRenderer.articleHTML(article: article, style: style)
|
rendering = ArticleRenderer.articleHTML(article: article, style: style)
|
||||||
} else {
|
} else {
|
||||||
rendering = ArticleRenderer.noSelectionHTML(style: style)
|
rendering = ArticleRenderer.noSelectionHTML(style: style)
|
||||||
@ -299,7 +299,7 @@ private extension WebViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startArticleExtractor() {
|
func startArticleExtractor() {
|
||||||
if let link = currentArticle?.preferredLink, let extractor = ArticleExtractor(link) {
|
if let link = articles?.first?.preferredLink, let extractor = ArticleExtractor(link) {
|
||||||
extractor.delegate = self
|
extractor.delegate = self
|
||||||
extractor.process()
|
extractor.process()
|
||||||
articleExtractor = extractor
|
articleExtractor = extractor
|
||||||
@ -315,7 +315,7 @@ private extension WebViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func reloadArticleImage() {
|
func reloadArticleImage() {
|
||||||
guard let article = currentArticle else { return }
|
guard let article = articles?.first else { return }
|
||||||
|
|
||||||
var components = URLComponents()
|
var components = URLComponents()
|
||||||
components.scheme = ArticleRenderer.imageIconScheme
|
components.scheme = ArticleRenderer.imageIconScheme
|
||||||
|
@ -211,8 +211,6 @@
|
|||||||
51707439232AA97100A461A3 /* ShareFolderPickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51707438232AA97100A461A3 /* ShareFolderPickerController.swift */; };
|
51707439232AA97100A461A3 /* ShareFolderPickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51707438232AA97100A461A3 /* ShareFolderPickerController.swift */; };
|
||||||
5171B4D424B7BABA00FB8D3B /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; };
|
5171B4D424B7BABA00FB8D3B /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; };
|
||||||
5171B4F624B7BABA00FB8D3B /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; };
|
5171B4F624B7BABA00FB8D3B /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; };
|
||||||
5171B4F824B7CB3600FB8D3B /* ArticleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5171B4F724B7CB3600FB8D3B /* ArticleManager.swift */; };
|
|
||||||
5171B4F924B7CB3600FB8D3B /* ArticleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5171B4F724B7CB3600FB8D3B /* ArticleManager.swift */; };
|
|
||||||
517630042336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; };
|
517630042336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; };
|
||||||
517630052336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; };
|
517630052336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; };
|
||||||
517630232336657E00E15FFF /* WebViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517630222336657E00E15FFF /* WebViewProvider.swift */; };
|
517630232336657E00E15FFF /* WebViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517630222336657E00E15FFF /* WebViewProvider.swift */; };
|
||||||
@ -1906,7 +1904,6 @@
|
|||||||
516AE9B22371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedTableViewSectionHeaderLayout.swift; sourceTree = "<group>"; };
|
516AE9B22371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedTableViewSectionHeaderLayout.swift; sourceTree = "<group>"; };
|
||||||
516AE9DE2372269A007DEEAA /* IconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconImage.swift; sourceTree = "<group>"; };
|
516AE9DE2372269A007DEEAA /* IconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconImage.swift; sourceTree = "<group>"; };
|
||||||
51707438232AA97100A461A3 /* ShareFolderPickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareFolderPickerController.swift; sourceTree = "<group>"; };
|
51707438232AA97100A461A3 /* ShareFolderPickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareFolderPickerController.swift; sourceTree = "<group>"; };
|
||||||
5171B4F724B7CB3600FB8D3B /* ArticleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleManager.swift; sourceTree = "<group>"; };
|
|
||||||
517630032336215100E15FFF /* main.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = main.js; sourceTree = "<group>"; };
|
517630032336215100E15FFF /* main.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = main.js; sourceTree = "<group>"; };
|
||||||
517630222336657E00E15FFF /* WebViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewProvider.swift; sourceTree = "<group>"; };
|
517630222336657E00E15FFF /* WebViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewProvider.swift; sourceTree = "<group>"; };
|
||||||
5177470224B2657F00EB0F74 /* TimelineToolbarModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineToolbarModifier.swift; sourceTree = "<group>"; };
|
5177470224B2657F00EB0F74 /* TimelineToolbarModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineToolbarModifier.swift; sourceTree = "<group>"; };
|
||||||
@ -2867,7 +2864,6 @@
|
|||||||
5177471324B37D4000EB0F74 /* PreloadedWebView.swift */,
|
5177471324B37D4000EB0F74 /* PreloadedWebView.swift */,
|
||||||
5177471924B3863000EB0F74 /* WebViewProvider.swift */,
|
5177471924B3863000EB0F74 /* WebViewProvider.swift */,
|
||||||
517B2EBB24B3E62A001AC46C /* WrapperScriptMessageHandler.swift */,
|
517B2EBB24B3E62A001AC46C /* WrapperScriptMessageHandler.swift */,
|
||||||
5171B4F724B7CB3600FB8D3B /* ArticleManager.swift */,
|
|
||||||
);
|
);
|
||||||
path = Article;
|
path = Article;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -5095,7 +5091,6 @@
|
|||||||
51E4990324A808BB00B667CB /* FaviconDownloader.swift in Sources */,
|
51E4990324A808BB00B667CB /* FaviconDownloader.swift in Sources */,
|
||||||
172199ED24AB2E0100A31D04 /* SafariView.swift in Sources */,
|
172199ED24AB2E0100A31D04 /* SafariView.swift in Sources */,
|
||||||
65ACE48624B477C9003AE06A /* SettingsAccountLabelView.swift in Sources */,
|
65ACE48624B477C9003AE06A /* SettingsAccountLabelView.swift in Sources */,
|
||||||
5171B4F824B7CB3600FB8D3B /* ArticleManager.swift in Sources */,
|
|
||||||
51E4990224A808BB00B667CB /* ColorHash.swift in Sources */,
|
51E4990224A808BB00B667CB /* ColorHash.swift in Sources */,
|
||||||
51919FAC24AA8CCA00541E64 /* UnreadCountView.swift in Sources */,
|
51919FAC24AA8CCA00541E64 /* UnreadCountView.swift in Sources */,
|
||||||
5177476224B3BC4700EB0F74 /* SettingsAboutView.swift in Sources */,
|
5177476224B3BC4700EB0F74 /* SettingsAboutView.swift in Sources */,
|
||||||
@ -5142,7 +5137,6 @@
|
|||||||
51E4996C24A8762D00B667CB /* ExtractedArticle.swift in Sources */,
|
51E4996C24A8762D00B667CB /* ExtractedArticle.swift in Sources */,
|
||||||
51E4990824A808C300B667CB /* RSHTMLMetadata+Extension.swift in Sources */,
|
51E4990824A808C300B667CB /* RSHTMLMetadata+Extension.swift in Sources */,
|
||||||
51919FF824AB8B7700541E64 /* TimelineView.swift in Sources */,
|
51919FF824AB8B7700541E64 /* TimelineView.swift in Sources */,
|
||||||
5171B4F924B7CB3600FB8D3B /* ArticleManager.swift in Sources */,
|
|
||||||
51E4992B24A8676300B667CB /* ArticleArray.swift in Sources */,
|
51E4992B24A8676300B667CB /* ArticleArray.swift in Sources */,
|
||||||
51B54AB324B5AC830014348B /* ArticleButtonState.swift in Sources */,
|
51B54AB324B5AC830014348B /* ArticleButtonState.swift in Sources */,
|
||||||
17D5F17224B0BC6700375168 /* SidebarToolbarModel.swift in Sources */,
|
17D5F17224B0BC6700375168 /* SidebarToolbarModel.swift in Sources */,
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
//
|
|
||||||
// NetNewsWire_multiplatform_widget_target.xcconfig
|
|
||||||
// NetNewsWire
|
|
||||||
//
|
|
||||||
// Created by Maurice Parker on 7/11/20.
|
|
||||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
// Configuration settings file format documentation can be found at:
|
|
||||||
// https://help.apple.com/xcode/#/dev745c5c974
|
|
Loading…
x
Reference in New Issue
Block a user