// 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. part of swarmlib; // 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.onTransitionEnd.first.then((e) { 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); new Timer(const Duration(milliseconds: 0), () { _animateDataSourceToMinimized(); sliderMenu.addClass('hidden'); // Make the fancy sliding into the window animation. new Timer(const Duration(milliseconds: 0), () { storyView.addClass('hidden-story'); addChild(storyView); new Timer(const Duration(milliseconds: 0), () { storyView.removeClass('hidden-story'); }); headerView.endTransitionToStoryView(); }); }); } } 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 (SectionView 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('
'); 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 const 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(''); } 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(''); } 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