O Angular é uma estrutura MVC. Portanto, precisamos definir o app de forma que um modelo, uma visualização e um controlador saiam dele logicamente. Felizmente, é fácil fazer isso no Angular.
A visualização é a mais fácil, então vamos começar por ela.
Por fim, queremos exibir a lista de arquivos do usuário. Para isso, um simples
para cada documento em nosso modelo de dados "docs". Cada item
contém um ícone de arquivo, um link para abrir o arquivo na Web e a data da última atualização.
Observação :para tornar o modelo um HTML válido, estamos usando atributos data-*
para o iterador ngRepeat do Angular, mas isso não é obrigatório. É possível escrever o repetidor facilmente como
<li ng-repeat="doc in docs">
.
Em seguida, precisamos informar ao Angular qual controlador vai supervisionar a renderização desse modelo. Para isso, usamos a diretiva ngController para instruir o DocsController
a reinar sobre o modelo
:
<body data-ng-controller="DocsController">
<section id="main">
<ul>
<li data-ng-repeat="doc in docs">
<img data-ng-src=""> <a href=""></a>
<span class="date"></span>
</li>
</ul>
</section>
</body>
Tenha em mente que o que você não vê aqui é a conexão de listeners de eventos ou propriedades para vinculação de
dados. O Angular está fazendo esse trabalho pesado para nós.
A última etapa é fazer o Angular iluminar nossos modelos. A maneira típica de fazer isso é incluir a
diretiva ngApp até :
<html data-ng-app="gDriveApp">
Também é possível reduzir o escopo do aplicativo para uma parte menor da página. Temos apenas
um controlador neste app, mas se fôssemos adicionar mais depois, colocar ngApp no elemento superior
torna a página inteira pronta para o Angular.
O produto final de main.html
vai ficar assim:
<html data-ng-app="gDriveApp">
<head>
…
<base target="_blank">
</head>
<body data-ng-controller="DocsController">
<section id="main">
<nav>
<h2>Google Drive Uploader</h2>
<button class="btn" data-ng-click="fetchDocs()">Refresh</button>
<button class="btn" id="close-button" title="Close"></button>
</nav>
<ul>
<li data-ng-repeat="doc in docs">
<img data-ng-src=""> <a href=""></a>
<span class="date"></span>
</li>
</ul>
</section>
Sobre a Política de Segurança de Conteúdo
Ao contrário de muitas outras estruturas JS MVC, o Angular v1.1.0+ não requer ajustes para funcionar em uma
CSP rigorosa. Ele simplesmente funciona, pronto para uso.
No entanto, se você estiver usando uma versão mais antiga do Angular entre a v1.0.1 e a v1.1.0, precisará solicitar que o Angular seja executado em um "modo de segurança de conteúdo". Isso é feito incluindo a diretiva ngCsp
com ngApp :
<html data-ng-app data-ng-csp>
Autorização de processamento
O modelo de dados não é gerado pelo próprio app. Em vez disso, ele é preenchido por uma API externa (a API Google Drive). Portanto, há um pouco de trabalho necessário para preencher os dados do aplicativo.
Antes de fazer uma solicitação de API, precisamos buscar um token OAuth para a Conta do Google do usuário.
Para isso, criamos um método para unir a chamada a chrome.identity.getAuthToken()
e armazenar o
accessToken
, que pode ser reutilizado em chamadas futuras para a API Drive.
GDocs.prototype.auth = function(opt_callback) {
try {
chrome.identity.getAuthToken({interactive: false}, function(token) {
if (token) {
this.accessToken = token;
opt_callback && opt_callback();
}
}.bind(this));
} catch(e) {
console.log(e);
}
};
Observação :transmitir o callback opcional nos dá a flexibilidade de saber quando o token OAuth está
pronto.
Observação :para simplificar um pouco, criamos a biblioteca gdocs.js para processar as tarefas da API.
Depois de receber o token, é hora de fazer solicitações à API Drive e preencher o modelo.
Controle de esqueleto
O "modelo" do Aplicativo de upload é uma matriz simples (chamada docs) de objetos que serão renderizados como aqueles
no modelo:
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();
});
}
Observe que gdocs.auth()
é chamado como parte do construtor DocsController. Quando o Angular cria o controlador, temos a garantia de ter um novo token OAuth aguardando o usuário.
Buscando dados
Modelo apresentado. Controle com scaffolding. token OAuth em mãos. E agora?
É hora de definir o método do controlador principal, fetchDocs()
. Ele é o mecanismo de trabalho do controlador, responsável por solicitar os arquivos do usuário e registrar a matriz de documentos com dados das respostas da API.
$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);
};
fetchDocs()
usa o serviço $http
do Angular para recuperar o feed principal por XHR. O token de acesso OAuth está incluído no cabeçalho Authorization
junto com outros cabeçalhos e parâmetros personalizados.
O successCallback
processa a resposta da API e cria um novo objeto de documento para cada entrada no feed.
Se você executar fetchDocs()
agora, tudo vai funcionar e a lista de arquivos vai aparecer:
Eba!
Espere, estamos sentindo falta desses ícones de arquivos organizados. What gives? Uma verificação rápida do console mostra
vários erros relacionados à CSP:
O motivo é que estamos tentando definir os ícones img.src
como URLs externos. Isso viola a CSP. Por
exemplo: https://ssl.gstatic.com/docs/doclist/images/icon_10_document_list.png
. Para corrigir isso, precisamos importar esses recursos remotos localmente no app.
Como importar ativos de imagem remota
Para que a CSP pare de gritar, usamos o XHR2 para "importar" os ícones de arquivo como Blobs e, em seguida, definimos o
img.src
como um blob: URL
criado pelo app.
Confira o successCallback
atualizado com o código XHR adicionado:
var successCallback = function(resp, status, headers, config) {
var docs = [];
var totalEntries = resp.feed.entry.length;
resp.feed.entry.forEach(function(entry, i) {
var doc = {
...
};
$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);
}
});
});
};
Agora que a CSP está satisfeita novamente com a gente, temos ícones de arquivos bonitos:
Como ficar off-line: armazenamento em cache de recursos externos
A otimização óbvia que precisa ser feita: não faça centenas de solicitações XHR para cada ícone de arquivo em
cada chamada para fetchDocs()
. Verifique isso no console das Ferramentas para desenvolvedores pressionando o botão "Atualizar"
várias vezes. Todas as vezes, n imagens são buscadas:
Vamos modificar o successCallback
para adicionar uma camada de armazenamento em cache. As adições estão destacadas em negrito:
$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 = {
...
};
// 'https://ssl.gstatic.com/doc_icon_128.png' -> 'doc_icon_128.png'
doc.iconFilename = doc.icon.substring(doc.icon.lastIndexOf('/') + 1);
// 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.
var fsURL = fs.root.toURL() + FOLDERNAME + '/' + doc.iconFilename;
window.webkitResolveLocalFileSystemURL(fsURL, function(entry) {
doc.icon = entry.toURL(); // should be === to fsURL, but whatevs.
$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.
}
}, 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);
}
});
});
});
};
var config = {
...
};
$http.get(gdocs.DOCLIST_FEED, config).success(successCallbackWithFsCaching);
};
Observe que, no callback webkitResolveLocalFileSystemURL()
, estamos chamando $scope.$apply()
quando
a última entrada é vista. Normalmente, não é necessário chamar $apply()
. O Angular detecta alterações
nos modelos de dados automaticamente. No entanto, no nosso caso, temos uma camada extra de callback assíncrono que
o Angular não conhece. Precisamos informar explicitamente ao Angular quando o modelo foi atualizado.
Na primeira execução, os ícones não estarão no sistema de arquivos HTML5, e as chamadas para
window.webkitResolveLocalFileSystemURL()
resultarão na invocação do callback de erro. Para esse
caso, podemos reutilizar a técnica de antes e buscar as imagens. A única diferença desta vez é que cada blob é gravado no sistema de arquivos. Consulte writeFile() . O console verifica esse comportamento:
Na próxima execução (ou ao pressionar o botão "Atualizar"), o URL transmitido para
webkitResolveLocalFileSystemURL()
existe porque o arquivo já foi armazenado em cache. O app define
o doc.icon
como o filesystem: URL
do arquivo e evita fazer o XHR caro para o ícone.
Envio por arrastar e soltar
Um app de upload é uma publicidade falsa quando não consegue enviar arquivos.
O app.js processa esse recurso implementando uma pequena biblioteca de arrastar e soltar HTML5 chamada DnDFileController
. Ele permite arrastar arquivos da área de trabalho e fazer o upload deles
para o Google Drive.
Basta adicionar isso ao serviço do gdocs:
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;
});