Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(326)

Side by Side Diff: client/samples/swarm/SwarmViews.dart

Issue 9314024: Final CL to kill off client/samples . (Closed) Base URL: http://dart.googlecode.com/svn/branches/bleeding_edge/dart/
Patch Set: Created 8 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « client/samples/swarm/SwarmState.dart ('k') | client/samples/swarm/UIState.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4
5 // TODO(jacobr): there is a lot of dead code in this class. Checking is as is
6 // and then doing a large pass to remove functionality that doesn't make sense
7 // given the UI layout.
8
9 /**
10 * Front page of Swarm.
11 */
12 // TODO(jacobr): this code now needs a large refactoring.
13 // Suggested refactorings:
14 // Move animation specific code into helper classes.
15 class FrontView extends CompositeView {
16 final Swarm swarm;
17
18 /** View containing all UI anchored to the top of the page. */
19 CompositeView topView;
20 /** View containing all UI anchored to the left side of the page. */
21 CompositeView bottomView;
22 HeaderView headerView;
23 SliderMenu sliderMenu;
24
25 /**
26 * When the user is viewing a story, the data source for that story is
27 * detached from the section and shown at the bottom of the screen. This keeps
28 * track of that so we can restore it later.
29 */
30 DataSourceView detachedView;
31
32 /**
33 * Map from section title to the View that shows this section. This
34 * is populated lazily.
35 */
36 StoryContentView storyView;
37 bool nextPrevShown;
38
39 ConveyorView sections;
40
41 /**
42 * The set of keys that produce a given behavior (going down one story,
43 * navigating to the column to the right, etc).
44 */
45 //TODO(jmesserly): we need a key code enumeration
46 final Set downKeyPresses;
47 final Set upKeyPresses;
48 final Set rightKeyPresses;
49 final Set leftKeyPresses;
50 final Set openKeyPresses;
51 final Set backKeyPresses;
52 final Set nextPageKeyPresses;
53 final Set previousPageKeyPresses;
54
55 FrontView(this.swarm)
56 : super('front-view fullpage'),
57 downKeyPresses = new Set.from([74 /*j*/, 40 /*down*/]),
58 upKeyPresses = new Set.from([75 /*k*/, 38 /*up*/]),
59 rightKeyPresses = new Set.from([39 /*right*/, 68 /*d*/, 76 /*l*/]),
60 leftKeyPresses = new Set.from([37 /*left*/, 65 /*a*/, 72 /*h*/]),
61 openKeyPresses = new Set.from([13 /*enter*/, 79 /*o*/]),
62 backKeyPresses = new Set.from([8 /*delete*/, 27 /*escape*/]),
63 nextPageKeyPresses = new Set.from([78 /*n*/]),
64 previousPageKeyPresses = new Set.from([80 /*p*/]),
65 nextPrevShown = false {
66 topView = new CompositeView('top-view', false, false, false);
67
68 headerView = new HeaderView(swarm);
69 topView.addChild(headerView);
70
71 sliderMenu = new SliderMenu(swarm.sections.sectionTitles,
72 (sectionTitle) {
73 swarm.state.moveToNewSection(sectionTitle);
74 _onSectionSelected(sectionTitle);
75 // Start with no articles selected.
76 swarm.state.selectedArticle.value = null;
77 });
78 topView.addChild(sliderMenu);
79 addChild(topView);
80
81 bottomView = new CompositeView('bottom-view', false, false, false);
82 addChild(bottomView);
83
84 sections = new ConveyorView();
85 sections.viewSelected = _onSectionTransitionEnded;
86 }
87
88 SectionView get currentSection() {
89 var view = sections.selectedView;
90 // TODO(jmesserly): this code works around a bug in the DartC --optimize
91 if (view == null) {
92 view = sections.childViews[0];
93 sections.selectView(view);
94 }
95 return view;
96 }
97
98 void afterRender(Element node) {
99 _createSectionViews();
100 attachWatch(swarm.state.currentArticle, (e) { _refreshCurrentArticle(); });
101 attachWatch(swarm.state.storyMaximized, (e) { _refreshMaximized(); });
102 }
103
104 void _refreshCurrentArticle() {
105 if (!swarm.state.inMainView) {
106 _animateToStory(swarm.state.currentArticle.value);
107 } else {
108 _animateToMainView();
109 }
110 }
111
112 /**
113 * Animates back from the story view to the main grid view.
114 */
115 void _animateToMainView() {
116 sliderMenu.removeClass('hidden');
117 storyView.addClass('hidden-story');
118 currentSection.storyMode = false;
119
120 headerView.startTransitionToMainView();
121
122 currentSection.dataSourceView.reattachSubview(
123 detachedView.source, detachedView, true);
124
125 storyView.node.on.transitionEnd.add(handler(e) {
126 // Only listen once.
127 // TODO(rnystrom): Look into adding .once() to EventListenerList to allow
128 // this for any event.
129 storyView.node.on.transitionEnd.remove(handler, false);
130
131 currentSection.hidden = false;
132 // TODO(rnystrom): Should move this "mode" into SwarmState and have
133 // header view respond to change events itself.
134 removeChild(storyView);
135 storyView = null;
136 detachedView.removeClass('sel');
137 detachedView = null;
138 });
139 }
140
141 void _animateToStory(Article item) {
142 final source = item.dataSource;
143
144 if (detachedView != null && detachedView.source != source) {
145 // Ignore spurious item selection clicks that occur while a data source
146 // is already selected. These are likely clicks that occur while an
147 // animation is in progress.
148 return;
149 }
150
151 if (storyView != null) {
152 // Remove the old story. This happens if we're already in the Story View
153 // and the user has clicked to see a new story.
154 removeChild(storyView);
155
156 // Create the new story view and place in the frame.
157 storyView = addChild(new StoryContentView(swarm, item));
158 } else {
159 // We are animating from the main view to the story view.
160 // TODO(jmesserly): make this code better
161 final view = currentSection.findView(source);
162
163 final newPosition = FxUtil.computeRelativePosition(
164 view.node, bottomView.node);
165 currentSection.dataSourceView.detachSubview(view.source);
166 detachedView = view;
167
168 FxUtil.setPosition(view.node, newPosition);
169 bottomView.addChild(view);
170 view.addClass('sel');
171 currentSection.storyMode = true;
172
173 // Create the new story view.
174 storyView = new StoryContentView(swarm, item);
175 window.setTimeout(() {
176 _animateDataSourceToMinimized();
177
178 sliderMenu.addClass('hidden');
179 // Make the fancy sliding into the window animation.
180 window.setTimeout(() {
181 storyView.addClass('hidden-story');
182 addChild(storyView);
183 window.setTimeout(() {
184 storyView.removeClass('hidden-story');
185 }, 0);
186 headerView.endTransitionToStoryView();
187 }, 0);
188 }, 0);
189 }
190 }
191
192 void _refreshMaximized() {
193 if (swarm.state.storyMaximized.value) {
194 _animateDataSourceToMaximized();
195 } else {
196 _animateDataSourceToMinimized();
197 }
198 }
199
200 void _animateDataSourceToMaximized() {
201 FxUtil.setWebkitTransform(topView.node, 0, -HeaderView.HEIGHT);
202 if (detachedView != null) {
203 FxUtil.setWebkitTransform(detachedView.node, 0,
204 -DataSourceView.TAB_ONLY_HEIGHT);
205 }
206 }
207
208 void _animateDataSourceToMinimized() {
209 if (detachedView != null) {
210 FxUtil.setWebkitTransform(detachedView.node, 0, 0);
211 FxUtil.setWebkitTransform(topView.node, 0, 0);
212 }
213 }
214
215 /**
216 * Called when the animation to switch to a section has completed.
217 */
218 void _onSectionTransitionEnded(SectionView selectedView) {
219 // Show the section and hide the others.
220 for (SectionView view in sections.childViews) {
221 if (view == selectedView) {
222 // Always refresh the sources in case they've changed.
223 view.showSources();
224 } else {
225 // Only show the current view for performance.
226 view.hideSources();
227 }
228 }
229 }
230
231 /**
232 * Called when the user chooses a section on the SliderMenu. Hides
233 * all views except the one they want to see.
234 */
235 void _onSectionSelected(String sectionTitle) {
236 final section = swarm.sections.findSection(sectionTitle);
237 // Find the view for this section.
238 for (final view in sections.childViews) {
239 if (view.section == section) {
240 // Have the conveyor show it.
241 sections.selectView(view);
242 break;
243 }
244 }
245 }
246
247 /**
248 * Create SectionViews for each Section in the app and add them to the
249 * conveyor. Note that the SectionViews won't actually populate or load data
250 * sources until they are shown in response to [:_onSectionSelected():].
251 */
252 void _createSectionViews() {
253 for (final section in swarm.sections) {
254 final viewFactory = new DataSourceViewFactory(swarm);
255 final sectionView = new SectionView(swarm, section, viewFactory);
256
257 // TODO(rnystrom): Hack temp. Access node to make sure SectionView has
258 // rendered and created scroller. This can go away when event registration
259 // is being deferred.
260 sectionView.node;
261
262 sections.addChild(sectionView);
263 }
264 addChild(sections);
265 }
266
267 /**
268 * Controls the logic of how to respond to keypresses and then update the
269 * UI accordingly.
270 */
271 void processKeyEvent(KeyboardEvent e) {
272 int code = e.keyCode;
273 if (swarm.state.inMainView) {
274 // Option 1: We're in the Main Grid mode.
275 if (!swarm.state.hasArticleSelected) {
276 // Then a key has been pressed. Select the first item in the
277 // top left corner.
278 swarm.state.goToFirstArticleInSection();
279 } else if (rightKeyPresses.contains(code)) {
280 // Store original state that is needed if we need to move
281 // to the next section.
282 swarm.state.goToNextFeed();
283 } else if (leftKeyPresses.contains(code)) {
284 // Store original state that is needed if we need to move
285 // to the next section.
286 swarm.state.goToPreviousFeed();
287 } else if (downKeyPresses.contains(code)) {
288 swarm.state.goToNextSelectedArticle();
289 } else if (upKeyPresses.contains(code)) {
290 swarm.state.goToPreviousSelectedArticle();
291 } else if (openKeyPresses.contains(code)) {
292 // View a story in the larger Story View.
293 swarm.state.selectStoryAsCurrent();
294 } else if (nextPageKeyPresses.contains(code)) {
295 swarm.state.goToNextSection(sliderMenu);
296 } else if (previousPageKeyPresses.contains(code)) {
297 swarm.state.goToPreviousSection(sliderMenu);
298 }
299 } else {
300 // Option 2: We're in Story Mode. In this mode, the user can move up
301 // and down through stories, which automatically loads the next story.
302 if (downKeyPresses.contains(code)) {
303 swarm.state.goToNextArticle();
304 } else if (upKeyPresses.contains(code)) {
305 swarm.state.goToPreviousArticle();
306 } else if (backKeyPresses.contains(code)) {
307 // Move back to the main grid view.
308 swarm.state.clearCurrentArticle();
309 }
310 }
311 }
312 }
313
314 /** Transitions the app back to the main screen. */
315 void _backToMain(SwarmState state) {
316 if (state.currentArticle.value != null) {
317 state.clearCurrentArticle();
318 state.storyTextMode.value = true;
319 state.pushToHistory();
320 }
321 }
322
323 /** A back button that sends the user back to the front page. */
324 class SwarmBackButton extends View {
325 Swarm swarm;
326
327 SwarmBackButton(this.swarm) : super();
328
329 Element render() => new Element.html('<div class="back-arrow button"></div>');
330
331 void afterRender(Element node) {
332 addOnClick((e) { _backToMain(swarm.state); });
333 }
334 }
335
336 /** Top view constaining the title and standard buttons. */
337 class HeaderView extends CompositeView {
338 // TODO(jacobr): make this value be coupled with the CSS file.
339 static final HEIGHT = 80;
340 Swarm swarm;
341
342 View _title;
343 View _infoButton;
344 View _configButton;
345 View _refreshButton;
346 SwarmBackButton _backButton;
347 View _infoDialog;
348 View _configDialog;
349
350 // For (text/web) article view controls
351 View _webBackButton;
352 View _webForwardButton;
353 View _newWindowButton;
354
355 HeaderView(this.swarm) : super('header-view') {
356 _backButton = addChild(new SwarmBackButton(swarm));
357 _title = addChild(View.div('app-title', 'Swarm'));
358 _configButton = addChild(View.div('config button'));
359 _refreshButton = addChild(View.div('refresh button'));
360 _infoButton = addChild(View.div('info-button button'));
361
362 // TODO(rnystrom): No more web/text mode (it's just text) so get rid of
363 // these.
364 _webBackButton = addChild(new WebBackButton());
365 _webForwardButton = addChild(new WebForwardButton());
366 _newWindowButton = addChild(View.div('new-window-button button'));
367 }
368
369 void afterRender(Element node) {
370 // Respond to changes to whether the story is being shown as text or web.
371 attachWatch(swarm.state.storyTextMode, (e) { refreshWebStoryButtons(); });
372
373 _title.addOnClick((e) { _backToMain(swarm.state); });
374
375 // Wire up the events.
376 _configButton.addOnClick((e) {
377 // Bring up the config dialog.
378 if (this._configDialog == null) {
379 // TODO(terry): Cleanup, HeaderView shouldn't be tangled with main view.
380 this._configDialog = new ConfigHintDialog(swarm.frontView, () {
381 swarm.frontView.removeChild(this._configDialog);
382 this._configDialog = null;
383
384 // TODO: Need to push these to the server on a per-user basis.
385 // Update the storage now.
386 swarm.sections.refresh();
387 });
388
389 swarm.frontView.addChild(this._configDialog);
390 }
391 // TODO(jimhug): Graceful redirection to reader.
392 });
393
394 // On click of the refresh button, refresh the swarm.
395 _refreshButton.addOnClick(EventBatch.wrap((e) {
396 swarm.refresh();
397 }));
398
399 // On click of the info button, show Dart info page in new window/tab.
400 _infoButton.addOnClick((e) {
401 // Bring up the config dialog.
402 if (this._infoDialog == null) {
403 // TODO(terry): Cleanup, HeaderView shouldn't be tangled with main view.
404 this._infoDialog = new HelpDialog(swarm.frontView, () {
405 swarm.frontView.removeChild(this._infoDialog);
406 this._infoDialog = null;
407
408 swarm.sections.refresh();
409 });
410
411 swarm.frontView.addChild(this._infoDialog);
412 }
413 });
414
415 // On click of the new window button, show web article in new window/tab.
416 _newWindowButton.addOnClick((e) {
417 String currentArticleSrcUrl = swarm.state.currentArticle.value.srcUrl;
418 window.open(currentArticleSrcUrl, '_blank');
419 });
420
421 startTransitionToMainView();
422 }
423
424
425 /**
426 * Refreshes whether or not the buttons specific to the display of a story in
427 * the web perspective are visible.
428 */
429 void refreshWebStoryButtons() {
430 bool webButtonsHidden = true;
431
432 if (swarm.state.currentArticle.value != null) {
433 // Set if web buttons are hidden
434 webButtonsHidden = swarm.state.storyTextMode.value;
435 }
436
437 _webBackButton.hidden = webButtonsHidden;
438 _webForwardButton.hidden = webButtonsHidden;
439 _newWindowButton.hidden = webButtonsHidden;
440 }
441
442 void startTransitionToMainView() {
443 _title.removeClass('in-story');
444 _backButton.removeClass('in-story');
445
446 _configButton.removeClass('in-story');
447 _refreshButton.removeClass('in-story');
448 _infoButton.removeClass('in-story');
449
450 refreshWebStoryButtons();
451 }
452
453 void endTransitionToStoryView() {
454 _title.addClass('in-story');
455 _backButton.addClass('in-story');
456
457 _configButton.addClass('in-story');
458 _refreshButton.addClass('in-story');
459 _infoButton.addClass('in-story');
460 }
461 }
462
463
464 /** A back button for the web view of a story that is equivalent to clicking
465 * "back" in the browser. */
466 // TODO(rnystrom): We have nearly identical versions of this littered through
467 // the sample apps. Should consolidate into one.
468 class WebBackButton extends View {
469 WebBackButton() : super();
470
471 Element render() {
472 return new Element.html('<div class="web-back-button button"></div>');
473 }
474
475 void afterRender(Element node) {
476 addOnClick((e) { back(); });
477 }
478
479 /** Equivalent to [window.history.back] */
480 static void back() {
481 window.history.back();
482 }
483 }
484
485 /** A back button for the web view of a story that is equivalent to clicking
486 * "forward" in the browser. */
487 // TODO(rnystrom): We have nearly identical versions of this littered through
488 // the sample apps. Should consolidate into one.
489 class WebForwardButton extends View {
490 WebForwardButton() : super();
491
492 Element render() {
493 return new Element.html('<div class="web-forward-button button"></div>');
494 }
495
496 void afterRender(Element node) {
497 addOnClick((e) { forward(); });
498 }
499
500 /** Equivalent to [window.history.forward] */
501 static void forward() {
502 window.history.forward();
503 }
504 }
505
506 /**
507 * A factory that creates a view for data sources.
508 */
509 class DataSourceViewFactory implements ViewFactory<Feed> {
510 Swarm swarm;
511
512 DataSourceViewFactory(this.swarm) {}
513
514 View newView(Feed data) => new DataSourceView(data, swarm);
515
516 int get width() => ArticleViewLayout.getSingleton().width;
517 int get height() => null; // Width for this view isn't known.
518 }
519
520
521 /**
522 * A view for the items from a single data source.
523 * Shows a title and a list of items.
524 */
525 class DataSourceView extends CompositeView {
526 // TODO(jacobr): make this value be coupled with the CSS file.
527 static final TAB_ONLY_HEIGHT = 34;
528
529 final Feed source;
530 VariableSizeListView<Article> itemsView;
531
532 DataSourceView(this.source, Swarm swarm) : super('query') {
533
534 // TODO(jacobr): make the title a view or decide it is sane for a subclass
535 // of component view to manually add some DOM cruft.
536 node.nodes.add(new Element.html(
537 '<h2>${source.title}</h2>'));
538
539 // TODO(jacobr): use named arguments when available.
540 itemsView = addChild(new VariableSizeListView<Article>(
541 source.articles,
542 new ArticleViewFactory(swarm),
543 true, /* scrollable */
544 true, /* vertical */
545 swarm.state.currentArticle, /* selectedItem */
546 !Device.supportsTouch /* snapToArticles */,
547 false /* paginate */,
548 true /* removeClippedViews */,
549 !Device.supportsTouch /* showScrollbar */));
550 itemsView.addClass('story-section');
551
552 node.nodes.add(new Element.html('<div class="query-name-shadow"></div>'));
553
554 // Clicking the view (i.e. its title area) unmaximizes to show the entire
555 // view.
556 node.on.mouseDown.add((e) {
557 swarm.state.storyMaximized.value = false;
558 }, false);
559 }
560 }
561
562 /** A button that toggles between states. */
563 class ToggleButton extends View {
564 EventListeners onChanged;
565 List<String> states;
566
567 ToggleButton(this.states)
568 : super(),
569 onChanged = new EventListeners();
570
571 Element render() => new Element.tag('button');
572
573 void afterRender(Element node) {
574 state = states[0];
575 node.on.click.add((event) { toggle(); }, false);
576 }
577
578 String get state() {
579 final currentState = node.innerHTML;
580 assert(states.indexOf(currentState, 0) >= 0);
581 return currentState;
582 }
583
584 void set state(String state) {
585 assert(states.indexOf(state, 0) >= 0);
586 node.innerHTML = state;
587 onChanged.fire(null);
588 }
589
590 void toggle() {
591 final oldState = state;
592 int index = states.indexOf(oldState, 0);
593 index = (index + 1) % states.length;
594 state = states[index];
595 }
596 }
597
598 /**
599 * A factory that creates a view for generic items.
600 */
601 class ArticleViewFactory implements VariableSizeViewFactory<Article> {
602 Swarm swarm;
603
604 ArticleViewLayout layout;
605 ArticleViewFactory(this.swarm)
606 : layout = ArticleViewLayout.getSingleton();
607
608 View newView(Article item) => new ArticleView(item, swarm, layout);
609
610 int getWidth(Article item) => layout.width;
611 int getHeight(Article item) => layout.computeHeight(item);
612 }
613
614 class ArticleViewMetrics {
615 final int height;
616 final int titleLines;
617 final int bodyLines;
618
619 const ArticleViewMetrics(this.height, this.titleLines, this.bodyLines);
620 }
621
622 class ArticleViewLayout {
623 // TODO(terry): clean this up once we have a framework for sharing constants
624 // between JS and CSS. See bug #5405307.
625 static final IPAD_WIDTH = 257;
626 static final DESKTOP_WIDTH = 297;
627 static final CHROME_OS_WIDTH = 317;
628 static final TITLE_MARGIN_LEFT = 257 - 150;
629 static final BODY_MARGIN_LEFT = 257 - 221;
630 static final LINE_HEIGHT = 18;
631 static final TITLE_FONT = 'bold 13px arial,sans-serif';
632 static final BODY_FONT = '13px arial,sans-serif';
633 static final TOTAL_MARGIN = 16 * 2 + 70;
634 static final MIN_TITLE_HEIGHT = 36;
635 static final MAX_TITLE_LINES = 2;
636 static final MAX_BODY_LINES = 4;
637
638 MeasureText measureTitleText;
639 MeasureText measureBodyText;
640
641 int width;
642 static ArticleViewLayout _singleton;
643 ArticleViewLayout() :
644 measureBodyText = new MeasureText(BODY_FONT),
645 measureTitleText = new MeasureText(TITLE_FONT) {
646 num screenWidth = window.screen.width;
647 width = DESKTOP_WIDTH;
648 }
649
650 static ArticleViewLayout getSingleton() {
651 if (_singleton == null) {
652 _singleton = new ArticleViewLayout();
653 }
654 return _singleton;
655 }
656
657 int computeHeight(Article item) {
658 if (item == null) {
659 // TODO(jacobr): find out why this is happening..
660 print('Null item encountered.');
661 return 0;
662 }
663
664 return computeLayout(item, null, null).height;
665 }
666
667 /**
668 * titleContainer and snippetContainer may be null in which case the size is
669 * computed but no actual layout is performed.
670 */
671 ArticleViewMetrics computeLayout(Article item,
672 StringBuffer titleBuffer,
673 StringBuffer snippetBuffer) {
674 int titleWidth = width - BODY_MARGIN_LEFT;
675
676 if (item.hasThumbnail) {
677 titleWidth = width - TITLE_MARGIN_LEFT;
678 }
679
680 final titleLines = measureTitleText.addLineBrokenText(titleBuffer,
681 item.title, titleWidth, MAX_TITLE_LINES);
682 final bodyLines = measureBodyText.addLineBrokenText(snippetBuffer,
683 item.textBody, width - BODY_MARGIN_LEFT, MAX_BODY_LINES);
684
685 int height = bodyLines * LINE_HEIGHT + TOTAL_MARGIN;
686
687 if (bodyLines == 0) {
688 height = 92;
689 }
690
691 return new ArticleViewMetrics(height, titleLines, bodyLines);
692 }
693 }
694
695 /**
696 * A view for a generic item.
697 */
698 class ArticleView extends View {
699 // Set to false to make inspecting the HTML more pleasant...
700 static final SAVE_IMAGES = false;
701
702 final Article item;
703 final Swarm swarm;
704 final ArticleViewLayout articleLayout;
705
706 ArticleView(this.item, this.swarm, this.articleLayout) : super();
707
708 Element render() {
709 Element node;
710
711 final byline = item.author.length > 0 ? item.author : item.dataSource.title;
712 final date = DateUtils.toRecentTimeString(item.date);
713
714 String storyClass = 'story no-thumb';
715 String thumbnail = '';
716
717 if (item.hasThumbnail) {
718 storyClass = 'story';
719 thumbnail = '<img src="${item.thumbUrl}"></img>';
720 }
721
722 final title = new StringBuffer();
723 final snippet = new StringBuffer();
724
725 // Note: also populates title and snippet elements.
726 final metrics = articleLayout.computeLayout(item, title, snippet);
727
728 node = new Element.html('''
729 <div class="$storyClass">
730 $thumbnail
731 <div class="title">$title</div>
732 <div class="byline">$byline</div>
733 <div class="dateline">$date</div>
734 <div class="snippet">$snippet</div>
735 </div>''');
736
737 // Remove the snippet entirely if it's empty. This keeps it from taking up
738 // space and pushing the padding down.
739 if ((item.textBody == null) || (item.textBody.trim() == '')) {
740 node.query('.snippet').remove();
741 }
742
743 return node;
744 }
745
746 void afterRender(Element node) {
747
748 // Select this view's item.
749 addOnClick((e) {
750 // Mark the item as read, so it shows as read in other views
751 item.unread.value = false;
752
753 final oldArticle = swarm.state.currentArticle.value;
754 swarm.state.currentArticle.value = item;
755 swarm.state.storyTextMode.value = true;
756 if (oldArticle == null) {
757 swarm.state.pushToHistory();
758 }
759 });
760
761 watch(swarm.state.currentArticle, (e) {
762 if (!swarm.state.inMainView) {
763 swarm.state.markCurrentAsRead();
764 }
765 _refreshSelected(swarm.state.currentArticle);
766 //TODO(efortuna): add in history stuff while reading articles?
767 });
768
769 watch(swarm.state.selectedArticle, (e) {
770 _refreshSelected(swarm.state.selectedArticle);
771 _updateViewForSelectedArticle();
772 });
773
774 watch(item.unread, (e) {
775 // TODO(rnystrom): Would be nice to do:
776 // node.classes.set('story-unread', item.unread.value)
777 if (item.unread.value) {
778 node.classes.add('story-unread');
779 } else {
780 node.classes.remove('story-unread');
781 }
782 });
783 }
784
785 /**
786 * Notify the view to jump to a different area if we are selecting an
787 * article that is currently outside of the visible area.
788 */
789 void _updateViewForSelectedArticle() {
790 Article selArticle = swarm.state.selectedArticle.value;
791 if (swarm.state.hasArticleSelected) {
792 // Ensure that the selected article is visible in the view.
793 if (!swarm.state.inMainView) {
794 // Story View.
795 swarm.frontView.detachedView.itemsView.showView(selArticle);
796 } else {
797 if(swarm.frontView.currentSection.inCurrentView(selArticle)) {
798 // Scroll horizontally if needed.
799 swarm.frontView.currentSection.dataSourceView.showView(
800 selArticle.dataSource);
801 DataSourceView dataView = swarm.frontView.currentSection
802 .findView(selArticle.dataSource);
803 if(dataView != null) {
804 dataView.itemsView.showView(selArticle);
805 }
806 }
807 }
808 }
809 }
810
811 String getDataUriForImage(final img) {
812 // TODO(hiltonc,jimhug) eval perf of this vs. reusing one canvas element
813 final canvas = new Element.html(
814 '<canvas width="${img.width}" height="${img.height}"></canvas>');
815
816 final ctx = canvas.getContext("2d");
817 ctx.drawImage(img, 0, 0, img.width, img.height);
818
819 return canvas.toDataURL("image/png");
820 }
821
822 /**
823 * Update this view's selected appearance based on the currently selected
824 * Article.
825 */
826 void _refreshSelected(curItem) {
827 if (curItem.value == item) {
828 addClass('sel');
829 } else {
830 removeClass('sel');
831 }
832 }
833
834 void _saveToStorage(String thumbUrl, ImageElement img) {
835 // TODO(jimhug): Reimplement caching of images.
836 }
837 }
838
839 /**
840 * An internal view of a story as text. In other words, the article is shown
841 * in-place as opposed to as an embedded web-page.
842 */
843 class StoryContentView extends View {
844 final Swarm swarm;
845 final Article item;
846
847 View _pagedStory;
848
849 StoryContentView(this.swarm, this.item) : super();
850
851 get childViews() => [_pagedStory];
852
853 Element render() {
854 final storyContent = new Element.html(
855 '<div class="story-content">${item.htmlBody}</div>');
856 for (Element element in storyContent.queryAll(
857 "iframe, script, style, object, embed, frameset, frame")) {
858 element.remove();
859 }
860 _pagedStory = new PagedContentView(new View.fromNode(storyContent));
861
862 // Modify all links to open in new windows....
863 // TODO(jacobr): would it be better to add an event listener on click that
864 // intercepts these instead?
865 for (final anchor in storyContent.queryAll('a')) {
866 anchor.target = '_blank';
867 }
868
869 final date = DateUtils.toRecentTimeString(item.date);
870 final container = new Element.html('''
871 <div class="story-view">
872 <div class="story-text-view">
873 <div class="story-header">
874 <a class="story-title" href="${item.srcUrl}" target="_blank">
875 ${item.title}</a>
876 <div class="story-byline">
877 ${item.author} - ${item.dataSource.title}
878 </div>
879 <div class="story-dateline">$date</div>
880 </div>
881 <div class="paged-story"></div>
882 <div class="spacer"></div>
883 </div>
884 </div>''');
885
886 container.query('.paged-story').replaceWith(_pagedStory.node);
887
888 return container;
889 }
890 }
891
892 class SectionView extends CompositeView {
893 final Section section;
894 final Swarm swarm;
895 final DataSourceViewFactory _viewFactory;
896 final View loadingText;
897 ListView<Feed> dataSourceView;
898 PageNumberView pageNumberView;
899 final PageState pageState;
900
901 SectionView(this.swarm, this.section, this._viewFactory)
902 : super('section-view'),
903 loadingText = new View.html('<div class="loading-section"></div>'),
904 pageState = new PageState() {
905 addChild(loadingText);
906 }
907
908 /**
909 * Hides the loading text, reloads the data sources, and shows them.
910 */
911 void showSources() {
912 loadingText.node.style.display = 'none';
913
914 // Lazy initialize the data source view.
915 if (dataSourceView == null) {
916 // TODO(jacobr): use named arguments when available.
917 dataSourceView = new ListView<Feed>(
918 section.feeds, _viewFactory,
919 true /* scrollable */,
920 false /* vertical */,
921 null /* selectedItem */,
922 true /* snapToItems */,
923 true /* paginate */,
924 true /* removeClippedViews */,
925 false, /* showScrollbar */
926 pageState);
927 dataSourceView.addClass("data-source-view");
928 addChild(dataSourceView);
929
930 pageNumberView = addChild(new PageNumberView(pageState));
931
932 node.style.opacity = '1';
933 } else {
934 addChild(dataSourceView);
935 addChild(pageNumberView);
936 node.style.opacity = '1';
937 }
938
939 // TODO(jacobr): get rid of this call to reconfigure when it is not needed.
940 dataSourceView.scroller.reconfigure(() {});
941 }
942
943 /**
944 * Hides the data sources and shows the loading text.
945 */
946 void hideSources() {
947 if (dataSourceView != null) {
948 node.style.opacity = '0.6';
949 removeChild(dataSourceView);
950 removeChild(pageNumberView);
951 }
952
953 loadingText.node.style.display = 'block';
954 }
955
956 set storyMode(bool inStoryMode) {
957 if (inStoryMode) {
958 addClass('hide-all-queries');
959 } else {
960 removeClass('hide-all-queries');
961 }
962 }
963
964 /**
965 * Find the [DataSourceView] in this SectionView that's displaying the given
966 * [Feed].
967 */
968 DataSourceView findView(Feed dataSource) {
969 return dataSourceView.getSubview(dataSourceView.findIndex(dataSource));
970 }
971
972 bool inCurrentView(Article article) {
973 return dataSourceView.findIndex(article.dataSource) != null;
974 }
975 }
OLDNEW
« no previous file with comments | « client/samples/swarm/SwarmState.dart ('k') | client/samples/swarm/UIState.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698