| Index: client/samples/swarm/SwarmViews.dart
|
| ===================================================================
|
| --- client/samples/swarm/SwarmViews.dart (revision 3770)
|
| +++ client/samples/swarm/SwarmViews.dart (working copy)
|
| @@ -1,975 +0,0 @@
|
| -// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
|
| -// for details. All rights reserved. Use of this source code is governed by a
|
| -// BSD-style license that can be found in the LICENSE file.
|
| -
|
| -// TODO(jacobr): there is a lot of dead code in this class. Checking is as is
|
| -// and then doing a large pass to remove functionality that doesn't make sense
|
| -// given the UI layout.
|
| -
|
| -/**
|
| - * Front page of Swarm.
|
| - */
|
| -// TODO(jacobr): this code now needs a large refactoring.
|
| -// Suggested refactorings:
|
| -// Move animation specific code into helper classes.
|
| -class FrontView extends CompositeView {
|
| - final Swarm swarm;
|
| -
|
| - /** View containing all UI anchored to the top of the page. */
|
| - CompositeView topView;
|
| - /** View containing all UI anchored to the left side of the page. */
|
| - CompositeView bottomView;
|
| - HeaderView headerView;
|
| - SliderMenu sliderMenu;
|
| -
|
| - /**
|
| - * When the user is viewing a story, the data source for that story is
|
| - * detached from the section and shown at the bottom of the screen. This keeps
|
| - * track of that so we can restore it later.
|
| - */
|
| - DataSourceView detachedView;
|
| -
|
| - /**
|
| - * Map from section title to the View that shows this section. This
|
| - * is populated lazily.
|
| - */
|
| - StoryContentView storyView;
|
| - bool nextPrevShown;
|
| -
|
| - ConveyorView sections;
|
| -
|
| - /**
|
| - * The set of keys that produce a given behavior (going down one story,
|
| - * navigating to the column to the right, etc).
|
| - */
|
| - //TODO(jmesserly): we need a key code enumeration
|
| - final Set downKeyPresses;
|
| - final Set upKeyPresses;
|
| - final Set rightKeyPresses;
|
| - final Set leftKeyPresses;
|
| - final Set openKeyPresses;
|
| - final Set backKeyPresses;
|
| - final Set nextPageKeyPresses;
|
| - final Set previousPageKeyPresses;
|
| -
|
| - FrontView(this.swarm)
|
| - : super('front-view fullpage'),
|
| - downKeyPresses = new Set.from([74 /*j*/, 40 /*down*/]),
|
| - upKeyPresses = new Set.from([75 /*k*/, 38 /*up*/]),
|
| - rightKeyPresses = new Set.from([39 /*right*/, 68 /*d*/, 76 /*l*/]),
|
| - leftKeyPresses = new Set.from([37 /*left*/, 65 /*a*/, 72 /*h*/]),
|
| - openKeyPresses = new Set.from([13 /*enter*/, 79 /*o*/]),
|
| - backKeyPresses = new Set.from([8 /*delete*/, 27 /*escape*/]),
|
| - nextPageKeyPresses = new Set.from([78 /*n*/]),
|
| - previousPageKeyPresses = new Set.from([80 /*p*/]),
|
| - nextPrevShown = false {
|
| - topView = new CompositeView('top-view', false, false, false);
|
| -
|
| - headerView = new HeaderView(swarm);
|
| - topView.addChild(headerView);
|
| -
|
| - sliderMenu = new SliderMenu(swarm.sections.sectionTitles,
|
| - (sectionTitle) {
|
| - swarm.state.moveToNewSection(sectionTitle);
|
| - _onSectionSelected(sectionTitle);
|
| - // Start with no articles selected.
|
| - swarm.state.selectedArticle.value = null;
|
| - });
|
| - topView.addChild(sliderMenu);
|
| - addChild(topView);
|
| -
|
| - bottomView = new CompositeView('bottom-view', false, false, false);
|
| - addChild(bottomView);
|
| -
|
| - sections = new ConveyorView();
|
| - sections.viewSelected = _onSectionTransitionEnded;
|
| - }
|
| -
|
| - SectionView get currentSection() {
|
| - var view = sections.selectedView;
|
| - // TODO(jmesserly): this code works around a bug in the DartC --optimize
|
| - if (view == null) {
|
| - view = sections.childViews[0];
|
| - sections.selectView(view);
|
| - }
|
| - return view;
|
| - }
|
| -
|
| - void afterRender(Element node) {
|
| - _createSectionViews();
|
| - attachWatch(swarm.state.currentArticle, (e) { _refreshCurrentArticle(); });
|
| - attachWatch(swarm.state.storyMaximized, (e) { _refreshMaximized(); });
|
| - }
|
| -
|
| - void _refreshCurrentArticle() {
|
| - if (!swarm.state.inMainView) {
|
| - _animateToStory(swarm.state.currentArticle.value);
|
| - } else {
|
| - _animateToMainView();
|
| - }
|
| - }
|
| -
|
| - /**
|
| - * Animates back from the story view to the main grid view.
|
| - */
|
| - void _animateToMainView() {
|
| - sliderMenu.removeClass('hidden');
|
| - storyView.addClass('hidden-story');
|
| - currentSection.storyMode = false;
|
| -
|
| - headerView.startTransitionToMainView();
|
| -
|
| - currentSection.dataSourceView.reattachSubview(
|
| - detachedView.source, detachedView, true);
|
| -
|
| - storyView.node.on.transitionEnd.add(handler(e) {
|
| - // Only listen once.
|
| - // TODO(rnystrom): Look into adding .once() to EventListenerList to allow
|
| - // this for any event.
|
| - storyView.node.on.transitionEnd.remove(handler, false);
|
| -
|
| - currentSection.hidden = false;
|
| - // TODO(rnystrom): Should move this "mode" into SwarmState and have
|
| - // header view respond to change events itself.
|
| - removeChild(storyView);
|
| - storyView = null;
|
| - detachedView.removeClass('sel');
|
| - detachedView = null;
|
| - });
|
| - }
|
| -
|
| - void _animateToStory(Article item) {
|
| - final source = item.dataSource;
|
| -
|
| - if (detachedView != null && detachedView.source != source) {
|
| - // Ignore spurious item selection clicks that occur while a data source
|
| - // is already selected. These are likely clicks that occur while an
|
| - // animation is in progress.
|
| - return;
|
| - }
|
| -
|
| - if (storyView != null) {
|
| - // Remove the old story. This happens if we're already in the Story View
|
| - // and the user has clicked to see a new story.
|
| - removeChild(storyView);
|
| -
|
| - // Create the new story view and place in the frame.
|
| - storyView = addChild(new StoryContentView(swarm, item));
|
| - } else {
|
| - // We are animating from the main view to the story view.
|
| - // TODO(jmesserly): make this code better
|
| - final view = currentSection.findView(source);
|
| -
|
| - final newPosition = FxUtil.computeRelativePosition(
|
| - view.node, bottomView.node);
|
| - currentSection.dataSourceView.detachSubview(view.source);
|
| - detachedView = view;
|
| -
|
| - FxUtil.setPosition(view.node, newPosition);
|
| - bottomView.addChild(view);
|
| - view.addClass('sel');
|
| - currentSection.storyMode = true;
|
| -
|
| - // Create the new story view.
|
| - storyView = new StoryContentView(swarm, item);
|
| - window.setTimeout(() {
|
| - _animateDataSourceToMinimized();
|
| -
|
| - sliderMenu.addClass('hidden');
|
| - // Make the fancy sliding into the window animation.
|
| - window.setTimeout(() {
|
| - storyView.addClass('hidden-story');
|
| - addChild(storyView);
|
| - window.setTimeout(() {
|
| - storyView.removeClass('hidden-story');
|
| - }, 0);
|
| - headerView.endTransitionToStoryView();
|
| - }, 0);
|
| - }, 0);
|
| - }
|
| - }
|
| -
|
| - void _refreshMaximized() {
|
| - if (swarm.state.storyMaximized.value) {
|
| - _animateDataSourceToMaximized();
|
| - } else {
|
| - _animateDataSourceToMinimized();
|
| - }
|
| - }
|
| -
|
| - void _animateDataSourceToMaximized() {
|
| - FxUtil.setWebkitTransform(topView.node, 0, -HeaderView.HEIGHT);
|
| - if (detachedView != null) {
|
| - FxUtil.setWebkitTransform(detachedView.node, 0,
|
| - -DataSourceView.TAB_ONLY_HEIGHT);
|
| - }
|
| - }
|
| -
|
| - void _animateDataSourceToMinimized() {
|
| - if (detachedView != null) {
|
| - FxUtil.setWebkitTransform(detachedView.node, 0, 0);
|
| - FxUtil.setWebkitTransform(topView.node, 0, 0);
|
| - }
|
| - }
|
| -
|
| - /**
|
| - * Called when the animation to switch to a section has completed.
|
| - */
|
| - void _onSectionTransitionEnded(SectionView selectedView) {
|
| - // Show the section and hide the others.
|
| - for (SectionView view in sections.childViews) {
|
| - if (view == selectedView) {
|
| - // Always refresh the sources in case they've changed.
|
| - view.showSources();
|
| - } else {
|
| - // Only show the current view for performance.
|
| - view.hideSources();
|
| - }
|
| - }
|
| - }
|
| -
|
| - /**
|
| - * Called when the user chooses a section on the SliderMenu. Hides
|
| - * all views except the one they want to see.
|
| - */
|
| - void _onSectionSelected(String sectionTitle) {
|
| - final section = swarm.sections.findSection(sectionTitle);
|
| - // Find the view for this section.
|
| - for (final view in sections.childViews) {
|
| - if (view.section == section) {
|
| - // Have the conveyor show it.
|
| - sections.selectView(view);
|
| - break;
|
| - }
|
| - }
|
| - }
|
| -
|
| - /**
|
| - * Create SectionViews for each Section in the app and add them to the
|
| - * conveyor. Note that the SectionViews won't actually populate or load data
|
| - * sources until they are shown in response to [:_onSectionSelected():].
|
| - */
|
| - void _createSectionViews() {
|
| - for (final section in swarm.sections) {
|
| - final viewFactory = new DataSourceViewFactory(swarm);
|
| - final sectionView = new SectionView(swarm, section, viewFactory);
|
| -
|
| - // TODO(rnystrom): Hack temp. Access node to make sure SectionView has
|
| - // rendered and created scroller. This can go away when event registration
|
| - // is being deferred.
|
| - sectionView.node;
|
| -
|
| - sections.addChild(sectionView);
|
| - }
|
| - addChild(sections);
|
| - }
|
| -
|
| - /**
|
| - * Controls the logic of how to respond to keypresses and then update the
|
| - * UI accordingly.
|
| - */
|
| - void processKeyEvent(KeyboardEvent e) {
|
| - int code = e.keyCode;
|
| - if (swarm.state.inMainView) {
|
| - // Option 1: We're in the Main Grid mode.
|
| - if (!swarm.state.hasArticleSelected) {
|
| - // Then a key has been pressed. Select the first item in the
|
| - // top left corner.
|
| - swarm.state.goToFirstArticleInSection();
|
| - } else if (rightKeyPresses.contains(code)) {
|
| - // Store original state that is needed if we need to move
|
| - // to the next section.
|
| - swarm.state.goToNextFeed();
|
| - } else if (leftKeyPresses.contains(code)) {
|
| - // Store original state that is needed if we need to move
|
| - // to the next section.
|
| - swarm.state.goToPreviousFeed();
|
| - } else if (downKeyPresses.contains(code)) {
|
| - swarm.state.goToNextSelectedArticle();
|
| - } else if (upKeyPresses.contains(code)) {
|
| - swarm.state.goToPreviousSelectedArticle();
|
| - } else if (openKeyPresses.contains(code)) {
|
| - // View a story in the larger Story View.
|
| - swarm.state.selectStoryAsCurrent();
|
| - } else if (nextPageKeyPresses.contains(code)) {
|
| - swarm.state.goToNextSection(sliderMenu);
|
| - } else if (previousPageKeyPresses.contains(code)) {
|
| - swarm.state.goToPreviousSection(sliderMenu);
|
| - }
|
| - } else {
|
| - // Option 2: We're in Story Mode. In this mode, the user can move up
|
| - // and down through stories, which automatically loads the next story.
|
| - if (downKeyPresses.contains(code)) {
|
| - swarm.state.goToNextArticle();
|
| - } else if (upKeyPresses.contains(code)) {
|
| - swarm.state.goToPreviousArticle();
|
| - } else if (backKeyPresses.contains(code)) {
|
| - // Move back to the main grid view.
|
| - swarm.state.clearCurrentArticle();
|
| - }
|
| - }
|
| - }
|
| -}
|
| -
|
| -/** Transitions the app back to the main screen. */
|
| -void _backToMain(SwarmState state) {
|
| - if (state.currentArticle.value != null) {
|
| - state.clearCurrentArticle();
|
| - state.storyTextMode.value = true;
|
| - state.pushToHistory();
|
| - }
|
| -}
|
| -
|
| -/** A back button that sends the user back to the front page. */
|
| -class SwarmBackButton extends View {
|
| - Swarm swarm;
|
| -
|
| - SwarmBackButton(this.swarm) : super();
|
| -
|
| - Element render() => new Element.html('<div class="back-arrow button"></div>');
|
| -
|
| - void afterRender(Element node) {
|
| - addOnClick((e) { _backToMain(swarm.state); });
|
| - }
|
| -}
|
| -
|
| -/** Top view constaining the title and standard buttons. */
|
| -class HeaderView extends CompositeView {
|
| - // TODO(jacobr): make this value be coupled with the CSS file.
|
| - static final HEIGHT = 80;
|
| - Swarm swarm;
|
| -
|
| - View _title;
|
| - View _infoButton;
|
| - View _configButton;
|
| - View _refreshButton;
|
| - SwarmBackButton _backButton;
|
| - View _infoDialog;
|
| - View _configDialog;
|
| -
|
| - // For (text/web) article view controls
|
| - View _webBackButton;
|
| - View _webForwardButton;
|
| - View _newWindowButton;
|
| -
|
| - HeaderView(this.swarm) : super('header-view') {
|
| - _backButton = addChild(new SwarmBackButton(swarm));
|
| - _title = addChild(View.div('app-title', 'Swarm'));
|
| - _configButton = addChild(View.div('config button'));
|
| - _refreshButton = addChild(View.div('refresh button'));
|
| - _infoButton = addChild(View.div('info-button button'));
|
| -
|
| - // TODO(rnystrom): No more web/text mode (it's just text) so get rid of
|
| - // these.
|
| - _webBackButton = addChild(new WebBackButton());
|
| - _webForwardButton = addChild(new WebForwardButton());
|
| - _newWindowButton = addChild(View.div('new-window-button button'));
|
| - }
|
| -
|
| - void afterRender(Element node) {
|
| - // Respond to changes to whether the story is being shown as text or web.
|
| - attachWatch(swarm.state.storyTextMode, (e) { refreshWebStoryButtons(); });
|
| -
|
| - _title.addOnClick((e) { _backToMain(swarm.state); });
|
| -
|
| - // Wire up the events.
|
| - _configButton.addOnClick((e) {
|
| - // Bring up the config dialog.
|
| - if (this._configDialog == null) {
|
| - // TODO(terry): Cleanup, HeaderView shouldn't be tangled with main view.
|
| - this._configDialog = new ConfigHintDialog(swarm.frontView, () {
|
| - swarm.frontView.removeChild(this._configDialog);
|
| - this._configDialog = null;
|
| -
|
| - // TODO: Need to push these to the server on a per-user basis.
|
| - // Update the storage now.
|
| - swarm.sections.refresh();
|
| - });
|
| -
|
| - swarm.frontView.addChild(this._configDialog);
|
| - }
|
| - // TODO(jimhug): Graceful redirection to reader.
|
| - });
|
| -
|
| - // On click of the refresh button, refresh the swarm.
|
| - _refreshButton.addOnClick(EventBatch.wrap((e) {
|
| - swarm.refresh();
|
| - }));
|
| -
|
| - // On click of the info button, show Dart info page in new window/tab.
|
| - _infoButton.addOnClick((e) {
|
| - // Bring up the config dialog.
|
| - if (this._infoDialog == null) {
|
| - // TODO(terry): Cleanup, HeaderView shouldn't be tangled with main view.
|
| - this._infoDialog = new HelpDialog(swarm.frontView, () {
|
| - swarm.frontView.removeChild(this._infoDialog);
|
| - this._infoDialog = null;
|
| -
|
| - swarm.sections.refresh();
|
| - });
|
| -
|
| - swarm.frontView.addChild(this._infoDialog);
|
| - }
|
| - });
|
| -
|
| - // On click of the new window button, show web article in new window/tab.
|
| - _newWindowButton.addOnClick((e) {
|
| - String currentArticleSrcUrl = swarm.state.currentArticle.value.srcUrl;
|
| - window.open(currentArticleSrcUrl, '_blank');
|
| - });
|
| -
|
| - startTransitionToMainView();
|
| - }
|
| -
|
| -
|
| - /**
|
| - * Refreshes whether or not the buttons specific to the display of a story in
|
| - * the web perspective are visible.
|
| - */
|
| - void refreshWebStoryButtons() {
|
| - bool webButtonsHidden = true;
|
| -
|
| - if (swarm.state.currentArticle.value != null) {
|
| - // Set if web buttons are hidden
|
| - webButtonsHidden = swarm.state.storyTextMode.value;
|
| - }
|
| -
|
| - _webBackButton.hidden = webButtonsHidden;
|
| - _webForwardButton.hidden = webButtonsHidden;
|
| - _newWindowButton.hidden = webButtonsHidden;
|
| - }
|
| -
|
| - void startTransitionToMainView() {
|
| - _title.removeClass('in-story');
|
| - _backButton.removeClass('in-story');
|
| -
|
| - _configButton.removeClass('in-story');
|
| - _refreshButton.removeClass('in-story');
|
| - _infoButton.removeClass('in-story');
|
| -
|
| - refreshWebStoryButtons();
|
| - }
|
| -
|
| - void endTransitionToStoryView() {
|
| - _title.addClass('in-story');
|
| - _backButton.addClass('in-story');
|
| -
|
| - _configButton.addClass('in-story');
|
| - _refreshButton.addClass('in-story');
|
| - _infoButton.addClass('in-story');
|
| - }
|
| -}
|
| -
|
| -
|
| -/** A back button for the web view of a story that is equivalent to clicking
|
| - * "back" in the browser. */
|
| -// TODO(rnystrom): We have nearly identical versions of this littered through
|
| -// the sample apps. Should consolidate into one.
|
| -class WebBackButton extends View {
|
| - WebBackButton() : super();
|
| -
|
| - Element render() {
|
| - return new Element.html('<div class="web-back-button button"></div>');
|
| - }
|
| -
|
| - void afterRender(Element node) {
|
| - addOnClick((e) { back(); });
|
| - }
|
| -
|
| - /** Equivalent to [window.history.back] */
|
| - static void back() {
|
| - window.history.back();
|
| - }
|
| -}
|
| -
|
| -/** A back button for the web view of a story that is equivalent to clicking
|
| - * "forward" in the browser. */
|
| -// TODO(rnystrom): We have nearly identical versions of this littered through
|
| -// the sample apps. Should consolidate into one.
|
| -class WebForwardButton extends View {
|
| - WebForwardButton() : super();
|
| -
|
| - Element render() {
|
| - return new Element.html('<div class="web-forward-button button"></div>');
|
| - }
|
| -
|
| - void afterRender(Element node) {
|
| - addOnClick((e) { forward(); });
|
| - }
|
| -
|
| - /** Equivalent to [window.history.forward] */
|
| - static void forward() {
|
| - window.history.forward();
|
| - }
|
| -}
|
| -
|
| -/**
|
| - * A factory that creates a view for data sources.
|
| - */
|
| -class DataSourceViewFactory implements ViewFactory<Feed> {
|
| - Swarm swarm;
|
| -
|
| - DataSourceViewFactory(this.swarm) {}
|
| -
|
| - View newView(Feed data) => new DataSourceView(data, swarm);
|
| -
|
| - int get width() => ArticleViewLayout.getSingleton().width;
|
| - int get height() => null; // Width for this view isn't known.
|
| -}
|
| -
|
| -
|
| -/**
|
| - * A view for the items from a single data source.
|
| - * Shows a title and a list of items.
|
| - */
|
| -class DataSourceView extends CompositeView {
|
| - // TODO(jacobr): make this value be coupled with the CSS file.
|
| - static final TAB_ONLY_HEIGHT = 34;
|
| -
|
| - final Feed source;
|
| - VariableSizeListView<Article> itemsView;
|
| -
|
| - DataSourceView(this.source, Swarm swarm) : super('query') {
|
| -
|
| - // TODO(jacobr): make the title a view or decide it is sane for a subclass
|
| - // of component view to manually add some DOM cruft.
|
| - node.nodes.add(new Element.html(
|
| - '<h2>${source.title}</h2>'));
|
| -
|
| - // TODO(jacobr): use named arguments when available.
|
| - itemsView = addChild(new VariableSizeListView<Article>(
|
| - source.articles,
|
| - new ArticleViewFactory(swarm),
|
| - true, /* scrollable */
|
| - true, /* vertical */
|
| - swarm.state.currentArticle, /* selectedItem */
|
| - !Device.supportsTouch /* snapToArticles */,
|
| - false /* paginate */,
|
| - true /* removeClippedViews */,
|
| - !Device.supportsTouch /* showScrollbar */));
|
| - itemsView.addClass('story-section');
|
| -
|
| - node.nodes.add(new Element.html('<div class="query-name-shadow"></div>'));
|
| -
|
| - // Clicking the view (i.e. its title area) unmaximizes to show the entire
|
| - // view.
|
| - node.on.mouseDown.add((e) {
|
| - swarm.state.storyMaximized.value = false;
|
| - }, false);
|
| - }
|
| -}
|
| -
|
| -/** A button that toggles between states. */
|
| -class ToggleButton extends View {
|
| - EventListeners onChanged;
|
| - List<String> states;
|
| -
|
| - ToggleButton(this.states)
|
| - : super(),
|
| - onChanged = new EventListeners();
|
| -
|
| - Element render() => new Element.tag('button');
|
| -
|
| - void afterRender(Element node) {
|
| - state = states[0];
|
| - node.on.click.add((event) { toggle(); }, false);
|
| - }
|
| -
|
| - String get state() {
|
| - final currentState = node.innerHTML;
|
| - assert(states.indexOf(currentState, 0) >= 0);
|
| - return currentState;
|
| - }
|
| -
|
| - void set state(String state) {
|
| - assert(states.indexOf(state, 0) >= 0);
|
| - node.innerHTML = state;
|
| - onChanged.fire(null);
|
| - }
|
| -
|
| - void toggle() {
|
| - final oldState = state;
|
| - int index = states.indexOf(oldState, 0);
|
| - index = (index + 1) % states.length;
|
| - state = states[index];
|
| - }
|
| -}
|
| -
|
| -/**
|
| - * A factory that creates a view for generic items.
|
| - */
|
| -class ArticleViewFactory implements VariableSizeViewFactory<Article> {
|
| - Swarm swarm;
|
| -
|
| - ArticleViewLayout layout;
|
| - ArticleViewFactory(this.swarm)
|
| - : layout = ArticleViewLayout.getSingleton();
|
| -
|
| - View newView(Article item) => new ArticleView(item, swarm, layout);
|
| -
|
| - int getWidth(Article item) => layout.width;
|
| - int getHeight(Article item) => layout.computeHeight(item);
|
| -}
|
| -
|
| -class ArticleViewMetrics {
|
| - final int height;
|
| - final int titleLines;
|
| - final int bodyLines;
|
| -
|
| - const ArticleViewMetrics(this.height, this.titleLines, this.bodyLines);
|
| -}
|
| -
|
| -class ArticleViewLayout {
|
| - // TODO(terry): clean this up once we have a framework for sharing constants
|
| - // between JS and CSS. See bug #5405307.
|
| - static final IPAD_WIDTH = 257;
|
| - static final DESKTOP_WIDTH = 297;
|
| - static final CHROME_OS_WIDTH = 317;
|
| - static final TITLE_MARGIN_LEFT = 257 - 150;
|
| - static final BODY_MARGIN_LEFT = 257 - 221;
|
| - static final LINE_HEIGHT = 18;
|
| - static final TITLE_FONT = 'bold 13px arial,sans-serif';
|
| - static final BODY_FONT = '13px arial,sans-serif';
|
| - static final TOTAL_MARGIN = 16 * 2 + 70;
|
| - static final MIN_TITLE_HEIGHT = 36;
|
| - static final MAX_TITLE_LINES = 2;
|
| - static final MAX_BODY_LINES = 4;
|
| -
|
| - MeasureText measureTitleText;
|
| - MeasureText measureBodyText;
|
| -
|
| - int width;
|
| - static ArticleViewLayout _singleton;
|
| - ArticleViewLayout() :
|
| - measureBodyText = new MeasureText(BODY_FONT),
|
| - measureTitleText = new MeasureText(TITLE_FONT) {
|
| - num screenWidth = window.screen.width;
|
| - width = DESKTOP_WIDTH;
|
| - }
|
| -
|
| - static ArticleViewLayout getSingleton() {
|
| - if (_singleton == null) {
|
| - _singleton = new ArticleViewLayout();
|
| - }
|
| - return _singleton;
|
| - }
|
| -
|
| - int computeHeight(Article item) {
|
| - if (item == null) {
|
| - // TODO(jacobr): find out why this is happening..
|
| - print('Null item encountered.');
|
| - return 0;
|
| - }
|
| -
|
| - return computeLayout(item, null, null).height;
|
| - }
|
| -
|
| - /**
|
| - * titleContainer and snippetContainer may be null in which case the size is
|
| - * computed but no actual layout is performed.
|
| - */
|
| - ArticleViewMetrics computeLayout(Article item,
|
| - StringBuffer titleBuffer,
|
| - StringBuffer snippetBuffer) {
|
| - int titleWidth = width - BODY_MARGIN_LEFT;
|
| -
|
| - if (item.hasThumbnail) {
|
| - titleWidth = width - TITLE_MARGIN_LEFT;
|
| - }
|
| -
|
| - final titleLines = measureTitleText.addLineBrokenText(titleBuffer,
|
| - item.title, titleWidth, MAX_TITLE_LINES);
|
| - final bodyLines = measureBodyText.addLineBrokenText(snippetBuffer,
|
| - item.textBody, width - BODY_MARGIN_LEFT, MAX_BODY_LINES);
|
| -
|
| - int height = bodyLines * LINE_HEIGHT + TOTAL_MARGIN;
|
| -
|
| - if (bodyLines == 0) {
|
| - height = 92;
|
| - }
|
| -
|
| - return new ArticleViewMetrics(height, titleLines, bodyLines);
|
| - }
|
| -}
|
| -
|
| -/**
|
| - * A view for a generic item.
|
| - */
|
| -class ArticleView extends View {
|
| - // Set to false to make inspecting the HTML more pleasant...
|
| - static final SAVE_IMAGES = false;
|
| -
|
| - final Article item;
|
| - final Swarm swarm;
|
| - final ArticleViewLayout articleLayout;
|
| -
|
| - ArticleView(this.item, this.swarm, this.articleLayout) : super();
|
| -
|
| - Element render() {
|
| - Element node;
|
| -
|
| - final byline = item.author.length > 0 ? item.author : item.dataSource.title;
|
| - final date = DateUtils.toRecentTimeString(item.date);
|
| -
|
| - String storyClass = 'story no-thumb';
|
| - String thumbnail = '';
|
| -
|
| - if (item.hasThumbnail) {
|
| - storyClass = 'story';
|
| - thumbnail = '<img src="${item.thumbUrl}"></img>';
|
| - }
|
| -
|
| - final title = new StringBuffer();
|
| - final snippet = new StringBuffer();
|
| -
|
| - // Note: also populates title and snippet elements.
|
| - final metrics = articleLayout.computeLayout(item, title, snippet);
|
| -
|
| - node = new Element.html('''
|
| -<div class="$storyClass">
|
| - $thumbnail
|
| - <div class="title">$title</div>
|
| - <div class="byline">$byline</div>
|
| - <div class="dateline">$date</div>
|
| - <div class="snippet">$snippet</div>
|
| -</div>''');
|
| -
|
| - // Remove the snippet entirely if it's empty. This keeps it from taking up
|
| - // space and pushing the padding down.
|
| - if ((item.textBody == null) || (item.textBody.trim() == '')) {
|
| - node.query('.snippet').remove();
|
| - }
|
| -
|
| - return node;
|
| - }
|
| -
|
| - void afterRender(Element node) {
|
| -
|
| - // Select this view's item.
|
| - addOnClick((e) {
|
| - // Mark the item as read, so it shows as read in other views
|
| - item.unread.value = false;
|
| -
|
| - final oldArticle = swarm.state.currentArticle.value;
|
| - swarm.state.currentArticle.value = item;
|
| - swarm.state.storyTextMode.value = true;
|
| - if (oldArticle == null) {
|
| - swarm.state.pushToHistory();
|
| - }
|
| - });
|
| -
|
| - watch(swarm.state.currentArticle, (e) {
|
| - if (!swarm.state.inMainView) {
|
| - swarm.state.markCurrentAsRead();
|
| - }
|
| - _refreshSelected(swarm.state.currentArticle);
|
| - //TODO(efortuna): add in history stuff while reading articles?
|
| - });
|
| -
|
| - watch(swarm.state.selectedArticle, (e) {
|
| - _refreshSelected(swarm.state.selectedArticle);
|
| - _updateViewForSelectedArticle();
|
| - });
|
| -
|
| - watch(item.unread, (e) {
|
| - // TODO(rnystrom): Would be nice to do:
|
| - // node.classes.set('story-unread', item.unread.value)
|
| - if (item.unread.value) {
|
| - node.classes.add('story-unread');
|
| - } else {
|
| - node.classes.remove('story-unread');
|
| - }
|
| - });
|
| - }
|
| -
|
| - /**
|
| - * Notify the view to jump to a different area if we are selecting an
|
| - * article that is currently outside of the visible area.
|
| - */
|
| - void _updateViewForSelectedArticle() {
|
| - Article selArticle = swarm.state.selectedArticle.value;
|
| - if (swarm.state.hasArticleSelected) {
|
| - // Ensure that the selected article is visible in the view.
|
| - if (!swarm.state.inMainView) {
|
| - // Story View.
|
| - swarm.frontView.detachedView.itemsView.showView(selArticle);
|
| - } else {
|
| - if(swarm.frontView.currentSection.inCurrentView(selArticle)) {
|
| - // Scroll horizontally if needed.
|
| - swarm.frontView.currentSection.dataSourceView.showView(
|
| - selArticle.dataSource);
|
| - DataSourceView dataView = swarm.frontView.currentSection
|
| - .findView(selArticle.dataSource);
|
| - if(dataView != null) {
|
| - dataView.itemsView.showView(selArticle);
|
| - }
|
| - }
|
| - }
|
| - }
|
| - }
|
| -
|
| - String getDataUriForImage(final img) {
|
| - // TODO(hiltonc,jimhug) eval perf of this vs. reusing one canvas element
|
| - final canvas = new Element.html(
|
| - '<canvas width="${img.width}" height="${img.height}"></canvas>');
|
| -
|
| - final ctx = canvas.getContext("2d");
|
| - ctx.drawImage(img, 0, 0, img.width, img.height);
|
| -
|
| - return canvas.toDataURL("image/png");
|
| - }
|
| -
|
| - /**
|
| - * Update this view's selected appearance based on the currently selected
|
| - * Article.
|
| - */
|
| - void _refreshSelected(curItem) {
|
| - if (curItem.value == item) {
|
| - addClass('sel');
|
| - } else {
|
| - removeClass('sel');
|
| - }
|
| - }
|
| -
|
| - void _saveToStorage(String thumbUrl, ImageElement img) {
|
| - // TODO(jimhug): Reimplement caching of images.
|
| - }
|
| -}
|
| -
|
| -/**
|
| - * An internal view of a story as text. In other words, the article is shown
|
| - * in-place as opposed to as an embedded web-page.
|
| - */
|
| -class StoryContentView extends View {
|
| - final Swarm swarm;
|
| - final Article item;
|
| -
|
| - View _pagedStory;
|
| -
|
| - StoryContentView(this.swarm, this.item) : super();
|
| -
|
| - get childViews() => [_pagedStory];
|
| -
|
| - Element render() {
|
| - final storyContent = new Element.html(
|
| - '<div class="story-content">${item.htmlBody}</div>');
|
| - for (Element element in storyContent.queryAll(
|
| - "iframe, script, style, object, embed, frameset, frame")) {
|
| - element.remove();
|
| - }
|
| - _pagedStory = new PagedContentView(new View.fromNode(storyContent));
|
| -
|
| - // Modify all links to open in new windows....
|
| - // TODO(jacobr): would it be better to add an event listener on click that
|
| - // intercepts these instead?
|
| - for (final anchor in storyContent.queryAll('a')) {
|
| - anchor.target = '_blank';
|
| - }
|
| -
|
| - final date = DateUtils.toRecentTimeString(item.date);
|
| - final container = new Element.html('''
|
| - <div class="story-view">
|
| - <div class="story-text-view">
|
| - <div class="story-header">
|
| - <a class="story-title" href="${item.srcUrl}" target="_blank">
|
| - ${item.title}</a>
|
| - <div class="story-byline">
|
| - ${item.author} - ${item.dataSource.title}
|
| - </div>
|
| - <div class="story-dateline">$date</div>
|
| - </div>
|
| - <div class="paged-story"></div>
|
| - <div class="spacer"></div>
|
| - </div>
|
| - </div>''');
|
| -
|
| - container.query('.paged-story').replaceWith(_pagedStory.node);
|
| -
|
| - return container;
|
| - }
|
| -}
|
| -
|
| -class SectionView extends CompositeView {
|
| - final Section section;
|
| - final Swarm swarm;
|
| - final DataSourceViewFactory _viewFactory;
|
| - final View loadingText;
|
| - ListView<Feed> dataSourceView;
|
| - PageNumberView pageNumberView;
|
| - final PageState pageState;
|
| -
|
| - SectionView(this.swarm, this.section, this._viewFactory)
|
| - : super('section-view'),
|
| - loadingText = new View.html('<div class="loading-section"></div>'),
|
| - pageState = new PageState() {
|
| - addChild(loadingText);
|
| - }
|
| -
|
| - /**
|
| - * Hides the loading text, reloads the data sources, and shows them.
|
| - */
|
| - void showSources() {
|
| - loadingText.node.style.display = 'none';
|
| -
|
| - // Lazy initialize the data source view.
|
| - if (dataSourceView == null) {
|
| - // TODO(jacobr): use named arguments when available.
|
| - dataSourceView = new ListView<Feed>(
|
| - section.feeds, _viewFactory,
|
| - true /* scrollable */,
|
| - false /* vertical */,
|
| - null /* selectedItem */,
|
| - true /* snapToItems */,
|
| - true /* paginate */,
|
| - true /* removeClippedViews */,
|
| - false, /* showScrollbar */
|
| - pageState);
|
| - dataSourceView.addClass("data-source-view");
|
| - addChild(dataSourceView);
|
| -
|
| - pageNumberView = addChild(new PageNumberView(pageState));
|
| -
|
| - node.style.opacity = '1';
|
| - } else {
|
| - addChild(dataSourceView);
|
| - addChild(pageNumberView);
|
| - node.style.opacity = '1';
|
| - }
|
| -
|
| - // TODO(jacobr): get rid of this call to reconfigure when it is not needed.
|
| - dataSourceView.scroller.reconfigure(() {});
|
| - }
|
| -
|
| - /**
|
| - * Hides the data sources and shows the loading text.
|
| - */
|
| - void hideSources() {
|
| - if (dataSourceView != null) {
|
| - node.style.opacity = '0.6';
|
| - removeChild(dataSourceView);
|
| - removeChild(pageNumberView);
|
| - }
|
| -
|
| - loadingText.node.style.display = 'block';
|
| - }
|
| -
|
| - set storyMode(bool inStoryMode) {
|
| - if (inStoryMode) {
|
| - addClass('hide-all-queries');
|
| - } else {
|
| - removeClass('hide-all-queries');
|
| - }
|
| - }
|
| -
|
| - /**
|
| - * Find the [DataSourceView] in this SectionView that's displaying the given
|
| - * [Feed].
|
| - */
|
| - DataSourceView findView(Feed dataSource) {
|
| - return dataSourceView.getSubview(dataSourceView.findIndex(dataSource));
|
| - }
|
| -
|
| - bool inCurrentView(Article article) {
|
| - return dataSourceView.findIndex(article.dataSource) != null;
|
| - }
|
| -}
|
|
|