| Index: ios/chrome/browser/ui/reading_list/reading_list_view_controller.mm
|
| diff --git a/ios/chrome/browser/ui/reading_list/reading_list_view_controller.mm b/ios/chrome/browser/ui/reading_list/reading_list_view_controller.mm
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..c7e64f015999d850520013f2e6df640e22a4e99a
|
| --- /dev/null
|
| +++ b/ios/chrome/browser/ui/reading_list/reading_list_view_controller.mm
|
| @@ -0,0 +1,998 @@
|
| +// Copyright 2016 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +#import "ios/chrome/browser/ui/reading_list/reading_list_view_controller.h"
|
| +
|
| +#include "base/bind.h"
|
| +#include "base/logging.h"
|
| +#import "base/mac/foundation_util.h"
|
| +#include "base/metrics/histogram_macros.h"
|
| +#include "base/metrics/user_metrics.h"
|
| +#include "base/metrics/user_metrics_action.h"
|
| +#include "base/strings/sys_string_conversions.h"
|
| +#include "base/time/time.h"
|
| +#include "components/reading_list/ios/reading_list_entry.h"
|
| +#include "components/reading_list/ios/reading_list_model.h"
|
| +#import "components/reading_list/ios/reading_list_model_bridge_observer.h"
|
| +#include "components/strings/grit/components_strings.h"
|
| +#include "components/url_formatter/url_formatter.h"
|
| +#include "ios/chrome/browser/reading_list/offline_url_utils.h"
|
| +#include "ios/chrome/browser/reading_list/reading_list_download_service.h"
|
| +#include "ios/chrome/browser/reading_list/reading_list_entry_loading_util.h"
|
| +#import "ios/chrome/browser/tabs/tab.h"
|
| +#import "ios/chrome/browser/tabs/tab_model.h"
|
| +#import "ios/chrome/browser/ui/alert_coordinator/action_sheet_coordinator.h"
|
| +#import "ios/chrome/browser/ui/collection_view/cells/collection_view_text_item.h"
|
| +#import "ios/chrome/browser/ui/collection_view/collection_view_model.h"
|
| +#import "ios/chrome/browser/ui/favicon_view.h"
|
| +#import "ios/chrome/browser/ui/material_components/utils.h"
|
| +#import "ios/chrome/browser/ui/reading_list/reading_list_collection_view_item.h"
|
| +#import "ios/chrome/browser/ui/reading_list/reading_list_toolbar.h"
|
| +#import "ios/chrome/browser/ui/uikit_ui_util.h"
|
| +#include "ios/chrome/browser/ui/url_loader.h"
|
| +#include "ios/chrome/grit/ios_strings.h"
|
| +#import "ios/third_party/material_components_ios/src/components/AppBar/src/MaterialAppBar.h"
|
| +#import "ios/third_party/material_components_ios/src/components/Palettes/src/MaterialPalettes.h"
|
| +#import "ios/third_party/material_roboto_font_loader_ios/src/src/MaterialRobotoFontLoader.h"
|
| +#include "ios/web/public/referrer.h"
|
| +#include "ios/web/public/web_state/web_state.h"
|
| +#include "net/base/network_change_notifier.h"
|
| +#include "ui/base/l10n/l10n_util_mac.h"
|
| +#include "ui/base/window_open_disposition.h"
|
| +
|
| +#if !defined(__has_feature) || !__has_feature(objc_arc)
|
| +#error "This file requires ARC support."
|
| +#endif
|
| +
|
| +namespace {
|
| +
|
| +NSString* const kEmptyReadingListBackgroundIcon = @"reading_list_icon";
|
| +NSString* const kEmptyReadingListShareIcon = @"share_icon";
|
| +NSString* const kShareIconMarker = @"SHARE_ICON";
|
| +
|
| +// Height of the toolbar.
|
| +const int kToolbarHeight = 48;
|
| +
|
| +// Background view constants.
|
| +const CGFloat kTextImageSpacing = 10;
|
| +const CGFloat kTextHorizontalMargin = 32;
|
| +const CGFloat kImageWidth = 60;
|
| +const CGFloat kImageHeight = 44;
|
| +const CGFloat kShareImageHeight = 14;
|
| +const CGFloat kShareImageWidth = 10;
|
| +const CGFloat kFontSize = 16;
|
| +
|
| +typedef NS_ENUM(NSInteger, SectionIdentifier) {
|
| + SectionIdentifierUnread = kSectionIdentifierEnumZero,
|
| + SectionIdentifierRead,
|
| +};
|
| +
|
| +typedef NS_ENUM(NSInteger, ItemType) {
|
| + ItemTypeUnreadHeader = kItemTypeEnumZero,
|
| + ItemTypeUnread,
|
| + ItemTypeReadHeader,
|
| + ItemTypeRead,
|
| +};
|
| +
|
| +// Typedef for a block taking a GURL as parameter and returning nothing.
|
| +typedef void (^EntryUpdater)(const GURL&);
|
| +
|
| +// Type for map used to sort ReadingListEntry by timestamp. Multiple entries can
|
| +// have the same timestamp.
|
| +using ItemsMapByDate = std::multimap<int64_t, ReadingListCollectionViewItem*>;
|
| +}
|
| +
|
| +@interface ReadingListViewController ()<ReadingListModelBridgeObserver> {
|
| + // Toolbar with the actions.
|
| + ReadingListToolbar* _toolbar;
|
| + // Action sheet presenting the subactions of the toolbar.
|
| + AlertCoordinator* _actionSheet;
|
| + std::unique_ptr<ReadingListModelBridge> _modelBridge;
|
| + UIView* _emptyCollectionBackground;
|
| +}
|
| +
|
| +// Lazily instantiated.
|
| +@property(nonatomic, strong, readonly)
|
| + FaviconAttributesProvider* attributesProvider;
|
| +
|
| +// Returns the UIView to be displayed when the reading list is empty.
|
| +- (UIView*)emptyCollectionBackground;
|
| +// Handles "Done" button touches.
|
| +- (void)donePressed;
|
| +// Loads all the items in all sections.
|
| +- (void)loadItems;
|
| +// Fills section |sectionIdentifier| with the items from |map| in reverse order
|
| +// of the map key.
|
| +- (void)loadItemsFromMap:(const ItemsMapByDate&)map
|
| + toSection:(SectionIdentifier)sectionIdentifier;
|
| +// Convenience method to create cell items for reading list entries.
|
| +- (ReadingListCollectionViewItem*)cellItemForReadingListEntry:
|
| + (const ReadingListEntry&)entry;
|
| +// Returns whether there are elements in the section identified by
|
| +// |sectionIdentifier|.
|
| +- (BOOL)hasItemInSection:(SectionIdentifier)sectionIdentifier;
|
| +// Adds the bottom toolbar with the edition options.
|
| +- (void)addToolbar;
|
| +// Updates the toolbar state according to the selected items.
|
| +- (void)updateToolbarState;
|
| +// Displays an action sheet to let the user choose to mark all the elements as
|
| +// read or as unread. Used when nothing is selected.
|
| +- (void)markAllItemsAs;
|
| +// Displays an action sheet to let the user choose to mark all the selected
|
| +// elements as read or as unread. Used if read and unread elements are selected.
|
| +- (void)markMixedItemsAs;
|
| +// Marks all items as read.
|
| +- (void)markAllRead;
|
| +// Marks all items as unread.
|
| +- (void)markAllUnread;
|
| +// Marks the selected items as read.
|
| +- (void)markItemsRead;
|
| +// Marks the selected items as unread.
|
| +- (void)markItemsUnread;
|
| +// Deletes all the read items.
|
| +- (void)deleteAllReadItems;
|
| +// Deletes all the selected items.
|
| +- (void)deleteSelectedItems;
|
| +// Initializes |_actionSheet| with |self| as base view controller, and the
|
| +// toolbar's mark button as anchor point.
|
| +- (void)initializeActionSheet;
|
| +// Exits the editing mode and update the toolbar state with animation.
|
| +- (void)exitEditingModeAnimated:(BOOL)animated;
|
| +// Applies |action| to every cell in the section |identifier|.
|
| +- (void)updateItemsInSectionIdentifier:(SectionIdentifier)identifier
|
| + usingEntryUpdater:(EntryUpdater)updater;
|
| +// Applies |action| to every selected element of collection view.
|
| +- (void)updateSelectedItemsWithEntryUpdater:(EntryUpdater)updater;
|
| +// Logs the deletions histograms for the entry with |url|.
|
| +- (void)logDeletionHistogramsForEntry:(const GURL&)url;
|
| +// Move all the items from |sourceSectionIdentifier| to
|
| +// |destinationSectionIdentifier| and removes the empty section from the
|
| +// collection.
|
| +- (void)moveItemsFromSection:(SectionIdentifier)sourceSectionIdentifier
|
| + toSection:(SectionIdentifier)destinationSectionIdentifier;
|
| +// Move the currently selected elements to |sectionIdentifier| and removes the
|
| +// empty sections.
|
| +- (void)moveSelectedItems:(NSArray*)sortedIndexPaths
|
| + toSection:(SectionIdentifier)sectionIdentifier;
|
| +// Makes sure |sectionIdentifier| exists with the correct header.
|
| +// Returns the index of the new section in the collection view; NSIntegerMax if
|
| +// no section has been created.
|
| +- (NSInteger)initializeSection:(SectionIdentifier)sectionIdentifier;
|
| +// Returns the header for the |sectionIdentifier|.
|
| +- (CollectionViewTextItem*)headerForSection:
|
| + (SectionIdentifier)sectionIdentifier;
|
| +// Removes the empty sections from the collection and the model.
|
| +- (void)removeEmptySections;
|
| +
|
| +@end
|
| +
|
| +@implementation ReadingListViewController
|
| +@synthesize readingListModel = _readingListModel;
|
| +@synthesize tabModel = _tabModel;
|
| +@synthesize largeIconService = _largeIconService;
|
| +@synthesize readingListDownloadService = _readingListDownloadService;
|
| +@synthesize attributesProvider = _attributesProvider;
|
| +
|
| +#pragma mark lifecycle
|
| +
|
| +- (instancetype)initWithModel:(ReadingListModel*)model
|
| + tabModel:(TabModel*)tabModel
|
| + largeIconService:(favicon::LargeIconService*)largeIconService
|
| + readingListDownloadService:
|
| + (ReadingListDownloadService*)readingListDownloadService {
|
| + self = [super initWithStyle:CollectionViewControllerStyleAppBar];
|
| + if (self) {
|
| + DCHECK(model);
|
| +
|
| + // Configure modal presentation.
|
| + [self setModalPresentationStyle:UIModalPresentationFormSheet];
|
| + [self setModalTransitionStyle:UIModalTransitionStyleCoverVertical];
|
| +
|
| + _readingListModel = model;
|
| + _tabModel = tabModel;
|
| + _largeIconService = largeIconService;
|
| + _readingListDownloadService = readingListDownloadService;
|
| + _emptyCollectionBackground = [self emptyCollectionBackground];
|
| + _toolbar = [[ReadingListToolbar alloc] initWithFrame:CGRectZero];
|
| +
|
| + _modelBridge.reset(new ReadingListModelBridge(self, model));
|
| + }
|
| + return self;
|
| +}
|
| +
|
| +#pragma mark - properties
|
| +
|
| +- (FaviconAttributesProvider*)attributesProvider {
|
| + if (_attributesProvider) {
|
| + return _attributesProvider;
|
| + }
|
| +
|
| + _attributesProvider = [[FaviconAttributesProvider alloc]
|
| + initWithFaviconSize:kFaviconPreferredSize
|
| + minFaviconSize:kFaviconMinSize
|
| + largeIconService:self.largeIconService];
|
| + return _attributesProvider;
|
| +}
|
| +
|
| +- (void)setToolbarState:(ReadingListToolbarState)toolbarState {
|
| + [_toolbar setState:toolbarState];
|
| +}
|
| +
|
| +#pragma mark - UIViewController
|
| +
|
| +- (void)updateViewConstraints {
|
| + NSDictionary* views = @{ @"toolbar" : _toolbar };
|
| + NSDictionary* metrics = @{ @"toolbarHeight" : @(kToolbarHeight) };
|
| + NSArray* constraints =
|
| + @[ @"V:[toolbar(==toolbarHeight)]|", @"H:|[toolbar]|" ];
|
| + ApplyVisualConstraintsWithMetrics(constraints, views, metrics);
|
| + [super updateViewConstraints];
|
| +}
|
| +
|
| +- (void)viewDidLoad {
|
| + [super viewDidLoad];
|
| + [self addToolbar];
|
| +
|
| + self.title = l10n_util::GetNSString(IDS_IOS_TOOLS_MENU_READING_LIST);
|
| +
|
| + // Add "Done" button.
|
| + UIBarButtonItem* doneItem = [[UIBarButtonItem alloc]
|
| + initWithTitle:l10n_util::GetNSString(IDS_IOS_READING_LIST_DONE_BUTTON)
|
| + style:UIBarButtonItemStylePlain
|
| + target:self
|
| + action:@selector(donePressed)];
|
| + doneItem.accessibilityIdentifier = @"Done";
|
| + self.navigationItem.rightBarButtonItem = doneItem;
|
| +
|
| + // Customize collection view settings.
|
| + self.styler.cellStyle = MDCCollectionViewCellStyleCard;
|
| + self.styler.separatorInset = UIEdgeInsetsMake(0, 16, 0, 16);
|
| +}
|
| +
|
| +#pragma mark - UICollectionViewDelegate
|
| +
|
| +- (UICollectionReusableView*)collectionView:(UICollectionView*)collectionView
|
| + viewForSupplementaryElementOfKind:(NSString*)kind
|
| + atIndexPath:(NSIndexPath*)indexPath {
|
| + UICollectionReusableView* cell = [super collectionView:collectionView
|
| + viewForSupplementaryElementOfKind:kind
|
| + atIndexPath:indexPath];
|
| + MDCCollectionViewTextCell* textCell =
|
| + base::mac::ObjCCast<MDCCollectionViewTextCell>(cell);
|
| + if (textCell) {
|
| + textCell.textLabel.textColor = [[MDCPalette greyPalette] tint500];
|
| + }
|
| + return cell;
|
| +};
|
| +
|
| +- (void)collectionView:(UICollectionView*)collectionView
|
| + didSelectItemAtIndexPath:(NSIndexPath*)indexPath {
|
| + [super collectionView:collectionView didSelectItemAtIndexPath:indexPath];
|
| +
|
| + if (self.editor.editing) {
|
| + [self updateToolbarState];
|
| + } else {
|
| + [self openItemAtIndexPath:indexPath];
|
| + }
|
| +}
|
| +
|
| +- (void)collectionView:(UICollectionView*)collectionView
|
| + didDeselectItemAtIndexPath:(NSIndexPath*)indexPath {
|
| + [super collectionView:collectionView didDeselectItemAtIndexPath:indexPath];
|
| + if (self.editor.editing) {
|
| + // When deselecting an item, if we are editing, we want to update the
|
| + // toolbar base on the selected items.
|
| + [self updateToolbarState];
|
| + }
|
| +}
|
| +
|
| +#pragma mark - MDCCollectionViewController
|
| +
|
| +- (void)updateFooterInfoBarIfNecessary {
|
| + // No-op. This prevents the default infobar from showing.
|
| + // TODO(crbug.com/653547): Remove this once the MDC adds an option for
|
| + // preventing the infobar from showing.
|
| +}
|
| +
|
| +#pragma mark - MDCCollectionViewStylingDelegate
|
| +
|
| +- (CGFloat)collectionView:(UICollectionView*)collectionView
|
| + cellHeightAtIndexPath:(NSIndexPath*)indexPath {
|
| + NSInteger type = [self.collectionViewModel itemTypeForIndexPath:indexPath];
|
| + if (type == ItemTypeUnread || type == ItemTypeRead)
|
| + return MDCCellDefaultTwoLineHeight;
|
| + else
|
| + return MDCCellDefaultOneLineHeight;
|
| +}
|
| +
|
| +#pragma mark - MDCCollectionViewEditingDelegate
|
| +
|
| +- (BOOL)collectionViewAllowsEditing:(nonnull UICollectionView*)collectionView {
|
| + return YES;
|
| +}
|
| +
|
| +#pragma mark - ReadingListModelBridgeObserver
|
| +
|
| +- (void)readingListModelLoaded:(const ReadingListModel*)model {
|
| + _readingListModel->ResetUnseenEntries();
|
| + [self loadModel];
|
| + UMA_HISTOGRAM_COUNTS_1000("ReadingList.Unread.Number", model->unread_size());
|
| + UMA_HISTOGRAM_COUNTS_1000("ReadingList.Read.Number",
|
| + model->size() - model->unread_size());
|
| + if ([self isViewLoaded]) {
|
| + [self.collectionView reloadData];
|
| + }
|
| +}
|
| +
|
| +- (void)readingListModelDidApplyChanges:(const ReadingListModel*)model {
|
| + // Ignore model updates when the view controller is being edited or doing
|
| + // batch updates.
|
| + if (model->IsPerformingBatchUpdates() || [self.editor isEditing]) {
|
| + return;
|
| + }
|
| +
|
| + [self reloadData];
|
| +}
|
| +
|
| +- (void)readingListModelCompletedBatchUpdates:(const ReadingListModel*)model {
|
| + // Ignore model updates when the view controller is being edited.
|
| + if ([self.editor isEditing]) {
|
| + return;
|
| + }
|
| +
|
| + [self reloadData];
|
| +}
|
| +
|
| +#pragma mark - private methods
|
| +
|
| +- (UIView*)emptyCollectionBackground {
|
| + UIView* emptyCollectionBackground = [[UIView alloc] initWithFrame:CGRectZero];
|
| +
|
| + NSString* emptyTitle =
|
| + l10n_util::GetNSString(IDS_IOS_READING_LIST_EMPTY_MESSAGE);
|
| + NSRange iconRange = [emptyTitle rangeOfString:kShareIconMarker];
|
| +
|
| + NSTextAttachment* textAttachment = [[NSTextAttachment alloc] init];
|
| + textAttachment.image = [[UIImage imageNamed:kEmptyReadingListShareIcon]
|
| + imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
| + textAttachment.bounds = CGRectMake(0, 0, kShareImageWidth, kShareImageHeight);
|
| +
|
| + NSAttributedString* attachmentAttributedString =
|
| + [NSAttributedString attributedStringWithAttachment:textAttachment];
|
| +
|
| + NSDictionary* attributes = @{
|
| + NSFontAttributeName :
|
| + [[MDFRobotoFontLoader sharedInstance] mediumFontOfSize:kFontSize],
|
| + NSForegroundColorAttributeName : [[MDCPalette greyPalette] tint700]
|
| + };
|
| +
|
| + NSMutableAttributedString* labelAttributedString =
|
| + [[NSMutableAttributedString alloc] initWithString:emptyTitle
|
| + attributes:attributes];
|
| + [labelAttributedString replaceCharactersInRange:iconRange
|
| + withAttributedString:attachmentAttributedString];
|
| +
|
| + UILabel* label = [[UILabel alloc] initWithFrame:CGRectZero];
|
| + label.attributedText = labelAttributedString;
|
| + label.lineBreakMode = NSLineBreakByWordWrapping;
|
| + label.numberOfLines = 0;
|
| + label.textAlignment = NSTextAlignmentCenter;
|
| + [label setTranslatesAutoresizingMaskIntoConstraints:NO];
|
| + [emptyCollectionBackground addSubview:label];
|
| +
|
| + UIImageView* imageView = [[UIImageView alloc] init];
|
| + imageView.image = [UIImage imageNamed:kEmptyReadingListBackgroundIcon];
|
| + [imageView setTranslatesAutoresizingMaskIntoConstraints:NO];
|
| + [emptyCollectionBackground addSubview:imageView];
|
| +
|
| + [NSLayoutConstraint activateConstraints:@[
|
| + [[imageView heightAnchor] constraintEqualToConstant:kImageHeight],
|
| + [[imageView widthAnchor] constraintEqualToConstant:kImageWidth],
|
| + [[emptyCollectionBackground centerXAnchor]
|
| + constraintEqualToAnchor:label.centerXAnchor],
|
| + [[emptyCollectionBackground centerXAnchor]
|
| + constraintEqualToAnchor:imageView.centerXAnchor],
|
| + [label.topAnchor constraintEqualToAnchor:imageView.bottomAnchor
|
| + constant:kTextImageSpacing]
|
| + ]];
|
| +
|
| + // Position the top of the image at 40% from the top.
|
| + NSLayoutConstraint* verticalAlignment =
|
| + [NSLayoutConstraint constraintWithItem:imageView
|
| + attribute:NSLayoutAttributeTop
|
| + relatedBy:NSLayoutRelationEqual
|
| + toItem:emptyCollectionBackground
|
| + attribute:NSLayoutAttributeBottom
|
| + multiplier:0.4
|
| + constant:0];
|
| + [emptyCollectionBackground addConstraints:@[ verticalAlignment ]];
|
| +
|
| + ApplyVisualConstraintsWithMetrics(@[ @"H:|-(margin)-[textLabel]-(margin)-|" ],
|
| + @{ @"textLabel" : label },
|
| + @{ @"margin" : @(kTextHorizontalMargin) });
|
| +
|
| + return emptyCollectionBackground;
|
| +}
|
| +
|
| +- (void)openItemAtIndexPath:(NSIndexPath*)indexPath {
|
| + ReadingListCollectionViewItem* readingListItem =
|
| + base::mac::ObjCCastStrict<ReadingListCollectionViewItem>(
|
| + [self.collectionViewModel itemAtIndexPath:indexPath]);
|
| + const ReadingListEntry* entry =
|
| + self.readingListModel->GetEntryByURL(readingListItem.url);
|
| + if (!entry) {
|
| + [self reloadData];
|
| + return;
|
| + }
|
| +
|
| + base::RecordAction(base::UserMetricsAction("MobileReadingListOpen"));
|
| +
|
| + // Reset observer to prevent further model update notifications.
|
| + _modelBridge.reset();
|
| +
|
| + Tab* currentTab = _tabModel.currentTab;
|
| + DCHECK(currentTab);
|
| + reading_list::LoadReadingListEntry(*entry, self.readingListModel,
|
| + currentTab.webState);
|
| + [self dismiss];
|
| +}
|
| +
|
| +- (void)donePressed {
|
| + [self dismiss];
|
| +}
|
| +
|
| +- (void)dismiss {
|
| + // Reset observer to prevent further model update notifications.
|
| + _modelBridge.reset();
|
| + [_actionSheet stop];
|
| + [self.presentingViewController dismissViewControllerAnimated:YES
|
| + completion:nil];
|
| +}
|
| +
|
| +- (void)loadModel {
|
| + [super loadModel];
|
| +
|
| + if (self.readingListModel->size() == 0) {
|
| + // The collection is empty, add background.
|
| + self.collectionView.backgroundView = _emptyCollectionBackground;
|
| + [_toolbar setHidden:YES];
|
| + } else {
|
| + [self loadItems];
|
| + self.collectionView.backgroundView = nil;
|
| + [_toolbar setHidden:NO];
|
| + }
|
| +}
|
| +
|
| +- (void)loadItemsFromMap:(const ItemsMapByDate&)map
|
| + toSection:(SectionIdentifier)sectionIdentifier {
|
| + if (map.size() == 0) {
|
| + return;
|
| + }
|
| + CollectionViewModel* model = self.collectionViewModel;
|
| + [model addSectionWithIdentifier:sectionIdentifier];
|
| + [model setHeader:[self headerForSection:sectionIdentifier]
|
| + forSectionWithIdentifier:sectionIdentifier];
|
| + // Reverse iterate to add newer entries at the top.
|
| + ItemsMapByDate::const_reverse_iterator iterator = map.rbegin();
|
| + for (; iterator != map.rend(); iterator++) {
|
| + [model addItem:iterator->second toSectionWithIdentifier:sectionIdentifier];
|
| + }
|
| +}
|
| +
|
| +- (void)loadItems {
|
| + ItemsMapByDate read_map;
|
| + ItemsMapByDate unread_map;
|
| + for (const auto& url : self.readingListModel->Keys()) {
|
| + const ReadingListEntry* entry = self.readingListModel->GetEntryByURL(url);
|
| + ReadingListCollectionViewItem* item =
|
| + [self cellItemForReadingListEntry:*entry];
|
| + if (entry->IsRead()) {
|
| + read_map.insert(std::make_pair(entry->UpdateTime(), item));
|
| + } else {
|
| + unread_map.insert(std::make_pair(entry->UpdateTime(), item));
|
| + }
|
| + }
|
| + [self loadItemsFromMap:unread_map toSection:SectionIdentifierUnread];
|
| + [self loadItemsFromMap:read_map toSection:SectionIdentifierRead];
|
| +
|
| + BOOL hasRead = read_map.size() > 0;
|
| + [_toolbar setHasReadItem:hasRead];
|
| +}
|
| +
|
| +- (void)reloadData {
|
| + [self loadModel];
|
| + if ([self isViewLoaded]) {
|
| + [self.collectionView reloadData];
|
| + }
|
| +}
|
| +
|
| +- (ReadingListCollectionViewItem*)cellItemForReadingListEntry:
|
| + (const ReadingListEntry&)entry {
|
| + GURL url = entry.URL();
|
| + ReadingListCollectionViewItem* item = [[ReadingListCollectionViewItem alloc]
|
| + initWithType:entry.IsRead() ? ItemTypeRead : ItemTypeUnread
|
| + attributesProvider:self.attributesProvider
|
| + url:url
|
| + distillationState:entry.DistilledState()];
|
| + base::string16 urlString = url_formatter::FormatUrl(url);
|
| + item.text = base::SysUTF8ToNSString(entry.Title());
|
| + item.detailText = base::SysUTF16ToNSString(urlString);
|
| + return item;
|
| +}
|
| +
|
| +- (BOOL)hasItemInSection:(SectionIdentifier)sectionIdentifier {
|
| + if (![self.collectionViewModel
|
| + hasSectionForSectionIdentifier:sectionIdentifier]) {
|
| + // No section.
|
| + return NO;
|
| + }
|
| +
|
| + NSInteger section =
|
| + [self.collectionViewModel sectionForSectionIdentifier:sectionIdentifier];
|
| + NSInteger numberOfItems =
|
| + [self.collectionViewModel numberOfItemsInSection:section];
|
| +
|
| + return numberOfItems > 0;
|
| +}
|
| +
|
| +#pragma mark - ReadingListToolbarDelegate
|
| +
|
| +- (void)markPressed {
|
| + switch ([_toolbar state]) {
|
| + case NoneSelected:
|
| + [self markAllItemsAs];
|
| + break;
|
| + case OnlyUnreadSelected:
|
| + [self markItemsRead];
|
| + break;
|
| + case OnlyReadSelected:
|
| + [self markItemsUnread];
|
| + break;
|
| + case MixedItemsSelected:
|
| + [self markMixedItemsAs];
|
| + break;
|
| + }
|
| +}
|
| +
|
| +- (void)deletePressed {
|
| + if ([_toolbar state] == NoneSelected) {
|
| + [self deleteAllReadItems];
|
| + } else {
|
| + [self deleteSelectedItems];
|
| + }
|
| +}
|
| +- (void)enterEditingModePressed {
|
| + self.toolbarState = NoneSelected;
|
| + [self.editor setEditing:YES animated:YES];
|
| + [_toolbar setEditing:YES];
|
| +}
|
| +
|
| +- (void)exitEditingModePressed {
|
| + [self exitEditingModeAnimated:YES];
|
| +}
|
| +
|
| +#pragma mark - Private methods - Toolbar
|
| +
|
| +- (void)addToolbar {
|
| + [_toolbar setDelegate:self];
|
| + [_toolbar setTranslatesAutoresizingMaskIntoConstraints:NO];
|
| + [self.view addSubview:_toolbar];
|
| + UIEdgeInsets insets = self.collectionView.contentInset;
|
| + insets.bottom += kToolbarHeight + 8;
|
| + self.collectionView.contentInset = insets;
|
| + [_toolbar setHidden:YES];
|
| +}
|
| +
|
| +- (void)updateToolbarState {
|
| + BOOL readSelected = NO;
|
| + BOOL unreadSelected = NO;
|
| +
|
| + if ([self.collectionView.indexPathsForSelectedItems count] == 0) {
|
| + // No entry selected.
|
| + self.toolbarState = NoneSelected;
|
| + return;
|
| + }
|
| +
|
| + // Sections for section identifiers.
|
| + NSInteger sectionRead = NSNotFound;
|
| + NSInteger sectionUnread = NSNotFound;
|
| +
|
| + if ([self hasItemInSection:SectionIdentifierRead]) {
|
| + sectionRead = [self.collectionViewModel
|
| + sectionForSectionIdentifier:SectionIdentifierRead];
|
| + }
|
| + if ([self hasItemInSection:SectionIdentifierUnread]) {
|
| + sectionUnread = [self.collectionViewModel
|
| + sectionForSectionIdentifier:SectionIdentifierUnread];
|
| + }
|
| +
|
| + // Check selected sections.
|
| + for (NSIndexPath* index in self.collectionView.indexPathsForSelectedItems) {
|
| + if (index.section == sectionRead) {
|
| + readSelected = YES;
|
| + } else if (index.section == sectionUnread) {
|
| + unreadSelected = YES;
|
| + }
|
| + }
|
| +
|
| + // Update toolbar state.
|
| + if (readSelected) {
|
| + if (unreadSelected) {
|
| + // Read and Unread selected.
|
| + self.toolbarState = MixedItemsSelected;
|
| + } else {
|
| + // Read selected.
|
| + self.toolbarState = OnlyReadSelected;
|
| + }
|
| + } else if (unreadSelected) {
|
| + // Unread selected.
|
| + self.toolbarState = OnlyUnreadSelected;
|
| + }
|
| +}
|
| +
|
| +- (void)markAllItemsAs {
|
| + [self initializeActionSheet];
|
| + __weak ReadingListViewController* weakSelf = self;
|
| + [_actionSheet addItemWithTitle:l10n_util::GetNSStringWithFixup(
|
| + IDS_IOS_READING_LIST_MARK_ALL_READ_ACTION)
|
| + action:^{
|
| + [weakSelf markAllRead];
|
| + }
|
| + style:UIAlertActionStyleDefault];
|
| + [_actionSheet
|
| + addItemWithTitle:l10n_util::GetNSStringWithFixup(
|
| + IDS_IOS_READING_LIST_MARK_ALL_UNREAD_ACTION)
|
| + action:^{
|
| + [weakSelf markAllUnread];
|
| + }
|
| + style:UIAlertActionStyleDefault];
|
| + [_actionSheet start];
|
| +}
|
| +
|
| +- (void)markMixedItemsAs {
|
| + [self initializeActionSheet];
|
| + __weak ReadingListViewController* weakSelf = self;
|
| + [_actionSheet addItemWithTitle:l10n_util::GetNSStringWithFixup(
|
| + IDS_IOS_READING_LIST_MARK_READ_BUTTON)
|
| + action:^{
|
| + [weakSelf markItemsRead];
|
| + }
|
| + style:UIAlertActionStyleDefault];
|
| + [_actionSheet addItemWithTitle:l10n_util::GetNSStringWithFixup(
|
| + IDS_IOS_READING_LIST_MARK_UNREAD_BUTTON)
|
| + action:^{
|
| + [weakSelf markItemsUnread];
|
| + }
|
| + style:UIAlertActionStyleDefault];
|
| + [_actionSheet start];
|
| +}
|
| +
|
| +- (void)initializeActionSheet {
|
| + _actionSheet = [_toolbar actionSheetForMarkWithBaseViewController:self];
|
| +
|
| + [_actionSheet addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_CANCEL)
|
| + action:nil
|
| + style:UIAlertActionStyleCancel];
|
| +}
|
| +
|
| +- (void)markAllRead {
|
| + if (![self hasItemInSection:SectionIdentifierUnread]) {
|
| + [self exitEditingModeAnimated:YES];
|
| + return;
|
| + }
|
| +
|
| + [self updateItemsInSectionIdentifier:SectionIdentifierUnread
|
| + usingEntryUpdater:^(const GURL& url) {
|
| + [self readingListModel]->SetReadStatus(url, true);
|
| + }];
|
| + [self exitEditingModeAnimated:YES];
|
| + [self moveItemsFromSection:SectionIdentifierUnread
|
| + toSection:SectionIdentifierRead];
|
| +}
|
| +
|
| +- (void)markAllUnread {
|
| + if (![self hasItemInSection:SectionIdentifierRead]) {
|
| + [self exitEditingModeAnimated:YES];
|
| + return;
|
| + }
|
| +
|
| + [self updateItemsInSectionIdentifier:SectionIdentifierRead
|
| + usingEntryUpdater:^(const GURL& url) {
|
| + [self readingListModel]->SetReadStatus(url, false);
|
| + }];
|
| + [self exitEditingModeAnimated:YES];
|
| + [self moveItemsFromSection:SectionIdentifierRead
|
| + toSection:SectionIdentifierUnread];
|
| +}
|
| +
|
| +- (void)markItemsRead {
|
| + base::RecordAction(base::UserMetricsAction("MobileReadingListMarkRead"));
|
| + [self updateSelectedItemsWithEntryUpdater:^(const GURL& url) {
|
| + [self readingListModel]->SetReadStatus(url, true);
|
| + }];
|
| +
|
| + NSArray* sortedIndexPaths = [self.collectionView.indexPathsForSelectedItems
|
| + sortedArrayUsingSelector:@selector(compare:)];
|
| + [self exitEditingModeAnimated:YES];
|
| + [self moveSelectedItems:sortedIndexPaths toSection:SectionIdentifierRead];
|
| +}
|
| +
|
| +- (void)markItemsUnread {
|
| + base::RecordAction(base::UserMetricsAction("MobileReadingListMarkUnread"));
|
| + [self updateSelectedItemsWithEntryUpdater:^(const GURL& url) {
|
| + [self readingListModel]->SetReadStatus(url, false);
|
| + }];
|
| +
|
| + NSArray* sortedIndexPaths = [self.collectionView.indexPathsForSelectedItems
|
| + sortedArrayUsingSelector:@selector(compare:)];
|
| + [self exitEditingModeAnimated:YES];
|
| + [self moveSelectedItems:sortedIndexPaths toSection:SectionIdentifierUnread];
|
| +}
|
| +
|
| +- (void)deleteAllReadItems {
|
| + base::RecordAction(base::UserMetricsAction("MobileReadingListDeleteRead"));
|
| + if (![self hasItemInSection:SectionIdentifierRead]) {
|
| + [self exitEditingModeAnimated:YES];
|
| + return;
|
| + }
|
| +
|
| + [self updateItemsInSectionIdentifier:SectionIdentifierRead
|
| + usingEntryUpdater:^(const GURL& url) {
|
| + [self logDeletionHistogramsForEntry:url];
|
| + [self readingListModel]->RemoveEntryByURL(url);
|
| + }];
|
| +
|
| + [self exitEditingModeAnimated:YES];
|
| + [self.collectionView performBatchUpdates:^{
|
| + NSInteger readSection = [self.collectionViewModel
|
| + sectionForSectionIdentifier:SectionIdentifierRead];
|
| + [self.collectionView
|
| + deleteSections:[NSIndexSet indexSetWithIndex:readSection]];
|
| + [self.collectionViewModel
|
| + removeSectionWithIdentifier:SectionIdentifierRead];
|
| + }
|
| + completion:^(BOOL) {
|
| + // Reload data to take into account possible sync events.
|
| + [self reloadData];
|
| + }];
|
| + // As we modified the section in the batch update block, remove the section in
|
| + // another block.
|
| + [self removeEmptySections];
|
| +}
|
| +
|
| +- (void)deleteSelectedItems {
|
| + [self updateSelectedItemsWithEntryUpdater:^(const GURL& url) {
|
| + [self logDeletionHistogramsForEntry:url];
|
| + [self readingListModel]->RemoveEntryByURL(url);
|
| + }];
|
| +
|
| + NSArray* indexPaths = [self.collectionView.indexPathsForSelectedItems copy];
|
| + [self exitEditingModeAnimated:YES];
|
| +
|
| + [self.collectionView performBatchUpdates:^{
|
| + [self collectionView:self.collectionView
|
| + willDeleteItemsAtIndexPaths:indexPaths];
|
| +
|
| + [self.collectionView deleteItemsAtIndexPaths:indexPaths];
|
| + }
|
| + completion:^(BOOL) {
|
| + // Reload data to take into account possible sync events.
|
| + [self reloadData];
|
| + }];
|
| + // As we modified the section in the batch update block, remove the section in
|
| + // another block.
|
| + [self removeEmptySections];
|
| +}
|
| +
|
| +- (void)updateItemsInSectionIdentifier:(SectionIdentifier)identifier
|
| + usingEntryUpdater:(EntryUpdater)updater {
|
| + auto token = self.readingListModel->BeginBatchUpdates();
|
| + NSArray* readItems =
|
| + [self.collectionViewModel itemsInSectionWithIdentifier:identifier];
|
| + for (id item in readItems) {
|
| + ReadingListCollectionViewItem* readingListItem =
|
| + base::mac::ObjCCastStrict<ReadingListCollectionViewItem>(item);
|
| + if (updater)
|
| + updater(readingListItem.url);
|
| + }
|
| +}
|
| +
|
| +- (void)updateSelectedItemsWithEntryUpdater:(EntryUpdater)updater {
|
| + auto token = self.readingListModel->BeginBatchUpdates();
|
| + for (NSIndexPath* index in self.collectionView.indexPathsForSelectedItems) {
|
| + CollectionViewItem* cell = [self.collectionViewModel itemAtIndexPath:index];
|
| + ReadingListCollectionViewItem* readingListItem =
|
| + base::mac::ObjCCastStrict<ReadingListCollectionViewItem>(cell);
|
| + if (updater)
|
| + updater(readingListItem.url);
|
| + }
|
| +}
|
| +
|
| +- (void)logDeletionHistogramsForEntry:(const GURL&)url {
|
| + const ReadingListEntry* entry = [self readingListModel]->GetEntryByURL(url);
|
| +
|
| + if (!entry)
|
| + return;
|
| +
|
| + int64_t firstRead = entry->FirstReadTime();
|
| + if (firstRead > 0) {
|
| + // Log 0 if the entry has never been read.
|
| + firstRead = (base::Time::Now() - base::Time::UnixEpoch()).InMicroseconds() -
|
| + firstRead;
|
| + // Convert it to hours.
|
| + firstRead = firstRead / base::Time::kMicrosecondsPerHour;
|
| + }
|
| + UMA_HISTOGRAM_COUNTS_10000("ReadingList.FirstReadAgeOnDeletion", firstRead);
|
| +
|
| + int64_t age = (base::Time::Now() - base::Time::UnixEpoch()).InMicroseconds() -
|
| + entry->CreationTime();
|
| + // Convert it to hours.
|
| + age = age / base::Time::kMicrosecondsPerHour;
|
| + if (entry->IsRead())
|
| + UMA_HISTOGRAM_COUNTS_10000("ReadingList.Read.AgeOnDeletion", age);
|
| + else
|
| + UMA_HISTOGRAM_COUNTS_10000("ReadingList.Unread.AgeOnDeletion", age);
|
| +}
|
| +
|
| +- (void)moveItemsFromSection:(SectionIdentifier)sourceSectionIdentifier
|
| + toSection:(SectionIdentifier)destinationSectionIdentifier {
|
| + [self initializeSection:destinationSectionIdentifier];
|
| +
|
| + NSInteger sourceSection = [self.collectionViewModel
|
| + sectionForSectionIdentifier:sourceSectionIdentifier];
|
| + NSInteger destinationSection = [self.collectionViewModel
|
| + sectionForSectionIdentifier:destinationSectionIdentifier];
|
| + NSInteger numberOfSourceItems =
|
| + [self.collectionViewModel numberOfItemsInSection:sourceSection];
|
| +
|
| + [self.collectionView performBatchUpdates:^{
|
| + for (int index = 0; index < numberOfSourceItems; index++) {
|
| + NSIndexPath* firstItemIndex =
|
| + [NSIndexPath indexPathForItem:0 inSection:sourceSection];
|
| + NSIndexPath* sourceItemIndex =
|
| + [NSIndexPath indexPathForItem:index inSection:sourceSection];
|
| + NSIndexPath* destinationItemIndex =
|
| + [NSIndexPath indexPathForItem:index inSection:destinationSection];
|
| +
|
| + // The collection view model gets updated instantaneously, the collection
|
| + // view does batch updates.
|
| + [self collectionView:self.collectionView
|
| + willMoveItemAtIndexPath:firstItemIndex
|
| + toIndexPath:destinationItemIndex];
|
| + [self.collectionView moveItemAtIndexPath:sourceItemIndex
|
| + toIndexPath:destinationItemIndex];
|
| + }
|
| + }
|
| + completion:^(BOOL) {
|
| + // Reload data to take into account possible sync events.
|
| + [self reloadData];
|
| + }];
|
| + // As we modified the section in the batch update block, remove the section in
|
| + // another block.
|
| + [self removeEmptySections];
|
| +}
|
| +
|
| +- (void)moveSelectedItems:(NSArray*)sortedIndexPaths
|
| + toSection:(SectionIdentifier)sectionIdentifier {
|
| + NSInteger sectionCreatedIndex = [self initializeSection:sectionIdentifier];
|
| +
|
| + [self.collectionView performBatchUpdates:^{
|
| + NSInteger section = [self.collectionViewModel
|
| + sectionForSectionIdentifier:sectionIdentifier];
|
| +
|
| + NSInteger newItemIndex = 0;
|
| + // In order to make sure the we do not end modifying the wrong item, we have
|
| + // to take the items in the reverse order of the indexPaths.
|
| + for (NSIndexPath* index in [sortedIndexPaths reverseObjectEnumerator]) {
|
| + // The |sortedIndexPaths| is a copy of the index paths before the
|
| + // destination section has been added if necessary. The section part of
|
| + // the index potentially needs to be updated.
|
| + NSInteger updatedSection = index.section;
|
| + if (updatedSection >= sectionCreatedIndex)
|
| + updatedSection++;
|
| + if (updatedSection == section) {
|
| + // The item is already in the targeted section, there is no need to move
|
| + // it.
|
| + continue;
|
| + }
|
| +
|
| + NSIndexPath* updatedIndex =
|
| + [NSIndexPath indexPathForItem:index.item inSection:updatedSection];
|
| +
|
| + // Index of the item in the new section. The newItemIndex is the index of
|
| + // this item in the targeted section.
|
| + NSIndexPath* newIndexPath =
|
| + [NSIndexPath indexPathForItem:newItemIndex++ inSection:section];
|
| + [self collectionView:self.collectionView
|
| + willMoveItemAtIndexPath:updatedIndex
|
| + toIndexPath:newIndexPath];
|
| + [self.collectionView moveItemAtIndexPath:updatedIndex
|
| + toIndexPath:newIndexPath];
|
| + }
|
| + }
|
| + completion:^(BOOL) {
|
| + // Reload data to take into account possible sync events.
|
| + [self reloadData];
|
| + }];
|
| + // As we modified the section in the batch update block, remove the section in
|
| + // another block.
|
| + [self removeEmptySections];
|
| +}
|
| +
|
| +- (NSInteger)initializeSection:(SectionIdentifier)sectionIdentifier {
|
| + if (![self.collectionViewModel
|
| + hasSectionForSectionIdentifier:sectionIdentifier]) {
|
| + // The new section IndexPath will be 1 if it is the read section with
|
| + // items in the unread section, 0 otherwise.
|
| + BOOL hasNonEmptySectionAbove =
|
| + sectionIdentifier == SectionIdentifierRead &&
|
| + [self hasItemInSection:SectionIdentifierUnread];
|
| + NSInteger sectionIndex = hasNonEmptySectionAbove ? 1 : 0;
|
| +
|
| + [self.collectionView performBatchUpdates:^{
|
| + [self.collectionViewModel insertSectionWithIdentifier:sectionIdentifier
|
| + atIndex:sectionIndex];
|
| +
|
| + [self.collectionViewModel
|
| + setHeader:[self headerForSection:sectionIdentifier]
|
| + forSectionWithIdentifier:sectionIdentifier];
|
| +
|
| + [self.collectionView
|
| + insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]];
|
| + }
|
| + completion:nil];
|
| +
|
| + return sectionIndex;
|
| + }
|
| + return NSIntegerMax;
|
| +}
|
| +
|
| +- (CollectionViewTextItem*)headerForSection:
|
| + (SectionIdentifier)sectionIdentifier {
|
| + CollectionViewTextItem* header = nil;
|
| +
|
| + switch (sectionIdentifier) {
|
| + case SectionIdentifierRead:
|
| + header = [[CollectionViewTextItem alloc] initWithType:ItemTypeReadHeader];
|
| + header.text = l10n_util::GetNSString(IDS_IOS_READING_LIST_READ_HEADER);
|
| + break;
|
| +
|
| + case SectionIdentifierUnread:
|
| + header =
|
| + [[CollectionViewTextItem alloc] initWithType:ItemTypeUnreadHeader];
|
| + header.text = l10n_util::GetNSString(IDS_IOS_READING_LIST_UNREAD_HEADER);
|
| + break;
|
| + }
|
| + return header;
|
| +}
|
| +
|
| +- (void)removeEmptySections {
|
| + [self.collectionView performBatchUpdates:^{
|
| +
|
| + SectionIdentifier a[] = {SectionIdentifierRead, SectionIdentifierUnread};
|
| + for (size_t i = 0; i < arraysize(a); i++) {
|
| + SectionIdentifier sectionIdentifier = a[i];
|
| +
|
| + if ([self.collectionViewModel
|
| + hasSectionForSectionIdentifier:sectionIdentifier] &&
|
| + ![self hasItemInSection:sectionIdentifier]) {
|
| + NSInteger section = [self.collectionViewModel
|
| + sectionForSectionIdentifier:sectionIdentifier];
|
| +
|
| + [self.collectionView
|
| + deleteSections:[NSIndexSet indexSetWithIndex:section]];
|
| + [self.collectionViewModel
|
| + removeSectionWithIdentifier:sectionIdentifier];
|
| + }
|
| + }
|
| + }
|
| + completion:nil];
|
| +}
|
| +
|
| +- (void)exitEditingModeAnimated:(BOOL)animated {
|
| + [self.editor setEditing:NO animated:animated];
|
| + [_toolbar setEditing:NO];
|
| +}
|
| +
|
| +@end
|
|
|