mirror of
https://github.com/samuelclay/NewsBlur.git
synced 2025-08-05 16:58:59 +00:00
#1720 (Grid view)
- Fixed list view stories incorrectly marked as read. - Fixed grid view wrong dates in story header. - Fixed grid view heading not matching story. - Fixed grid view showing the wrong story. - Fixed grid view duplicate stories. - Tapping grid view story heading now scrolls to show that story’s card. - Now always shows the Sites button in non-left layout, since the collapse fullscreen button doesn’t seem to work. - Fixed crash with malformed story. - Fixed loading feeds in different layouts. - And some other tweaks.
This commit is contained in:
parent
8687a6c7c5
commit
75e4fb7e85
9 changed files with 84 additions and 25 deletions
|
@ -251,7 +251,7 @@ class DetailViewController: BaseViewController {
|
||||||
var feedDetailViewController: FeedDetailViewController?
|
var feedDetailViewController: FeedDetailViewController?
|
||||||
|
|
||||||
/// Whether or not the grid layout was used the last time checking the view controllers.
|
/// Whether or not the grid layout was used the last time checking the view controllers.
|
||||||
private var wasGrid = false
|
var wasGrid = false
|
||||||
|
|
||||||
/// The horizontal page view controller. [Not currently used; might be used for #1351 (gestures in vertical scrolling).]
|
/// The horizontal page view controller. [Not currently used; might be used for #1351 (gestures in vertical scrolling).]
|
||||||
// var horizontalPageViewController: HorizontalPageViewController?
|
// var horizontalPageViewController: HorizontalPageViewController?
|
||||||
|
@ -284,19 +284,13 @@ class DetailViewController: BaseViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
if layout != .left, let controller = feedDetailViewController {
|
if layout != .left, let controller = feedDetailViewController {
|
||||||
if behavior == .overlay {
|
navigationItem.leftBarButtonItems = [controller.feedsBarButton, controller.settingsBarButton]
|
||||||
navigationItem.leftBarButtonItems = [controller.feedsBarButton, controller.settingsBarButton]
|
|
||||||
} else {
|
|
||||||
navigationItem.leftBarButtonItems = [controller.settingsBarButton]
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
navigationItem.leftBarButtonItems = []
|
navigationItem.leftBarButtonItems = []
|
||||||
}
|
}
|
||||||
|
|
||||||
if reload {
|
if reload {
|
||||||
DispatchQueue.main.async {
|
self.feedDetailViewController?.reload()
|
||||||
self.feedDetailViewController?.reload()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -103,14 +103,16 @@ struct FeedDetailGridView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: cache.selected) { [oldSelected = cache.selected] newSelected in
|
.onChange(of: cache.selected) { [oldSelected = cache.selected] newSelected in
|
||||||
guard let newSelected, oldSelected?.hash != newSelected.hash else {
|
guard oldSelected?.hash != newSelected?.hash else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
print("\(oldSelected?.title ?? "none") -> \(newSelected.title)")
|
print("\(oldSelected?.title ?? "none") -> \(newSelected?.title ?? "none")")
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
if !cache.isGrid {
|
if newSelected == nil, !cache.isPhone, let oldSelected, let story = cache.story(with: oldSelected.index) {
|
||||||
|
scroller.scrollTo(story.id, anchor: .top)
|
||||||
|
} else if let newSelected, !cache.isGrid {
|
||||||
withAnimation(Animation.spring().delay(0.5)) {
|
withAnimation(Animation.spring().delay(0.5)) {
|
||||||
scroller.scrollTo(newSelected.id)
|
scroller.scrollTo(newSelected.id)
|
||||||
}
|
}
|
||||||
|
@ -118,7 +120,7 @@ struct FeedDetailGridView: View {
|
||||||
withAnimation(Animation.spring().delay(0.5)) {
|
withAnimation(Animation.spring().delay(0.5)) {
|
||||||
scroller.scrollTo(storyViewID, anchor: .top)
|
scroller.scrollTo(storyViewID, anchor: .top)
|
||||||
}
|
}
|
||||||
} else {
|
} else if let newSelected {
|
||||||
scroller.scrollTo(newSelected.id, anchor: .top)
|
scroller.scrollTo(newSelected.id, anchor: .top)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,7 @@
|
||||||
- (void)loadingFeed;
|
- (void)loadingFeed;
|
||||||
- (void)changedLayout;
|
- (void)changedLayout;
|
||||||
- (void)reload;
|
- (void)reload;
|
||||||
|
- (void)reloadImmediately;
|
||||||
- (void)reloadTable;
|
- (void)reloadTable;
|
||||||
- (void)reloadIndexPath:(NSIndexPath *)indexPath withRowAnimation:(UITableViewRowAnimation)rowAnimation;
|
- (void)reloadIndexPath:(NSIndexPath *)indexPath withRowAnimation:(UITableViewRowAnimation)rowAnimation;
|
||||||
- (void)reloadWithSizing;
|
- (void)reloadWithSizing;
|
||||||
|
|
|
@ -344,6 +344,10 @@ typedef NS_ENUM(NSUInteger, FeedSection)
|
||||||
@throw [NSException exceptionWithName:@"Missing reload implementation" reason:@"This is implemented in the Swift subclass, so should never reach here." userInfo:nil];
|
@throw [NSException exceptionWithName:@"Missing reload implementation" reason:@"This is implemented in the Swift subclass, so should never reach here." userInfo:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)reloadImmediately {
|
||||||
|
@throw [NSException exceptionWithName:@"Missing reloadImmediately implementation" reason:@"This is implemented in the Swift subclass, so should never reach here." userInfo:nil];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)reloadIndexPath:(NSIndexPath *)indexPath withRowAnimation:(UITableViewRowAnimation)rowAnimation {
|
- (void)reloadIndexPath:(NSIndexPath *)indexPath withRowAnimation:(UITableViewRowAnimation)rowAnimation {
|
||||||
@throw [NSException exceptionWithName:@"Missing reloadIndexPath implementation" reason:@"This is implemented in the Swift subclass, so should never reach here." userInfo:nil];
|
@throw [NSException exceptionWithName:@"Missing reloadIndexPath implementation" reason:@"This is implemented in the Swift subclass, so should never reach here." userInfo:nil];
|
||||||
}
|
}
|
||||||
|
@ -707,7 +711,7 @@ typedef NS_ENUM(NSUInteger, FeedSection)
|
||||||
[self.notifier hideIn:0];
|
[self.notifier hideIn:0];
|
||||||
[self beginOfflineTimer];
|
[self beginOfflineTimer];
|
||||||
[appDelegate.cacheImagesOperationQueue cancelAllOperations];
|
[appDelegate.cacheImagesOperationQueue cancelAllOperations];
|
||||||
[self reload];
|
// [self reload];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)reloadStories {
|
- (void)reloadStories {
|
||||||
|
|
|
@ -33,6 +33,18 @@ class FeedDetailViewController: FeedDetailObjCViewController {
|
||||||
return appDelegate.detailViewController.layout == .grid
|
return appDelegate.detailViewController.layout == .grid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var wasGrid: Bool {
|
||||||
|
return appDelegate.detailViewController.wasGrid
|
||||||
|
}
|
||||||
|
|
||||||
|
var isExperimental: Bool {
|
||||||
|
return appDelegate.detailViewController.style == .experimental
|
||||||
|
}
|
||||||
|
|
||||||
|
var isSwiftUI: Bool {
|
||||||
|
return isGrid || isExperimental
|
||||||
|
}
|
||||||
|
|
||||||
var feedColumns: Int {
|
var feedColumns: Int {
|
||||||
guard let pref = UserDefaults.standard.string(forKey: "grid_columns"), let columns = Int(pref) else {
|
guard let pref = UserDefaults.standard.string(forKey: "grid_columns"), let columns = Int(pref) else {
|
||||||
return 4
|
return 4
|
||||||
|
@ -92,8 +104,14 @@ class FeedDetailViewController: FeedDetailObjCViewController {
|
||||||
if appDelegate.detailViewController.isPhone {
|
if appDelegate.detailViewController.isPhone {
|
||||||
changedLayout()
|
changedLayout()
|
||||||
} else {
|
} else {
|
||||||
DispatchQueue.main.async {
|
let wasGrid = wasGrid
|
||||||
self.appDelegate.detailViewController.updateLayout(reload: true, fetchFeeds: false)
|
|
||||||
|
self.appDelegate.detailViewController.updateLayout(reload: false, fetchFeeds: false)
|
||||||
|
|
||||||
|
if wasGrid != isGrid {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
|
||||||
|
self.appDelegate.detailViewController.updateLayout(reload: true, fetchFeeds: false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,20 +123,38 @@ class FeedDetailViewController: FeedDetailObjCViewController {
|
||||||
storyTitlesTable.isHidden = !isLegacyTable
|
storyTitlesTable.isHidden = !isLegacyTable
|
||||||
gridViewController.view.isHidden = isLegacyTable
|
gridViewController.view.isHidden = isLegacyTable
|
||||||
|
|
||||||
|
print("changedLayout for \(isLegacyTable ? "legacy table" : "SwiftUI grid layout")")
|
||||||
|
|
||||||
deferredReload()
|
deferredReload()
|
||||||
}
|
}
|
||||||
|
|
||||||
var reloadWorkItem: DispatchWorkItem?
|
var reloadWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
|
var pendingStories = [Story.ID : Story]()
|
||||||
|
|
||||||
func deferredReload(story: Story? = nil) {
|
func deferredReload(story: Story? = nil) {
|
||||||
reloadWorkItem?.cancel()
|
reloadWorkItem?.cancel()
|
||||||
|
|
||||||
|
if let story {
|
||||||
|
pendingStories[story.id] = story
|
||||||
|
} else {
|
||||||
|
pendingStories.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
let workItem = DispatchWorkItem { [weak self] in
|
let workItem = DispatchWorkItem { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
configureDataSource(story: story)
|
if pendingStories.isEmpty {
|
||||||
|
configureDataSource()
|
||||||
|
} else {
|
||||||
|
for story in pendingStories.values {
|
||||||
|
configureDataSource(story: story)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingStories.removeAll()
|
||||||
reloadWorkItem = nil
|
reloadWorkItem = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,6 +162,10 @@ class FeedDetailViewController: FeedDetailObjCViewController {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100), execute: workItem)
|
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100), execute: workItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc override func reloadImmediately() {
|
||||||
|
configureDataSource()
|
||||||
|
}
|
||||||
|
|
||||||
@objc override func reload() {
|
@objc override func reload() {
|
||||||
deferredReload()
|
deferredReload()
|
||||||
}
|
}
|
||||||
|
@ -207,7 +247,7 @@ extension FeedDetailViewController: FeedDetailInteraction {
|
||||||
func read(story: Story) {
|
func read(story: Story) {
|
||||||
let dict = story.dictionary
|
let dict = story.dictionary
|
||||||
|
|
||||||
if storiesCollection.isStoryUnread(dict) {
|
if isSwiftUI, storiesCollection.isStoryUnread(dict) {
|
||||||
print("marking as read '\(story.title)'")
|
print("marking as read '\(story.title)'")
|
||||||
|
|
||||||
storiesCollection.markStoryRead(dict)
|
storiesCollection.markStoryRead(dict)
|
||||||
|
@ -222,7 +262,7 @@ extension FeedDetailViewController: FeedDetailInteraction {
|
||||||
func unread(story: Story) {
|
func unread(story: Story) {
|
||||||
let dict = story.dictionary
|
let dict = story.dictionary
|
||||||
|
|
||||||
if !storiesCollection.isStoryUnread(dict) {
|
if isSwiftUI, !storiesCollection.isStoryUnread(dict) {
|
||||||
print("marking as unread '\(story.title)'")
|
print("marking as unread '\(story.title)'")
|
||||||
|
|
||||||
storiesCollection.markStoryUnread(dict)
|
storiesCollection.markStoryUnread(dict)
|
||||||
|
|
|
@ -42,4 +42,8 @@ class SplitViewDelegate: NSObject, UISplitViewControllerDelegate {
|
||||||
return proposedDisplayMode
|
return proposedDisplayMode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func splitViewController(_ svc: UISplitViewController, willChangeTo displayMode: UISplitViewController.DisplayMode) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -374,6 +374,10 @@
|
||||||
#pragma mark - Story Management
|
#pragma mark - Story Management
|
||||||
|
|
||||||
- (void)addStories:(NSArray *)stories {
|
- (void)addStories:(NSArray *)stories {
|
||||||
|
if (self.activeFeedStories == nil) {
|
||||||
|
NSLog(@"addStories: activeFeedStories was nil!");
|
||||||
|
self.activeFeedStories = [NSMutableArray array];
|
||||||
|
}
|
||||||
self.activeFeedStories = [self.activeFeedStories arrayByAddingObjectsFromArray:stories];
|
self.activeFeedStories = [self.activeFeedStories arrayByAddingObjectsFromArray:stories];
|
||||||
self.storyCount = (int)[self.activeFeedStories count];
|
self.storyCount = (int)[self.activeFeedStories count];
|
||||||
[self calculateStoryLocations];
|
[self calculateStoryLocations];
|
||||||
|
@ -424,6 +428,10 @@
|
||||||
NSString *hash = story[@"story_hash"];
|
NSString *hash = story[@"story_hash"];
|
||||||
NSString *title = story[@"story_title"];
|
NSString *title = story[@"story_title"];
|
||||||
|
|
||||||
|
if (!hash) {
|
||||||
|
NSLog(@"🔧 trying to sync as read with no hash: %@: %@", hash, title); // log
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (self.recentlyReadHashes[hash]) {
|
if (self.recentlyReadHashes[hash]) {
|
||||||
NSLog(@"🔧 trying to sync as read when already read: %@: %@", hash, title); // log
|
NSLog(@"🔧 trying to sync as read when already read: %@: %@", hash, title); // log
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -32,9 +32,7 @@ class Story: Identifiable {
|
||||||
var author = ""
|
var author = ""
|
||||||
|
|
||||||
var dateAndAuthor: String {
|
var dateAndAuthor: String {
|
||||||
let date = Utilities.formatShortDate(fromTimestamp: timestamp) ?? ""
|
return author.isEmpty ? dateString : "\(dateString) · \(author)"
|
||||||
|
|
||||||
return author.isEmpty ? date : "\(date) · \(author)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var isRiverOrSocial = true
|
var isRiverOrSocial = true
|
||||||
|
@ -90,8 +88,8 @@ class Story: Identifiable {
|
||||||
title = (string(for: "story_title") as NSString).decodingHTMLEntities()
|
title = (string(for: "story_title") as NSString).decodingHTMLEntities()
|
||||||
content = String(string(for: "story_content").convertHTML().decodingXMLEntities().decodingHTMLEntities().replacingOccurrences(of: "\n", with: " ").prefix(500))
|
content = String(string(for: "story_content").convertHTML().decodingXMLEntities().decodingHTMLEntities().replacingOccurrences(of: "\n", with: " ").prefix(500))
|
||||||
author = string(for: "story_authors").replacingOccurrences(of: "\"", with: "")
|
author = string(for: "story_authors").replacingOccurrences(of: "\"", with: "")
|
||||||
dateString = string(for: "short_parsed_date")
|
|
||||||
timestamp = dictionary["story_timestamp"] as? Int ?? 0
|
timestamp = dictionary["story_timestamp"] as? Int ?? 0
|
||||||
|
dateString = Utilities.formatShortDate(fromTimestamp: timestamp) ?? ""
|
||||||
isSaved = dictionary["starred"] as? Bool ?? false
|
isSaved = dictionary["starred"] as? Bool ?? false
|
||||||
isShared = dictionary["shared"] as? Bool ?? false
|
isShared = dictionary["shared"] as? Bool ?? false
|
||||||
hash = string(for: "story_hash")
|
hash = string(for: "story_hash")
|
||||||
|
@ -124,7 +122,11 @@ extension Story: Equatable {
|
||||||
|
|
||||||
extension Story: CustomDebugStringConvertible {
|
extension Story: CustomDebugStringConvertible {
|
||||||
var debugDescription: String {
|
var debugDescription: String {
|
||||||
return "Story \"\(title)\" in \(feedName)"
|
if isLoaded {
|
||||||
|
return "Story #\(index) \"\(title)\" in \(feedName)"
|
||||||
|
} else {
|
||||||
|
return "Story #\(index) (not loaded)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,6 +164,10 @@ class StoryCache: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func story(with index: Int) -> Story? {
|
||||||
|
return all.first(where: { $0.index == index } )
|
||||||
|
}
|
||||||
|
|
||||||
func reload() {
|
func reload() {
|
||||||
let storyCount = Int(appDelegate.storiesCollection.storyLocationsCount)
|
let storyCount = Int(appDelegate.storiesCollection.storyLocationsCount)
|
||||||
var beforeSelection = [Int]()
|
var beforeSelection = [Int]()
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<objects>
|
<objects>
|
||||||
<viewController storyboardIdentifier="DetailViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="djW-7k-haK" customClass="DetailViewController" customModule="NewsBlur" customModuleProvider="target" sceneMemberID="viewController">
|
<viewController storyboardIdentifier="DetailViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="djW-7k-haK" customClass="DetailViewController" customModule="NewsBlur" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<view key="view" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="jTZ-4O-xyT">
|
<view key="view" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="jTZ-4O-xyT">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="1194" height="834"/>
|
<rect key="frame" x="0.0" y="0.0" width="818.5" height="834"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bPa-u1-Aml">
|
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bPa-u1-Aml">
|
||||||
<rect key="frame" x="0.0" y="74" width="1194" height="580"/>
|
<rect key="frame" x="0.0" y="74" width="1194" height="580"/>
|
||||||
|
|
Loading…
Add table
Reference in a new issue