├── .bowerrc ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .gitmodules ├── .jshintrc ├── Gruntfile.js ├── LICENSE ├── README.md ├── app ├── .buildignore ├── .htaccess ├── 404.html ├── favicon.ico ├── index.html ├── robots.txt ├── scripts │ ├── app.js │ ├── controllers │ │ ├── create.js │ │ ├── install.js │ │ └── todos.js │ ├── directives │ │ ├── collaborative.js │ │ ├── focus.js │ │ └── on-blur.js │ └── services │ │ └── storage.js ├── styles │ └── main.scss └── views │ ├── install.html │ ├── loading.html │ └── main.html ├── bower.json ├── package.json └── screenshot.png /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/components" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 4 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea/ 3 | .tmp 4 | .sass-cache 5 | app/components 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/realtime-tasks/67311d2942035b05c13f55ded1c9ca9cf9c638de/.gitmodules -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "es5": true, 5 | "esnext": true, 6 | "bitwise": true, 7 | "camelcase": false, 8 | "curly": true, 9 | "eqeqeq": true, 10 | "immed": true, 11 | "indent": 2, 12 | "latedef": true, 13 | "newcap": true, 14 | "noarg": true, 15 | "quotmark": "single", 16 | "regexp": true, 17 | "undef": true, 18 | "unused": true, 19 | "strict": true, 20 | "trailing": true, 21 | "smarttabs": true, 22 | "globals": { 23 | "angular": false, 24 | "gapi" : false, 25 | "$" : false, 26 | "CONFIG": true, 27 | "app": true 28 | }, 29 | "white": false 30 | } 31 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var lrSnippet = require('grunt-contrib-livereload/lib/utils').livereloadSnippet; 3 | var mountFolder = function (connect, dir) { 4 | return connect.static(require('path').resolve(dir)); 5 | }; 6 | 7 | module.exports = function (grunt) { 8 | // load all grunt tasks 9 | require('matchdep').filterDev('grunt-*').concat(['gruntacular']).forEach(grunt.loadNpmTasks); 10 | 11 | // configurable paths 12 | var yeomanConfig = { 13 | app: 'app', 14 | dist: 'dist' 15 | }; 16 | 17 | try { 18 | yeomanConfig.app = require('./component.json').appPath || yeomanConfig.app; 19 | } catch (e) {} 20 | 21 | grunt.initConfig({ 22 | yeoman: yeomanConfig, 23 | watch: { 24 | compass: { 25 | files: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'], 26 | tasks: ['compass'] 27 | }, 28 | livereload: { 29 | files: [ 30 | '<%= yeoman.app %>/{,*/}*.html', 31 | '{.tmp,<%= yeoman.app %>}/styles/{,*/}*.css', 32 | '{.tmp,<%= yeoman.app %>}/scripts/{,*/}*.js', 33 | '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg}' 34 | ], 35 | tasks: ['livereload'] 36 | } 37 | }, 38 | connect: { 39 | livereload: { 40 | options: { 41 | port: 9000, 42 | // Change this to '0.0.0.0' to access the server from outside. 43 | hostname: 'localhost', 44 | middleware: function (connect) { 45 | return [ 46 | lrSnippet, 47 | mountFolder(connect, '.tmp'), 48 | mountFolder(connect, yeomanConfig.app) 49 | ]; 50 | } 51 | } 52 | } 53 | }, 54 | open: { 55 | server: { 56 | url: 'http://localhost:<%= connect.livereload.options.port %>' 57 | } 58 | }, 59 | clean: { 60 | dist: ['.tmp', '<%= yeoman.dist %>/*'], 61 | server: '.tmp' 62 | }, 63 | jshint: { 64 | options: { 65 | jshintrc: '.jshintrc' 66 | }, 67 | all: [ 68 | 'Gruntfile.js', 69 | '<%= yeoman.app %>/scripts/{,*/}*.js' 70 | ] 71 | }, 72 | compass: { 73 | options: { 74 | sassDir: '<%= yeoman.app %>/styles', 75 | cssDir: '.tmp/styles', 76 | imagesDir: '<%= yeoman.app %>/images', 77 | javascriptsDir: '<%= yeoman.app %>/scripts', 78 | fontsDir: '<%= yeoman.app %>/styles/fonts', 79 | importPath: '<%= yeoman.app %>/components', 80 | relativeAssets: true 81 | }, 82 | dist: {}, 83 | server: { 84 | options: { 85 | debugInfo: true 86 | } 87 | } 88 | }, 89 | concat: { 90 | dist: { 91 | files: { 92 | '<%= yeoman.dist %>/scripts/scripts.js': [ 93 | '.tmp/scripts/{,*/}*.js', 94 | '<%= yeoman.app %>/scripts/{,*/}*.js' 95 | ] 96 | } 97 | } 98 | }, 99 | useminPrepare: { 100 | html: '<%= yeoman.app %>/index.html', 101 | options: { 102 | dest: '<%= yeoman.dist %>' 103 | } 104 | }, 105 | usemin: { 106 | html: ['<%= yeoman.dist %>/{,*/}*.html'], 107 | css: ['<%= yeoman.dist %>/styles/{,*/}*.css'], 108 | options: { 109 | dirs: ['<%= yeoman.dist %>'] 110 | } 111 | }, 112 | imagemin: { 113 | dist: { 114 | files: [{ 115 | expand: true, 116 | cwd: '<%= yeoman.app %>/images', 117 | src: '{,*/}*.{png,jpg,jpeg}', 118 | dest: '<%= yeoman.dist %>/images' 119 | }] 120 | } 121 | }, 122 | cssmin: { 123 | dist: { 124 | files: { 125 | '<%= yeoman.dist %>/styles/main.css': [ 126 | '.tmp/styles/{,*/}*.css', 127 | '<%= yeoman.app %>/styles/{,*/}*.css' 128 | ] 129 | } 130 | } 131 | }, 132 | htmlmin: { 133 | dist: { 134 | options: { 135 | removeCommentsFromCDATA: true, 136 | // https://github.com/yeoman/grunt-usemin/issues/44 137 | //collapseWhitespace: true, 138 | collapseBooleanAttributes: true, 139 | removeAttributeQuotes: true, 140 | removeRedundantAttributes: true, 141 | useShortDoctype: true, 142 | removeEmptyAttributes: true, 143 | removeOptionalTags: true 144 | }, 145 | files: [{ 146 | expand: true, 147 | cwd: '<%= yeoman.app %>', 148 | src: ['*.html', 'views/*.html'], 149 | dest: '<%= yeoman.dist %>' 150 | }] 151 | } 152 | }, 153 | cdnify: { 154 | dist: { 155 | html: ['<%= yeoman.dist %>/*.html'] 156 | } 157 | }, 158 | ngmin: { 159 | dist: { 160 | files: [{ 161 | expand: true, 162 | cwd: '<%= yeoman.dist %>/scripts', 163 | src: '*.js', 164 | dest: '<%= yeoman.dist %>/scripts' 165 | }] 166 | } 167 | }, 168 | uglify: { 169 | dist: { 170 | files: { 171 | '<%= yeoman.dist %>/scripts/scripts.js': [ 172 | '<%= yeoman.dist %>/scripts/scripts.js' 173 | ], 174 | } 175 | } 176 | }, 177 | copy: { 178 | dist: { 179 | files: [{ 180 | expand: true, 181 | dot: true, 182 | cwd: '<%= yeoman.app %>', 183 | dest: '<%= yeoman.dist %>', 184 | src: [ 185 | '*.{ico,txt}', 186 | '.htaccess', 187 | 'components/**/*' 188 | ] 189 | }] 190 | } 191 | } 192 | }); 193 | 194 | grunt.renameTask('regarde', 'watch'); 195 | // remove when mincss task is renamed 196 | grunt.renameTask('mincss', 'cssmin'); 197 | 198 | grunt.registerTask('server', [ 199 | 'clean:server', 200 | 'compass:server', 201 | 'livereload-start', 202 | 'connect:livereload', 203 | 'open', 204 | 'watch' 205 | ]); 206 | 207 | grunt.registerTask('build', [ 208 | 'clean:dist', 209 | 'jshint', 210 | 'compass:dist', 211 | 'useminPrepare', 212 | 'imagemin', 213 | 'cssmin', 214 | 'htmlmin', 215 | 'concat', 216 | 'copy', 217 | 'cdnify', 218 | 'usemin', 219 | 'ngmin', 220 | 'uglify' 221 | ]); 222 | 223 | grunt.registerTask('default', ['build']); 224 | }; 225 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2013 Google Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Collaborative Todos 2 | 3 | [![Todos Screenshot](https://github.com/googledrive/realtime-tasks/raw/master/screenshot.png)](http://googledrive.github.io/realtime-tasks) 4 | 5 | ## Overview 6 | 7 | A simple todo list based off [TodoMVC](http://todomvc.com/), [AngularJS](https://www.angularjs.org), 8 | and the [Google Drive Realtime API](https://developers.google.com/drive/realtime). 9 | 10 | You can try out the app on its [live instance](http://googledrive.github.io/realtime-tasks). 11 | 12 | ## Installation and Configuration 13 | 14 | The project can run on any static web server. 15 | 16 | ## Building from source 17 | 18 | If you don't already have [Yeoman](http://yeoman.io/), [Grunt](http://gruntjs.com/), and [Bower](http://twitter.github.com/bower/) you can install with: 19 | 20 | npm install -g yo grunt-cli bower 21 | 22 | Fetch the source for the app: 23 | 24 | git clone https://github.com/googledrive/realtime-tasks.git 25 | cd realtime-tasks 26 | npm install 27 | bower install 28 | 29 | ### Create a Google APIs project and Activate the Drive API 30 | 31 | First, you need to activate the Drive API for your app. You can do it by configuring your API project in the Google APIs Console. 32 | 33 | - Create an API project in the [Google Developers Console](https://developers.google.com/console). 34 | - Select the tab "APIs & Auth > APIs" and enable the Drive API and Drive SDK. 35 | - Select the tab "APIs & Auth > Credentials" and click "Create new Client ID". 36 | - In the resulting dialog, do the following: 37 | - Select "Web application" for the Application type 38 | - List your hostname in the "Authorized JavaScript Origins" field. 39 | - Click "Create Client ID". 40 | - Note the **Client ID** string created. 41 | - Select the tab "APIs & Auth > Consent screen" and ensure the Email Address and Product Name are set. 42 | 43 | To enable integration with the Drive UI, including the sharing dialog, perform the following steps. 44 | 45 | - Select the tab "APIs & Auth > APIs" and click the gear icon next to "Drive SDK". 46 | - Click the link to return to the original console. 47 | - Fill out the Application Name and upload at least one Application Icon. 48 | - Set the **Open URL** to `http://YOURHOST/#/todos/{ids}/?user={userId}` 49 | - Check the **Create With** option and set the **New URL** to `http://YOURHOST/#/create?user={userId}` 50 | 51 | Adjust the above URLs as needed for the correct hostname or path. Localhost is currently not allowed. 52 | 53 | ### Setup your App information 54 | 55 | Update the `CONFIG` object in `app.js` with the **Client ID** string you created. 56 | 57 | ### Deploy, run that's it! 58 | 59 | You can run a local server with grunt: 60 | 61 | grunt server 62 | 63 | 64 | ## Contributing 65 | 66 | Before creating a pull request, please fill out either the individual or 67 | corporate Contributor License Agreement. 68 | 69 | * If you are an individual writing original source code and you're sure you 70 | own the intellectual property, then you'll need to sign an 71 | [individual CLA](http://code.google.com/legal/individual-cla-v1.0.html). 72 | * If you work for a company that wants to allow you to contribute your work 73 | to this client library, then you'll need to sign a 74 | [corporate CLA](http://code.google.com/legal/corporate-cla-v1.0.html). 75 | 76 | Follow either of the two links above to access the appropriate CLA and 77 | instructions for how to sign and return it. Once we receive it, we'll add you 78 | to the official list of contributors and be able to accept your patches. 79 | -------------------------------------------------------------------------------- /app/.buildignore: -------------------------------------------------------------------------------- 1 | *.coffee -------------------------------------------------------------------------------- /app/.htaccess: -------------------------------------------------------------------------------- 1 | # Apache configuration file 2 | # httpd.apache.org/docs/2.2/mod/quickreference.html 3 | 4 | # Note .htaccess files are an overhead, this logic should be in your Apache 5 | # config if possible: httpd.apache.org/docs/2.2/howto/htaccess.html 6 | 7 | # Techniques in here adapted from all over, including: 8 | # Kroc Camen: camendesign.com/.htaccess 9 | # perishablepress.com/press/2006/01/10/stupid-htaccess-tricks/ 10 | # Sample .htaccess file of CMS MODx: modxcms.com 11 | 12 | 13 | # ---------------------------------------------------------------------- 14 | # Better website experience for IE users 15 | # ---------------------------------------------------------------------- 16 | 17 | # Force the latest IE version, in various cases when it may fall back to IE7 mode 18 | # github.com/rails/rails/commit/123eb25#commitcomment-118920 19 | # Use ChromeFrame if it's installed for a better experience for the poor IE folk 20 | 21 | 22 | Header set X-UA-Compatible "IE=Edge,chrome=1" 23 | # mod_headers can't match by content-type, but we don't want to send this header on *everything*... 24 | 25 | Header unset X-UA-Compatible 26 | 27 | 28 | 29 | 30 | # ---------------------------------------------------------------------- 31 | # Cross-domain AJAX requests 32 | # ---------------------------------------------------------------------- 33 | 34 | # Serve cross-domain Ajax requests, disabled by default. 35 | # enable-cors.org 36 | # code.google.com/p/html5security/wiki/CrossOriginRequestSecurity 37 | 38 | # 39 | # Header set Access-Control-Allow-Origin "*" 40 | # 41 | 42 | 43 | # ---------------------------------------------------------------------- 44 | # CORS-enabled images (@crossorigin) 45 | # ---------------------------------------------------------------------- 46 | 47 | # Send CORS headers if browsers request them; enabled by default for images. 48 | # developer.mozilla.org/en/CORS_Enabled_Image 49 | # blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html 50 | # hacks.mozilla.org/2011/11/using-cors-to-load-webgl-textures-from-cross-domain-images/ 51 | # wiki.mozilla.org/Security/Reviews/crossoriginAttribute 52 | 53 | 54 | 55 | # mod_headers, y u no match by Content-Type?! 56 | 57 | SetEnvIf Origin ":" IS_CORS 58 | Header set Access-Control-Allow-Origin "*" env=IS_CORS 59 | 60 | 61 | 62 | 63 | 64 | # ---------------------------------------------------------------------- 65 | # Webfont access 66 | # ---------------------------------------------------------------------- 67 | 68 | # Allow access from all domains for webfonts. 69 | # Alternatively you could only whitelist your 70 | # subdomains like "subdomain.example.com". 71 | 72 | 73 | 74 | Header set Access-Control-Allow-Origin "*" 75 | 76 | 77 | 78 | 79 | # ---------------------------------------------------------------------- 80 | # Proper MIME type for all files 81 | # ---------------------------------------------------------------------- 82 | 83 | # JavaScript 84 | # Normalize to standard type (it's sniffed in IE anyways) 85 | # tools.ietf.org/html/rfc4329#section-7.2 86 | AddType application/javascript js jsonp 87 | AddType application/json json 88 | 89 | # Audio 90 | AddType audio/ogg oga ogg 91 | AddType audio/mp4 m4a f4a f4b 92 | 93 | # Video 94 | AddType video/ogg ogv 95 | AddType video/mp4 mp4 m4v f4v f4p 96 | AddType video/webm webm 97 | AddType video/x-flv flv 98 | 99 | # SVG 100 | # Required for svg webfonts on iPad 101 | # twitter.com/FontSquirrel/status/14855840545 102 | AddType image/svg+xml svg svgz 103 | AddEncoding gzip svgz 104 | 105 | # Webfonts 106 | AddType application/vnd.ms-fontobject eot 107 | AddType application/x-font-ttf ttf ttc 108 | AddType font/opentype otf 109 | AddType application/x-font-woff woff 110 | 111 | # Assorted types 112 | AddType image/x-icon ico 113 | AddType image/webp webp 114 | AddType text/cache-manifest appcache manifest 115 | AddType text/x-component htc 116 | AddType application/xml rss atom xml rdf 117 | AddType application/x-chrome-extension crx 118 | AddType application/x-opera-extension oex 119 | AddType application/x-xpinstall xpi 120 | AddType application/octet-stream safariextz 121 | AddType application/x-web-app-manifest+json webapp 122 | AddType text/x-vcard vcf 123 | AddType application/x-shockwave-flash swf 124 | AddType text/vtt vtt 125 | 126 | 127 | # ---------------------------------------------------------------------- 128 | # Allow concatenation from within specific js and css files 129 | # ---------------------------------------------------------------------- 130 | 131 | # e.g. Inside of script.combined.js you could have 132 | # 133 | # 134 | # and they would be included into this single file. 135 | 136 | # This is not in use in the boilerplate as it stands. You may 137 | # choose to use this technique if you do not have a build process. 138 | 139 | # 140 | # Options +Includes 141 | # AddOutputFilterByType INCLUDES application/javascript application/json 142 | # SetOutputFilter INCLUDES 143 | # 144 | 145 | # 146 | # Options +Includes 147 | # AddOutputFilterByType INCLUDES text/css 148 | # SetOutputFilter INCLUDES 149 | # 150 | 151 | 152 | # ---------------------------------------------------------------------- 153 | # Gzip compression 154 | # ---------------------------------------------------------------------- 155 | 156 | 157 | 158 | # Force deflate for mangled headers developer.yahoo.com/blogs/ydn/posts/2010/12/pushing-beyond-gzipping/ 159 | 160 | 161 | SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding 162 | RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding 163 | 164 | 165 | 166 | # HTML, TXT, CSS, JavaScript, JSON, XML, HTC: 167 | 168 | FilterDeclare COMPRESS 169 | FilterProvider COMPRESS DEFLATE resp=Content-Type $text/html 170 | FilterProvider COMPRESS DEFLATE resp=Content-Type $text/css 171 | FilterProvider COMPRESS DEFLATE resp=Content-Type $text/plain 172 | FilterProvider COMPRESS DEFLATE resp=Content-Type $text/xml 173 | FilterProvider COMPRESS DEFLATE resp=Content-Type $text/x-component 174 | FilterProvider COMPRESS DEFLATE resp=Content-Type $application/javascript 175 | FilterProvider COMPRESS DEFLATE resp=Content-Type $application/json 176 | FilterProvider COMPRESS DEFLATE resp=Content-Type $application/xml 177 | FilterProvider COMPRESS DEFLATE resp=Content-Type $application/xhtml+xml 178 | FilterProvider COMPRESS DEFLATE resp=Content-Type $application/rss+xml 179 | FilterProvider COMPRESS DEFLATE resp=Content-Type $application/atom+xml 180 | FilterProvider COMPRESS DEFLATE resp=Content-Type $application/vnd.ms-fontobject 181 | FilterProvider COMPRESS DEFLATE resp=Content-Type $image/svg+xml 182 | FilterProvider COMPRESS DEFLATE resp=Content-Type $image/x-icon 183 | FilterProvider COMPRESS DEFLATE resp=Content-Type $application/x-font-ttf 184 | FilterProvider COMPRESS DEFLATE resp=Content-Type $font/opentype 185 | FilterChain COMPRESS 186 | FilterProtocol COMPRESS DEFLATE change=yes;byteranges=no 187 | 188 | 189 | 190 | # Legacy versions of Apache 191 | AddOutputFilterByType DEFLATE text/html text/plain text/css application/json 192 | AddOutputFilterByType DEFLATE application/javascript 193 | AddOutputFilterByType DEFLATE text/xml application/xml text/x-component 194 | AddOutputFilterByType DEFLATE application/xhtml+xml application/rss+xml application/atom+xml 195 | AddOutputFilterByType DEFLATE image/x-icon image/svg+xml application/vnd.ms-fontobject application/x-font-ttf font/opentype 196 | 197 | 198 | 199 | 200 | 201 | # ---------------------------------------------------------------------- 202 | # Expires headers (for better cache control) 203 | # ---------------------------------------------------------------------- 204 | 205 | # These are pretty far-future expires headers. 206 | # They assume you control versioning with filename-based cache busting 207 | # Additionally, consider that outdated proxies may miscache 208 | # www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/ 209 | 210 | # If you don't use filenames to version, lower the CSS and JS to something like 211 | # "access plus 1 week". 212 | 213 | 214 | ExpiresActive on 215 | 216 | # Perhaps better to whitelist expires rules? Perhaps. 217 | ExpiresDefault "access plus 1 month" 218 | 219 | # cache.appcache needs re-requests in FF 3.6 (thanks Remy ~Introducing HTML5) 220 | ExpiresByType text/cache-manifest "access plus 0 seconds" 221 | 222 | # Your document html 223 | ExpiresByType text/html "access plus 0 seconds" 224 | 225 | # Data 226 | ExpiresByType text/xml "access plus 0 seconds" 227 | ExpiresByType application/xml "access plus 0 seconds" 228 | ExpiresByType application/json "access plus 0 seconds" 229 | 230 | # Feed 231 | ExpiresByType application/rss+xml "access plus 1 hour" 232 | ExpiresByType application/atom+xml "access plus 1 hour" 233 | 234 | # Favicon (cannot be renamed) 235 | ExpiresByType image/x-icon "access plus 1 week" 236 | 237 | # Media: images, video, audio 238 | ExpiresByType image/gif "access plus 1 month" 239 | ExpiresByType image/png "access plus 1 month" 240 | ExpiresByType image/jpeg "access plus 1 month" 241 | ExpiresByType video/ogg "access plus 1 month" 242 | ExpiresByType audio/ogg "access plus 1 month" 243 | ExpiresByType video/mp4 "access plus 1 month" 244 | ExpiresByType video/webm "access plus 1 month" 245 | 246 | # HTC files (css3pie) 247 | ExpiresByType text/x-component "access plus 1 month" 248 | 249 | # Webfonts 250 | ExpiresByType application/x-font-ttf "access plus 1 month" 251 | ExpiresByType font/opentype "access plus 1 month" 252 | ExpiresByType application/x-font-woff "access plus 1 month" 253 | ExpiresByType image/svg+xml "access plus 1 month" 254 | ExpiresByType application/vnd.ms-fontobject "access plus 1 month" 255 | 256 | # CSS and JavaScript 257 | ExpiresByType text/css "access plus 1 year" 258 | ExpiresByType application/javascript "access plus 1 year" 259 | 260 | 261 | 262 | 263 | # ---------------------------------------------------------------------- 264 | # Prevent mobile network providers from modifying your site 265 | # ---------------------------------------------------------------------- 266 | 267 | # The following header prevents modification of your code over 3G on some 268 | # European providers. 269 | # This is the official 'bypass' suggested by O2 in the UK. 270 | 271 | # 272 | # Header set Cache-Control "no-transform" 273 | # 274 | 275 | 276 | # ---------------------------------------------------------------------- 277 | # ETag removal 278 | # ---------------------------------------------------------------------- 279 | 280 | # FileETag None is not enough for every server. 281 | 282 | Header unset ETag 283 | 284 | 285 | # Since we're sending far-future expires, we don't need ETags for 286 | # static content. 287 | # developer.yahoo.com/performance/rules.html#etags 288 | FileETag None 289 | 290 | 291 | # ---------------------------------------------------------------------- 292 | # Stop screen flicker in IE on CSS rollovers 293 | # ---------------------------------------------------------------------- 294 | 295 | # The following directives stop screen flicker in IE on CSS rollovers - in 296 | # combination with the "ExpiresByType" rules for images (see above). 297 | 298 | # BrowserMatch "MSIE" brokenvary=1 299 | # BrowserMatch "Mozilla/4.[0-9]{2}" brokenvary=1 300 | # BrowserMatch "Opera" !brokenvary 301 | # SetEnvIf brokenvary 1 force-no-vary 302 | 303 | 304 | # ---------------------------------------------------------------------- 305 | # Set Keep-Alive Header 306 | # ---------------------------------------------------------------------- 307 | 308 | # Keep-Alive allows the server to send multiple requests through one 309 | # TCP-connection. Be aware of possible disadvantages of this setting. Turn on 310 | # if you serve a lot of static content. 311 | 312 | # 313 | # Header set Connection Keep-Alive 314 | # 315 | 316 | 317 | # ---------------------------------------------------------------------- 318 | # Cookie setting from iframes 319 | # ---------------------------------------------------------------------- 320 | 321 | # Allow cookies to be set from iframes (for IE only) 322 | # If needed, specify a path or regex in the Location directive. 323 | 324 | # 325 | # Header set P3P "policyref=\"/w3c/p3p.xml\", CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"" 326 | # 327 | 328 | 329 | # ---------------------------------------------------------------------- 330 | # Start rewrite engine 331 | # ---------------------------------------------------------------------- 332 | 333 | # Turning on the rewrite engine is necessary for the following rules and 334 | # features. FollowSymLinks must be enabled for this to work. 335 | 336 | # Some cloud hosting services require RewriteBase to be set: goo.gl/HOcPN 337 | # If using the h5bp in a subdirectory, use `RewriteBase /foo` instead where 338 | # 'foo' is your directory. 339 | 340 | # If your web host doesn't allow the FollowSymlinks option, you may need to 341 | # comment it out and use `Options +SymLinksOfOwnerMatch`, but be aware of the 342 | # performance impact: http://goo.gl/Mluzd 343 | 344 | 345 | Options +FollowSymlinks 346 | # Options +SymLinksIfOwnerMatch 347 | Options +FollowSymlinks 348 | RewriteEngine On 349 | # RewriteBase / 350 | 351 | 352 | 353 | # ---------------------------------------------------------------------- 354 | # Suppress or force the "www." at the beginning of URLs 355 | # ---------------------------------------------------------------------- 356 | 357 | # The same content should never be available under two different URLs - 358 | # especially not with and without "www." at the beginning, since this can cause 359 | # SEO problems (duplicate content). That's why you should choose one of the 360 | # alternatives and redirect the other one. 361 | 362 | # By default option 1 (no "www.") is activated. 363 | # no-www.org/faq.php?q=class_b 364 | 365 | # If you'd prefer to use option 2, just comment out all option 1 lines 366 | # and uncomment option 2. 367 | 368 | # IMPORTANT: NEVER USE BOTH RULES AT THE SAME TIME! 369 | 370 | # ---------------------------------------------------------------------- 371 | 372 | # Option 1: 373 | # Rewrite "www.example.com -> example.com". 374 | 375 | 376 | RewriteCond %{HTTPS} !=on 377 | RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] 378 | RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L] 379 | 380 | 381 | # ---------------------------------------------------------------------- 382 | 383 | # Option 2: 384 | # Rewrite "example.com -> www.example.com". 385 | # Be aware that the following rule might not be a good idea if you use "real" 386 | # subdomains for certain parts of your website. 387 | 388 | # 389 | # RewriteCond %{HTTPS} !=on 390 | # RewriteCond %{HTTP_HOST} !^www\..+$ [NC] 391 | # RewriteRule ^ http://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L] 392 | # 393 | 394 | 395 | # ---------------------------------------------------------------------- 396 | # Built-in filename-based cache busting 397 | # ---------------------------------------------------------------------- 398 | 399 | # If you're not using the build script to manage your filename version revving, 400 | # you might want to consider enabling this, which will route requests for 401 | # /css/style.20110203.css to /css/style.css 402 | 403 | # To understand why this is important and a better idea than all.css?v1231, 404 | # read: github.com/h5bp/html5-boilerplate/wiki/cachebusting 405 | 406 | # 407 | # RewriteCond %{REQUEST_FILENAME} !-f 408 | # RewriteCond %{REQUEST_FILENAME} !-d 409 | # RewriteRule ^(.+)\.(\d+)\.(js|css|png|jpg|gif)$ $1.$3 [L] 410 | # 411 | 412 | 413 | # ---------------------------------------------------------------------- 414 | # Prevent SSL cert warnings 415 | # ---------------------------------------------------------------------- 416 | 417 | # Rewrite secure requests properly to prevent SSL cert warnings, e.g. prevent 418 | # https://www.example.com when your cert only allows https://secure.example.com 419 | 420 | # 421 | # RewriteCond %{SERVER_PORT} !^443 422 | # RewriteRule ^ https://example-domain-please-change-me.com%{REQUEST_URI} [R=301,L] 423 | # 424 | 425 | 426 | # ---------------------------------------------------------------------- 427 | # Prevent 404 errors for non-existing redirected folders 428 | # ---------------------------------------------------------------------- 429 | 430 | # without -MultiViews, Apache will give a 404 for a rewrite if a folder of the 431 | # same name does not exist. 432 | # webmasterworld.com/apache/3808792.htm 433 | 434 | Options -MultiViews 435 | 436 | 437 | # ---------------------------------------------------------------------- 438 | # Custom 404 page 439 | # ---------------------------------------------------------------------- 440 | 441 | # You can add custom pages to handle 500 or 403 pretty easily, if you like. 442 | # If you are hosting your site in subdirectory, adjust this accordingly 443 | # e.g. ErrorDocument 404 /subdir/404.html 444 | ErrorDocument 404 /404.html 445 | 446 | 447 | # ---------------------------------------------------------------------- 448 | # UTF-8 encoding 449 | # ---------------------------------------------------------------------- 450 | 451 | # Use UTF-8 encoding for anything served text/plain or text/html 452 | AddDefaultCharset utf-8 453 | 454 | # Force UTF-8 for a number of file formats 455 | AddCharset utf-8 .atom .css .js .json .rss .vtt .xml 456 | 457 | 458 | # ---------------------------------------------------------------------- 459 | # A little more security 460 | # ---------------------------------------------------------------------- 461 | 462 | # To avoid displaying the exact version number of Apache being used, add the 463 | # following to httpd.conf (it will not work in .htaccess): 464 | # ServerTokens Prod 465 | 466 | # "-Indexes" will have Apache block users from browsing folders without a 467 | # default document Usually you should leave this activated, because you 468 | # shouldn't allow everybody to surf through every folder on your server (which 469 | # includes rather private places like CMS system folders). 470 | 471 | Options -Indexes 472 | 473 | 474 | # Block access to "hidden" directories or files whose names begin with a 475 | # period. This includes directories used by version control systems such as 476 | # Subversion or Git. 477 | 478 | RewriteCond %{SCRIPT_FILENAME} -d [OR] 479 | RewriteCond %{SCRIPT_FILENAME} -f 480 | RewriteRule "(^|/)\." - [F] 481 | 482 | 483 | # Block access to backup and source files. These files may be left by some 484 | # text/html editors and pose a great security danger, when anyone can access 485 | # them. 486 | 487 | Order allow,deny 488 | Deny from all 489 | Satisfy All 490 | 491 | 492 | # If your server is not already configured as such, the following directive 493 | # should be uncommented in order to set PHP's register_globals option to OFF. 494 | # This closes a major security hole that is abused by most XSS (cross-site 495 | # scripting) attacks. For more information: http://php.net/register_globals 496 | # 497 | # IF REGISTER_GLOBALS DIRECTIVE CAUSES 500 INTERNAL SERVER ERRORS: 498 | # 499 | # Your server does not allow PHP directives to be set via .htaccess. In that 500 | # case you must make this change in your php.ini file instead. If you are 501 | # using a commercial web host, contact the administrators for assistance in 502 | # doing this. Not all servers allow local php.ini files, and they should 503 | # include all PHP configurations (not just this one), or you will effectively 504 | # reset everything to PHP defaults. Consult www.php.net for more detailed 505 | # information about setting PHP directives. 506 | 507 | # php_flag register_globals Off 508 | 509 | # Rename session cookie to something else, than PHPSESSID 510 | # php_value session.name sid 511 | 512 | # Disable magic quotes (This feature has been DEPRECATED as of PHP 5.3.0 and REMOVED as of PHP 5.4.0.) 513 | # php_flag magic_quotes_gpc Off 514 | 515 | # Do not show you are using PHP 516 | # Note: Move this line to php.ini since it won't work in .htaccess 517 | # php_flag expose_php Off 518 | 519 | # Level of log detail - log all errors 520 | # php_value error_reporting -1 521 | 522 | # Write errors to log file 523 | # php_flag log_errors On 524 | 525 | # Do not display errors in browser (production - Off, development - On) 526 | # php_flag display_errors Off 527 | 528 | # Do not display startup errors (production - Off, development - On) 529 | # php_flag display_startup_errors Off 530 | 531 | # Format errors in plain text 532 | # Note: Leave this setting 'On' for xdebug's var_dump() output 533 | # php_flag html_errors Off 534 | 535 | # Show multiple occurrence of error 536 | # php_flag ignore_repeated_errors Off 537 | 538 | # Show same errors from different sources 539 | # php_flag ignore_repeated_source Off 540 | 541 | # Size limit for error messages 542 | # php_value log_errors_max_len 1024 543 | 544 | # Don't precede error with string (doesn't accept empty string, use whitespace if you need) 545 | # php_value error_prepend_string " " 546 | 547 | # Don't prepend to error (doesn't accept empty string, use whitespace if you need) 548 | # php_value error_append_string " " 549 | 550 | # Increase cookie security 551 | 552 | php_value session.cookie_httponly true 553 | 554 | -------------------------------------------------------------------------------- /app/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found :( 6 | 141 | 142 | 143 |
144 |

Not found :(

145 |

Sorry, but the page you were trying to view does not exist.

146 |

It looks like this was the result of either:

147 | 151 | 154 | 155 |
156 | 157 | 158 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/realtime-tasks/67311d2942035b05c13f55ded1c9ca9cf9c638de/app/favicon.ico -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /app/scripts/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | 'use strict'; 16 | 17 | var CONFIG = { 18 | clientId: '502747173299.apps.googleusercontent.com', 19 | scopes: [ 20 | 'https://www.googleapis.com/auth/drive.file', 21 | 'https://www.googleapis.com/auth/drive.install' 22 | ] 23 | }; 24 | 25 | var app = {}; 26 | 27 | app.module = angular.module('todos', []); 28 | 29 | /** 30 | * A simple type for todo items. 31 | * @constructor 32 | */ 33 | app.Todo = function () { 34 | }; 35 | 36 | /** 37 | * Initializer for constructing via the realtime API 38 | * 39 | * @param title 40 | */ 41 | app.Todo.prototype.initialize = function (title) { 42 | var model = gapi.drive.realtime.custom.getModel(this); 43 | this.title = model.createString(title); 44 | this.completed = false; 45 | this.setup(); 46 | }; 47 | 48 | /** 49 | * Adds a "text" property to collaborative strings for ng-model compatibility 50 | * after a model is created or loaded. 51 | */ 52 | app.Todo.prototype.setup = function() { 53 | Object.defineProperty(this.title, 'text', { 54 | set: this.title.setText, 55 | get: this.title.getText 56 | }); 57 | }; 58 | 59 | /** 60 | * Loads the document. Used to inject the collaborative document 61 | * into the main controller. 62 | * 63 | * @param $route 64 | * @param storage 65 | * @returns {*} 66 | */ 67 | app.loadFile = function ($route, storage) { 68 | var id = $route.current.params.fileId; 69 | var userId = $route.current.params.user; 70 | return storage.requireAuth(true, userId).then(function () { 71 | return storage.getDocument(id); 72 | }); 73 | }; 74 | app.loadFile.$inject = ['$route', 'storage']; 75 | 76 | /** 77 | * Initialize our application routes 78 | */ 79 | app.module.config(['$routeProvider', 80 | function ($routeProvider) { 81 | $routeProvider 82 | .when('/todos/:fileId/:filter', { 83 | templateUrl: 'views/main.html', 84 | controller: 'MainCtrl', 85 | resolve: { 86 | realtimeDocument: app.loadFile 87 | } 88 | }) 89 | .when('/create', { 90 | templateUrl: 'views/loading.html', 91 | controller: 'CreateCtrl' 92 | }) 93 | .when('/install', { 94 | templateUrl: 'views/install.html', 95 | controller: 'InstallCtrl' 96 | }) 97 | .otherwise({ 98 | redirectTo: '/install' 99 | }); 100 | }] 101 | ); 102 | 103 | app.module.value('config', CONFIG); 104 | 105 | /** 106 | * Set up handlers for various authorization issues that may arise if the access token 107 | * is revoked or expired. 108 | */ 109 | app.module.run(['$rootScope', '$location', 'storage', function ($rootScope, $location, storage) { 110 | // Error loading the document, likely due revoked access. Redirect back to home/install page 111 | $rootScope.$on('$routeChangeError', function () { 112 | $location.url('/install?target=' + encodeURIComponent($location.url())); 113 | }); 114 | 115 | // Token expired, refresh 116 | $rootScope.$on('todos.token_refresh_required', function () { 117 | storage.requireAuth(true).then(function () { 118 | // no-op 119 | }, function () { 120 | $location.url('/install?target=' + encodeURIComponent($location.url())); 121 | }); 122 | }); 123 | }]); 124 | 125 | /** 126 | * Bootstrap the app 127 | */ 128 | gapi.load('auth:client:drive-share:drive-realtime', function () { 129 | gapi.auth.init(); 130 | 131 | // Register our Todo class 132 | app.Todo.prototype.title = gapi.drive.realtime.custom.collaborativeField('title'); 133 | app.Todo.prototype.completed = gapi.drive.realtime.custom.collaborativeField('completed'); 134 | 135 | gapi.drive.realtime.custom.registerType(app.Todo, 'todo'); 136 | gapi.drive.realtime.custom.setInitializer(app.Todo, app.Todo.prototype.initialize); 137 | gapi.drive.realtime.custom.setOnLoaded(app.Todo, app.Todo.prototype.setup); 138 | 139 | $(document).ready(function () { 140 | angular.bootstrap(document, ['todos']); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /app/scripts/controllers/create.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | 'use strict'; 16 | 17 | angular.module('todos').controller('CreateCtrl', ['$scope', '$location', 'storage', 18 | /** 19 | * Controller for creating new documents 20 | * 21 | * @param {angular.Scope} $scope 22 | * @param {angular.$location} $location 23 | * @param {!object} storage 24 | * @constructor 25 | */ 26 | function ($scope, $location, storage) { 27 | $scope.message = 'Please wait'; 28 | storage.requireAuth().then(function () { 29 | storage.createDocument('New Project').then(function (file) { 30 | $location.url('/todos/' + file.id + '/'); 31 | }); 32 | }, function () { 33 | $location.url('/install?target=' + encodeURIComponent($location.url())); 34 | }); 35 | }] 36 | ); 37 | -------------------------------------------------------------------------------- /app/scripts/controllers/install.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | 'use strict'; 16 | 17 | angular.module('todos').controller('InstallCtrl', ['$scope', '$location', 'storage', 18 | /** 19 | * Controller for installing the app and/or re-authorizing access. 20 | * 21 | * @param {angular.Scope} $scope 22 | * @param {angular.$location} $location 23 | * @param storage 24 | * @constructor 25 | */ 26 | function ($scope, $location, storage) { 27 | 28 | /** 29 | * Requests authorization from the user. Redirects to the previous target 30 | * or to create a new doc if no other action once complete. 31 | */ 32 | $scope.authorize = function () { 33 | storage.requireAuth(false).then(function () { 34 | var target = $location.search().target; 35 | if (target) { 36 | $location.url(target); 37 | } else { 38 | $location.url('/create'); 39 | } 40 | }); 41 | }; 42 | }] 43 | ); 44 | -------------------------------------------------------------------------------- /app/scripts/controllers/todos.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | 'use strict'; 16 | 17 | angular.module('todos').controller('MainCtrl', ['$scope', '$routeParams', 'realtimeDocument', 18 | /** 19 | * Controller for editing the tasks lists 20 | * 21 | * @param {angular.Scope} $scope 22 | * @param {angular.$routeParams} $routeParams 23 | * @param document 24 | * @constructor 25 | */ 26 | function ($scope, $routeParams, realtimeDocument) { 27 | $scope.fileId = $routeParams.fileId; 28 | $scope.filter = $routeParams.filter; 29 | 30 | $scope.realtimeDocument = realtimeDocument; 31 | $scope.todos = realtimeDocument.getModel().getRoot().get('todos'); 32 | $scope.newTodo = ''; 33 | 34 | 35 | /** 36 | * Count the number of incomplete todo items. 37 | * 38 | * @returns {number} 39 | */ 40 | $scope.remainingCount = function () { 41 | var remaining = 0; 42 | angular.forEach(this.todos.asArray(), function (todo) { 43 | remaining += todo.completed ? 0 : 1; 44 | }); 45 | return remaining; 46 | }; 47 | 48 | /** 49 | * Add a new todo item to the list, resets the new item text. 50 | */ 51 | $scope.addTodo = function () { 52 | if (this.newTodo) { 53 | realtimeDocument.getModel().beginCompoundOperation(); 54 | var todo = realtimeDocument.getModel().create(app.Todo, this.newTodo); 55 | this.newTodo = ''; 56 | this.todos.push(todo); 57 | realtimeDocument.getModel().endCompoundOperation(); 58 | } 59 | }; 60 | 61 | /** 62 | * Begin editing of an item. 63 | */ 64 | $scope.editTodo = function (todo) { 65 | $scope.editedTodo = todo; 66 | }; 67 | 68 | /** 69 | * Cancel editing. 70 | */ 71 | $scope.doneEditing = function () { 72 | $scope.editedTodo = null; 73 | }; 74 | 75 | /** 76 | * Delete an individual todo by removing it from the list. 77 | * 78 | * @param todo 79 | */ 80 | $scope.removeTodo = function (todo) { 81 | this.todos.removeValue(todo); 82 | }; 83 | 84 | /** 85 | * Remove all completed todos from the list 86 | */ 87 | $scope.clearDoneTodos = function () { 88 | var todos = this.todos; 89 | realtimeDocument.getModel().beginCompoundOperation(); 90 | angular.forEach(this.todos.asArray(), function (todo) { 91 | if (todo.completed) { 92 | todos.removeValue(todo); 93 | } 94 | }); 95 | realtimeDocument.getModel().endCompoundOperation(); 96 | }; 97 | 98 | /** 99 | * Toggle the completed status of all items. 100 | * 101 | * @param done 102 | */ 103 | $scope.markAll = function (done) { 104 | realtimeDocument.getModel().beginCompoundOperation(); 105 | angular.forEach(this.todos.asArray(), function (todo) { 106 | todo.completed = done; 107 | }); 108 | realtimeDocument.getModel().endCompoundOperation(); 109 | }; 110 | 111 | $scope.$watch('filter', function (filter) { 112 | $scope.statusFilter = (filter === 'active') ? 113 | { completed: false } : (filter === 'completed') ? 114 | { completed: true } : null; 115 | }); 116 | 117 | /** 118 | * Undo local changes 119 | */ 120 | $scope.undo = function () { 121 | realtimeDocument.getModel().undo(); 122 | }; 123 | 124 | /** 125 | * Check if there are undoable changes. 126 | * @returns {boolean} 127 | */ 128 | $scope.canUndo = function () { 129 | return realtimeDocument.getModel().canUndo; 130 | }; 131 | 132 | /** 133 | * Undo local changes 134 | */ 135 | $scope.redo = function () { 136 | realtimeDocument.getModel().redo(); 137 | }; 138 | 139 | /** 140 | * Check if there are redoable changes. 141 | * @returns {boolean} 142 | */ 143 | $scope.canRedo = function () { 144 | return realtimeDocument.getModel().canRedo; 145 | }; 146 | }] 147 | ); 148 | 149 | angular.module('todos').controller('CollaboratorsCtrl', ['$scope', 'config', 150 | /** 151 | * Controller for displaying the list of current collaborators. Expects 152 | * to inherit the document from a parent scope. 153 | * 154 | * @param {angular.Scope} $scope 155 | * @param {object} config 156 | * @constructor 157 | */ 158 | function ($scope, config) { 159 | var appId = config.clientId.match(/^(\d+)/)[0]; 160 | 161 | var collaboratorListener = function () { 162 | $scope.$apply(function () { 163 | $scope.collaborators = $scope.realtimeDocument.getCollaborators(); 164 | }); 165 | }; 166 | $scope.collaborators = $scope.realtimeDocument.getCollaborators(); 167 | 168 | $scope.realtimeDocument.addEventListener(gapi.drive.realtime.EventType.COLLABORATOR_LEFT, collaboratorListener); 169 | $scope.realtimeDocument.addEventListener(gapi.drive.realtime.EventType.COLLABORATOR_JOINED, collaboratorListener); 170 | 171 | $scope.$on('$destroy', function () { 172 | var doc = $scope.realtimeDocument; 173 | if (doc) { 174 | doc.removeEventListener(gapi.drive.realtime.EventType.COLLABORATOR_LEFT, collaboratorListener); 175 | doc.removeEventListener(gapi.drive.realtime.EventType.COLLABORATOR_JOINED, collaboratorListener); 176 | } 177 | }); 178 | 179 | $scope.share = function () { 180 | var fileId = this.fileId; 181 | var client = new gapi.drive.share.ShareClient(appId); 182 | client.setItemIds([fileId]); 183 | client.showSettingsDialog(); 184 | }; 185 | 186 | }] 187 | ); 188 | -------------------------------------------------------------------------------- /app/scripts/directives/collaborative.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 'use strict'; 15 | 16 | angular.module('todos').directive('collaborative', 17 | /** 18 | * Directive for adding cursor management to text fields bound to 19 | * collaboraative strings. Regular data binding works fine without it, 20 | * but remote updates will not properly maintain the cursor. Including 21 | * this directive ensures correct logical positioning of the cursor 22 | * on active fields. 23 | * 24 | * @returns {{restrict: string, scope: boolean, require: string, compile: Function}} 25 | */ 26 | function () { 27 | /** 28 | * Handles the cursor management for a text field. 29 | * 30 | * @param scope 31 | * @param element 32 | * @param string 33 | * @param controller 34 | * @constructor 35 | */ 36 | var TextBinder = function (scope, element, string, controller) { 37 | this.element = element; 38 | this.string = string; 39 | this.scope = scope; 40 | 41 | this._insertListener = angular.bind(this, function (event) { 42 | if (!event.isLocal) { 43 | this.updateText(event.index, event.text.length); 44 | } 45 | }); 46 | this._deleteListener = angular.bind(this, function (event) { 47 | if (!event.isLocal) { 48 | this.updateText(event.index, -event.text.length); 49 | } 50 | }); 51 | this.updateText = function (position, size) { 52 | var element = this.element[0]; 53 | var isActive = (element === document.activeElement); 54 | if (isActive) { 55 | var value = this.string.text; 56 | var selectionStart = element.selectionStart; 57 | var selectionEnd = element.selectionEnd; 58 | 59 | if (position <= selectionStart) { 60 | selectionStart += size; 61 | } 62 | if (position < selectionEnd) { 63 | selectionEnd += size; 64 | } 65 | if (selectionEnd < selectionStart) { 66 | selectionEnd = selectionStart; 67 | } 68 | 69 | scope.$apply(function () { 70 | // Copied from ngModelController 71 | var formatters = controller.$formatters; 72 | var idx = formatters.length; 73 | 74 | controller.$modelValue = value; 75 | while (idx--) { 76 | value = formatters[idx](value); 77 | } 78 | 79 | if (controller.$viewValue !== value) { 80 | controller.$viewValue = value; 81 | controller.$render(); 82 | } 83 | element.setSelectionRange(selectionStart, selectionEnd); 84 | }); 85 | 86 | } 87 | }; 88 | 89 | this.unbind = function () { 90 | console.log('Removing listeners'); 91 | this.string.removeEventListener(gapi.drive.realtime.EventType.TEXT_INSERTED, this._insertListener); 92 | this.string.removeEventListener(gapi.drive.realtime.EventType.TEXT_DELETED, this._deleteListener); 93 | }; 94 | 95 | this.string.addEventListener(gapi.drive.realtime.EventType.TEXT_INSERTED, this._insertListener); 96 | this.string.addEventListener(gapi.drive.realtime.EventType.TEXT_DELETED, this._deleteListener); 97 | }; 98 | 99 | return { 100 | restrict: 'A', 101 | scope: false, 102 | require: 'ngModel', 103 | compile: function (tElement, tAttrs) { 104 | var expression = tAttrs.ngModel.replace(/\.text$/, ''); 105 | if (expression === tAttrs.ngModel) { 106 | console.log('Model does not appear to be collaborative string. Bind ng-model to .text property'); 107 | return null; 108 | } 109 | return function (scope, iElement, iAttrs, controller) { 110 | scope.$watch(expression, function (newValue) { 111 | if (scope.binder) { 112 | scope.binder.unbind(); 113 | } 114 | if (newValue) { 115 | scope.binder = new TextBinder(scope, iElement, newValue, controller); 116 | } 117 | }); 118 | scope.$on('$destroy', function () { 119 | if (scope.binder) { 120 | scope.binder.unbind(); 121 | scope.binder = null; 122 | } 123 | }); 124 | }; 125 | } 126 | }; 127 | } 128 | ); 129 | -------------------------------------------------------------------------------- /app/scripts/directives/focus.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 'use strict'; 15 | 16 | angular.module('todos').directive('focus', ['$timeout', 17 | /** 18 | * Directive that places focus on the element it is applied to when the expression it binds to evaluates to true. 19 | */ 20 | function ($timeout) { 21 | return function (scope, elem, attrs) { 22 | scope.$watch(attrs.focus, function (value) { 23 | if (value) { 24 | $timeout(function () { 25 | elem[0].focus(); 26 | }, 0, false); 27 | } 28 | }); 29 | }; 30 | }] 31 | ); -------------------------------------------------------------------------------- /app/scripts/directives/on-blur.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | 'use strict'; 16 | 17 | angular.module('todos').directive('onBlur', 18 | /** 19 | * Directive that executes an expression when the element is blurred 20 | */ 21 | function () { 22 | return function (scope, element, attributes) { 23 | element.bind('blur', function () { 24 | if (!element.is(':hidden')) { 25 | scope.$apply(attributes.onBlur); 26 | } 27 | }); 28 | element.bind('keypress', function (event) { 29 | if (event.which === 13) { 30 | scope.$apply(attributes.onBlur); 31 | } 32 | }); 33 | }; 34 | } 35 | ); -------------------------------------------------------------------------------- /app/scripts/services/storage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | 'use strict'; 16 | 17 | angular.module('todos').service('storage', ['$q', '$rootScope', 'config', 18 | /** 19 | * Handles document creation & loading for the app. Keeps only 20 | * one document loaded at a time. 21 | * 22 | * @param $q 23 | * @param $rootScope 24 | * @param config 25 | */ 26 | function ($q, $rootScope, config) { 27 | this.id = null; 28 | this.document = null; 29 | 30 | /** 31 | * Close the current document. 32 | */ 33 | this.closeDocument = function () { 34 | this.document.close(); 35 | this.document = null; 36 | this.id = null; 37 | }; 38 | 39 | /** 40 | * Ensure the document is loaded. 41 | * 42 | * @param id 43 | * @returns {angular.$q.promise} 44 | */ 45 | this.getDocument = function (id) { 46 | if (this.id === id) { 47 | return $q.when(this.document); 48 | } else if (this.document) { 49 | this.closeDocument(); 50 | } 51 | return this.load(id); 52 | }; 53 | 54 | /** 55 | * Creates a new document. 56 | * 57 | * @param title 58 | * @returns {angular.$q.promise} 59 | */ 60 | this.createDocument = function (title) { 61 | var deferred = $q.defer(); 62 | var onComplete = function (result) { 63 | if (result && !result.error) { 64 | deferred.resolve(result); 65 | } else { 66 | deferred.reject(result); 67 | } 68 | $rootScope.$digest(); 69 | }; 70 | gapi.client.request({ 71 | 'path': '/drive/v2/files', 72 | 'method': 'POST', 73 | 'body': JSON.stringify({ 74 | title: title, 75 | mimeType: 'application/vnd.google-apps.drive-sdk' 76 | }) 77 | }).execute(onComplete); 78 | return deferred.promise; 79 | }; 80 | 81 | /** 82 | * Checks to make sure the user is currently authorized and the access 83 | * token hasn't expired. 84 | * 85 | * @param immediateMode 86 | * @param userId 87 | * @returns {angular.$q.promise} 88 | */ 89 | this.requireAuth = function (immediateMode, userId) { 90 | /* jshint camelCase: false */ 91 | var token = gapi.auth.getToken(); 92 | var now = Date.now() / 1000; 93 | if (token && ((token.expires_at - now) > (60))) { 94 | return $q.when(token); 95 | } else { 96 | var params = { 97 | 'client_id': config.clientId, 98 | 'scope': config.scopes, 99 | 'immediate': immediateMode, 100 | 'user_id': userId 101 | }; 102 | var deferred = $q.defer(); 103 | gapi.auth.authorize(params, function (result) { 104 | if (result && !result.error) { 105 | deferred.resolve(result); 106 | } else { 107 | deferred.reject(result); 108 | } 109 | $rootScope.$digest(); 110 | }); 111 | return deferred.promise; 112 | } 113 | }; 114 | 115 | /** 116 | * Actually load a document. If the document is new, initializes 117 | * the model with an empty list of todos. 118 | * 119 | * @param id 120 | * @returns {angular.$q.promise} 121 | */ 122 | this.load = function (id) { 123 | var deferred = $q.defer(); 124 | var initialize = function (model) { 125 | model.getRoot().set('todos', model.createList()); 126 | }; 127 | var onLoad = function (document) { 128 | this.setDocument(id, document); 129 | deferred.resolve(document); 130 | $rootScope.$digest(); 131 | }.bind(this); 132 | var onError = function (error) { 133 | if (error.type === gapi.drive.realtime.ErrorType.TOKEN_REFRESH_REQUIRED) { 134 | $rootScope.$emit('todos.token_refresh_required'); 135 | } else if (error.type === gapi.drive.realtime.ErrorType.CLIENT_ERROR) { 136 | $rootScope.$emit('todos.client_error'); 137 | } else if (error.type === gapi.drive.realtime.ErrorType.NOT_FOUND) { 138 | deferred.reject(error); 139 | $rootScope.$emit('todos.not_found', id); 140 | } 141 | $rootScope.$digest(); 142 | }; 143 | gapi.drive.realtime.load(id, onLoad, initialize, onError); 144 | return deferred.promise; 145 | }; 146 | 147 | /** 148 | * Watches the model for any remote changes to force a digest cycle 149 | * 150 | * @param event 151 | */ 152 | this.changeListener = function (event) { 153 | if (!event.isLocal) { 154 | $rootScope.$digest(); 155 | } 156 | }; 157 | 158 | this.setDocument = function (id, document) { 159 | document.getModel().getRoot().addEventListener( 160 | gapi.drive.realtime.EventType.OBJECT_CHANGED, 161 | this.changeListener); 162 | this.document = document; 163 | this.id = id; 164 | }; 165 | }] 166 | ); 167 | -------------------------------------------------------------------------------- /app/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import "../components/todomvc-common/base.css"; 2 | 3 | body { 4 | width: 800px !important; 5 | } 6 | 7 | #undo, #redo { 8 | float: right; 9 | position: relative; 10 | line-height: 20px; 11 | text-decoration: none; 12 | background: rgba(0, 0, 0, 0.1); 13 | font-size: 11px; 14 | padding: 0 10px; 15 | margin: 0 0px 0px 2px; 16 | border-radius: 3px; 17 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2); 18 | 19 | &:hover { 20 | background: rgba(0, 0, 0, 0.15); 21 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3); 22 | } 23 | } 24 | 25 | #collaborators { 26 | position: absolute; 27 | right: 20px; 28 | top: 20px; 29 | float: right; 30 | margin-right: 25px; 31 | } 32 | 33 | #collaborators > img { 34 | margin-right: 5px; 35 | height: 30px; 36 | width: 30px; 37 | padding-bottom: 5px; 38 | margin-top: -3px; 39 | border-radius: 3px; 40 | } 41 | 42 | #signinButton { 43 | width: 100%; 44 | text-align: center; 45 | margin: 0 auto; 46 | } 47 | 48 | #loading { 49 | margin: 130px 0 40px 0; 50 | text-align: center; 51 | position: relative; 52 | } 53 | 54 | 55 | #share { 56 | -webkit-box-shadow: none; 57 | -moz-box-shadow: none; 58 | box-shadow: none; 59 | background-color: #4d90fe; 60 | background-image: -webkit-linear-gradient(top,#4d90fe,#4787ed); 61 | background-image: -moz-linear-gradient(top,#4d90fe,#4787ed); 62 | background-image: -ms-linear-gradient(top,#4d90fe,#4787ed); 63 | background-image: -o-linear-gradient(top,#4d90fe,#4787ed); 64 | background-image: linear-gradient(top,#4d90fe,#4787ed); 65 | -webkit-border-radius: 2px; 66 | border-radius: 2px; 67 | -webkit-user-select: none; 68 | -webkit-font-smoothing: antialiased; 69 | border: 1px solid #3079ed; 70 | color: #fff; 71 | font-size: 14px; 72 | height: 35px; 73 | vertical-align: middle; 74 | line-height: 30px; 75 | font-weight: bold; 76 | display: inline-block; 77 | font-family: arial,sans-serif; 78 | outline: none; 79 | overflow: hidden; 80 | text-decoration: none; 81 | white-space: nowrap; 82 | display: inline-block; 83 | margin-top: -3px; 84 | min-width: 54px; 85 | vertical-align: top; 86 | }; 87 | 88 | #signin { 89 | background-color: #dd4b39; 90 | -webkit-box-shadow: 0 1px 0 rgba(0,0,0,0.10); 91 | box-shadow: 0 1px 0 rgba(0,0,0,0.10); 92 | -webkit-border-radius: 3px; 93 | border-radius: 3px; 94 | font-size: 18px; 95 | height: 43px; 96 | line-height: 43px; 97 | font-weight: bold; 98 | -webkit-user-select: none; 99 | -webkit-font-smoothing: antialiased; 100 | display: inline-block; 101 | font-family: arial,sans-serif; 102 | outline: none; 103 | overflow: hidden; 104 | position: relative; 105 | text-decoration: none; 106 | white-space: nowrap; 107 | 108 | &:hover { 109 | background: #e74b37; 110 | cursor: hand; 111 | } 112 | 113 | span.label { 114 | font-weight: bold; 115 | } 116 | 117 | span.buttonText { 118 | display: inline-block; 119 | vertical-align: middle; 120 | color: #fff; 121 | padding: 0 30px; 122 | } 123 | 124 | } 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /app/views/install.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | 7 | 8 | 34 |
35 |
36 | 37 |
38 | 41 |
42 | 43 | -------------------------------------------------------------------------------- /app/views/loading.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{message}}

4 |
5 |
6 | -------------------------------------------------------------------------------- /app/views/main.html: -------------------------------------------------------------------------------- 1 |
2 | {{collaborator.displayName}} 7 | 8 |
9 | 10 |
11 | 17 |
18 | 19 | 20 | 32 |
33 | 52 |
53 | 57 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "realtime-tasks", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "jquery": "1.9", 6 | "angular": "~1.0.5", 7 | "todomvc-common": "0.1.2" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "realtime-tasks", 3 | "version": "0.0.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/googledrive/realtime-tasks.git" 7 | }, 8 | "dependencies": {}, 9 | "devDependencies": { 10 | "grunt": "0.4.5", 11 | "grunt-contrib-copy": "0.4.0rc7", 12 | "grunt-contrib-concat": "0.1.2rc6", 13 | "grunt-contrib-uglify": "0.1.1rc6", 14 | "grunt-contrib-compass": "0.1.1rc8", 15 | "grunt-contrib-jshint": "0.1.1rc6", 16 | "grunt-contrib-mincss": "0.4.0rc7", 17 | "grunt-contrib-connect": "0.1.1rc6", 18 | "grunt-contrib-clean": "0.4.0rc6", 19 | "grunt-contrib-htmlmin": "0.1.1rc7", 20 | "grunt-contrib-imagemin": "0.1.1rc8", 21 | "grunt-contrib-livereload": "0.1.0rc8", 22 | "grunt-usemin": "2.6.2", 23 | "grunt-regarde": "~0.1.1", 24 | "grunt-open": "~0.1.0", 25 | "matchdep": "~0.1.1", 26 | "grunt-google-cdn": "0.4.3", 27 | "grunt-ngmin": "~0.0.1" 28 | }, 29 | "engines": { 30 | "node": ">=0.8.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/realtime-tasks/67311d2942035b05c13f55ded1c9ca9cf9c638de/screenshot.png --------------------------------------------------------------------------------