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; |
- } |
-} |