| // 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; |
| |
| /** The top-level collection of all sections for a user. */ |
| // TODO(jimhug): This is known as UserData in the server model. |
| class Sections extends IterableBase<Section> { |
| final List<Section> _sections; |
| |
| Sections(this._sections); |
| |
| operator [](int i) => _sections[i]; |
| |
| int get length => _sections.length; |
| |
| List<String> get sectionTitles => _sections.map((s) => s.title).toList(); |
| |
| void refresh() { |
| // TODO(jimhug): http://b/issue?id=5351067 |
| } |
| |
| /** |
| * Find the Section object that has a given title. |
| * This is used to integrate well with [ConveyorView]. |
| */ |
| Section findSection(String name) { |
| return CollectionUtils.find(_sections, (sect) => sect.title == name); |
| } |
| |
| // TODO(jimhug): Track down callers! |
| Iterator<Section> get iterator => _sections.iterator; |
| |
| // TODO(jimhug): Better support for switching between local dev and server. |
| static bool get runningFromFile { |
| return window.location.protocol.startsWith('file:'); |
| } |
| |
| static String get home { |
| // TODO(jmesserly): window.location.origin not available on Safari 4. |
| // Move this workaround to the DOM code. See bug 5389503. |
| return '${window.location.protocol}//${window.location.host}'; |
| } |
| |
| // This method is exposed for tests. |
| static void initializeFromData(String data, void callback(Sections sects)) { |
| final decoder = new Decoder(data); |
| int nSections = decoder.readInt(); |
| final sections = new List<Section>(); |
| |
| for (int i = 0; i < nSections; i++) { |
| sections.add(Section.decode(decoder)); |
| } |
| callback(new Sections(sections)); |
| } |
| |
| static void initializeFromUrl( |
| bool useCannedData, void callback(Sections sections)) { |
| if (Sections.runningFromFile || useCannedData) { |
| initializeFromData(CannedData.data['user.data'], callback); |
| } else { |
| // TODO(jmesserly): display an error if we fail here! Silent failure bad. |
| HttpRequest |
| .getString('data/user.data') |
| .then(EventBatch.wrap((responseText) { |
| // TODO(jimhug): Nice response if get error back from server. |
| // TODO(jimhug): Might be more efficient to parse request |
| // in sections. |
| initializeFromData(responseText, callback); |
| })); |
| } |
| } |
| |
| Section findSectionById(String id) { |
| return CollectionUtils.find(_sections, (section) => section.id == id); |
| } |
| |
| /** |
| * Given the name of a section, find its index in the set. |
| */ |
| int findSectionIndex(String name) { |
| for (int i = 0; i < _sections.length; i++) { |
| if (name == _sections[i].title) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| List<Section> get sections => _sections; |
| |
| // TODO(jmesserly): this should be a property |
| bool get isEmpty => length == 0; |
| } |
| |
| /** A collection of data sources representing a page in the UI. */ |
| class Section { |
| final String id; |
| final String title; |
| ObservableList<Feed> feeds; |
| |
| // Public for testing. TODO(jacobr): find a cleaner solution. |
| Section(this.id, this.title, this.feeds); |
| |
| void refresh() { |
| for (final feed in feeds) { |
| // TODO(jimhug): http://b/issue?id=5351067 |
| } |
| } |
| |
| static Section decode(Decoder decoder) { |
| final sectionId = decoder.readString(); |
| final sectionTitle = decoder.readString(); |
| |
| final nSources = decoder.readInt(); |
| final feeds = new ObservableList<Feed>(); |
| for (int j = 0; j < nSources; j++) { |
| feeds.add(Feed.decode(decoder)); |
| } |
| return new Section(sectionId, sectionTitle, feeds); |
| } |
| |
| Feed findFeed(String id_) { |
| return CollectionUtils.find(feeds, (feed) => feed.id == id_); |
| } |
| } |
| |
| /** Provider of a news feed. */ |
| class Feed { |
| String id; |
| final String title; |
| final String iconUrl; |
| final String description; |
| ObservableList<Article> articles; |
| ObservableValue<bool> error; // TODO(jimhug): Check if dead code. |
| |
| Feed(this.id, this.title, this.iconUrl, {this.description: ''}) |
| : articles = new ObservableList<Article>(), |
| error = new ObservableValue<bool>(false); |
| |
| static Feed decode(Decoder decoder) { |
| final sourceId = decoder.readString(); |
| final sourceTitle = decoder.readString(); |
| final sourceIcon = decoder.readString(); |
| final feed = new Feed(sourceId, sourceTitle, sourceIcon); |
| final nItems = decoder.readInt(); |
| |
| for (int i = 0; i < nItems; i++) { |
| feed.articles.add(Article.decodeHeader(feed, decoder)); |
| } |
| return feed; |
| } |
| |
| Article findArticle(String id_) { |
| return CollectionUtils.find(articles, (article) => article.id == id_); |
| } |
| |
| void refresh() {} |
| } |
| |
| /** A single article or posting to display. */ |
| class Article { |
| final String id; |
| DateTime date; |
| final String title; |
| final String author; |
| final bool hasThumbnail; |
| String textBody; // TODO(jimhug): rename to snippet. |
| final Feed dataSource; // TODO(jimhug): rename to feed. |
| String _htmlBody; |
| String srcUrl; |
| final ObservableValue<bool> unread; // TODO(jimhug): persist to server. |
| |
| bool error; // TODO(jimhug): Check if this is dead and remove. |
| |
| Article(this.dataSource, this.id, this.date, this.title, this.author, |
| this.srcUrl, this.hasThumbnail, this.textBody, |
| {htmlBody: null, bool unread: true, this.error: false}) |
| : unread = new ObservableValue<bool>(unread), |
| this._htmlBody = htmlBody; |
| |
| String get htmlBody { |
| _ensureLoaded(); |
| return _htmlBody; |
| } |
| |
| String get dataUri { |
| return SwarmUri |
| .encodeComponent(id) |
| .replaceAll('%2F', '/') |
| .replaceAll('%253A', '%3A'); |
| } |
| |
| String get thumbUrl { |
| if (!hasThumbnail) return null; |
| |
| var home; |
| if (Sections.runningFromFile) { |
| home = 'http://dart.googleplex.com'; |
| } else { |
| home = Sections.home; |
| } |
| // By default images from the real server are cached. |
| // Bump the version flag if you change the thumbnail size, and you want to |
| // get the new images. Our server ignores the query params but it gets |
| // around appengine server side caching and the client side cache. |
| return 'data/$dataUri.jpg'; |
| } |
| |
| // TODO(jimhug): need to return a lazy Observable<String> and also |
| // add support for preloading. |
| void _ensureLoaded() { |
| if (_htmlBody != null) return; |
| |
| var name = '$dataUri.html'; |
| if (Sections.runningFromFile) { |
| _htmlBody = CannedData.data[name]; |
| } else { |
| // TODO(jimhug): Remove this truly evil synchronoush xhr. |
| final req = new HttpRequest(); |
| req.open('GET', 'data/$name', async: false); |
| req.send(); |
| _htmlBody = req.responseText; |
| } |
| } |
| |
| static Article decodeHeader(Feed source, Decoder decoder) { |
| final id = decoder.readString(); |
| final title = decoder.readString(); |
| final srcUrl = decoder.readString(); |
| final hasThumbnail = decoder.readBool(); |
| final author = decoder.readString(); |
| final dateInSeconds = decoder.readInt(); |
| final snippet = decoder.readString(); |
| final date = new DateTime.fromMillisecondsSinceEpoch(dateInSeconds * 1000, |
| isUtc: true); |
| return new Article( |
| source, id, date, title, author, srcUrl, hasThumbnail, snippet); |
| } |
| } |