| Index: chrome/common/extensions/docs/templates/articles/angular_framework.html
|
| diff --git a/chrome/common/extensions/docs/templates/articles/angular_framework.html b/chrome/common/extensions/docs/templates/articles/angular_framework.html
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..46495f7aefe929544495156f14d93eed4eb47054
|
| --- /dev/null
|
| +++ b/chrome/common/extensions/docs/templates/articles/angular_framework.html
|
| @@ -0,0 +1,771 @@
|
| +<meta name="doc-family" content="apps">
|
| +<h1>Build Apps with AngularJS</h1>
|
| +<!--Article written by Eric Bidelman-->
|
| +<p>
|
| +This guide gets you started building packaged apps
|
| +with the <a href="http://angularjs.org/">AngularJS</a> MVC framework.
|
| +To illustrate Angular in action,
|
| +we'll be referencing an actual app built using the framework,
|
| +the Google Drive Uploader.
|
| +The <a href="https://github.com/GoogleChrome/chrome-app-samples/tree/master/gdocs">source code</a>
|
| +is available on GitHub.
|
| +</p>
|
| +
|
| +<h2 id="first">About the app</h2>
|
| +
|
| +<img src="{{static}}/images/uploader.png"
|
| + width="296"
|
| + height="347"
|
| + style="float: right; padding-left: 5px"
|
| + alt="Google Drive Uploader">
|
| +
|
| +<p>
|
| +The Google Drive Uploader allows users to quickly view and interact
|
| +with files stored in their Google Drive account
|
| +as well as upload new files using the
|
| +<a href="http://www.html5rocks.com/en/tutorials/dnd/basics/">HTML Drag and Drop APIs</a>.
|
| +It's a great example of building an app which talks
|
| +to one of <a href="https://developers.google.com/apis-explorer/#p/">Google's APIs</a>;
|
| +in this case, the Google Drive API.
|
| +</p>
|
| +
|
| +<p class="note">
|
| +<strong>Note: </strong>
|
| +You can also build apps which talk to 3rd party APIs/services
|
| +that are OAuth2-enabled.
|
| +See <a href="http://developer.chrome.com/trunk/apps/app_identity.html#non">non-Google Account authentication</a>.
|
| +</p>
|
| +
|
| +<p>
|
| +The Uploader uses OAuth2 to access the user's data. The
|
| +<a href="http://developer.chrome.com/trunk/apps/experimental.identity.html">chrome.experimental.identity API</a>
|
| +handles fetching an OAuth token for the logged-in user,
|
| +so the hard work is done for us!
|
| +Once we have a long-lived access token,
|
| +the apps uses the
|
| +<a href="https://developers.google.com/drive/get-started">Google Drive API</a>
|
| +to access the user's data.
|
| +</p>
|
| +
|
| +<p>
|
| +Key features this app uses:
|
| +</p>
|
| +
|
| +<ul>
|
| + <li>Angular JS's autodetection for
|
| + <a href="http://developer.chrome.com/trunk/apps/app_csp.html">CSP</a></li>
|
| + <li>Render a list of files fetched from the
|
| + <a href="https://developers.google.com/drive/get-started">Google Drive API</a></li>
|
| + <li><a href="http://www.html5rocks.com/en/tutorials/file/filesystem/">HTML5 Filesystem API</a>
|
| + to store file icons offline</li>
|
| + <li><a href="http://www.html5rocks.com/en/tutorials/dnd/basics/">HTML5 Drag and Drop</a>
|
| + for importing/uploading new files from the desktop</li>
|
| + <li>XHR2 to load images, cross-domain</li>
|
| + <li><a href="http://developer.chrome.com/trunk/apps/app_identity.html">chrome.experimental.identity API</a>
|
| + for OAuth authorization</li>
|
| + <li>Chromeless frames to define the app's own navbar look and feel</li>
|
| +</ul>
|
| +
|
| +<h2 id="second">Creating the manifest</h2>
|
| +
|
| +<p>
|
| +All packaged apps require a <code>manifest.json</code> file
|
| +which contains the information Chrome needs to launch the app.
|
| +The manifest contains relevant metadata and
|
| +lists any special permissions the app needs to run.
|
| +</p>
|
| +
|
| +<p>
|
| +A stripped down version of the Uploader's manifest looks like this:
|
| +</p>
|
| +
|
| +<pre>
|
| +{
|
| + "name": "Google Drive Uploader",
|
| + "version": "0.0.1",
|
| + "manifest_version": 2,
|
| + "oauth2": {
|
| + "client_id": "665859454684.apps.googleusercontent.com",
|
| + "scopes": [
|
| + "https://docs.google.com/feeds/",
|
| + "https://docs.googleusercontent.com/",
|
| + "https://spreadsheets.google.com/feeds/",
|
| + "https://www.googleapis.com/auth/drive"
|
| + ]
|
| + },
|
| + ...
|
| + "permissions": [
|
| + "experimental",
|
| + "https://docs.google.com/feeds/",
|
| + "https://docs.googleusercontent.com/",
|
| + "https://spreadsheets.google.com/feeds/",
|
| + "https://ssl.gstatic.com/",
|
| + "https://www.googleapis.com/"
|
| + ]
|
| +}
|
| +</pre>
|
| +
|
| +<p>
|
| +The most important parts of this manifest are the "oauth2" and "permissions" sections.
|
| +</p>
|
| +
|
| +<p>
|
| +The "oauth2" section defines the required parameters by OAuth2 to do its magic.
|
| +To create a "client_id", follow the instructions in
|
| +<a href="http://developer.chrome.com/apps/app_identity.html#client_id">Get your client id</a>.
|
| +The "scopes" list the authorization scopes
|
| +that the OAuth token will be valid for (for example, the APIs the app wants to access).
|
| +</p>
|
| +
|
| +<p class="note">
|
| +<strong>Note: </strong>
|
| +The Uploader actually uses the
|
| +<a href="https://developers.google.com/google-apps/documents-list/">Documents List API v3</a>
|
| +to access Google Drive content,
|
| +hence needing the first several scopes to access all the different types of files.
|
| +However, when using the Google Drive API, you only need
|
| +<a href="https://www.googleapis.com/auth/drive">"https://www.googleapis.com/auth/drive"</a>.
|
| +</p>
|
| +
|
| +<p>
|
| +The "permissions" section includes the "experimental" bit
|
| +(necessary because the current chrome.identity API is still experimental)
|
| +and additional URLs that the app will access via XHR2.
|
| +The URL prefixes are required in order for Chrome
|
| +to know which cross-domain requests to allow.
|
| +</p>
|
| +
|
| +<h2 id="three">Creating the event page</h2>
|
| +
|
| +<p>
|
| +All packaged apps require a background script/page
|
| +to launch the app and respond to system events.
|
| +</p>
|
| +
|
| +<p>
|
| +In its
|
| +<a href="https://github.com/GoogleChrome/chrome-app-samples/blob/master/gdocs/js/background.js">background.js</a>
|
| +script,
|
| +Drive Uploader opens a 500x600px window to the main page.
|
| +It also specifies a minimum height and width for the window
|
| +so the content doesn't become too crunched:
|
| +</p>
|
| +
|
| +<pre>
|
| +chrome.app.runtime.onLaunched.addListener(function(launchData) {
|
| + chrome.app.window.create('../main.html', {
|
| + width: 500,
|
| + height: 600,
|
| + minWidth: 500,
|
| + minHeight: 600,
|
| + frame: 'none'
|
| + });
|
| +});
|
| +</pre>
|
| +
|
| +<p>
|
| +The window is created as a chromeless window (frame: 'none').
|
| +By default, windows render with the OS's default close/expand/minimize bar:
|
| +</p>
|
| +
|
| +<img src="{{static}}/images/noframe.png"
|
| + width="508"
|
| + height="75"
|
| + alt="Google Drive Uploader with no frame">
|
| +
|
| +<p>
|
| +The Uploader uses <code>frame: 'none'</code> to render the window as a "blank slate"
|
| +and creates a custom close button in <code>main.html</code>:
|
| +</p>
|
| +
|
| +<img src="{{static}}/images/customframe.png"
|
| + width="504"
|
| + height="50"
|
| + alt="Google Drive Uploader with custom frame">
|
| +
|
| +<p>
|
| +The entire navigational area is wrapped in a <nav> (see next section).
|
| +To declutter the app a bit,
|
| +the custom close button is hidden until the user interacts with this the area:
|
| +</p>
|
| +
|
| +<pre>
|
| +<style>
|
| +nav:hover #close-button {
|
| + opacity: 1;
|
| +}
|
| +
|
| +#close-button {
|
| + float: right;
|
| + padding: 0 5px 2px 5px;
|
| + font-weight: bold;
|
| + opacity: 0;
|
| + -webkit-transition: all 0.3s ease-in-out;
|
| +}
|
| +</style>
|
| +
|
| +<button class="btn" id="close-button" title="Close">x</button>
|
| +</pre>
|
| +
|
| +<p>
|
| +In
|
| +<a href="https://github.com/GoogleChrome/chrome-app-samples/blob/master/gdocs/js/app.js">app.js</a>,
|
| +this button is hooked up to <code>window.close()</code>.
|
| +</p>
|
| +
|
| +<h2 id="four">Designing the app the Angular way</h2>
|
| +
|
| +<p>
|
| +Angular is an MVC framework, so we need to define the app in such a way that a
|
| +model, view, and controller logically fall out of it. Luckily, this is trivial when using Angular.
|
| +</p>
|
| +
|
| +<p>
|
| +The View is the easiest, so let's start there.
|
| +</p>
|
| +
|
| +<h3 id="view">Creating the view</h3>
|
| +
|
| +<p>
|
| +<a href="https://github.com/GoogleChrome/chrome-app-samples/blob/master/gdocs/main.html">main.html</a>
|
| +is the "V" in MVC; where we define HTML templates to render data into.
|
| +In Angular, templates are simple blocks of HTML with some special sauce.
|
| +</p>
|
| +
|
| +<p>
|
| +Ultimately we want to display the user's list of files.
|
| +For that, a simple <ul> list makes sense.
|
| +The Angular bits are highlighted in bold:
|
| +</p>
|
| +
|
| +<pre>
|
| +<ul>
|
| + <li <strong>data-ng-repeat="doc in docs"</strong>>
|
| + <img data-ng-src=<strong>"{{doc.icon}}"</strong>> <a href=<strong>"{{doc.alternateLink}}"</strong>><strong>{{doc.title}}</strong></a>
|
| +<strong>{{doc.size}}</strong>
|
| + <span class="date"><strong>{{doc.updatedDate}}</strong></span>
|
| + </li>
|
| +</ul>
|
| +</pre>
|
| +
|
| +<p>
|
| +This reads exactly as it looks:
|
| +stamp out an <li> for every doc in our data model "docs".
|
| +Each item contains a file icon, link to open the file on the web,
|
| +and last updatedDate.
|
| +</p>
|
| +
|
| +<p class="note">
|
| +<strong>Note: </strong>
|
| +To make the template valid HTML,
|
| +we're using <code>data-*</code> attributes for Angular's
|
| +<a href="http://docs.angularjs.org/api/ng.directive:ngRepeat">nRepeat</a> iterator,
|
| +but you don't have to.
|
| +You could easily write the repeater as <code><li ng-repeat="doc in docs"></code>.
|
| +</p>
|
| +
|
| +<p>
|
| +Next, we need to tell Angular which controller will oversee this template's rendering.
|
| +For that, we use the
|
| +<a href="http://docs.angularjs.org/api/ng.directive:ngController">ngController</a>
|
| +directive to tell the <code>DocsController</code> to have reign over the template <body>:
|
| +</p>
|
| +
|
| +<pre>
|
| +<body <strong>data-ng-controller="DocsController"</strong>>
|
| +<section id="main">
|
| + <ul>
|
| + <li data-ng-repeat="doc in docs">
|
| + <img data-ng-src="{{doc.icon}}"> <a href="{{doc.alternateLink}}">{{doc.title}}</a> {{doc.size}}
|
| + <span class="date">{{doc.updatedDate}}</span>
|
| + </li>
|
| + </ul>
|
| +</section>
|
| +</body>
|
| +</pre>
|
| +
|
| +<p>
|
| +Keep in mind,
|
| +what you don't see here is us hooking up event listeners or properties for data binding.
|
| +Angular is doing that heavy lifting for us!
|
| +</p>
|
| +
|
| +<p>
|
| +The last step is to make Angular light up our templates.
|
| +The typical way to do that is include the
|
| +<a href="http://docs.angularjs.org/api/ng.directive:ngApp">ngApp</a>
|
| +directive all the way up on <html>:
|
| +</p>
|
| +
|
| +<pre>
|
| +<html <strong>data-ng-app="gDriveApp"</strong>>
|
| +</pre>
|
| +
|
| +<p>
|
| +You could also scope the app down
|
| +to a smaller portion of the page if you wanted to.
|
| +We only have one controller in this app,
|
| +but if we were to add more later,
|
| +putting <a href="http://docs.angularjs.org/api/ng.directive:ngApp">ngApp</a>
|
| +on the topmost element makes the entire page Angular-ready.
|
| +</p>
|
| +
|
| +<p>
|
| +The final product for <code>main.html</code> looks something like this:
|
| +</p>
|
| +
|
| +<pre>
|
| +<html <strong>data-ng-app="gDriveApp"</strong>>
|
| +<head>
|
| + …
|
| + <!-- crbug.com/120693: so we don't need target="_blank" on every anchor. -->
|
| + <base target="_blank">
|
| +</head>
|
| +<body <strong>data-ng-controller="DocsController"</strong>>
|
| +<section id="main">
|
| + <nav>
|
| + <h2>Google Drive Uploader</h2>
|
| + <button class="btn" <strong>data-ng-click="fetchDocs()"</strong>>Refresh</button>
|
| + <button class="btn" id="close-button" title="Close"></button>
|
| + </nav>
|
| + <ul>
|
| + <li <strong>data-ng-repeat="doc in docs"</strong>>
|
| + <img data-ng-src=<strong>"{{doc.icon}}"</strong>> <a href=<strong>"{{doc.alternateLink}}"</strong>><strong>{{doc.title}}</strong></a> <strong>{{doc.size}}</strong>
|
| + <span class="date"><strong>{{doc.updatedDate}}</strong></span>
|
| + </li>
|
| + </ul>
|
| +</section>
|
| +</pre>
|
| +
|
| +<h3 id="csp">A word on Content Security Policy</h3>
|
| +
|
| +<p>
|
| +Unlike many other JS MVC frameworks,
|
| +Angular v1.1.0+ requires no tweaks to work within a strict
|
| +<a href="http://developer.chrome.com/trunk/apps/app_csp.html">CSP</a>.
|
| +It just works, out of the box!
|
| +</p>
|
| +
|
| +<p>
|
| +However, if you're using an older version
|
| +of Angular between v1.0.1 and v1.1.0,
|
| +you'll need tell Angular to run in a "content security mode".
|
| +This is done by including the
|
| +<a href="http://docs.angularjs.org/api/ng.directive:ngCsp">ngCsp</a>
|
| +directive alongside <a href="http://docs.angularjs.org/api/ng.directive:ngApp">ngApp</a>:
|
| +</p>
|
| +
|
| +<pre>
|
| +<html data-ng-app data-ng-csp>
|
| +</pre>
|
| +
|
| +<h3 id="authorization">Handling authorization</h3>
|
| +
|
| +<p>
|
| +The data model isn't generated by the app itself.
|
| +Instead, it's populated from an external API (the Google Drive API).
|
| +Thus, there's a bit of work necessary in order to populate the app's data.
|
| +</p>
|
| +
|
| +<p>
|
| +Before we can make an API request,
|
| +we need to fetch an OAuth token for the user's Google Account.
|
| +For that, we've created a method to wrap the call
|
| +to <code>chrome.experimental.identity.getAuthToken()</code> and
|
| +store the <code>accessToken</code>,
|
| +which we can reuse for future calls to the Drive API.
|
| +</p>
|
| +
|
| +<pre>
|
| +GDocs.prototype.auth = function(opt_callback) {
|
| + try {
|
| + <strong>chrome.experimental.identity.getAuthToken({interactive: false}, function(token) {</strong>
|
| + if (token) {
|
| + this.accessToken = token;
|
| + opt_callback && opt_callback();
|
| + }
|
| + }.bind(this));
|
| + } catch(e) {
|
| + console.log(e);
|
| + }
|
| +};
|
| +</pre>
|
| +
|
| +<p class="note">
|
| +<strong>Note: </strong>
|
| +Passing the optional callback gives us the flexibility
|
| +of knowing when the OAuth token is ready.
|
| +</p>
|
| +
|
| +<p class="note">
|
| +<strong>Note: </strong>
|
| +To simplify things a bit,
|
| +we've created a library,
|
| +<a href="https://github.com/GoogleChrome/chrome-app-samples/blob/master/gdocs/js/gdocs.js">gdocs.js</a>
|
| +to handle API tasks.
|
| +</p>
|
| +
|
| +<p>
|
| +Once we have the token,
|
| +it's time to make requests against the Drive API and populate the model.
|
| +</p>
|
| +
|
| +<h3 id="skeleton">Skeleton controller</h3>
|
| +
|
| +<p>
|
| +The "model" for the Uploader is a simple array (called docs)
|
| +of objects that will get rendered as those <li>s in the template:
|
| +</p>
|
| +
|
| +<pre>
|
| +var gDriveApp = angular.module('gDriveApp', []);
|
| +
|
| +gDriveApp.factory('gdocs', function() {
|
| + var gdocs = new GDocs();
|
| + return gdocs;
|
| +});
|
| +
|
| +function DocsController($scope, $http, gdocs) {
|
| + $scope.docs = [];
|
| +
|
| + $scope.fetchDocs = function() {
|
| + ...
|
| + };
|
| +
|
| + // Invoke on ctor call. Fetch docs after we have the oauth token.
|
| + gdocs.auth(function() {
|
| + $scope.fetchDocs();
|
| + });
|
| +
|
| +}
|
| +</pre>
|
| +
|
| +<p>
|
| +Notice that <code>gdocs.auth()</code> is called
|
| +as part of the DocsController constructor.
|
| +When Angular's internals create the controller,
|
| +we're insured to have a fresh OAuth token waiting for the user.
|
| +</p>
|
| +
|
| +<h2 id="five">Fetching data</h2>
|
| +
|
| +<p>
|
| +Template laid out.
|
| +Controller scaffolded.
|
| +OAuth token in hand.
|
| +Now what?
|
| +</p>
|
| +
|
| +<p>
|
| +It's time to define the main controller method,
|
| +<code>fetchDocs()</code>.
|
| +It's the workhorse of the controller,
|
| +responsible for requesting the user's files and
|
| +filing the docs array with data from API responses.
|
| +</p>
|
| +
|
| +<pre>
|
| +$scope.fetchDocs = function() {
|
| + $scope.docs = []; // First, clear out any old results
|
| +
|
| + // Response handler that doesn't cache file icons.
|
| + var successCallback = function(resp, status, headers, config) {
|
| + var docs = [];
|
| + var totalEntries = resp.feed.entry.length;
|
| +
|
| + resp.feed.entry.forEach(function(entry, i) {
|
| + var doc = {
|
| + title: entry.title.$t,
|
| + updatedDate: Util.formatDate(entry.updated.$t),
|
| + updatedDateFull: entry.updated.$t,
|
| + icon: gdocs.getLink(entry.link,
|
| + 'http://schemas.google.com/docs/2007#icon').href,
|
| + alternateLink: gdocs.getLink(entry.link, 'alternate').href,
|
| + size: entry.docs$size ? '( ' + entry.docs$size.$t + ' bytes)' : null
|
| + };
|
| +
|
| + $scope.docs.push(doc);
|
| +
|
| + // Only sort when last entry is seen.
|
| + if (totalEntries - 1 == i) {
|
| + $scope.docs.sort(Util.sortByDate);
|
| + }
|
| + });
|
| + };
|
| +
|
| + var config = {
|
| + params: {'alt': 'json'},
|
| + headers: {
|
| + 'Authorization': 'Bearer ' + gdocs.accessToken,
|
| + 'GData-Version': '3.0'
|
| + }
|
| + };
|
| +
|
| + $http.get(gdocs.DOCLIST_FEED, config).success(successCallback);
|
| +};
|
| +</pre>
|
| +
|
| +<p>
|
| +<code>fetchDocs()</code> uses Angular's <code>$http</code> service
|
| +to retrieve the main feed over XHR.
|
| +The oauth access token is included
|
| +in the <code>Authorization</code> header
|
| +along with other custom headers and parameters.
|
| +</p>
|
| +
|
| +<p>
|
| +The <code>successCallback</code> processes the API response and
|
| +creates a new doc object for each entry in the feed.
|
| +</p>
|
| +
|
| +<p>
|
| +If you run <code>fetchDocs()</code> right now,
|
| +everything works and the list of files shows up:
|
| +</p>
|
| +
|
| +<img src="{{static}}/images/listoffiles.png"
|
| + width="580"
|
| + height="680"
|
| + alt="Fetched list of files in Google Drive Uploader">
|
| +
|
| +<p>
|
| +Woot!
|
| +</p>
|
| +
|
| +<p>
|
| +Wait,...we're missing those neat file icons.
|
| +What gives?
|
| +A quick check of the console shows a bunch of CSP-related errors:
|
| +</p>
|
| +
|
| +<img src="{{static}}/images/csperrors.png"
|
| + width="947"
|
| + height="84"
|
| + alt="CSP errors in developer console">
|
| +
|
| +<p>
|
| +The reason is that we're trying
|
| +to set the icons <code>img.src</code> to external URLs.
|
| +This violates CSP.
|
| +For example:
|
| +<code>https://ssl.gstatic.com/docs/doclist/images/icon_10_document_list.png</code>.
|
| +To fix this,
|
| +we need to pull in these remote assets locally to the app.
|
| +</p>
|
| +
|
| +<h3 id="import">Importing remote image assets</h3>
|
| +
|
| +<p>
|
| +For CSP to stop yelling at us,
|
| +we use XHR2 to "import" the file icons as Blobs,
|
| +then set the <code>img.src</code>
|
| +to a <code>blob: URL</code> created by the app.
|
| +</p>
|
| +
|
| +<p>
|
| +Here's the updated <code>successCallback</code>
|
| +with the added XHR code:
|
| +</p>
|
| +
|
| +<pre>
|
| +var successCallback = function(resp, status, headers, config) {
|
| + var docs = [];
|
| + var totalEntries = resp.feed.entry.length;
|
| +
|
| + resp.feed.entry.forEach(function(entry, i) {
|
| + var doc = {
|
| + ...
|
| + };
|
| +
|
| + <strong>$http.get(doc.icon, {responseType: 'blob'}).success(function(blob) {
|
| + console.log('Fetched icon via XHR');
|
| +
|
| + blob.name = doc.iconFilename; // Add icon filename to blob.
|
| +
|
| + writeFile(blob); // Write is async, but that's ok.
|
| +
|
| + doc.icon = window.URL.createObjectURL(blob);
|
| +
|
| + $scope.docs.push(doc);
|
| +
|
| + // Only sort when last entry is seen.
|
| + if (totalEntries - 1 == i) {
|
| + $scope.docs.sort(Util.sortByDate);
|
| + }
|
| + });</strong>
|
| + });
|
| +};
|
| +</pre>
|
| +
|
| +<p>
|
| +Now that CSP is happy with us again,
|
| +we get nice file icons:
|
| +</p>
|
| +
|
| +<img src="{{static}}/images/fileicons.png"
|
| + width="580"
|
| + height="680"
|
| + alt="Google Drive Uploader with file icons">
|
| +
|
| +<h2 id="six">Going offline: caching external resources</h2>
|
| +
|
| +<p>
|
| +The obvious optimization that needs to be made:
|
| +not make 100s of XHR requests for each file icon
|
| +on every call to <code>fetchDocs()</code>.
|
| +Verify this in the Developer Tools console
|
| +by pressing the "Refresh" button several times.
|
| +Every time, n images are fetched:
|
| +</p>
|
| +
|
| +<img src="{{static}}/images/fetchedicon.png"
|
| + width="118"
|
| + height="19"
|
| + alt="Console log 65: Fetched icon via XHR">
|
| +
|
| +<p>
|
| +Let's modify <code>successCallback</code>
|
| +to add a caching layer.
|
| +The additions are highlighted in bold:
|
| +</p>
|
| +
|
| +<pre>
|
| +$scope.fetchDocs = function() {
|
| + ...
|
| +
|
| + // Response handler that caches file icons in the filesystem API.
|
| + var successCallbackWithFsCaching = function(resp, status, headers, config) {
|
| + var docs = [];
|
| + var totalEntries = resp.feed.entry.length;
|
| +
|
| + resp.feed.entry.forEach(function(entry, i) {
|
| + var doc = {
|
| + ...
|
| + };
|
| +
|
| + <strong>// 'https://ssl.gstatic.com/doc_icon_128.png' -> 'doc_icon_128.png'
|
| + doc.iconFilename = doc.icon.substring(doc.icon.lastIndexOf('/') + 1);</strong>
|
| +
|
| + // If file exists, it we'll get back a FileEntry for the filesystem URL.
|
| + // Otherwise, the error callback will fire and we need to XHR it in and
|
| + // write it to the FS.
|
| + <strong>var fsURL = fs.root.toURL() + FOLDERNAME + '/' + doc.iconFilename;
|
| + window.webkitResolveLocalFileSystemURL(fsURL, function(entry) {
|
| + doc.icon = entry.toURL(); // should be === to fsURL, but whatevs.</strong>
|
| +
|
| + $scope.docs.push(doc); // add doc to model.
|
| +
|
| + // Only want to sort and call $apply() when we have all entries.
|
| + if (totalEntries - 1 == i) {
|
| + $scope.docs.sort(Util.sortByDate);
|
| + $scope.$apply(function($scope) {}); // Inform angular that we made changes.
|
| + }
|
| +
|
| + <strong>}, function(e) {
|
| + // Error: file doesn't exist yet. XHR it in and write it to the FS.
|
| +
|
| + $http.get(doc.icon, {responseType: 'blob'}).success(function(blob) {
|
| + console.log('Fetched icon via XHR');
|
| +
|
| + blob.name = doc.iconFilename; // Add icon filename to blob.
|
| +
|
| + writeFile(blob); // Write is async, but that's ok.
|
| +
|
| + doc.icon = window.URL.createObjectURL(blob);
|
| +
|
| + $scope.docs.push(doc);
|
| +
|
| + // Only sort when last entry is seen.
|
| + if (totalEntries - 1 == i) {
|
| + $scope.docs.sort(Util.sortByDate);
|
| + }
|
| + });
|
| +
|
| + });</strong>
|
| + });
|
| + };
|
| +
|
| + var config = {
|
| + ...
|
| + };
|
| +
|
| + $http.get(gdocs.DOCLIST_FEED, config).success(successCallbackWithFsCaching);
|
| +};
|
| +</pre>
|
| +
|
| +<p>
|
| +Notice that in the <code>webkitResolveLocalFileSystemURL()</code> callback
|
| +we're calling <code>$scope.$apply()</code>
|
| +when the last entry is seen.
|
| +Normally calling <code>$apply()</code> isn't necessary.
|
| +Angular detects changes to data models automagically.
|
| +However in our case,
|
| +we have an addition layer of asynchronous callback
|
| +that Angular isn't aware of.
|
| +We must explicitly tell Angular when our model has been updated.
|
| +</p>
|
| +
|
| +<p>
|
| +On first run,
|
| +the icons won't be in the HTML5 Filesystem and the calls to
|
| +<code>window.webkitResolveLocalFileSystemURL()</code> will result
|
| +in its error callback being invoked.
|
| +For that case,
|
| +we can reuse the technique from before and fetch the images.
|
| +The only difference this time is that
|
| +each blob is written to the filesystem (see
|
| +<a href="https://github.com/GoogleChrome/chrome-app-samples/blob/master/gdocs/js/app.js#L25">writeBlob()</a>).
|
| +The console verifies this behavior:
|
| +</p>
|
| +
|
| +<img src="{{static}}/images/writecompleted.png"
|
| + width="804"
|
| + height="42"
|
| + alt="Console log 100: Write completed">
|
| +
|
| +<p>
|
| +Upon next run (or press of the "Refresh" button),
|
| +the URL passed to <code>webkitResolveLocalFileSystemURL()</code> exists
|
| +because the file has been previously cached.
|
| +The app sets the <code>doc.icon</code>
|
| +to the file's <code>filesystem: URL</code> and
|
| +avoids making the costly XHR for the icon.
|
| +</p>
|
| +
|
| +<h2 id="seven">Drag and drop uploading</h2>
|
| +
|
| +<p>
|
| +An uploader app is false advertising
|
| +if it can't upload files!
|
| +</p>
|
| +
|
| +<p>
|
| +<a href="https://github.com/GoogleChrome/chrome-app-samples/blob/master/gdocs/js/app.js#L177">app.js</a>
|
| +handles this feature by implementing a small library
|
| +around HTML5 Drag and Drop called <code>DnDFileController</code>.
|
| +It gives the ability to drag in files from the desktop
|
| +and have them uploaded to Google Drive.
|
| +</p>
|
| +
|
| +<p>
|
| +Simply adding this to the gdocs service does the job:
|
| +</p>
|
| +
|
| +<pre>
|
| +gDriveApp.factory('gdocs', function() {
|
| + var gdocs = new GDocs();
|
| +
|
| + var dnd = new DnDFileController('body', function(files) {
|
| + var $scope = angular.element(this).scope();
|
| + Util.toArray(files).forEach(function(file, i) {
|
| + gdocs.upload(file, function() {
|
| + $scope.fetchDocs();
|
| + });
|
| + });
|
| + });
|
| +
|
| + return gdocs;
|
| +});
|
| +</pre>
|
| +
|
| +<p class="backtotop"><a href="#top">Back to top</a></p>
|
|
|