Initial github commit for getting stuff working

Readd samples, samples-dev, tools/testing/bin, third_party removed by cleanup (no history, sorry)

Add DEPS file which will replace the old all.deps and standalone.deps

Add tools/deps/dartium.deps replacing the old dartium.deps (but now editable from a normal checkout)

Fixup tools/utils.py to use the new archiving schema (git count for be, version number for dev/stable

Fix codereview.settings
This commit is contained in:
Rico Wind 2015-05-16 15:17:10 +02:00
parent 9ba7eef1c4
commit f34fae854a
412 changed files with 49811 additions and 39 deletions

309
DEPS Normal file
View file

@ -0,0 +1,309 @@
# Copyright (c) 2012, 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.
vars = {
# The dart_root is the root of our sdk checkout. This is normally
# simply sdk, but if using special gclient specs it can be different.
"dart_root": "sdk",
# The svn location to pull out dependencies from
"third_party": "http://dart.googlecode.com/svn/third_party",
# The svn location for pulling pinned revisions of bleeding edge dependencies.
"bleeding_edge": "http://dart.googlecode.com/svn/branches/bleeding_edge",
# Use this googlecode_url variable only if there is an internal mirror for it.
# If you do not know, use the full path while defining your new deps entry.
"googlecode_url": "http://%s.googlecode.com/svn",
"gyp_rev": "@1752",
"co19_rev": "@801",
"chromium_url": "http://src.chromium.org/svn",
"chromium_git": "https://chromium.googlesource.com",
# Revisions of /third_party/* dependencies.
"7zip_rev" : "@19997",
"analyzer_cli_tag" : "@1.0.1",
"args_tag": "@0.13.0",
"barback_rev" : "@29ee90dbcf77cfd64632fa2797a4c8a4f29a4b51",
"chrome_rev" : "@19997",
"clang_rev" : "@28450",
"collection_rev": "@1da9a07f32efa2ba0c391b289e2037391e31da0e",
"crypto_rev" : "@2df57a1e26dd88e8d0614207d4b062c73209917d",
"csslib_tag" : "@0.12.0",
"async_await_rev" : "@8b401a9f2e5e81dca5f70dbe7564112a0823dee6",
"dart_services_rev" : "@7aea2574e6f3924bf409a80afb8ad52aa2be4f97",
"dart_style_tag": "@0.1.8",
"d8_rev" : "@39739",
"fake_async_rev" : "@38614",
"firefox_jsshell_rev" : "@45554",
"glob_rev": "@704cf75e4f26b417505c5c611bdaacd8808467dd",
"gsutil_rev" : "@33376",
"html_tag" : "@0.12.1+1",
"http_rev" : "@9b93e1542c753090c50b46ef1592d44bc858bfe7",
"http_multi_server_tag" : "@1.3.2",
"http_parser_rev" : "@8b179e36aba985208e4c5fb15cfddd386b6370a4",
"http_throttle_rev" : "@a81f08be942cdd608883c7b67795c12226abc235",
"idl_parser_rev": "@6316d5982dc24b34d09dd8b10fbeaaff28d83a48",
"intl_rev": "@32047558bd220a53c1f4d93a26d54b83533b1475",
"jinja2_rev": "@2222b31554f03e62600cd7e383376a7c187967a1",
"json_rpc_2_rev": "@a38eefd116d910199de205f962af92fed87c164c",
"linter_tag": "@0.0.2+2",
"logging_rev": "@85d83e002670545e9039ad3985f0018ab640e597",
"markdown_rev": "@56b0fd6c018d6103862d07e8e27407b9ea3b963d",
"matcher_tag": "@0.12.0",
"metatest_rev": "@e5aa8e4e19fc4188ac2f6d38368a47d8f07c3df1",
"mime_rev": "@75890811d4af5af080351ba8a2853ad4c8df98dd",
"net_nss_rev": "@f81948e9a402db94287a43bb34a07ee0daf56cb5",
"nss_rev": "@87b96db4268293187d7cf741907a6d5d1d8080e0",
"oauth2_rev": "@1bff41f4d54505c36f2d1a001b83b8b745c452f5",
"observe_rev": "@eee2b8ec34236fa46982575fbccff84f61202ac6",
"observatory_pub_packages_rev": "@45565",
"path_rev": "@93b3e2aa1db0ac0c8bab9d341588d77acda60320",
"petitparser_rev" : "@37878",
"ply_rev": "@604b32590ffad5cbb82e4afef1d305512d06ae93",
"plugin_tag": "@0.1.0",
"pool_rev": "@22e12aeb16ad0b626900dbe79e4a25391ddfb28c",
"pub_semver_tag": "@1.2.0",
"scheduled_test_tag": "@0.11.8+1",
"shelf_rev": "@1e87b79b21ac5e6fa2f93576d6c06eaa65285ef4",
"smoke_rev" : "@f3361191cc2a85ebc1e4d4c33aec672d7915aba9",
"source_maps_rev": "@379b4f31c4e2987eb15934d1ad8b419c6cc897b3",
"sqlite_rev": "@38811b79f42801662adc0458a25270ab690a6b81",
"shelf_web_socket_rev": "@ff170cec2c0e4e5722cdf47c557be63b5035a602",
"source_span_rev": "@42501132e43599a151ba6727d340e44442f86c05",
"stack_trace_tag": "@1.2.1",
"string_scanner_rev": "@3e7617d6f74ba382e9b6130b1cc12091d89a9bc5",
"sunflower_rev": "@879b704933413414679396b129f5dfa96f7a0b1e",
"test_tag": "@0.12.1",
"test_reflective_loader_tag": "@0.0.3",
"utf_rev": "@1f55027068759e2d52f2c12de6a57cce5f3c5ee6",
"unittest_tag": "@0.11.6",
"usage_rev": "@b5080dac0d26a5609b266f8fdb0d053bc4c1c638",
"watcher_tag": "@0.9.5",
"web_components_rev": "@0e636b534d9b12c9e96f841e6679398e91a986ec",
"WebCore_rev" : "@44061",
"yaml_rev": "@563a5ffd4a800a2897b8f4dd6b19f2a370df2f2b",
"zlib_rev": "@c3d0a6190f2f8c924a05ab6cc97b8f975bddd33f",
"font_awesome_rev": "@31824",
"barback-0.13.0_rev": "@34853",
"barback-0.14.0_rev": "@36398",
"barback-0.14.1_rev": "@38525",
"source_maps-0.9.4_rev": "@38524",
}
deps = {
# Stuff needed for GYP to run.
Var("dart_root") + "/third_party/gyp":
(Var("googlecode_url") % "gyp") + "/trunk" + Var("gyp_rev"),
Var("dart_root") + "/tests/co19/src": ((Var("googlecode_url") % "co19") +
"/trunk/co19/tests/co19/src" + Var("co19_rev")),
Var("dart_root") + "/third_party/nss":
Var("chromium_git") + "/chromium/deps/nss.git" + Var("nss_rev"),
Var("dart_root") + "/third_party/sqlite":
Var("chromium_git") + "/chromium/src/third_party/sqlite.git" +
Var("sqlite_rev"),
Var("dart_root") + "/third_party/zlib":
Var("chromium_git") + "/chromium/src/third_party/zlib.git" +
Var("zlib_rev"),
Var("dart_root") + "/third_party/net_nss":
Var("chromium_git") + "/chromium/src/net/third_party/nss.git" +
Var("net_nss_rev"),
Var("dart_root") + "/third_party/jinja2":
Var("chromium_git") + "/chromium/src/third_party/jinja2.git" +
Var("jinja2_rev"),
Var("dart_root") + "/third_party/ply":
Var("chromium_git") + "/chromium/src/third_party/ply.git" +
Var("ply_rev"),
Var("dart_root") + "/third_party/idl_parser":
Var("chromium_git") + "/chromium/src/tools/idl_parser.git" +
Var("idl_parser_rev"),
Var("dart_root") + "/third_party/7zip":
Var("third_party") + "/7zip" + Var("7zip_rev"),
Var("dart_root") + "/third_party/chrome":
Var("third_party") + "/chrome" + Var("chrome_rev"),
Var("dart_root") + "/third_party/pkg/fake_async":
Var("third_party") + "/fake_async" + Var("fake_async_rev"),
Var("dart_root") + "/third_party/firefox_jsshell":
Var("third_party") + "/firefox_jsshell" + Var("firefox_jsshell_rev"),
Var("dart_root") + "/third_party/font-awesome":
Var("third_party") + "/font-awesome" + Var("font_awesome_rev"),
Var("dart_root") + "/third_party/gsutil":
Var("third_party") + "/gsutil" + Var("gsutil_rev"),
Var("dart_root") + "/third_party/pkg/petitparser":
Var("third_party") + "/petitparser" + Var("petitparser_rev"),
Var("dart_root") + "/third_party/d8":
Var("third_party") + "/d8" + Var("d8_rev"),
Var("dart_root") + "/third_party/WebCore":
Var("third_party") + "/WebCore" + Var("WebCore_rev"),
Var("dart_root") + "/third_party/observatory_pub_packages":
Var("third_party") + "/observatory_pub_packages" +
Var("observatory_pub_packages_rev"),
Var("dart_root") + "/third_party/dart-services":
"https://github.com/dart-lang/dart-services.git" +
Var("dart_services_rev"),
Var("dart_root") + "/third_party/pkg_tested/analyzer_cli":
"https://github.com/dart-lang/analyzer_cli.git" + Var("analyzer_cli_tag"),
Var("dart_root") + "/third_party/pkg/args":
"https://github.com/dart-lang/args.git" + Var("args_tag"),
Var("dart_root") + "/third_party/pkg/async_await":
"https://github.com/dart-lang/async_await.git" + Var("async_await_rev"),
Var("dart_root") + "/third_party/pkg/barback":
"https://github.com/dart-lang/barback.git" + Var("barback_rev"),
Var("dart_root") + "/third_party/pkg/collection":
"https://github.com/dart-lang/collection.git" + Var("collection_rev"),
Var("dart_root") + "/third_party/pkg/crypto":
"https://github.com/dart-lang/crypto.git" + Var("crypto_rev"),
Var("dart_root") + "/third_party/pkg/csslib":
"https://github.com/dart-lang/csslib.git" + Var("csslib_tag"),
Var("dart_root") + "/third_party/pkg_tested/dart_style":
"https://github.com/dart-lang/dart_style.git" + Var("dart_style_tag"),
Var("dart_root") + "/third_party/pkg/glob":
"https://github.com/dart-lang/glob.git" + Var("glob_rev"),
Var("dart_root") + "/third_party/pkg/html":
"https://github.com/dart-lang/html.git" + Var("html_tag"),
Var("dart_root") + "/third_party/pkg/http":
"https://github.com/dart-lang/http.git" + Var("http_rev"),
Var("dart_root") + "/third_party/pkg/http_multi_server":
"https://github.com/dart-lang/http_multi_server.git" +
Var("http_multi_server_tag"),
Var("dart_root") + "/third_party/pkg/http_parser":
"https://github.com/dart-lang/http_parser.git" + Var("http_parser_rev"),
Var("dart_root") + "/third_party/pkg/http_throttle":
"https://github.com/dart-lang/http_throttle.git" +
Var("http_throttle_rev"),
Var("dart_root") + "/third_party/pkg/intl":
"https://github.com/dart-lang/intl.git" + Var("intl_rev"),
Var("dart_root") + "/third_party/pkg/json_rpc_2":
"https://github.com/dart-lang/json_rpc_2.git" + Var("json_rpc_2_rev"),
Var("dart_root") + "/third_party/pkg/linter":
"https://github.com/dart-lang/linter.git" + Var("linter_tag"),
Var("dart_root") + "/third_party/pkg/logging":
"https://github.com/dart-lang/logging.git" + Var("logging_rev"),
Var("dart_root") + "/third_party/pkg/markdown":
"https://github.com/dpeek/dart-markdown.git" + Var("markdown_rev"),
Var("dart_root") + "/third_party/pkg/matcher":
"https://github.com/dart-lang/matcher.git" + Var("matcher_tag"),
Var("dart_root") + "/third_party/pkg/metatest":
"https://github.com/dart-lang/metatest.git" + Var("metatest_rev"),
Var("dart_root") + "/third_party/pkg/mime":
"https://github.com/dart-lang/mime.git" + Var("mime_rev"),
Var("dart_root") + "/third_party/pkg/oauth2":
"https://github.com/dart-lang/oauth2.git" + Var("oauth2_rev"),
Var("dart_root") + "/third_party/pkg/observe":
"https://github.com/dart-lang/observe.git" + Var("observe_rev"),
Var("dart_root") + "/third_party/pkg/path":
"https://github.com/dart-lang/path.git" + Var("path_rev"),
Var("dart_root") + "/third_party/pkg/plugin":
"https://github.com/dart-lang/plugin.git" + Var("plugin_tag"),
Var("dart_root") + "/third_party/pkg/pool":
"https://github.com/dart-lang/pool.git" + Var("pool_rev"),
Var("dart_root") + "/third_party/pkg/pub_semver":
"https://github.com/dart-lang/pub_semver.git" + Var("pub_semver_tag"),
Var("dart_root") + "/third_party/pkg/scheduled_test":
"https://github.com/dart-lang/scheduled_test.git" +
Var("scheduled_test_tag"),
Var("dart_root") + "/third_party/pkg/shelf":
"https://github.com/dart-lang/shelf.git" + Var("shelf_rev"),
Var("dart_root") + "/third_party/pkg/shelf_web_socket":
"https://github.com/dart-lang/shelf_web_socket.git" +
Var("shelf_web_socket_rev"),
Var("dart_root") + "/third_party/pkg/smoke":
"https://github.com/dart-lang/smoke.git" + Var("smoke_rev"),
Var("dart_root") + "/third_party/pkg/source_maps":
"https://github.com/dart-lang/source_maps.git" + Var("source_maps_rev"),
Var("dart_root") + "/third_party/pkg/source_span":
"https://github.com/dart-lang/source_span.git" + Var("source_span_rev"),
Var("dart_root") + "/third_party/pkg/stack_trace":
"https://github.com/dart-lang/stack_trace.git" + Var("stack_trace_tag"),
Var("dart_root") + "/third_party/pkg/string_scanner":
"https://github.com/dart-lang/string_scanner.git" +
Var("string_scanner_rev"),
Var("dart_root") + "/third_party/sunflower":
"https://github.com/dart-lang/sample-sunflower.git" +
Var("sunflower_rev"),
Var("dart_root") + "/third_party/pkg/test":
"https://github.com/dart-lang/test.git" + Var("test_tag"),
Var("dart_root") + "/third_party/pkg/test_reflective_loader":
"https://github.com/dart-lang/test_reflective_loader.git" + Var("test_reflective_loader_tag"),
Var("dart_root") + "/third_party/pkg/unittest":
"https://github.com/dart-lang/test.git" + Var("unittest_tag"),
Var("dart_root") + "/third_party/pkg/usage":
"https://github.com/dart-lang/usage.git" + Var("usage_rev"),
Var("dart_root") + "/third_party/pkg/utf":
"https://github.com/dart-lang/utf.git" + Var("utf_rev"),
Var("dart_root") + "/third_party/pkg/watcher":
"https://github.com/dart-lang/watcher.git" + Var("watcher_tag"),
Var("dart_root") + "/third_party/pkg/web_components":
"https://github.com/dart-lang/web-components.git" +
Var("web_components_rev"),
Var("dart_root") + "/third_party/pkg/yaml":
"https://github.com/dart-lang/yaml.git" + Var("yaml_rev"),
# These specific versions of barback and source_maps are used for testing and
# should be pulled from bleeding_edge even on channels.
Var("dart_root") + "/third_party/pkg/barback-0.13.0":
Var("bleeding_edge") + "/dart/pkg/barback" + Var("barback-0.13.0_rev"),
Var("dart_root") + "/third_party/pkg/barback-0.14.0+3":
Var("bleeding_edge") + "/dart/pkg/barback" + Var("barback-0.14.0_rev"),
Var("dart_root") + "/third_party/pkg/barback-0.14.1+4":
Var("bleeding_edge") + "/dart/pkg/barback" + Var("barback-0.14.1_rev"),
Var("dart_root") + "/third_party/pkg/source_maps-0.9.4":
Var("bleeding_edge") + "/dart/pkg/source_maps" +
Var("source_maps-0.9.4_rev"),
}
deps_os = {
"android": {
Var("dart_root") + "/third_party/android_tools":
Var("chromium_git") + "/android_tools.git" +
"@aaeda3d69df4b4352e3cac7c16bea7f16bd1ec12",
},
"win": {
Var("dart_root") + "/third_party/cygwin":
Var("chromium_git") + "/chromium/deps/cygwin.git" +
"@c89e446b273697fadf3a10ff1007a97c0b7de6df",
Var("dart_root") + "/third_party/drt_resources":
Var("chromium_url") +
"/trunk/src/webkit/tools/test_shell/resources@157099",
},
"unix": {
Var("dart_root") + "/third_party/clang":
Var("third_party") + "/clang" + Var("clang_rev"),
},
}
# TODO(iposva): Move the necessary tools so that hooks can be run
# without the runtime being available.
hooks = [
{
"pattern": ".",
"action": ["python", Var("dart_root") + "/tools/gyp_dart.py"],
},
{
'name': 'checked_in_dart_binaries',
'pattern': '.',
'action': [
'download_from_google_storage',
'--no_auth',
'--no_resume',
'--bucket',
'dart-dependencies',
'-d',
'-r',
Var('dart_root') + '/tools/testing/bin',
],
},
]

View file

@ -1,4 +1,4 @@
# This file is used by gcl to get repository specific information.
CODE_REVIEW_SERVER: http://codereview.chromium.org/
VIEW_VC: https://code.google.com/p/dart/source/detail?r=
VIEW_VC: https://github.com/dart-lang/sdk/commit/
CC_LIST: reviews@dartlang.org

View file

@ -0,0 +1,37 @@
# Copyright (c) 2014, 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.
[ $compiler == dart2js && $runtime == drt ]
swarm/test/swarm_test: Pass, Crash, Fail # Issue 10950
[ $runtime == safari ]
swarm/test/swarm_test: Pass, Fail # Issue 14523
[ $runtime == opera ]
swarm/test/swarm_ui_lib/touch/touch_test: Fail
swarm/test/swarm_test: Fail
[ $runtime == vm ]
swarm: Skip
[ $compiler == dart2js && $runtime == chromeOnAndroid ]
swarm/test/swarm_test: Fail # TODO(kasperl): Please triage.
swarm/test/swarm_ui_lib/layout/layout_test: Fail # TODO(kasperl): Please triage.
[ $browser ]
# This may be related to issue 157
swarm/test/swarm_ui_lib/touch/touch_test: Fail # Expectation: Solver. Expect.approxEquals(expected:9, actual:8.990625000000001, tolerance:0.0009) fails
[ $compiler == dart2js && $runtime == ff ]
swarm/test/swarm_test: Fail # Issue 5633
[ $compiler == dart2js && $runtime == drt && $system == windows ]
swarm/test/swarm_test: Fail # Issue 4517
[ $compiler == dartanalyzer || $compiler == dart2analyzer ]
swarm/test/swarm_test: StaticWarning
swarm/test/swarm_ui_lib/layout/layout_test: StaticWarning
swarm/test/swarm_ui_lib/observable/observable_test: StaticWarning
swarm/test/swarm_ui_lib/touch/touch_test: StaticWarning
swarm/test/swarm_ui_lib/util/util_test: StaticWarning
swarm/test/swarm_ui_lib/view/view_test: StaticWarning

111
samples-dev/swarm/App.dart Normal file
View file

@ -0,0 +1,111 @@
// 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 base class that should be extended by all HTML applications.
*
* It should both be easy to use for users coming over from JavaScript, but
* also offer a clear notion of OO encapsulation.
*
* This class or something similar belongs in the standard DOM library.
*/
class App {
App() {}
/** Begins executing code in this [App]. */
void run() {
// If the script is async, by the time we get here the DOM content may
// already be loaded, so waiting on the DOMContentLoaded event is a no-op.
// Guard against this by checking whether the document readiness state has
// gotten as far as "interactive". (We believe the transition to
// "interactive" is when the DOMContentLoaded event fires, but haven't
// found that specified; if that's not true it leaves a race bug.)
if (document.readyState == "interactive" ||
document.readyState == "complete" ||
document.readyState == "loaded") {
// We use a timer to insure that onLoad is always called in an async
// manner even if the document is already loaded.
Timer.run(onLoad);
} else {
window.onContentLoaded.listen(
// TODO(sigmund): Consider eliminating the call to "wrap", for
// instance, modify event listeners to always wrap, or extend DOM code
// to intercept the beginning & end of each event loop
EventBatch.wrap((Event event) => onLoad()));
}
}
/**
* Called when the DOM is fully loaded but potentially before resources.
*
* For most apps, any startup code should be in this method. Be sure to call
* the superclass implementation.
*/
void onLoad() {
// Prevent the default browser behavior of scrolling the window.
document.onTouchMove.listen((Event event) => event.preventDefault());
// Swap and reload the cache if ready
if (!swapAndReloadCache()) {
// Otherwise wait until an update to the cache is ready
window.applicationCache.onUpdateReady.listen(
(e) => swapAndReloadCache());
}
}
/**
* Erase the static splash screen.
*
* Assumption: if a splash screen exists, an element #appSplash contains it.
*/
void eraseSplashScreen() {
final splash = document.querySelector("#appSplash");
// Delete it if found, but it's okay for it not to be -- maybe
// somebody just didn't want to use our splash mechanism.
if (splash != null) {
splash.remove();
}
}
/**
* Swaps and reloads the app cache if an update is ready. Returns false if
* an update is not ready.
*/
bool swapAndReloadCache() {
ApplicationCache appCache = window.applicationCache;
if (!identical(appCache.status, ApplicationCache.UPDATEREADY)) {
return false;
}
print('App cache update ready, now swapping...');
window.applicationCache.swapCache();
print('App cache swapped, now reloading page...');
window.location.reload();
return true;
}
/** Returns true if we are running as a packaged application. */
static bool get isPackaged {
return window.location.protocol == 'chrome-extension:';
}
/**
* Gets the server URL. This is needed when we are loaded from a packaged
* Chrome app.
*/
static String serverUrl(String url) {
if (isPackaged) {
// TODO(jmesserly): Several problems with this:
// * How do we authenticate against the server?
// * How do we talk to a server other than thump?
assert(url.startsWith('/'));
return 'http://thump.googleplex.com$url';
} else {
return url;
}
}
}

View file

@ -0,0 +1,69 @@
part of swarmlib;
/**
* An iterator that allows the user to move forward and backward though
* a set of items. (Bi-directional)
*/
class BiIterator<E> {
/**
* Provides forward and backward iterator functionality to keep track
* which item is currently selected.
*/
ObservableValue<int> currentIndex;
/**
* The collection of items we will be iterating through.
*/
List<E> list;
BiIterator(this.list, [List<ChangeListener> oldListeners = null])
: currentIndex = new ObservableValue<int>(0) {
if (oldListeners != null) {
currentIndex.listeners = oldListeners;
}
}
/**
* Returns the next section from the sections, given the current
* position. Returns the last source if there is no next section.
*/
E next() {
if (currentIndex.value < list.length - 1) {
currentIndex.value += 1;
}
return list[currentIndex.value];
}
/**
* Returns the current Section (page in the UI) that the user is
* looking at.
*/
E get current {
return list[currentIndex.value];
}
/**
* Returns the previous section from the sections, given the current
* position. Returns the front section if we are already at the front of
* the list.
*/
E previous() {
if (currentIndex.value > 0) {
currentIndex.value -= 1;
}
return list[currentIndex.value];
}
/**
* Move the iterator pointer over so that it points to a given list item.
*/
void jumpToValue(E val) {
for (int i = 0; i < list.length; i++) {
if (identical(list[i], val)) {
currentIndex.value = i;
break;
}
}
}
}

View file

@ -0,0 +1,91 @@
// File generated by Dart CSS from source file swarm.scss
// Do not edit.
part of swarmlib;
class CSS {
// CSS class selectors:
static const String HBOX = 'hbox';
static const String VBOX = 'vbox';
static const String CENTER = 'center';
static const String PAGED_CONTENT = 'paged-content';
static const String FULLPAGE = 'fullpage';
static const String FLEX_ITEM = 'flex-item';
static const String FLEX = 'flex';
static const String CONVEYOR_VIEW = 'conveyor-view';
static const String CONVEYOR_ITEM = 'conveyor-item';
static const String CONVEYOR_VIEW_CONTAINER = 'conveyor-view-container';
static const String PAGED_COLUMN_CONTAINER = 'paged-column-container';
static const String PAGED_COLUMN = 'paged-column';
static const String PAGE_NUMBER_LEFT = 'page-number-left';
static const String PAGE_NUMBER_LABEL = 'page-number-label';
static const String PAGE_NUMBER_RIGHT = 'page-number-right';
static const String SM_ITEM = 'sm-item';
static const String SM_ITEM_BOX = 'sm-item-box';
static const String SM_ROOT = 'sm-root';
static const String HIDDEN = 'hidden';
static const String SEL = 'sel';
static const String SM_SLIDER_BOX = 'sm-slider-box';
static const String SM_TRIANGLE = 'sm-triangle';
static const String INVISIBLE = 'invisible';
static const String TOUCH_SCROLLBAR = 'touch-scrollbar';
static const String DRAG = 'drag';
static const String TOUCH_SCROLLBAR_VERTICAL = 'touch-scrollbar-vertical';
static const String TOUCH_SCROLLBAR_HORIZONTAL = 'touch-scrollbar-horizontal';
static const String FRONT_VIEW = 'front-view';
static const String BOTTOM_VIEW = 'bottom-view';
static const String TOP_VIEW = 'top-view';
static const String QUERY = 'query';
static const String QUERY_NAME_SHADOW = 'query-name-shadow';
static const String SECTION_VIEW = 'section-view';
static const String LOADING_SECTION = 'loading-section';
static const String STORY_SECTION = 'story-section';
static const String STORY = 'story';
static const String SNIPPET = 'snippet';
static const String TITLE = 'title';
static const String NO_THUMB = 'no-thumb';
static const String BYLINE = 'byline';
static const String STORY_SHADOW = 'story-shadow';
static const String DATELINE = 'dateline';
static const String STORY_UNREAD = 'story-unread';
static const String TEXT = 'text';
static const String CAPTION = 'caption';
static const String STORY_VIEW = 'story-view';
static const String PAGE_NUMBER = 'page-number';
static const String STORY_CONTENT = 'story-content';
static const String STORY_IMAGE = 'story-image';
static const String STORY_TEXT_VIEW = 'story-text-view';
static const String STORY_HEADER = 'story-header';
static const String STORY_TITLE = 'story-title';
static const String STORY_BYLINE = 'story-byline';
static const String STORY_DATELINE = 'story-dateline';
static const String HEADER_VIEW = 'header-view';
static const String APP_TITLE = 'app-title';
static const String IN_STORY = 'in-story';
static const String BACK_ARROW = 'back-arrow';
static const String CONFIG = 'config';
static const String REFRESH = 'refresh';
static const String INFO_BUTTON = 'info-button';
static const String NEW_WINDOW_BUTTON = 'new-window-button';
static const String WEB_BACK_BUTTON = 'web-back-button';
static const String WEB_FORWARD_BUTTON = 'web-forward-button';
static const String WEB_VIEW_BUTTON = 'web-view-button';
static const String TEXT_VIEW_BUTTON = 'text-view-button';
static const String ACTIVE = 'active';
static const String BUTTON = 'button';
static const String DATA_SOURCE_VIEW = 'data-source-view';
static const String DIALOG_MODAL = 'dialog-modal';
static const String DIALOG = 'dialog';
static const String DIALOG_TITLE_AREA = 'dialog-title-area';
static const String DIALOG_TITLE = 'dialog-title';
static const String DIALOG_BODY = 'dialog-body';
static const String DONE_BUTTON = 'done-button';
static const String HIDDEN_STORY = 'hidden-story';
static const String HEADER_BACKGROUND = 'header-background';
static const String HEADER = 'header';
static const String SPLASH = 'splash';
static const String FOOTER = 'footer';
static const String SPLASHIMG = 'splashImg';
static const String HIDE_ALL_QUERIES = 'hide-all-queries';
static const String TRANSPARENT = 'transparent';
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,35 @@
// 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;
/** A placeholder dialog that just passes the buck to Reader on feed
configuration. */
class ConfigHintDialog extends DialogView {
CompositeView _parent;
Function _doneHandler;
factory ConfigHintDialog(CompositeView parent, Function doneHandler) {
View content = ConfigHintDialog.makeContent();
return new ConfigHintDialog._impl(parent, doneHandler, content);
}
ConfigHintDialog._impl(this._parent, this._doneHandler, View content)
: super('Feed configuration', '', content);
void onDone() { _doneHandler(); }
static View makeContent() {
return new View.html(
'''
<div>
Add or remove feeds in
<a href="https://www.google.com/reader" target="_blank">
Google Reader</a>'s "Subscriptions".
Then come back here and click "Done" and we'll load your updated
list of subscriptions.
</div>
''');
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

View file

@ -0,0 +1,240 @@
// 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 snipppet.
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);
}
}

View file

@ -0,0 +1,43 @@
// 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(jimhug): Fill out methods, add encoder, move to shared lib.
class Decoder {
int index;
String data;
Decoder(this.data) {
this.index = 0;
}
// Reads numbers in variable-length 7-bit encoding. This matches the
// varint encoding used by protobufs except that it only uses 7
// bits per byte so it can be efficiently passed as UTF8.
// For more info, see appengine/encoder.py.
int readInt() {
var r = 0;
for (var i=0; ; i++) {
var v = data.codeUnitAt(index++);
r |= (v & 0x3F) << (6 * i);
if ((v & 0x40) == 0) break;
}
return r.toInt();
}
bool readBool() {
final ch = data[index++];
assert (ch == 'T' || ch == 'F');
return ch == 'T';
}
String readString() {
int len = readInt();
String s = data.substring(index, index+len);
index += len;
return s;
}
}

View file

@ -0,0 +1,84 @@
// 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;
/**
* An informational dialog that shows keyboard shortcuts and provides a
* link to the Dart language webpage.
*/
//TODO(efortuna): fix DialogView so it doesn't require the HTML passed to
// the constructor.
class HelpDialog extends DialogView {
CompositeView _parent;
Function _doneHandler;
HelpDialog(this._parent, this._doneHandler)
: super('Information', '', makeContent());
void onDone() { _doneHandler(); }
static View makeContent() {
return new View.html(
'''
<div>
<p>
Keyboard shortcuts:
${generateTableHtml()}
</p>
<p>
<div id="dart-logo">
<a href="http://dartlang.org">
Dart, the programming language</a>.
</div>
</p>
</div>
''');
}
static String generateTableHtml() {
String cellStart = '''<th valign="middle" align="center">''';
return '''<table width="90%" border=1 cellspacing="0" cellpadding="2">
<tr bgcolor="#c3d9ff">
${cellStart} Shortcut Key </th>
${cellStart} Action </th>
</tr>
<tr>
${cellStart} j, &lt;down arrow&gt; </th>
${cellStart} Next Article </th>
</tr>
<tr>
${cellStart} k, &lt;up arrow&gt; </th>
${cellStart} Previous Article </th>
</tr>
<tr>
${cellStart} o, &lt;enter&gt; </th>
${cellStart} Open Article </th>
</tr>
<tr>
${cellStart} &lt;esc&gt;, &lt;delete&gt; </th>
${cellStart} Back </th>
</tr>
<tr>
${cellStart} a, h, &lt;left arrow&gt; </th>
${cellStart} Left </th>
</tr>
<tr>
${cellStart} d, l, &lt;right arrow&gt; </th>
${cellStart} Right </th>
</tr>
<tr>
${cellStart} n </th>
${cellStart} Next Category </th>
</tr>
<tr>
${cellStart} p </th>
${cellStart} Previous Category </th>
</tr>
</table>''';
}
}

31
samples-dev/swarm/README Normal file
View file

@ -0,0 +1,31 @@
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.
A sample news reader application.
Running in the Editor with demo data (demo only, no feeds):
1. open the Dart Editor
2. from the menu, click File / Open Folder and load this directory
3. click the green run button
4. alternatively, to run as javascript:
- compile (under the Tools / Generate JavaScript menu option)
- load the same url shown in Dartium in step 3 in any other browser
Running in Dartium from the file system (demo only, no feeds):
1. run the Dartium Chrome binary from the command line with the following flag: --allow-file-access-from-files
2. navigate to the swarm.html file in this directory
Running from App Engine (TODO: these are stale instructions):
1. To run on App Engine, install the App Engine SDK for your platform, then
$ easy_install --upgrade google-api-python-client per http://code.google.com/p/google-api-python-client/wiki/Installation
$ cd samples/swarm/appengine
$ enable-app-engine-project . per http://code.google.com/p/google-api-python-client/wiki/GoogleAppEngine
Start the App Engine dev server and browse to /swarm-js.html

View file

@ -0,0 +1,89 @@
// 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;
/**
* A simple news reader in Dart.
*/
class Swarm extends App {
/**
* Flag to insure the onLoad isn't called when callback from initializeFromUrl
* could occur before the document's onload event.
*/
bool onLoadFired;
/** Collections of datafeeds to show per page. */
Sections sections;
/** The front page of the app. */
FrontView frontView;
/** Observable UI state. */
SwarmState state;
Swarm({bool useCannedData : false}) : super(), onLoadFired = false {
Sections.initializeFromUrl(useCannedData, (currSections) {
sections = currSections;
state = new SwarmState(sections);
setupApp();
});
// Catch user keypresses and decide whether to use them for the
// Streams app or pass them on to the browser.
document.onKeyUp.listen((e) {
if (frontView != null) {
frontView.processKeyEvent(e);
}
});
}
/**
* Tells each data source to check the server for the latest data.
*/
void refresh() {
sections.refresh();
// Hook up listeners about any data source additions or deletions. We don't
// differeniate additions or deletions just the fact that data feeds have
// changed. We might want more fidelity later.
sections.sectionTitles.forEach((title) {
Section section = sections.findSection(title);
// TODO(terry): addChangeListener needs to return an id so previous
// listener can be removed, otherwise anonymous functions
// can't easily be used. See b/5063673
section.feeds.addChangeListener((data) {
// TODO(jacobr): implement this.
print("Refresh sections not impl yet.");
});
});
}
/** The page load event handler. */
void onLoad() {
onLoadFired = true;
super.onLoad();
setupApp();
}
/**
* Setup the application's world.
*/
void setupApp() {
// TODO(terry): Should be able to spinup the app w/o waiting for data.
// If the document is already loaded so we can setup the app anytime.
// Otherwise, we'll wait to setup the world until the document is ready
// to render.
if (onLoadFired && state != null) {
render();
// This call loads the initial data.
refresh();
eraseSplashScreen();
}
}
void render() {
frontView = new FrontView(this);
frontView.addToDocument(document.body);
}
}

View file

@ -0,0 +1,299 @@
// 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 class for the UI state. UI state is essentially a "model" from
* the view's perspective but whose data just describes the UI itself. It
* contains data like the currently selected story, etc.
*/
// TODO(jimhug): Split the two classes here into framework and app-specific.
class SwarmState extends UIState {
/** Core data source for the app. */
final Sections _dataModel;
/**
* Which article the user is currently viewing, or null if they aren't
* viewing an Article.
*/
final ObservableValue<Article> currentArticle;
/**
* Which article the user currently has selected (for traversing articles
* via keyboard shortcuts).
*/
final ObservableValue<Article> selectedArticle;
/**
* True if the story view is maximized and the top and bottom UI elements
* are hidden.
*/
final ObservableValue<bool> storyMaximized;
/**
* True if the maximized story, if any, is being displayed in text mode
* rather than as an embedded web-page.
*/
final ObservableValue<bool> storyTextMode;
/**
* Which article the user currently has selected (by keyboard shortcuts),
* or null if an article isn't selected by the keyboard.
*/
BiIterator<Article> _articleIterator;
/**
* Which feed is currently selected (for keyboard shortcuts).
*/
BiIterator<Feed> _feedIterator;
/**
* Which section is currently selected (for keyboard shortcuts).
*/
BiIterator<Section> _sectionIterator;
SwarmState(this._dataModel)
: super(),
currentArticle = new ObservableValue<Article>(null),
selectedArticle = new ObservableValue<Article>(null),
storyMaximized = new ObservableValue<bool>(false),
storyTextMode = new ObservableValue<bool>(true) {
startHistoryTracking();
// TODO(efortuna): consider having this class just hold observable
// currentIndecies instead of iterators with observablevalues..
_sectionIterator = new BiIterator<Section>(_dataModel.sections);
_feedIterator = new BiIterator<Feed>(_sectionIterator.current.feeds);
_articleIterator =
new BiIterator<Article>(_feedIterator.current.articles);
currentArticle.addChangeListener((e) {
_articleIterator.jumpToValue(currentArticle.value);
});
}
/**
* Registers an event to fire on any state change
*
* TODO(jmesserly): fix this so we don't have to enumerate all of our fields
* again. One idea here is UIState becomes Observable, Observables have
* parents and notifications bubble up the parent chain.
*/
void addChangeListener(ChangeListener listener) {
_sectionIterator.currentIndex.addChangeListener(listener);
_feedIterator.currentIndex.addChangeListener(listener);
_articleIterator.currentIndex.addChangeListener(listener);
currentArticle.addChangeListener(listener);
}
Map<String, String> toHistory() {
final data = {};
data['section'] = currentSection.id;
data['feed'] = currentFeed.id;
if (currentArticle.value != null) {
data['article'] = currentArticle.value.id;
}
return data;
}
void loadFromHistory(Map values) {
// TODO(jimhug): There's a better way of doing this...
if (values['section'] != null) {
_sectionIterator.jumpToValue(_dataModel.
findSectionById(values['section']));
} else {
_sectionIterator = new BiIterator<Section>(_dataModel.sections);
}
if (values['feed'] != null && currentSection != null) {
_feedIterator.jumpToValue(currentSection.findFeed(values['feed']));
} else {
_feedIterator = new BiIterator<Feed>(_sectionIterator.current.feeds);
}
if (values['article'] != null && currentFeed != null) {
currentArticle.value = currentFeed.findArticle(values['article']);
_articleIterator.jumpToValue(currentArticle.value);
} else {
_articleIterator =
new BiIterator<Article>(_feedIterator.current.articles);
currentArticle.value = null;
}
storyMaximized.value = false;
}
/**
* Move the currentArticle pointer to the next item in the Feed.
*/
void goToNextArticle() {
currentArticle.value = _articleIterator.next();
selectedArticle.value = _articleIterator.current;
}
/**
* Move the currentArticle pointer to the previous item in the Feed.
*/
void goToPreviousArticle() {
currentArticle.value = _articleIterator.previous();
selectedArticle.value = _articleIterator.current;
}
/**
* Move the selectedArticle pointer to the next item in the Feed.
*/
void goToNextSelectedArticle() {
selectedArticle.value = _articleIterator.next();
}
/**
* Move the selectedArticle pointer to the previous item in the Feed.
*/
void goToPreviousSelectedArticle() {
selectedArticle.value = _articleIterator.previous();
}
/**
* Move the pointers for selectedArticle to point to the next
* Feed.
*/
void goToNextFeed() {
var newFeed = _feedIterator.next();
int oldIndex = _articleIterator.currentIndex.value;
_articleIterator = new BiIterator<Article>(newFeed.articles,
_articleIterator.currentIndex.listeners);
_articleIterator.currentIndex.value = oldIndex;
selectedArticle.value = _articleIterator.current;
}
/**
* Move the pointers for selectedArticle to point to the previous
* DataSource.
*/
void goToPreviousFeed() {
var newFeed = _feedIterator.previous();
int oldIndex = _articleIterator.currentIndex.value;
_articleIterator = new BiIterator<Article>(newFeed.articles,
_articleIterator.currentIndex.listeners);
_articleIterator.currentIndex.value = oldIndex;
selectedArticle.value = _articleIterator.current;
}
/**
* Move to the next section (page) of feeds in the UI.
* @param index the previous index (how far down in a given feed)
* from the Source we are moving from.
* This method takes sliderMenu in the event that it needs to move
* to a previous section, it can notify the UI to update.
*/
void goToNextSection(SliderMenu sliderMenu) {
//TODO(efortuna): move sections?
var oldSection = currentSection;
int oldIndex = _articleIterator.currentIndex.value;
sliderMenu.selectNext(true);
// This check prevents our selector from wrapping around when we try to
// go to the "next section", but we're already at the last section.
if (oldSection != _sectionIterator.current) {
_feedIterator = new BiIterator<Feed>(_sectionIterator.current.feeds,
_feedIterator.currentIndex.listeners);
_articleIterator =
new BiIterator<Article>(_feedIterator.current.articles,
_articleIterator.currentIndex.listeners);
_articleIterator.currentIndex.value = oldIndex;
selectedArticle.value = _articleIterator.current;
}
}
/**
* Move to the previous section (page) of feeds in the UI.
* @param index the previous index (how far down in a given feed)
* from the Source we are moving from.
* @param oldSection the original starting section (before the slider
* menu moved)
* This method takes sliderMenu in the event that it needs to move
* to a previous section, it can notify the UI to update.
*/
void goToPreviousSection(SliderMenu sliderMenu) {
//TODO(efortuna): don't pass sliderMenu here. Just update in view!
var oldSection = currentSection;
int oldIndex = _articleIterator.currentIndex.value;
sliderMenu.selectPrevious(true);
// This check prevents our selector from wrapping around when we try to
// go to the "previous section", but we're already at the first section.
if (oldSection != _sectionIterator.current) {
_feedIterator = new BiIterator<Feed>(_sectionIterator.current.feeds,
_feedIterator.currentIndex.listeners);
// Jump to back of feed set if we are moving backwards through sections.
_feedIterator.currentIndex.value = _feedIterator.list.length - 1;
_articleIterator =
new BiIterator<Article>(_feedIterator.current.articles,
_articleIterator.currentIndex.listeners);
_articleIterator.currentIndex.value = oldIndex;
selectedArticle.value = _articleIterator.current;
}
}
/**
* Set the selected story as the current story (for viewing in the larger
* Story View.)
*/
void selectStoryAsCurrent() {
currentArticle.value = _articleIterator.current;
selectedArticle.value = _articleIterator.current;
}
/**
* Remove our currentArticle selection, to move back to the Main Grid view.
*/
void clearCurrentArticle() {
currentArticle.value = null;
}
/**
* Set the selectedArticle as the first item in that section (UI page).
*/
void goToFirstArticleInSection() {
selectedArticle.value = _articleIterator.current;
}
/**
* Returns true if the UI is currently in the Story View state.
*/
bool get inMainView => currentArticle.value == null;
/**
* Returns true if we currently have an Article selected (for keyboard
* shortcuts browsing).
*/
bool get hasArticleSelected => selectedArticle.value != null;
/**
* Mark the current article as read
*/
bool markCurrentAsRead() {
currentArticle.value.unread.value = false;
}
/**
* The user has moved to a new section (page). This can occur either
* if the user clicked on a section page, or used keyboard shortcuts.
* The default behavior is to move to the first article in the first
* column. The location of the selected item depends on the previous
* selected item location if the user used keyboard shortcuts. These
* are manipulated in goToPrevious/NextSection().
*/
void moveToNewSection(String sectionTitle) {
_sectionIterator.currentIndex.value =
_dataModel.findSectionIndex(sectionTitle);
_feedIterator = new BiIterator<Feed>(_sectionIterator.current.feeds,
_feedIterator.currentIndex.listeners);
_articleIterator =
new BiIterator<Article>(_feedIterator.current.articles,
_articleIterator.currentIndex.listeners);
}
Section get currentSection => _sectionIterator.current;
Feed get currentFeed => _feedIterator.current;
}

View file

@ -0,0 +1,972 @@
// 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('<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 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('<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 const 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.onMouseDown.listen((e) {
swarm.state.storyMaximized.value = 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.onClick.listen((event) { toggle(); });
}
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 const IPAD_WIDTH = 257;
static const DESKTOP_WIDTH = 297;
static const CHROME_OS_WIDTH = 317;
static const TITLE_MARGIN_LEFT = 257 - 150;
static const BODY_MARGIN_LEFT = 257 - 221;
static const LINE_HEIGHT = 18;
static const TITLE_FONT = 'bold 13px arial,sans-serif';
static const BODY_FONT = '13px arial,sans-serif';
static const TOTAL_MARGIN = 16 * 2 + 70;
static const MIN_TITLE_HEIGHT = 36;
static const MAX_TITLE_LINES = 2;
static const 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 const 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.querySelector('.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 CanvasElement canvas = new CanvasElement(
height: img.height, width: img.width);
final CanvasRenderingContext2D 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.querySelectorAll(
"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 (AnchorElement anchor in storyContent.querySelectorAll('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.querySelector('.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;
}
}

View file

@ -0,0 +1,75 @@
// 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 base class for UI state that intends to support browser history.
*/
abstract class UIState {
/**
* The event listener we hook to the window's "popstate" event.
* This event is triggered by the back button or by the first page load.
*/
StreamSubscription _historyTracking;
UIState();
void startHistoryTracking() {
stopHistoryTracking();
bool firstEvent = true;
var handler = EventBatch.wrap((event) {
String state = window.location.hash;
if (state.startsWith('#')) {
// TODO(jimhug): Support default argument on substring.
state = state.substring(1, state.length);
}
if (firstEvent && state != '') {
// TODO(jmesserly): When loading a bookmark or refreshing, we replace
// the app state with a clean app state so the back button works. It
// would be better to support jumping to the previous story.
// We'd need to do some history manipulation here and some fixes to
// the views for this.
window.history.replaceState(null, document.title, '#');
} else if (state != '') {
loadFromHistory(JSON.decode(state));
}
firstEvent = false;
});
_historyTracking = window.onPopState.listen(handler);
}
void stopHistoryTracking() {
if (_historyTracking != null) {
_historyTracking.cancel();
}
}
/** Pushes a state onto the browser history stack */
void pushToHistory() {
if (_historyTracking == null) {
throw 'history tracking not started';
}
String state = JSON.encode(toHistory());
// TODO(jmesserly): [state] should be an Object, and we should pass it to
// the state parameter instead of as a #hash URL. Right now we're working
// around b/4582542.
window.history.pushState(null, '${document.title}#$state');
}
/**
* Serialize the state to a form suitable for storing in browser history.
*/
Map<String, String> toHistory();
/**
* Load the UI state from the given [values].
*/
void loadFromHistory(Map<String, String> values);
}

View file

@ -0,0 +1,984 @@
// 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;
// This file contains View framework classes.
// As it grows, it may need to be split into multiple files.
/** A factory that creates a view from a data model. */
abstract class ViewFactory<D> {
View newView(D item);
/** The width of the created view or null if the width is not fixed. */
int get width;
/** The height of the created view or null if the height is not fixed. */
int get height;
}
abstract class VariableSizeViewFactory<D> {
View newView(D item);
/** The width of the created view for a specific data model. */
int getWidth(D item);
/** The height of the created view for a specific data model. */
int getHeight(D item);
}
/** A collection of event listeners. */
class EventListeners {
var listeners;
EventListeners() {
listeners = new List();
}
void addListener(listener) {
listeners.add(listener);
}
void fire(var event) {
for (final listener in listeners) {
listener(event);
}
}
}
/**
* Private view class used to store placeholder views for detatched ListView
* elements.
*/
class _PlaceholderView extends View {
_PlaceholderView() : super() {}
Element render() => new Element.tag('div');
}
/**
* Class providing all metrics required to layout a data driven list view.
*/
abstract class ListViewLayout<D> {
void onDataChange();
// TODO(jacobr): placing the newView member function on this class seems like
// the wrong design.
View newView(int index);
/** Get the height of the view. Possibly expensive to compute. */
int getHeight(int viewLength);
/** Get the width of the view. Possibly expensive to compute. */
int getWidth(int viewLength);
/** Get the length of the view. Possible expensive to compute. */
int getLength(int viewLength);
/** Estimated height of the view. Guaranteed to be fast to compute. */
int getEstimatedHeight(int viewLength);
/** Estimated with of the view. Guaranteed to be fast to compute. */
int getEstimatedWidth(int viewLength);
/**
* Returns the offset in px that the ith item in the view should be placed
* at.
*/
int getOffset(int index);
/**
* The page the ith item in the view should be placed in.
*/
int getPage(int index, int viewLength);
int getPageStartIndex(int index, int viewLength);
int getEstimatedLength(int viewLength);
/**
* Snap a specified index to the nearest visible view given the [viewLength].
*/
int getSnapIndex(num offset, num viewLength);
/**
* Returns an interval specifying what views are currently visible given a
* particular [:offset:].
*/
Interval computeVisibleInterval(num offset, num viewLength,
num bufferLength);
}
/**
* Base class used for the simple fixed size item [:ListView:] classes and more
* complex list view classes such as [:VariableSizeListView:] using a
* [:ListViewLayout:] class to drive the actual layout.
*/
class GenericListView<D> extends View {
/** Minimum throw distance in pixels to trigger snapping to the next item. */
static const SNAP_TO_NEXT_THROW_THRESHOLD = 15;
static const INDEX_DATA_ATTRIBUTE = 'data-index';
final bool _scrollable;
final bool _showScrollbar;
final bool _snapToItems;
Scroller scroller;
Scrollbar _scrollbar;
List<D> _data;
ObservableValue<D> _selectedItem;
Map<int, View> _itemViews;
Element _containerElem;
bool _vertical;
/** Length of the scrollable dimension of the view in px. */
int _viewLength = 0;
Interval _activeInterval;
bool _paginate;
bool _removeClippedViews;
ListViewLayout<D> _layout;
D _lastSelectedItem;
PageState _pages;
/**
* Creates a new GenericListView with the given layout and data. If [:_data:]
* is an [:ObservableList<T>:] then it will listen to changes to the list
* and update the view appropriately.
*/
GenericListView(
this._layout,
this._data,
this._scrollable,
this._vertical,
this._selectedItem,
this._snapToItems,
this._paginate,
this._removeClippedViews,
this._showScrollbar,
this._pages)
: super(),
_activeInterval = new Interval(0, 0),
_itemViews = new Map<int, View>() {
// TODO(rnystrom): Move this into enterDocument once we have an exitDocument
// that we can use to unregister it.
if (_scrollable) {
window.onResize.listen((Event event) {
if (isInDocument) {
onResize();
}
});
}
}
void onSelectedItemChange() {
// TODO(rnystrom): use Observable to track the last value of _selectedItem
// rather than tracking it ourselves.
_select(findIndex(_lastSelectedItem), false);
_select(findIndex(_selectedItem.value), true);
_lastSelectedItem = _selectedItem.value;
}
Iterable<View> get childViews {
return _itemViews.values.toList();
}
void _onClick(MouseEvent e) {
int index = _findAssociatedIndex(e.target);
if (index != null) {
_selectedItem.value = _data[index];
}
}
int _findAssociatedIndex(Node leafNode) {
Node node = leafNode;
while (node != null && node != _containerElem) {
if (node.parent == _containerElem) {
return _nodeToIndex(node);
}
node = node.parent;
}
return null;
}
int _nodeToIndex(Element node) {
// TODO(jacobr): use data attributes when available.
String index = node.attributes[INDEX_DATA_ATTRIBUTE];
if (index != null && index.length > 0) {
return int.parse(index);
}
return null;
}
Element render() {
final node = new Element.tag('div');
if (_scrollable) {
_containerElem = new Element.tag('div');
_containerElem.tabIndex = -1;
node.nodes.add(_containerElem);
} else {
_containerElem = node;
}
if (_scrollable) {
scroller = new Scroller(
_containerElem,
_vertical /* verticalScrollEnabled */,
!_vertical /* horizontalScrollEnabled */,
true /* momentumEnabled */,
() {
num width = _layout.getWidth(_viewLength);
num height = _layout.getHeight(_viewLength);
width = width != null ? width : 0;
height = height != null ? height : 0;
return new Size(width, height);
},
_paginate && _snapToItems ?
Scroller.FAST_SNAP_DECELERATION_FACTOR : 1);
scroller.onContentMoved.listen((e) => renderVisibleItems(false));
if (_pages != null) {
watch(_pages.target, (s) => _onPageSelected());
}
if (_snapToItems) {
scroller.onDecelStart.listen((e) => _decelStart());
scroller.onScrollerDragEnd.listen((e) => _decelStart());
}
if (_showScrollbar) {
_scrollbar = new Scrollbar(scroller, true);
}
} else {
_reserveArea();
renderVisibleItems(true);
}
return node;
}
void afterRender(Element node) {
// If our data source is observable, observe it.
if (_data is ObservableList<D>) {
ObservableList<D> observable = _data;
attachWatch(observable, (EventSummary e) {
if (e.target == observable) {
onDataChange();
}
});
}
if (_selectedItem != null) {
addOnClick((Event e) { _onClick(e); });
}
if (_selectedItem != null) {
watch(_selectedItem, (EventSummary summary) => onSelectedItemChange());
}
}
void onDataChange() {
_layout.onDataChange();
_renderItems();
}
void _reserveArea() {
final style = _containerElem.style;
int width = _layout.getWidth(_viewLength);
int height = _layout.getHeight(_viewLength);
if (width != null) {
style.width = '${width}px';
}
if (height != null) {
style.height = '${height}px';
}
// TODO(jacobr): this should be specified by the default CSS for a
// GenericListView.
style.overflow = 'hidden';
}
void onResize() {
int lastViewLength = _viewLength;
scheduleMicrotask(() {
_viewLength = _vertical ? node.offset.height : node.offset.width;
if (_viewLength != lastViewLength) {
if (_scrollbar != null) {
_scrollbar.refresh();
}
renderVisibleItems(true);
}
});
}
void enterDocument() {
if (scroller != null) {
onResize();
if (_scrollbar != null) {
_scrollbar.initialize();
}
}
}
int getNextIndex(int index, bool forward) {
int delta = forward ? 1 : -1;
if (_paginate) {
int newPage = Math.max(0, _layout.getPage(index, _viewLength) + delta);
index = _layout.getPageStartIndex(newPage, _viewLength);
} else {
index += delta;
}
return GoogleMath.clamp(index, 0, _data.length - 1);
}
void _decelStart() {
num currentTarget = scroller.verticalEnabled ?
scroller.currentTarget.y : scroller.currentTarget.x;
num current = scroller.verticalEnabled ?
scroller.contentOffset.y : scroller.contentOffset.x;
num targetIndex = _layout.getSnapIndex(currentTarget, _viewLength);
if (current != currentTarget) {
// The user is throwing rather than statically releasing.
// For this case, we want to move them to the next snap interval
// as long as they made at least a minimal throw gesture.
num currentIndex = _layout.getSnapIndex(current, _viewLength);
if (currentIndex == targetIndex &&
(currentTarget - current).abs() > SNAP_TO_NEXT_THROW_THRESHOLD &&
-_layout.getOffset(targetIndex) != currentTarget) {
num snappedCurrentPosition = -_layout.getOffset(targetIndex);
targetIndex = getNextIndex(targetIndex, currentTarget < current);
}
}
num targetPosition = -_layout.getOffset(targetIndex);
if (currentTarget != targetPosition) {
if (scroller.verticalEnabled) {
scroller.throwTo(scroller.contentOffset.x, targetPosition);
} else {
scroller.throwTo(targetPosition, scroller.contentOffset.y);
}
} else {
// Update the target page only after we are all done animating.
if (_pages != null) {
_pages.target.value =_layout.getPage(targetIndex, _viewLength);
}
}
}
void _renderItems() {
for (int i = _activeInterval.start; i < _activeInterval.end; i++) {
_removeView(i);
}
_itemViews.clear();
_activeInterval = new Interval(0, 0);
if (scroller == null) {
_reserveArea();
}
renderVisibleItems(false);
}
void _onPageSelected() {
if (_pages.target !=
_layout.getPage(_activeInterval.start, _viewLength)) {
_throwTo(_layout.getOffset(
_layout.getPageStartIndex(_pages.target.value, _viewLength)));
}
}
num get _offset {
return scroller.verticalEnabled ?
scroller.getVerticalOffset() : scroller.getHorizontalOffset();
}
/**
* Calculates visible interval, based on the scroller position.
*/
Interval getVisibleInterval() {
return _layout.computeVisibleInterval(_offset, _viewLength, 0);
}
void renderVisibleItems(bool lengthChanged) {
Interval targetInterval;
if (scroller != null) {
targetInterval = getVisibleInterval();
} else {
// If the view is not scrollable, render all elements.
targetInterval = new Interval(0, _data.length);
}
if (_pages != null) {
_pages.current.value =
_layout.getPage(targetInterval.start, _viewLength);
}
if (_pages != null) {
_pages.length.value = _data.length > 0 ?
_layout.getPage(_data.length - 1, _viewLength) + 1 : 0;
}
if (!_removeClippedViews) {
// Avoid removing clipped views by extending the target interval to
// include the existing interval of rendered views.
targetInterval = targetInterval.union(_activeInterval);
}
if (lengthChanged == false && targetInterval == _activeInterval) {
return;
}
// TODO(jacobr): add unittests that this code behaves correctly.
// Remove views that are not needed anymore
for (int i = _activeInterval.start,
end = Math.min(targetInterval.start, _activeInterval.end);
i < end; i++) {
_removeView(i);
}
for (int i = Math.max(targetInterval.end, _activeInterval.start);
i < _activeInterval.end; i++) {
_removeView(i);
}
// Add new views
for (int i = targetInterval.start,
end = Math.min(_activeInterval.start, targetInterval.end);
i < end; i++) {
_addView(i);
}
for (int i = Math.max(_activeInterval.end, targetInterval.start);
i < targetInterval.end; i++) {
_addView(i);
}
_activeInterval = targetInterval;
}
void _removeView(int index) {
// Do not remove placeholder views as they need to stay present in case
// they scroll out of view and then back into view.
if (!(_itemViews[index] is _PlaceholderView)) {
// Remove from the active DOM but don't destroy.
_itemViews[index].node.remove();
childViewRemoved(_itemViews[index]);
}
}
View _newView(int index) {
final view = _layout.newView(index);
view.node.attributes[INDEX_DATA_ATTRIBUTE] = index.toString();
return view;
}
View _addView(int index) {
if (_itemViews.containsKey(index)) {
final view = _itemViews[index];
_addViewHelper(view, index);
childViewAdded(view);
return view;
}
final view = _newView(index);
_itemViews[index] = view;
// TODO(jacobr): its ugly to put this here... but its needed
// as typical even-odd css queries won't work as we only display some
// children at a time.
if (index == 0) {
view.addClass('first-child');
}
_selectHelper(view, _data[index] == _lastSelectedItem);
// The order of the child elements doesn't matter as we use absolute
// positioning.
_addViewHelper(view, index);
childViewAdded(view);
return view;
}
void _addViewHelper(View view, int index) {
_positionSubview(view.node, index);
// The view might already be attached.
if (view.node.parent != _containerElem) {
_containerElem.nodes.add(view.node);
}
}
/**
* Detach a subview from the view replacing it with an empty placeholder view.
* The detatched subview can be safely reparented.
*/
View detachSubview(D itemData) {
int index = findIndex(itemData);
View view = _itemViews[index];
if (view == null) {
// Edge case: add the view so we can detatch as the view is currently
// outside but might soon be inside the visible area.
assert(!_activeInterval.contains(index));
_addView(index);
view = _itemViews[index];
}
final placeholder = new _PlaceholderView();
view.node.replaceWith(placeholder.node);
_itemViews[index] = placeholder;
return view;
}
/**
* Reattach a subview from the view that was detached from the view
* by calling detachSubview. [callback] is called once the subview is
* reattached and done animating into position.
*/
void reattachSubview(D data, View view, bool animate) {
int index = findIndex(data);
// TODO(jacobr): perform some validation that the view is
// really detached.
var currentPosition;
if (animate) {
currentPosition =
FxUtil.computeRelativePosition(view.node, _containerElem);
}
assert (_itemViews[index] is _PlaceholderView);
view.enterDocument();
_itemViews[index].node.replaceWith(view.node);
_itemViews[index] = view;
if (animate) {
FxUtil.setTranslate(view.node, currentPosition.x, currentPosition.y, 0);
// The view's position is unchanged except now re-parented to
// the list view.
Timer.run(() { _positionSubview(view.node, index); });
} else {
_positionSubview(view.node, index);
}
}
int findIndex(D targetItem) {
// TODO(jacobr): move this to a util library or modify this class so that
// the data is an List not a Collection.
int i = 0;
for (D item in _data) {
if (item == targetItem) {
return i;
}
i++;
}
return null;
}
void _positionSubview(Element node, int index) {
if (_vertical) {
FxUtil.setTranslate(node, 0, _layout.getOffset(index), 0);
} else {
FxUtil.setTranslate(node, _layout.getOffset(index), 0, 0);
}
node.style.zIndex = index.toString();
}
void _select(int index, bool selected) {
if (index != null) {
final subview = getSubview(index);
if (subview != null) {
_selectHelper(subview, selected);
}
}
}
void _selectHelper(View view, bool selected) {
if (selected) {
view.addClass('sel');
} else {
view.removeClass('sel');
}
}
View getSubview(int index) {
return _itemViews[index];
}
void showView(D targetItem) {
int index = findIndex(targetItem);
if (index != null) {
if (_layout.getOffset(index) < -_offset) {
_throwTo(_layout.getOffset(index));
} else if (_layout.getOffset(index + 1) > (-_offset + _viewLength)) {
// TODO(jacobr): for completeness we should check whether
// the current view is longer than _viewLength in which case
// there are some nasty edge cases.
_throwTo(_layout.getOffset(index + 1) - _viewLength);
}
}
}
void _throwTo(num offset) {
if (_vertical) {
scroller.throwTo(0, -offset);
} else {
scroller.throwTo(-offset, 0);
}
}
}
class FixedSizeListViewLayout<D> implements ListViewLayout<D> {
final ViewFactory<D> itemViewFactory;
final bool _vertical;
List<D> _data;
bool _paginate;
FixedSizeListViewLayout(this.itemViewFactory, this._data, this._vertical,
this._paginate);
void onDataChange() {}
View newView(int index) {
return itemViewFactory.newView(_data[index]);
}
int get _itemLength {
return _vertical ? itemViewFactory.height : itemViewFactory.width;
}
int getWidth(int viewLength) {
return _vertical ? itemViewFactory.width : getLength(viewLength);
}
int getHeight(int viewLength) {
return _vertical ? getLength(viewLength) : itemViewFactory.height;
}
int getEstimatedHeight(int viewLength) {
// Returns the exact height as it is trivial to compute for this layout.
return getHeight(viewLength);
}
int getEstimatedWidth(int viewLength) {
// Returns the exact height as it is trivial to compute for this layout.
return getWidth(viewLength);
}
int getEstimatedLength(int viewLength) {
// Returns the exact length as it is trivial to compute for this layout.
return getLength(viewLength);
}
int getLength(int viewLength) {
int itemLength =
_vertical ? itemViewFactory.height : itemViewFactory.width;
if (viewLength == null || viewLength == 0) {
return itemLength * _data.length;
} else if (_paginate) {
if (_data.length > 0) {
final pageLength = getPageLength(viewLength);
return getPage(_data.length - 1, viewLength)
* pageLength + Math.max(viewLength, pageLength);
} else {
return 0;
}
} else {
return itemLength * (_data.length - 1) + Math.max(viewLength, itemLength);
}
}
int getOffset(int index) {
return index * _itemLength;
}
int getPageLength(int viewLength) {
final itemsPerPage = viewLength ~/ _itemLength;
return Math.max(1, itemsPerPage) * _itemLength;
}
int getPage(int index, int viewLength) {
return getOffset(index) ~/ getPageLength(viewLength);
}
int getPageStartIndex(int page, int viewLength) {
return getPageLength(viewLength) ~/ _itemLength * page;
}
int getSnapIndex(num offset, int viewLength) {
int index = (-offset / _itemLength).round();
if (_paginate) {
index = getPageStartIndex(getPage(index, viewLength), viewLength);
}
return GoogleMath.clamp(index, 0, _data.length - 1);
}
Interval computeVisibleInterval(
num offset, num viewLength, num bufferLength) {
int targetIntervalStart =
Math.max(0, (-offset - bufferLength) ~/ _itemLength);
num targetIntervalEnd = GoogleMath.clamp(
((-offset + viewLength + bufferLength) / _itemLength).ceil(),
targetIntervalStart,
_data.length);
return new Interval(targetIntervalStart, targetIntervalEnd.toInt());
}
}
/**
* Simple list view class where each item has fixed width and height.
*/
class ListView<D> extends GenericListView<D> {
/**
* Creates a new ListView for the given data. If [:_data:] is an
* [:ObservableList<T>:] then it will listen to changes to the list and
* update the view appropriately.
*/
ListView(List<D> data, ViewFactory<D> itemViewFactory, bool scrollable,
bool vertical, ObservableValue<D> selectedItem,
[bool snapToItems = false,
bool paginate = false,
bool removeClippedViews = false,
bool showScrollbar = false,
PageState pages = null])
: super(new FixedSizeListViewLayout<D>(itemViewFactory, data, vertical,
paginate),
data, scrollable, vertical, selectedItem, snapToItems, paginate,
removeClippedViews, showScrollbar, pages);
}
/**
* Layout where each item may have variable size along the axis the list view
* extends.
*/
class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
List<D> _data;
List<int> _itemOffsets;
List<int> _lengths;
int _lastOffset = 0;
bool _vertical;
bool _paginate;
VariableSizeViewFactory<D> itemViewFactory;
Interval _lastVisibleInterval;
VariableSizeListViewLayout(this.itemViewFactory, data, this._vertical,
this._paginate) :
_data = data,
_lastVisibleInterval = new Interval(0, 0) {
_itemOffsets = <int>[];
_lengths = <int>[];
_itemOffsets.add(0);
}
void onDataChange() {
_itemOffsets.clear();
_itemOffsets.add(0);
_lengths.clear();
}
View newView(int index) => itemViewFactory.newView(_data[index]);
int getWidth(int viewLength) {
if (_vertical) {
return itemViewFactory.getWidth(null);
} else {
return getLength(viewLength);
}
}
int getHeight(int viewLength) {
if (_vertical) {
return getLength(viewLength);
} else {
return itemViewFactory.getHeight(null);
}
}
int getEstimatedHeight(int viewLength) {
if (_vertical) {
return getEstimatedLength(viewLength);
} else {
return itemViewFactory.getHeight(null);
}
}
int getEstimatedWidth(int viewLength) {
if (_vertical) {
return itemViewFactory.getWidth(null);
} else {
return getEstimatedLength(viewLength);
}
}
// TODO(jacobr): this logic is overly complicated. Replace with something
// simpler.
int getEstimatedLength(int viewLength) {
if (_lengths.length == _data.length) {
// No need to estimate... we have all the data already.
return getLength(viewLength);
}
if (_itemOffsets.length > 1 && _lengths.length > 0) {
// Estimate length by taking the average of the lengths
// of the known views.
num lengthFromAllButLastElement = 0;
if (_itemOffsets.length > 2) {
lengthFromAllButLastElement =
(getOffset(_itemOffsets.length - 2) -
getOffset(0)) *
(_data.length / (_itemOffsets.length - 2));
}
return (lengthFromAllButLastElement +
Math.max(viewLength, _lengths[_lengths.length - 1])).toInt();
} else {
if (_lengths.length == 1) {
return Math.max(viewLength, _lengths[0]);
} else {
return viewLength;
}
}
}
int getLength(int viewLength) {
if (_data.length == 0) {
return viewLength;
} else {
// Hack so that _lengths[length - 1] is available.
getOffset(_data.length);
return (getOffset(_data.length - 1) - getOffset(0)) +
Math.max(_lengths[_lengths.length - 1], viewLength);
}
}
int getOffset(int index) {
if (index >= _itemOffsets.length) {
int offset = _itemOffsets[_itemOffsets.length - 1];
for (int i = _itemOffsets.length; i <= index; i++) {
int length = _vertical ? itemViewFactory.getHeight(_data[i - 1])
: itemViewFactory.getWidth(_data[i - 1]);
offset += length;
_itemOffsets.add(offset);
_lengths.add(length);
}
}
return _itemOffsets[index];
}
int getPage(int index, int viewLength) {
// TODO(jacobr): implement.
throw 'Not implemented';
}
int getPageStartIndex(int page, int viewLength) {
// TODO(jacobr): implement.
throw 'Not implemented';
}
int getSnapIndex(num offset, int viewLength) {
for (int i = 1; i < _data.length; i++) {
if (getOffset(i) + getOffset(i - 1) > -offset * 2) {
return i - 1;
}
}
return _data.length - 1;
}
Interval computeVisibleInterval(
num offset, num viewLength, num bufferLength) {
offset = offset.toInt();
int start = _findFirstItemBefore(
-offset - bufferLength,
_lastVisibleInterval != null ? _lastVisibleInterval.start : 0);
int end = _findFirstItemAfter(
-offset + viewLength + bufferLength,
_lastVisibleInterval != null ? _lastVisibleInterval.end : 0);
_lastVisibleInterval = new Interval(start, Math.max(start, end));
_lastOffset = offset;
return _lastVisibleInterval;
}
int _findFirstItemAfter(num target, int hint) {
for (int i = 0; i < _data.length; i++) {
if (getOffset(i) > target) {
return i;
}
}
return _data.length;
}
// TODO(jacobr): use hint.
int _findFirstItemBefore(num target, int hint) {
// We go search this direction delaying computing the actual view size
// as long as possible.
for (int i = 1; i < _data.length; i++) {
if (getOffset(i) >= target) {
return i - 1;
}
}
return Math.max(_data.length - 1, 0);
}
}
class VariableSizeListView<D> extends GenericListView<D> {
VariableSizeListView(List<D> data,
VariableSizeViewFactory<D> itemViewFactory,
bool scrollable,
bool vertical,
ObservableValue<D> selectedItem,
[bool snapToItems = false,
bool paginate = false,
bool removeClippedViews = false,
bool showScrollbar = false,
PageState pages = null])
: super(new VariableSizeListViewLayout(itemViewFactory, data, vertical,
paginate),
data, scrollable, vertical, selectedItem, snapToItems,
paginate, removeClippedViews, showScrollbar, pages);
}
/** A back button that is equivalent to clicking "back" in the browser. */
class BackButton extends View {
BackButton() : super();
Element render() => new Element.html('<div class="back-arrow button"></div>');
void afterRender(Element node) {
addOnClick((e) => window.history.back());
}
}
// TODO(terry): Maybe should be part of ButtonView class in appstack/view?
/** OS button. */
class PushButtonView extends View {
final String _text;
final String _cssClass;
final _clickHandler;
PushButtonView(this._text, this._cssClass, this._clickHandler) : super();
Element render() {
return new Element.html('<button class="${_cssClass}">${_text}</button>');
}
void afterRender(Element node) {
addOnClick(_clickHandler);
}
}
// TODO(terry): Add a drop shadow around edge and corners need to be rounded.
// Need to support conveyor for contents of dialog so it's not
// larger than the parent window.
/** A generic dialog view supports title, done button and dialog content. */
class DialogView extends View {
final String _title;
final String _cssName;
final View _content;
Element container;
PushButtonView _done;
DialogView(this._title, this._cssName, this._content) : super() {}
Element render() {
final node = new Element.html('''
<div class="dialog-modal">
<div class="dialog $_cssName">
<div class="dialog-title-area">
<span class="dialog-title">$_title</span>
</div>
<div class="dialog-body"></div>
</div>
</div>''');
_done = new PushButtonView('Done', 'done-button',
EventBatch.wrap((e) => onDone()));
final titleArea = node.querySelector('.dialog-title-area');
titleArea.nodes.add(_done.node);
container = node.querySelector('.dialog-body');
container.nodes.add(_content.node);
return node;
}
/** Override to handle dialog done. */
void onDone() { }
}

View file

@ -0,0 +1,36 @@
# 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.
application: google.com:dart
version: 1
runtime: python
api_version: 1
default_expiration: 0m 1s
builtins:
- datastore_admin: on
handlers:
- url: /data/.*
script: main.py
- url: /update/.*
script: main.py
- url: /oauth2callback
script: main.py
- url: /dev
script: main.py
# This static_dir is only used at dev time.
- url: /dev
static_dir: ../../../
login: required
- url: /.*
script: main.py

View file

@ -0,0 +1,8 @@
# 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.
cron:
- description: check for new data on registered feeds
url: /update/allFeeds
schedule: every 20 minutes

View file

@ -0,0 +1,86 @@
<!-- 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(jimhug): This could be a lot prettier -->
<html>
<body>
<h1>Getting started with Swarm development.</h1>
<p>First, get a copy of an up-to-date CannedData.dart so that you can work
against relatively live data directly while running the app off of the
local filesystem.</p>
<p>Put this file <a href='/data/CannedData.dart'>CannedData.dart</a> in your
working tree as <pre>dart/samples/swarm/CannedData.dart</pre></p>
<p>You should now be able to run your app out of your local filesystem
for UI development using something like: <pre>file:///Users/jimhug/dart-all/dart/samples/swarm/swarm.html</pre>.</p>
<p>When you are ready to test your new UI on this live server, first you
need to run <pre>python update.py</pre> from your
<pre>dart/samples/swarm</pre> directory. This will build
both a self-contained html file for both js and dart code. Then,
use the link below to upload your files to this server. If you are
uploading for testing, keep the version as shown and you will be able
to access the js version at
<a href="http://dart.googleplex.com/{{user.nickname}}-swarm-js.html">
http://dart.googleplex.com/{{user.nickname}}-swarm-js.html</a>.
By submitting with no prefix, you will update the default demo
version of the app.
</p>
<form action="/update/html" method="POST" enctype="multipart/form-data">
Upload File: <input type="file" multiple="" name="files"><br>
Version: <input type="text" value="{{user.nickname}}" name="version"><br>
<input type="submit" name="submit" value="Submit">
</form>
<br />
<h2>Fully offline mode</h2>
<p>Download <a href='/data/CannedData.zip'>CannedData.zip</a> (right click, save as) and extract it to a location that your static file webserver serves. You'll want to run the following steps on Mac:
<ol>
<li>In Sharing Preferences, enable Web Sharing.</li>
<li>Open this file in a text editor:<pre>/private/etc/apache2/httpd.conf</pre></li>
<li>Find the code like this, change "Deny from all" to "Allow from all":
<pre>
&lt;Directory /&gt;
Options FollowSymLinks
AllowOverride None
Order deny,allow
Allow from all
&lt;/Directory&gt;
AddType text/html .data
</pre>
</li>
<li>
<pre>
sudo apachectl restart
pushd /Library/WebServer/Document
# If you are replacing old data:
# rm -rf data/
unzip ~/Downloads/CannedData.zip
chmod -R +rw data
# Warning: this step takes a while
# It downloads all images from all of the stories and inlines them in the HTML
# You can skip this if you're iterating on the app, it's only needed for
# "full offline" demo mode
path/to/dart/dart/samples/swarm/cacheimages.py data/
</pre>
<li>Download a copy of swarm-js.html from this server (or create with update.py) and save it as: /Library/WebServer/Document/index.html</li>
<li>Try out your app at: <a href="http://localhost/">http://localhost/</a></li>
</ol>
</p>
<h2>Development tools</h2>
<ul>
<li><a href="https://appengine.google.com/dashboard?app_id=google.com:dart">
AppEngine dashboard</a></li>
</ul>
</body>
</html>

View file

@ -0,0 +1,60 @@
# 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.
'''
This Encoder shares a lot in common with protobufs. It uses variable length
ints and size-encoded strings and binary values. Other than being hugely
stripped down, the major conceptual difference is that this encoding
is UTF8 "safe". This means that it generates a form that should be passed
on the wire as UTF8 and then can be very efficiently decoded by JS in the
browser which natively handles these kinds of strings. To stay efficient in
this range, all numeric data is encoded in only 7 bits.
'''
import base64
class Encoder:
def __init__(self):
self.data = []
def writeInt(self, value):
'''Uses a 7-bit per byte encoding to stay UTF-8 "safe".'''
bits = value & 0x3f
value >>= 6
while value:
self.data.append(chr(0x40|bits))
bits = value & 0x3f
value >>= 6
self.data.append(chr(bits))
def writeBool(self, b):
self.data.append(('F', 'T')[b])
def writeString(self, s):
if not s: s = ''
self.writeInt(len(s))
self.data.append(s)
def writeBinary(self, s):
'''Encode binary data using base64. This is less efficient than a 7-bit
encoding would be; however, it can be decoded much faster on most
browsers due to native support for the format.'''
v = base64.b64encode(s)
self.writeInt(len(v))
self.data.append(v)
def writeList(self, l):
self.writeInt(len(l))
for i in l:
i.encode(self)
def writeRaw(self, s):
self.data.append(s)
def finish(self):
d = ''.join(self.data)
return _encVarInt(len(d)) + d
def getRaw(self):
return ''.join(self.data)

View file

@ -0,0 +1,17 @@
indexes:
# AUTOGENERATED
# This index.yaml is automatically updated whenever the dev_appserver
# detects that a new type of query is run. If you want to manage the
# index.yaml file manually, remove the above marker line (the line
# saying "# AUTOGENERATED"). If you want to manage some indexes
# manually, move them above the marker line. The index.yaml file is
# automatically uploaded to the admin console when you next deploy
# your application using appcfg.py.
- kind: Article
properties:
- name: feed
- name: date
direction: desc

View file

@ -0,0 +1,15 @@
<!-- 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. -->
<html>
<body>
<div>Please choose an option for setting up your feeds.</div>
<ul>
<li><a href="/update/defaultFeeds">Use the default set of feeds.</a></li>
<li><a href="/update/testFeeds">Use a really ugly set of test feeds.</a></li>
<li><a href="{{authorize}}">Connect to your Google Reader account to
get feeds.</a></li>
</ul>
</body>
</html>

View file

@ -0,0 +1,739 @@
# 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.
#!/usr/bin/env python
#
import re, base64, logging, pickle, httplib2, time, urlparse, urllib2, urllib, StringIO, gzip, zipfile
from google.appengine.ext import webapp, db
from google.appengine.api import taskqueue, urlfetch, memcache, images, users
from google.appengine.ext.webapp.util import login_required
from google.appengine.ext.webapp import template
from django.utils import simplejson as json
from django.utils.html import strip_tags
from oauth2client.appengine import CredentialsProperty
from oauth2client.client import OAuth2WebServerFlow
import encoder
# TODO(jimhug): Allow client to request desired thumb size.
THUMB_SIZE = (57, 57)
READER_API = 'http://www.google.com/reader/api/0'
MAX_SECTIONS = 5
MAX_ARTICLES = 20
class UserData(db.Model):
credentials = CredentialsProperty()
sections = db.ListProperty(db.Key)
def getEncodedData(self, articleKeys=None):
enc = encoder.Encoder()
# TODO(jimhug): Only return initially visible section in first reply.
maxSections = min(MAX_SECTIONS, len(self.sections))
enc.writeInt(maxSections)
for section in db.get(self.sections[:maxSections]):
section.encode(enc, articleKeys)
return enc.getRaw()
class Section(db.Model):
title = db.TextProperty()
feeds = db.ListProperty(db.Key)
def fixedTitle(self):
return self.title.split('_')[0]
def encode(self, enc, articleKeys=None):
# TODO(jimhug): Need to optimize format and support incremental updates.
enc.writeString(self.key().name())
enc.writeString(self.fixedTitle())
enc.writeInt(len(self.feeds))
for feed in db.get(self.feeds):
feed.ensureEncodedFeed()
enc.writeRaw(feed.encodedFeed3)
if articleKeys is not None:
articleKeys.extend(feed.topArticles)
class Feed(db.Model):
title = db.TextProperty()
iconUrl = db.TextProperty()
lastUpdated = db.IntegerProperty()
encodedFeed3 = db.TextProperty()
topArticles = db.ListProperty(db.Key)
def ensureEncodedFeed(self, force=False):
if force or self.encodedFeed3 is None:
enc = encoder.Encoder()
articleSet = []
self.encode(enc, MAX_ARTICLES, articleSet)
logging.info('articleSet length is %s' % len(articleSet))
self.topArticles = articleSet
self.encodedFeed3 = enc.getRaw()
self.put()
def encode(self, enc, maxArticles, articleSet):
enc.writeString(self.key().name())
enc.writeString(self.title)
enc.writeString(self.iconUrl)
logging.info('encoding feed: %s' % self.title)
encodedArts = []
for article in self.article_set.order('-date').fetch(limit=maxArticles):
encodedArts.append(article.encodeHeader())
articleSet.append(article.key())
enc.writeInt(len(encodedArts))
enc.writeRaw(''.join(encodedArts))
class Article(db.Model):
feed = db.ReferenceProperty(Feed)
title = db.TextProperty()
author = db.TextProperty()
content = db.TextProperty()
snippet = db.TextProperty()
thumbnail = db.BlobProperty()
thumbnailSize = db.TextProperty()
srcurl = db.TextProperty()
date = db.IntegerProperty()
def ensureThumbnail(self):
# If our desired thumbnail size has changed, regenerate it and cache.
if self.thumbnailSize != str(THUMB_SIZE):
self.thumbnail = makeThumbnail(self.content)
self.thumbnailSize = str(THUMB_SIZE)
self.put()
def encodeHeader(self):
# TODO(jmesserly): for now always unescape until the crawler catches up
enc = encoder.Encoder()
enc.writeString(self.key().name())
enc.writeString(unescape(self.title))
enc.writeString(self.srcurl)
enc.writeBool(self.thumbnail is not None)
enc.writeString(self.author)
enc.writeInt(self.date)
enc.writeString(unescape(self.snippet))
return enc.getRaw()
class HtmlFile(db.Model):
content = db.BlobProperty()
compressed = db.BooleanProperty()
filename = db.StringProperty()
author = db.UserProperty(auto_current_user=True)
date = db.DateTimeProperty(auto_now_add=True)
class UpdateHtml(webapp.RequestHandler):
def post(self):
upload_files = self.request.POST.multi.__dict__['_items']
version = self.request.get('version')
logging.info('files: %r' % upload_files)
for data in upload_files:
if data[0] != 'files': continue
file = data[1]
filename = file.filename
if version:
filename = '%s-%s' % (version, filename)
logging.info('upload: %r' % filename)
htmlFile = HtmlFile.get_or_insert(filename)
htmlFile.filename = filename
# If text > (1MB - 1KB) then gzip text to fit in 1MB space
text = file.value
if len(text) > 1024*1023:
data = StringIO.StringIO()
gz = gzip.GzipFile(str(filename), 'wb', fileobj=data)
gz.write(text)
gz.close()
htmlFile.content = data.getvalue()
htmlFile.compressed = True
else:
htmlFile.content = text
htmlFile.compressed = False
htmlFile.put()
self.redirect('/')
class TopHandler(webapp.RequestHandler):
@login_required
def get(self):
user = users.get_current_user()
prefs = UserData.get_by_key_name(user.user_id())
if prefs is None:
self.redirect('/update/user')
return
params = {'files': HtmlFile.all().order('-date').fetch(limit=30)}
self.response.out.write(template.render('top.html', params))
class MainHandler(webapp.RequestHandler):
@login_required
def get(self, name):
if name == 'dev':
return self.handleDev()
elif name == 'login':
return self.handleLogin()
elif name == 'upload':
return self.handleUpload()
user = users.get_current_user()
prefs = UserData.get_by_key_name(user.user_id())
if prefs is None:
return self.handleLogin()
html = HtmlFile.get_by_key_name(name)
if html is None:
self.error(404)
return
self.response.headers['Content-Type'] = 'text/html'
if html.compressed:
# TODO(jimhug): This slightly sucks ;-)
# Can we write directly to the response.out?
gz = gzip.GzipFile(name, 'rb', fileobj=StringIO.StringIO(html.content))
self.response.out.write(gz.read())
gz.close()
else:
self.response.out.write(html.content)
# TODO(jimhug): Include first data packet with html.
def handleLogin(self):
user = users.get_current_user()
# TODO(jimhug): Manage secrets for dart.googleplex.com better.
# TODO(jimhug): Confirm that we need client_secret.
flow = OAuth2WebServerFlow(
client_id='267793340506.apps.googleusercontent.com',
client_secret='5m8H-zyamfTYg5vnpYu1uGMU',
scope=READER_API,
user_agent='swarm')
callback = self.request.relative_url('/oauth2callback')
authorize_url = flow.step1_get_authorize_url(callback)
memcache.set(user.user_id(), pickle.dumps(flow))
content = template.render('login.html', {'authorize': authorize_url})
self.response.out.write(content)
def handleDev(self):
user = users.get_current_user()
content = template.render('dev.html', {'user': user})
self.response.out.write(content)
def handleUpload(self):
user = users.get_current_user()
content = template.render('upload.html', {'user': user})
self.response.out.write(content)
class UploadFeed(webapp.RequestHandler):
def post(self):
upload_files = self.request.POST.multi.__dict__['_items']
version = self.request.get('version')
logging.info('files: %r' % upload_files)
for data in upload_files:
if data[0] != 'files': continue
file = data[1]
logging.info('upload feed: %r' % file.filename)
data = json.loads(file.value)
feedId = file.filename
feed = Feed.get_or_insert(feedId)
# Find the section to add it to.
sectionTitle = data['section']
section = findSectionByTitle(sectionTitle)
if section != None:
if feed.key() in section.feeds:
logging.warn('Already contains feed %s, replacing' % feedId)
section.feeds.remove(feed.key())
# Add the feed to the section.
section.feeds.insert(0, feed.key())
section.put()
# Add the articles.
collectFeed(feed, data)
else:
logging.error('Could not find section %s to add the feed to' %
sectionTitle)
self.redirect('/')
# TODO(jimhug): Batch these up and request them more agressively.
class DataHandler(webapp.RequestHandler):
def get(self, name):
if name.endswith('.jpg'):
# Must be a thumbnail
key = urllib2.unquote(name[:-len('.jpg')])
article = Article.get_by_key_name(key)
self.response.headers['Content-Type'] = 'image/jpeg'
# cache images for 10 hours
self.response.headers['Cache-Control'] = 'public,max-age=36000'
article.ensureThumbnail()
self.response.out.write(article.thumbnail)
elif name.endswith('.html'):
# Must be article content
key = urllib2.unquote(name[:-len('.html')])
article = Article.get_by_key_name(key)
self.response.headers['Content-Type'] = 'text/html'
if article is None:
content = '<h2>Missing article</h2>'
else:
content = article.content
# cache article content for 10 hours
self.response.headers['Cache-Control'] = 'public,max-age=36000'
self.response.out.write(content)
elif name == 'user.data':
self.response.out.write(self.getUserData())
elif name == 'CannedData.dart':
self.canData()
elif name == 'CannedData.zip':
self.canDataZip()
else:
self.error(404)
def getUserData(self, articleKeys=None):
user = users.get_current_user()
user_id = user.user_id()
key = 'data_' + user_id
# need to flush memcache fairly frequently...
data = memcache.get(key)
if data is None:
prefs = UserData.get_or_insert(user_id)
if prefs is None:
# TODO(jimhug): Graceful failure for unknown users.
pass
data = prefs.getEncodedData(articleKeys)
# TODO(jimhug): memcache.set(key, data)
return data
def canData(self):
def makeDartSafe(data):
return repr(unicode(data))[1:].replace('$', '\\$')
lines = ['// TODO(jimhug): Work out correct copyright for this file.',
'class CannedData {']
user = users.get_current_user()
prefs = UserData.get_by_key_name(user.user_id())
articleKeys = []
data = prefs.getEncodedData(articleKeys)
lines.append(' static const Map<String,String> data = const {')
for article in db.get(articleKeys):
key = makeDartSafe(urllib.quote(article.key().name())+'.html')
lines.append(' %s:%s, ' % (key, makeDartSafe(article.content)))
lines.append(' "user.data":%s' % makeDartSafe(data))
lines.append(' };')
lines.append('}')
self.response.headers['Content-Type'] = 'application/dart'
self.response.out.write('\n'.join(lines))
# Get canned static data
def canDataZip(self):
# We need to zip into an in-memory buffer to get the right string encoding
# behavior.
data = StringIO.StringIO()
result = zipfile.ZipFile(data, 'w')
articleKeys = []
result.writestr('data/user.data',
self.getUserData(articleKeys).encode('utf-8'))
logging.info(' adding articles %s' % len(articleKeys))
images = []
for article in db.get(articleKeys):
article.ensureThumbnail()
path = 'data/' + article.key().name() + '.html'
result.writestr(path.encode('utf-8'), article.content.encode('utf-8'))
if article.thumbnail:
path = 'data/' + article.key().name() + '.jpg'
result.writestr(path.encode('utf-8'), article.thumbnail)
result.close()
logging.info('writing CannedData.zip')
self.response.headers['Content-Type'] = 'multipart/x-zip'
disposition = 'attachment; filename=CannedData.zip'
self.response.headers['Content-Disposition'] = disposition
self.response.out.write(data.getvalue())
data.close()
class SetDefaultFeeds(webapp.RequestHandler):
@login_required
def get(self):
user = users.get_current_user()
prefs = UserData.get_or_insert(user.user_id())
prefs.sections = [
db.Key.from_path('Section', 'user/17857667084667353155/label/Top'),
db.Key.from_path('Section', 'user/17857667084667353155/label/Design'),
db.Key.from_path('Section', 'user/17857667084667353155/label/Eco'),
db.Key.from_path('Section', 'user/17857667084667353155/label/Geek'),
db.Key.from_path('Section', 'user/17857667084667353155/label/Google'),
db.Key.from_path('Section', 'user/17857667084667353155/label/Seattle'),
db.Key.from_path('Section', 'user/17857667084667353155/label/Tech'),
db.Key.from_path('Section', 'user/17857667084667353155/label/Web')]
prefs.put()
self.redirect('/')
class SetTestFeeds(webapp.RequestHandler):
@login_required
def get(self):
user = users.get_current_user()
prefs = UserData.get_or_insert(user.user_id())
sections = []
for i in range(3):
s1 = Section.get_or_insert('Test%d' % i)
s1.title = 'Section %d' % (i+1)
feeds = []
for j in range(4):
label = '%d_%d' % (i, j)
f1 = Feed.get_or_insert('Test%s' % label)
f1.title = 'Feed %s' % label
f1.iconUrl = getFeedIcon('http://google.com')
f1.lastUpdated = 0
f1.put()
feeds.append(f1.key())
for k in range(8):
label = '%d_%d_%d' % (i, j, k)
a1 = Article.get_or_insert('Test%s' % label)
if a1.title is None:
a1.feed = f1
a1.title = 'Article %s' % label
a1.author = 'anon'
a1.content = 'Lorem ipsum something or other...'
a1.snippet = 'Lorem ipsum something or other...'
a1.thumbnail = None
a1.srcurl = ''
a1.date = 0
s1.feeds = feeds
s1.put()
sections.append(s1.key())
prefs.sections = sections
prefs.put()
self.redirect('/')
class UserLoginHandler(webapp.RequestHandler):
@login_required
def get(self):
user = users.get_current_user()
prefs = UserData.get_or_insert(user.user_id())
if prefs.credentials:
http = prefs.credentials.authorize(httplib2.Http())
response, content = http.request('%s/subscription/list?output=json' %
READER_API)
self.collectFeeds(prefs, content)
self.redirect('/')
else:
self.redirect('/login')
def collectFeeds(self, prefs, content):
data = json.loads(content)
queue_name = self.request.get('queue_name', 'priority-queue')
sections = {}
for feedData in data['subscriptions']:
feed = Feed.get_or_insert(feedData['id'])
feed.put()
category = feedData['categories'][0]
categoryId = category['id']
if not sections.has_key(categoryId):
sections[categoryId] = (category['label'], [])
# TODO(jimhug): Use Reader preferences to sort feeds in a section.
sections[categoryId][1].append(feed.key())
# Kick off a high priority feed update
taskqueue.add(url='/update/feed', queue_name=queue_name,
params={'id': feed.key().name()})
sectionKeys = []
for name, (title, feeds) in sections.items():
section = Section.get_or_insert(name)
section.feeds = feeds
section.title = title
section.put()
# Forces Top to be the first section
if title == 'Top': title = '0Top'
sectionKeys.append( (title, section.key()) )
# TODO(jimhug): Use Reader preferences API to get users true sort order.
prefs.sections = [key for t, key in sorted(sectionKeys)]
prefs.put()
class AllFeedsCollector(webapp.RequestHandler):
'''Ensures that a given feed object is locally up to date.'''
def post(self): return self.get()
def get(self):
queue_name = self.request.get('queue_name', 'background')
for feed in Feed.all():
taskqueue.add(url='/update/feed', queue_name=queue_name,
params={'id': feed.key().name()})
UPDATE_COUNT = 4 # The number of articles to request on periodic updates.
INITIAL_COUNT = 40 # The number of articles to get first for a new queue.
SNIPPET_SIZE = 180 # The length of plain-text snippet to extract.
class FeedCollector(webapp.RequestHandler):
def post(self): return self.get()
def get(self):
feedId = self.request.get('id')
feed = Feed.get_or_insert(feedId)
if feed.lastUpdated is None:
self.fetchn(feed, feedId, INITIAL_COUNT)
else:
self.fetchn(feed, feedId, UPDATE_COUNT)
self.response.headers['Content-Type'] = "text/plain"
def fetchn(self, feed, feedId, n, continuation=None):
# basic pattern is to read by ARTICLE_COUNT until we hit existing.
if continuation is None:
apiUrl = '%s/stream/contents/%s?n=%d' % (
READER_API, feedId, n)
else:
apiUrl = '%s/stream/contents/%s?n=%d&c=%s' % (
READER_API, feedId, n, continuation)
logging.info('fetching: %s' % apiUrl)
result = urlfetch.fetch(apiUrl)
if result.status_code == 200:
data = json.loads(result.content)
collectFeed(feed, data, continuation)
elif result.status_code == 401:
self.response.out.write( '<pre>%s</pre>' % result.content)
else:
self.response.out.write(result.status_code)
def findSectionByTitle(title):
for section in Section.all():
if section.fixedTitle() == title:
return section
return None
def collectFeed(feed, data, continuation=None):
'''
Reads a feed from the given JSON object and populates the given feed object
in the datastore with its data.
'''
if continuation is None:
if 'alternate' in data:
feed.iconUrl = getFeedIcon(data['alternate'][0]['href'])
feed.title = data['title']
feed.lastUpdated = data['updated']
articles = data['items']
logging.info('%d new articles for %s' % (len(articles), feed.title))
for articleData in articles:
if not collectArticle(feed, articleData):
feed.put()
return False
if len(articles) > 0 and data.has_key('continuation'):
logging.info('would have looked for more articles')
# TODO(jimhug): Enable this continuation check when more robust
#self.fetchn(feed, feedId, data['continuation'])
feed.ensureEncodedFeed(force=True)
feed.put()
return True
def collectArticle(feed, data):
'''
Reads an article from the given JSON object and populates the datastore with
it.
'''
if not 'title' in data:
# Skip this articles without titles
return True
articleId = data['id']
article = Article.get_or_insert(articleId)
# TODO(jimhug): This aborts too early - at lease for one adafruit case.
if article.date == data['published']:
logging.info('found existing, aborting: %r, %r' %
(articleId, article.date))
return False
if data.has_key('content'):
content = data['content']['content']
elif data.has_key('summary'):
content = data['summary']['content']
else:
content = ''
#TODO(jimhug): better summary?
article.content = content
article.date = data['published']
article.title = unescape(data['title'])
article.snippet = unescape(strip_tags(content)[:SNIPPET_SIZE])
article.feed = feed
# TODO(jimhug): make this canonical so UX can change for this state
article.author = data.get('author', 'anonymous')
article.ensureThumbnail()
article.srcurl = ''
if data.has_key('alternate'):
for alt in data['alternate']:
if alt.has_key('href'):
article.srcurl = alt['href']
return True
def unescape(html):
"Inverse of Django's utils.html.escape function"
if not isinstance(html, basestring):
html = str(html)
html = html.replace('&#39;', "'").replace('&quot;', '"')
return html.replace('&gt;', '>').replace('&lt;', '<').replace('&amp;', '&')
def getFeedIcon(url):
url = urlparse.urlparse(url).netloc
return 'http://s2.googleusercontent.com/s2/favicons?domain=%s&alt=feed' % url
def findImage(text):
img = findImgTag(text, 'jpg|jpeg|png')
if img is not None:
return img
img = findVideoTag(text)
if img is not None:
return img
img = findImgTag(text, 'gif')
return img
def findImgTag(text, extensions):
m = re.search(r'src="(http://\S+\.(%s))(\?.*)?"' % extensions, text)
if m is None:
return None
return m.group(1)
def findVideoTag(text):
# TODO(jimhug): Add other videos beyond youtube.
m = re.search(r'src="http://www.youtube.com/(\S+)/(\S+)[/|"]', text)
if m is None:
return None
return 'http://img.youtube.com/vi/%s/0.jpg' % m.group(2)
def makeThumbnail(text):
url = None
try:
url = findImage(text)
if url is None:
return None
return generateThumbnail(url)
except:
logging.info('error decoding: %s' % (url or text))
return None
def generateThumbnail(url):
logging.info('generating thumbnail: %s' % url)
thumbWidth, thumbHeight = THUMB_SIZE
result = urlfetch.fetch(url)
img = images.Image(result.content)
w, h = img.width, img.height
aspect = float(w) / h
thumbAspect = float(thumbWidth) / thumbHeight
if aspect > thumbAspect:
# Too wide, so crop on the sides.
normalizedCrop = (w - h * thumbAspect) / (2.0 * w)
img.crop(normalizedCrop, 0., 1. - normalizedCrop, 1. )
elif aspect < thumbAspect:
# Too tall, so crop out the bottom.
normalizedCrop = (h - w / thumbAspect) / h
img.crop(0., 0., 1., 1. - normalizedCrop)
img.resize(thumbWidth, thumbHeight)
# Chose JPEG encoding because informal experiments showed it generated
# the best size to quality ratio for thumbnail images.
nimg = img.execute_transforms(output_encoding=images.JPEG)
logging.info(' finished thumbnail: %s' % url)
return nimg
class OAuthHandler(webapp.RequestHandler):
@login_required
def get(self):
user = users.get_current_user()
flow = pickle.loads(memcache.get(user.user_id()))
if flow:
prefs = UserData.get_or_insert(user.user_id())
prefs.credentials = flow.step2_exchange(self.request.params)
prefs.put()
self.redirect('/update/user')
else:
pass
def main():
application = webapp.WSGIApplication(
[
('/data/(.*)', DataHandler),
# This is called periodically from cron.yaml.
('/update/allFeeds', AllFeedsCollector),
('/update/feed', FeedCollector),
('/update/user', UserLoginHandler),
('/update/defaultFeeds', SetDefaultFeeds),
('/update/testFeeds', SetTestFeeds),
('/update/html', UpdateHtml),
('/update/upload', UploadFeed),
('/oauth2callback', OAuthHandler),
('/', TopHandler),
('/(.*)', MainHandler),
],
debug=True)
webapp.util.run_wsgi_app(application)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,12 @@
# 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.
queue:
- name: priority-queue
rate: 50/s
bucket_size: 50
- name: background
rate: 5/s
bucket_size: 20

View file

@ -0,0 +1,17 @@
<!-- 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(jimhug): Care about appearance. -->
<html>
<body>
<h1>Run the official app <a href='swarm-js.html'>swarm-js.html</a></h1>
<h2>Or pick an experimental version from below</h2>
{% for file in files %}
<div><a href="{{file.filename}}">{{file.filename}}</a>
uploaded on {{file.date}} by {{file.author.nickname}}</div>
{% endfor %}
<p>If you want to upload your own fake feed, click <a href="/upload">here</a>.
</body>
</html>

View file

@ -0,0 +1,72 @@
<!-- 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. -->
<html>
<body>
<h1>Upload New Fake Feed</h1>
<p>This magnificent administration interface is your gateway to an exciting
career in uploading hand-curated fake news feeds into this server. You can
then view them directly on the server, or download them back down in
long-tasting and great-tasting CannedData form.</p>
<ol>
<li>Make a fake feed. This is a JSON file.</li>
<li>Go here. It seems you've already completed this step.</li>
<li>Click the "JSON File" button and select your local file.</li>
<li>Click "Submit". It is not necessary to kneel beforehand, but it may
help. Your call.</li>
<li>You're done!</li>
</ol>
<p>Here's a sample of what the JSON file should look like:</p>
<pre>
{
"section":"awesome",
"id":"feed/http://www.example.com/feed/",
"title":"Example Feed",
"alternate":[{"href":"http://www.example.com"}],
"updated":1307338620,
"items":[
{
"id":"fake/example.com/1",
"title":"I am the first post!",
"published":1307337590,
"alternate":[{"href":"http://www.example.com/first"}],
"content":{
"content":"This is the text of the first post."
},
"author":"Fred Example"
},
{
"id":"fake/example.com/2",
"title":"I am the second post!",
"published":1307336590,
"alternate":[{"href":"http://www.example.com/second"}],
"content":{
"content":"This is the text of the second post."
},
"author":"Fred Example"
},
{
"id":"fake/example.com/3",
"title":"I am the third post!",
"published":1307335590,
"alternate":[{"href":"http://www.example.com/third"}],
"content":{
"content":"This is the text of the third post."
},
"author":"Fred Example"
}
]
}
</pre>
<form action="/update/upload" method="POST" enctype="multipart/form-data">
JSON File: <input type="file" multiple="" name="files"><br>
<input type="submit" name="submit" value="Submit">
</form>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

80
samples-dev/swarm/buildapp.py Executable file
View file

@ -0,0 +1,80 @@
# 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.
#!/usr/bin/env python
#
# This script builds a Chrome App file (.crx) for Swarm
import os
import platform
import subprocess
import sys
DART_PATH = os.path.normpath(os.path.dirname(__file__) + '/../../..')
CLIENT_PATH = os.path.normpath(DART_PATH + '/client')
# Add the tools directory so we can find utils.py.
sys.path.append(os.path.abspath(DART_PATH + '/tools'))
import utils
buildRoot = CLIENT_PATH + '/' + utils.GetBuildRoot(
utils.GuessOS(), 'debug', 'dartc')
def execute(*command):
'''
Executes the given command in a new process. If the command fails (returns
non-zero) halts the script and returns that exit code.
'''
exitcode = subprocess.call(command)
if exitcode != 0:
sys.exit(exitcode)
def createChromeApp(buildRoot, antTarget, resultFile):
buildDir = os.path.join(buildRoot, 'war')
# Use ant to create the 'war' directory
# TODO(jmesserly): we should factor out as much as possible from the ant file
# It's not really doing anything useful for us besides compiling Dart code
# with DartC and copying files. But for now, it helps us share code with
# our appengine update.py, which is good.
execute(
DART_PATH + '/third_party/apache_ant/v1_7_1/bin/ant',
'-f', 'build-appengine.xml',
'-Dbuild.dir=' + buildRoot,
antTarget)
# Call Dartium (could be any Chrome--but we know Dartium will be there) and
# ask it to create the .crx file for us using the checked in developer key.
chrome = CLIENT_PATH + '/tests/drt/chrome'
# On Mac Chrome is under a .app folder
if platform.system() == 'Darwin':
chrome = CLIENT_PATH + '/tests/drt/Chromium.app/Contents/MacOS/Chromium'
keyFile = DART_PATH + '/samples/swarm/swarm-dev.pem'
execute(chrome, '--pack-extension=' + buildDir,
'--pack-extension-key=' + keyFile)
resultFile = os.path.join(buildRoot, resultFile)
os.rename(buildDir + '.crx', resultFile)
return os.path.abspath(resultFile)
def main():
# Create a DartC and Dartium app
dartiumResult = createChromeApp(buildRoot, 'build_dart_app', 'swarm.crx')
dartCResult = createChromeApp(buildRoot, 'build_js_app', 'swarm-js.crx')
print '''
Successfully created Chrome apps!
Dartium: file://%s
DartC/JS: file://%s
To install, open this URL in Chrome and select Continue at the bottom.
''' % (dartiumResult, dartCResult)
return 0
if __name__ == '__main__':
sys.exit(main())

View file

@ -0,0 +1,70 @@
#!/usr/bin/env python
# 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.
'''
This script finds all HTML pages in a folder and downloads all images, replacing
the urls with local ones.
'''
import os, sys, optparse, subprocess, multiprocessing
from os.path import abspath, basename, dirname, join
SWARM_PATH = dirname(abspath(__file__))
CLIENT_PATH = dirname(dirname(SWARM_PATH))
CLIENT_TOOLS_PATH = join(CLIENT_PATH, 'tools')
# Add the client tools directory so we can find htmlconverter.py.
sys.path.append(CLIENT_TOOLS_PATH)
import htmlconverter
converter = CLIENT_TOOLS_PATH + '/htmlconverter.py'
# This has to be a top level function to use with multiprocessing
def convertImgs(infile):
global options
try:
htmlconverter.convertForOffline(
infile, infile,
verbose=options.verbose,
encode_images=options.inline_images)
print 'Converted ' + infile
except BaseException, e:
print 'Caught error: %s' % e
def Flags():
""" Constructs a parser for extracting flags from the command line. """
parser = optparse.OptionParser()
parser.add_option("--inline_images",
help=("Encode img payloads as data:// URLs rather than local files."),
default=False,
action='store_true')
parser.add_option("--verbose",
help="Print verbose output",
default=False,
action="store_true")
return parser
def main():
global options
parser = Flags()
options, args = parser.parse_args()
print "args: %s" % args
if len(args) < 1 or 'help' in args[0]:
print 'Usage: %s DIRECTORY' % basename(sys.argv[0])
return 1
dirname = args[0]
print 'Searching directory ' + dirname
files = []
for root, dirs, fnames in os.walk(dirname):
for fname in fnames:
if fname.endswith('.html'):
files.append(join(root, fname))
count = 4 * multiprocessing.cpu_count()
pool = multiprocessing.Pool(processes=count)
# Note: need a timeout to get keyboard interrupt due to a Python bug
pool.map_async(convertImgs, files).get(3600) # one hour
if __name__ == '__main__':
main()

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

View file

@ -0,0 +1 @@
Lorem ipsum something or other...

Some files were not shown because too many files have changed in this diff Show more