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?
|
||||
|
||||
/// 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).]
|
||||
// var horizontalPageViewController: HorizontalPageViewController?
|
||||
|
@ -284,19 +284,13 @@ class DetailViewController: BaseViewController {
|
|||
}
|
||||
|
||||
if layout != .left, let controller = feedDetailViewController {
|
||||
if behavior == .overlay {
|
||||
navigationItem.leftBarButtonItems = [controller.feedsBarButton, controller.settingsBarButton]
|
||||
} else {
|
||||
navigationItem.leftBarButtonItems = [controller.settingsBarButton]
|
||||
}
|
||||
navigationItem.leftBarButtonItems = [controller.feedsBarButton, controller.settingsBarButton]
|
||||
} else {
|
||||
navigationItem.leftBarButtonItems = []
|
||||
}
|
||||
|
||||
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
|
||||
guard let newSelected, oldSelected?.hash != newSelected.hash else {
|
||||
guard oldSelected?.hash != newSelected?.hash else {
|
||||
return
|
||||
}
|
||||
|
||||
print("\(oldSelected?.title ?? "none") -> \(newSelected.title)")
|
||||
print("\(oldSelected?.title ?? "none") -> \(newSelected?.title ?? "none")")
|
||||
|
||||
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)) {
|
||||
scroller.scrollTo(newSelected.id)
|
||||
}
|
||||
|
@ -118,7 +120,7 @@ struct FeedDetailGridView: View {
|
|||
withAnimation(Animation.spring().delay(0.5)) {
|
||||
scroller.scrollTo(storyViewID, anchor: .top)
|
||||
}
|
||||
} else {
|
||||
} else if let newSelected {
|
||||
scroller.scrollTo(newSelected.id, anchor: .top)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
- (void)loadingFeed;
|
||||
- (void)changedLayout;
|
||||
- (void)reload;
|
||||
- (void)reloadImmediately;
|
||||
- (void)reloadTable;
|
||||
- (void)reloadIndexPath:(NSIndexPath *)indexPath withRowAnimation:(UITableViewRowAnimation)rowAnimation;
|
||||
- (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];
|
||||
}
|
||||
|
||||
- (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 {
|
||||
@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 beginOfflineTimer];
|
||||
[appDelegate.cacheImagesOperationQueue cancelAllOperations];
|
||||
[self reload];
|
||||
// [self reload];
|
||||
}
|
||||
|
||||
- (void)reloadStories {
|
||||
|
|
|
@ -33,6 +33,18 @@ class FeedDetailViewController: FeedDetailObjCViewController {
|
|||
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 {
|
||||
guard let pref = UserDefaults.standard.string(forKey: "grid_columns"), let columns = Int(pref) else {
|
||||
return 4
|
||||
|
@ -92,8 +104,14 @@ class FeedDetailViewController: FeedDetailObjCViewController {
|
|||
if appDelegate.detailViewController.isPhone {
|
||||
changedLayout()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.appDelegate.detailViewController.updateLayout(reload: true, fetchFeeds: false)
|
||||
let wasGrid = wasGrid
|
||||
|
||||
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
|
||||
gridViewController.view.isHidden = isLegacyTable
|
||||
|
||||
print("changedLayout for \(isLegacyTable ? "legacy table" : "SwiftUI grid layout")")
|
||||
|
||||
deferredReload()
|
||||
}
|
||||
|
||||
var reloadWorkItem: DispatchWorkItem?
|
||||
|
||||
var pendingStories = [Story.ID : Story]()
|
||||
|
||||
func deferredReload(story: Story? = nil) {
|
||||
reloadWorkItem?.cancel()
|
||||
|
||||
if let story {
|
||||
pendingStories[story.id] = story
|
||||
} else {
|
||||
pendingStories.removeAll()
|
||||
}
|
||||
|
||||
let workItem = DispatchWorkItem { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
configureDataSource(story: story)
|
||||
if pendingStories.isEmpty {
|
||||
configureDataSource()
|
||||
} else {
|
||||
for story in pendingStories.values {
|
||||
configureDataSource(story: story)
|
||||
}
|
||||
}
|
||||
|
||||
pendingStories.removeAll()
|
||||
reloadWorkItem = nil
|
||||
}
|
||||
|
||||
|
@ -126,6 +162,10 @@ class FeedDetailViewController: FeedDetailObjCViewController {
|
|||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100), execute: workItem)
|
||||
}
|
||||
|
||||
@objc override func reloadImmediately() {
|
||||
configureDataSource()
|
||||
}
|
||||
|
||||
@objc override func reload() {
|
||||
deferredReload()
|
||||
}
|
||||
|
@ -207,7 +247,7 @@ extension FeedDetailViewController: FeedDetailInteraction {
|
|||
func read(story: Story) {
|
||||
let dict = story.dictionary
|
||||
|
||||
if storiesCollection.isStoryUnread(dict) {
|
||||
if isSwiftUI, storiesCollection.isStoryUnread(dict) {
|
||||
print("marking as read '\(story.title)'")
|
||||
|
||||
storiesCollection.markStoryRead(dict)
|
||||
|
@ -222,7 +262,7 @@ extension FeedDetailViewController: FeedDetailInteraction {
|
|||
func unread(story: Story) {
|
||||
let dict = story.dictionary
|
||||
|
||||
if !storiesCollection.isStoryUnread(dict) {
|
||||
if isSwiftUI, !storiesCollection.isStoryUnread(dict) {
|
||||
print("marking as unread '\(story.title)'")
|
||||
|
||||
storiesCollection.markStoryUnread(dict)
|
||||
|
|
|
@ -42,4 +42,8 @@ class SplitViewDelegate: NSObject, UISplitViewControllerDelegate {
|
|||
return proposedDisplayMode
|
||||
}
|
||||
}
|
||||
|
||||
func splitViewController(_ svc: UISplitViewController, willChangeTo displayMode: UISplitViewController.DisplayMode) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -374,6 +374,10 @@
|
|||
#pragma mark - Story Management
|
||||
|
||||
- (void)addStories:(NSArray *)stories {
|
||||
if (self.activeFeedStories == nil) {
|
||||
NSLog(@"addStories: activeFeedStories was nil!");
|
||||
self.activeFeedStories = [NSMutableArray array];
|
||||
}
|
||||
self.activeFeedStories = [self.activeFeedStories arrayByAddingObjectsFromArray:stories];
|
||||
self.storyCount = (int)[self.activeFeedStories count];
|
||||
[self calculateStoryLocations];
|
||||
|
@ -424,6 +428,10 @@
|
|||
NSString *hash = story[@"story_hash"];
|
||||
NSString *title = story[@"story_title"];
|
||||
|
||||
if (!hash) {
|
||||
NSLog(@"🔧 trying to sync as read with no hash: %@: %@", hash, title); // log
|
||||
return;
|
||||
}
|
||||
if (self.recentlyReadHashes[hash]) {
|
||||
NSLog(@"🔧 trying to sync as read when already read: %@: %@", hash, title); // log
|
||||
return;
|
||||
|
|
|
@ -32,9 +32,7 @@ class Story: Identifiable {
|
|||
var author = ""
|
||||
|
||||
var dateAndAuthor: String {
|
||||
let date = Utilities.formatShortDate(fromTimestamp: timestamp) ?? ""
|
||||
|
||||
return author.isEmpty ? date : "\(date) · \(author)"
|
||||
return author.isEmpty ? dateString : "\(dateString) · \(author)"
|
||||
}
|
||||
|
||||
var isRiverOrSocial = true
|
||||
|
@ -90,8 +88,8 @@ class Story: Identifiable {
|
|||
title = (string(for: "story_title") as NSString).decodingHTMLEntities()
|
||||
content = String(string(for: "story_content").convertHTML().decodingXMLEntities().decodingHTMLEntities().replacingOccurrences(of: "\n", with: " ").prefix(500))
|
||||
author = string(for: "story_authors").replacingOccurrences(of: "\"", with: "")
|
||||
dateString = string(for: "short_parsed_date")
|
||||
timestamp = dictionary["story_timestamp"] as? Int ?? 0
|
||||
dateString = Utilities.formatShortDate(fromTimestamp: timestamp) ?? ""
|
||||
isSaved = dictionary["starred"] as? Bool ?? false
|
||||
isShared = dictionary["shared"] as? Bool ?? false
|
||||
hash = string(for: "story_hash")
|
||||
|
@ -124,7 +122,11 @@ extension Story: Equatable {
|
|||
|
||||
extension Story: CustomDebugStringConvertible {
|
||||
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() {
|
||||
let storyCount = Int(appDelegate.storiesCollection.storyLocationsCount)
|
||||
var beforeSelection = [Int]()
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<objects>
|
||||
<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">
|
||||
<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>
|
||||
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bPa-u1-Aml">
|
||||
<rect key="frame" x="0.0" y="74" width="1194" height="580"/>
|
||||
|
|
Loading…
Add table
Reference in a new issue