├── .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 | [](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 |
148 |
a mistyped address
149 |
an out-of-date link
150 |
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 |
3 |