127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Collaborative Crowd Game #
2 |
3 | [SayThat](https://saythat.io) is a game where players watch a video together on a big screen (such as a projector), and speak or type to their phones in their language of choice what they see on the screen. Players score points when they correctly guess a word that the Cloud Video Intelligence API produced when analyzing the video.
4 |
5 | The game was first created for a talk at Google I/O 2017 titled **Supercharging Firebase Apps with Machine Learning and Cloud Function**. You can watch the video [here](https://www.youtube.com/watch?v=RdqV_N0sCpM).
6 |
7 | This game highlights how [Cloud Functions for Firebase](https://firebase.google.com/docs/functions/) can be used to easily add Machine Learning to an app. The code in this repository demonstrates uses of:
8 |
9 | * Cloud Speech API
10 | * Cloud Translate API
11 |
12 | Additionally, to analyze the video and produce the words to be guessed, we propose using the Cloud Video Intelligence API to annotate your videos.
13 |
14 | ### Gameplay Architecture
15 | A game involves the following steps:
16 |
17 | * The administrator prepares a number of videos to show, and populates a list of relevant words to guess in the Firebase Database, using the data layout described below. Specifically, they add nouns under the path `/admin/scenes/{scene}/nouns/en-US/{noun}`.
18 | * The Cloud Function `translateNoun` triggers, to translate the noun into all supported languages.
19 | * The translations are written to `/admin/scene/{scene}/nouns/{language_code}/{noun}`.
20 | * Users go to the game and log in.
21 | * The Cloud Function `setDefaults` triggers to set the user's default language.
22 | * Users speak a word.
23 | * An audio file with the recording is uploaded to Cloud Storage for Firebase.
24 | * The Cloud Function `analyzeSpeech` triggers, which uses the Cloud Speech API to transcribe the spoken word.
25 | * The guessed word (noun) is written back to the Firebase Database, as if it were typed in directly by the user.
26 | * User types a word.
27 | * The guess is written to the Firebase Realtime Database.
28 | * The Cloud Function `judgeGuessedNoun` triggers, which assigns a score (0 or 1) to the guessed noun.
29 | * The score is written back to the Firebase Realtime Database.
30 | * The score is updated.
31 | * The Cloud Function `updateCollectiveScores` triggers, which updates all audience-total scores.
32 |
33 | ### Code layout
34 | This repository follows the common Firebase pattern of storing its static public files in a `public` directory, and storing its server-side Cloud Functions for Firebase code in the `functions` directory.
35 |
36 | The game has two main screens:
37 |
38 | * The game screen is the one which users use to play the game on their phones. It is served from `public/index.html`, executing JavaScript code in `public/scripts/index.js`.
39 | * The projector screen is the one which is displayed on the large screen all users are watching. It is served from `public/projector/index.html`, executing JavaScript code in `public/scripts/projector.js`.
40 |
41 | The videos to play the game are not provided with the source code; the developers will want to source their own. The resulting videos, e.g. `dog.mp4` should be placed in the `public/videos` folder, e.g. as `public/videos/dog.mp4`.
42 |
43 | All the server-side Cloud Functions for Firebase code is written in TypeScript, and is found in the `functions/src` directory. It is unit-tested by the code in the `functions/spec` directory. To compile this code, run:
44 |
45 | ```
46 | $ npm run build
47 | ```
48 |
49 | To build and run unit tests, run:
50 | ```
51 | $ npm test
52 | ```
53 |
54 | To build, run unit tests, and deploy to the Firebase project with the `prod` alias, run:
55 | ```
56 | $ npm run deploy-prod
57 | ```
58 |
59 | ### Data layout
60 | SayThat uses the Firebase Realtime Database to store its data and synchronize it with clients and servers. The minimal data structure is:
61 | ```
62 | + "admin"
63 | + "active": true // Flag to enable/disable guess-buttons on clients. Start with 'true'.
64 | + "current_scene": ... // The name of the current scene. E.g. "beach".
65 | + "scenes"
66 | + {scene_name} // E.g. "beach".
67 | + "nouns"
68 | + "en-US" // Words added to this list will automatically get translated.
69 | + {some_noun}: {some_noun} // E.g. "sand": "sand".
70 | + {some_other_noun}: {some_other_noun} // E.g. "surf": "surf".
71 | + ...
72 | + {another_scene_name} // E.g. "dog".
73 | + ...
74 | ```
75 | The remainder of the data structure is generated automatically by the code.
76 |
77 | ### Deploying the code
78 | Begin by creating a project in the [Firebase Console](https://console.firebase.google.com/). Use the Console to pre-fill the Firebase Database with the data structure described above.
79 |
80 | Make sure you have the latest `firebase` command-line tool installed by running:
81 | ```
82 | $ npm install -g firebase-tools
83 | ```
84 |
85 | Next, clone this repository to a convenient directory on your machine. Within that...
86 |
87 | * Create a `public/videos` folder.
88 | * Add a few videos to that folder, using names like `beach.mp4` for a video that's associated with the scene `beach`.
89 |
90 | Next, within the project folder, run:
91 | ```
92 | $ firebase init
93 | ```
94 | In the wizard, choose to enable the Database, Functions and Hosting.
95 |
96 | We must allow the Cloud Speech API to read objects uploaded to Cloud Storage for Firebase. This is easiest to do from Google's larger [Cloud Console](https://console.cloud.google.com/), which serves both Firebase and the larger Google Cloud Platform. In the Cloud Console...
97 |
98 | * Make sure you select the correct project in the drop-down menu at the top.
99 | * Click the "hamburger menu" (three horizontal stripes) at the left-top.
100 | * Click "Storage" in the menu that pops out. You'll see a list of your Cloud Storage "buckets".
101 | * On the far right of the first bucket, click the three vertical dots that open its overflow menu.
102 | * Click "Edit default object permissions".
103 | * Add an entry that specifies:
104 | * Entity: "User"
105 | * Name: "allUsers"
106 | * Access: "Reader"
107 | * Click "Save".
108 |
109 | The database security rules are currently configured to only allow users with an email address that ends in "@google.com" to access administrative parts of the database. You can change this behavior by modifying **database.rules.json**, and replacing every instance of `/.*@google.com$/` with `your_email@domain.com` or `/.*@your_domain.com$/`. You can learn more about Firebase database rules [here](https://firebase.google.com/docs/database/security/).
110 |
111 | We're now ready to deploy our code, by running:
112 | ```
113 | $ npm run build
114 | $ npm test
115 | $ firebase deploy
116 | ```
117 |
118 | If you want to use multiple projects, such as a staging-project and a production-project, we suggest adding a `prod` alias for your production project:
119 | ```
120 | $ firebase use --add
121 | ```
122 |
123 | You may now shortcut the build, test and deploy step with a single command:
124 | ```
125 | $ npm run deploy-prod
126 | ```
127 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 | Page Not Found
20 |
85 |
86 |
87 |
Page Not Found
88 |
This specified file was not found on this website. Please check the URL for mistakes and try again.
89 |
Why am I seeing this?
90 |
This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.
91 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/public/stylesheets/custom.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2017 Google Inc. All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | body {
17 | font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
18 | line-height: 1.25;
19 | margin: 0px;
20 | background: #f9f9f9;
21 | }
22 |
23 | header div.mdl-layout__header-row {
24 | background:#fff;
25 | border-bottom: 1px solid #f0f0f0;
26 | color: #000;
27 | }
28 |
29 | div.logo_header {
30 | background: url(/images/logo_header.png) no-repeat;
31 | background-size: 122px 23px;
32 | width: 122px;
33 | height: 23px;
34 | }
35 |
36 | a#sign-out {
37 | color: #bbb;
38 | width: 54px;
39 | max-width: 54px;
40 | text-align: right;
41 | }
42 | a#change-lang {
43 | color: #bbb;
44 | width: 54px;
45 | max-width: 54px;
46 | display:inline-block;
47 | /* Required for long language names for header*/
48 | text-overflow: ellipsis;
49 | overflow:hidden;
50 | white-space:nowrap;
51 | }
52 | .page-content {
53 | margin: 10px auto 0px;
54 | max-width: 1200px;
55 | }
56 |
57 | #sign-out {
58 | cursor: pointer;
59 | }
60 |
61 | div#activity-stream {
62 | margin: 0px 2px 10px 2px;
63 | max-height: 400px;
64 | }
65 |
66 | span.chip {
67 | font-size: 14px;
68 | display: inline-block;
69 | padding: 8px 16px;
70 | border-radius: 500px;
71 | margin-bottom: 8px;
72 | margin-right: 7px;
73 | background: #E9E9E9;
74 | }
75 | span.chip.true {
76 | display: inline-block;
77 | background-color: #21B2F3;
78 | color: #fff;
79 | opacity: 1.0;
80 | }
81 |
82 | span.chip.false {
83 | display: inline-block;
84 | background-color:#EB9696;
85 | color: #fff;
86 | }
87 |
88 | div#input-container {
89 | text-align: center;
90 | }
91 | div#input-container label.helper {
92 | color: #888;
93 | line-height: 1.5em;
94 | font-size: 0.8em;
95 | display: block;
96 | margin-top: 14px;
97 | text-align: center;
98 | }
99 |
100 | div.clearfix {
101 | clear: both;
102 | }
103 |
104 | div#left_pane {
105 | min-width: 320px;
106 | }
107 |
108 | div#keyboard_input {
109 | max-width: 320px;
110 | }
111 | div#keyboard_input div#noun_guess {
112 | width: 180px;
113 | margin-left: 10px;
114 | }
115 |
116 | div#keyboard_input button {
117 | width: 98px;
118 | padding-left: 4px;
119 | padding-right: 4px;
120 | }
121 |
122 | div.mdl-layout__header-row {
123 | padding-left: 20px;
124 | }
125 |
126 | div#firebaseui-container {
127 | margin-top: 50px;
128 | }
129 |
130 | div.mdl-textfield#noun_guess {
131 | margin-right: 20px;
132 | }
133 |
134 | a#change-lang {
135 | cursor: pointer;
136 | font-weight: 500;
137 | }
138 |
139 | a#change-lang i.material-icons {
140 | width: 18px;
141 | height: 18px;
142 | margin-bottom: 8px;
143 | }
144 |
145 | div.lang_wrapper {
146 | background: #fff;
147 | text-align:center;
148 | color: #222;
149 | margin-top: -10px;
150 | margin-bottom: 20px;
151 | }
152 | div.lang_wrapper div#lang-picker-container {
153 | padding: 10px 0;
154 | height:100vh;
155 | }
156 |
157 | div.lang_wrapper select {
158 | border: 1px solid #ddd;
159 | padding: 7px;
160 | margin-right: 10px;
161 | margin-top: 10px;
162 | }
163 |
164 | div#game-container {
165 | margin-top: 10px;
166 | max-width: 1200px;
167 | justify-content: center;
168 | text-align: center;
169 | }
170 |
171 | table.header-stats {
172 | width: 190px;
173 | margin: 10px auto 0px;;
174 | padding-bottom: 10px;
175 | padding-left: 10px;
176 | border-bottom: 1px solid #eee;
177 | }
178 |
179 | table.header-stats span.mdl-layout-title {
180 | font-size: 14px;
181 | }
182 | table.header-stats div.stat {
183 | display: inline-block;
184 | }
185 | table.header-stats span.title {
186 | display: block;
187 | font-size: 1.0em;
188 | text-align: center;
189 | color: #ccc;
190 | width: 92px;
191 | margin: 4px 8px 0px;
192 | }
193 |
194 | table.header-stats span.score {
195 | display: block;
196 | font-size: 1.6em;
197 | font-weight: 500;
198 | text-align: center;
199 | color: #444;
200 | width: 92px;
201 | margin: 0px 8px;
202 | }
203 |
204 | div#personal_stats {
205 | min-width: 320px;
206 | }
207 |
208 | div#personal_stats table {
209 | width: 100%;
210 | }
211 |
212 | div#personal_stats table.mdl-data-table thead td {
213 | background: #f8f8f8;
214 | height: 28px;
215 | }
216 |
217 | footer.mic {
218 | position: absolute;
219 | bottom: 2%;
220 | left: 0;
221 | width: 100%;
222 | }
223 |
224 | div#listening-indicator {
225 | margin: 0px auto 20px;
226 | max-width: 300px;
227 | text-align:center;
228 | }
229 |
230 | div#listening-indicator span {
231 | font-size: 14px;
232 | margin-bottom: 6px;
233 | display: block;
234 | color: #ccc;
235 |
236 | }
237 |
238 | i.icon-toggle {
239 | font-size: 28px;
240 | opacity: 0.2;
241 | margin-top: 0px;
242 | cursor: pointer;
243 | }
244 |
245 | .firebaseui-idp-text {
246 | color: #757575;
247 | }
248 |
249 | #scene-selector-container {
250 | padding-bottom: 10px;
251 | margin-bottom: 10px;
252 | }
253 | select#scene-selector {
254 | padding: 4px 10px;
255 | background: #fff;
256 | height: 34px;
257 | margin-right: 10px;
258 | border: 1px solid #eee;
259 | font-size: 16px;
260 | width: 200px;
261 | }
262 | select:focus {
263 | outline: none;
264 | }
265 |
266 | #scores-wrapper {
267 | text-align:center;
268 | margin: 0px auto 30px;
269 | width: 280px;
270 | }
271 |
272 | #scores-wrapper div {
273 | width: 40%;
274 | margin-left: 10px;
275 | margin-right: 10px;
276 | }
277 | div.float-left {
278 | float: left;
279 | }
280 |
281 | div.float-right {
282 | float: right;
283 | }
284 |
285 | #scores-wrapper span {
286 | text-align: center;
287 | font-size: 36px;
288 | font-weight: normal;
289 | display: block;
290 | margin-bottom: 10px;
291 | }
292 | label {
293 | display: block;
294 | font-size: 16px;
295 | color: #b0b0b0;
296 | font-weight: 100;
297 | }
298 |
299 | #summary-dialog {
300 | width: 650px;
301 | }
302 |
303 | table#summary-table.mdl-data-table {
304 | border: 0px;
305 | }
306 | table#summary-table.mdl-data-table td {
307 | border-top: 1px solid #f0f0f0;
308 | border-bottom: 0px;
309 | }
310 | #summary-table thead tr th {
311 | background: #f7f7f7;
312 | height: 44px;
313 | }
314 | #summary-table td,th {
315 | min-width: 200px;
316 | font-size: 16px;
317 | color: #555;
318 | }
319 | #summary-table td.noun {
320 | text-align: left;
321 | }
322 | @media only screen and (max-width: 900px) {
323 | @media only screen and (max-width: 840px) {
324 | div#mid-spacer {
325 | display: none;
326 | }
327 | }
328 | }
329 |
330 | @media only screen and (max-width: 480px) {
331 | .page-content {
332 | margin: 10px auto 0px;
333 | }
334 |
335 | div#personal_stats {
336 | margin: 10px auto 0px;
337 | }
338 | }
339 |
340 | @media only screen and (-moz-min-device-pixel-ratio: 1.5),
341 | only screen and (-o-min-device-pixel-ratio: 3/2),
342 | only screen and (-webkit-min-device-pixel-ratio: 1.5),
343 | only screen and (min-devicepixel-ratio: 1.5),
344 | only screen and (min-resolution: 1.5dppx) {
345 | div.logo_header {
346 | background-image: url(/images/logo_header@2x.png);
347 | }
348 | }
349 |
--------------------------------------------------------------------------------
/functions/src/saythat.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2017 Google Inc. All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | import * as _ from 'lodash';
17 | import * as speechAPI from '@google-cloud/speech';
18 | import * as translateAPI from '@google-cloud/translate';
19 | import * as db from './db';
20 | import * as ProfanityFilter from 'bad-words';
21 |
22 | const speech = speechAPI();
23 | const translate = translateAPI();
24 | const profanityFilter = new ProfanityFilter({ replaceRegex: /[A-Za-z0-9가-힣_]/g });
25 |
26 | export function setDefaults(userId: string): Promise {
27 | return db.set(`/users/${userId}`, {
28 | lang: { // Default language to English
29 | name: 'English',
30 | code: 'en-US'
31 | }
32 | });
33 | }
34 |
35 | const speechFilenameRegex = /(\w*).([a-zA-Z\-]*).(\d*).raw/;
36 | export async function analyzeSpeech(url, filename) {
37 | // Parse the filename into its components, which give us user ID, language,
38 | // and timestamp.
39 | const components = filename.match(speechFilenameRegex);
40 | if (components == null) {
41 | console.error('Failed to parse filename ' + filename);
42 | return;
43 | }
44 | const userId = components[1];
45 | const languageCode = components[2];
46 | const timestamp = components[3];
47 |
48 | // Detect speech in the audio file using the Cloud Speech API.
49 | const request = {
50 | encoding: 'LINEAR16',
51 | languageCode: languageCode,
52 | profanityFilter: true,
53 | };
54 | const results = await speech.recognize(url, request);
55 | let transcription = results[0];
56 | const scene = await db.get('/admin/current_scene');
57 | if (transcription == '') {
58 | console.log('Empty transcription, not written.');
59 | await markProcessCompleted(userId, scene, timestamp);
60 | return;
61 | }
62 | let nouns = transcription.split(' ');
63 |
64 | // Persist user guesses in the Firebase Realtime Database.
65 | await writeNounsAsGuesses(nouns, userId, scene);
66 | await markProcessCompleted(userId, scene, timestamp);
67 | }
68 |
69 | function writeNounsAsGuesses(nouns, userId, scene): Promise {
70 | let operations = [];
71 | for (let index in nouns) {
72 | let noun = nouns[index].toLowerCase();
73 | operations.push(db.set(`/users/${userId}/scenes/${scene}/nouns/${noun}`, 'maybe...'));
74 | }
75 | return Promise.all(operations);
76 | }
77 |
78 | function markProcessCompleted(userId, scene, timestamp) {
79 | return db.remove(`/users/${userId}/scenes/${scene}/in_progress/${timestamp}`);
80 | }
81 |
82 | export async function judgeGuessedNoun(userId, scene, noun, guessed_before): Promise {
83 | noun = profanityFilter.clean(noun).toLowerCase();
84 |
85 | // Determine the user's chosen language.
86 | let lang = await getUserLanguage(userId);
87 |
88 | // Determine if the guessed noun appears in the scene, and its English
89 | // translation.
90 | let english = await getOriginalNoun(noun, scene, lang);
91 | let correct = english !== null ? 'true' : 'false';
92 | let score_diff = correct === 'true' && !guessed_before ? 1 : 0;
93 |
94 | // Write the score to all parts of the Firebase Realtime Database that need to
95 | // know.
96 | return Promise.all([
97 | updateAllGuesses(scene, noun, correct, lang, english),
98 | updateCorrectness(userId, scene, noun, correct),
99 | updateScore(userId, scene, score_diff),
100 | updateSummary(scene, english, lang, score_diff),
101 | ]);
102 | }
103 |
104 | function getUserLanguage(userId: string): Promise {
105 | return db.get(`/users/${userId}/lang/code`);
106 | }
107 |
108 | // Returns null if the given noun was not found for the given scene and
109 | // language.
110 | async function getOriginalNoun(
111 | noun: string, scene: string, lang: string): Promise {
112 |
113 | let nouns = await db.get(`/admin/scenes/${scene}/nouns/${lang}`);
114 | if (!_.has(nouns, noun)) {
115 | return null;
116 | }
117 | return nouns[noun];
118 | }
119 |
120 | function updateAllGuesses(
121 | scene: string, noun: string, correct: string, lang: string, english: string) {
122 |
123 | return db.push(`/all_guesses/${scene}`, {
124 | original: noun,
125 | correctness: correct,
126 | lang: lang,
127 | translated: english,
128 | });
129 | }
130 |
131 | function updateCorrectness(userId: string, scene: string, noun: string, correct: string) {
132 | return db.set(`/users/${userId}/scenes/${scene}/nouns/${noun}`, correct);
133 | }
134 |
135 | function updateScore(userId: string, scene: string, diff: number): Promise {
136 | if (diff === 0) return;
137 | return db.transaction(`/users/${userId}/scenes/${scene}/score`,
138 | val => val ? val + diff : diff);
139 | }
140 |
141 | function updateSummary(scene: string, english_noun: string, lang: string, score_diff: number) {
142 | if (score_diff <= 0) return;
143 | return db.transaction(`/summary/${scene}/${english_noun}`, val => {
144 | if (val === null) {
145 | val = {};
146 | }
147 | if (val.langs === undefined || val.langs === null || val.langs === 0) {
148 | val.langs = {};
149 | }
150 | if (!_.has(val.langs, lang)) {
151 | val.langs[lang] = score_diff;
152 | } else {
153 | val.langs[lang] += score_diff;
154 | }
155 | if (val.langs[lang] === 0) {
156 | delete val.langs[lang];
157 | }
158 | val.num_langs = _.size(val.langs);
159 | if (val.score === undefined || val.score === null) {
160 | val.score = 0;
161 | }
162 | val.score += score_diff;
163 | return val;
164 | });
165 | }
166 |
167 | export async function updateCollectiveScores(userId: string, scene: string, diff: number) {
168 | let userLang = await getUserLanguage(userId);
169 |
170 | let operations = [];
171 | operations.push(db.transaction(`/total_scores/${scene}`, val => val + diff));
172 | operations.push(db.transaction(`/total_langs/${scene}`, val => {
173 | if (val === null) {
174 | val = {};
175 | }
176 | if (!_.has(val, 'numLanguages')) {
177 | val['numLanguages'] = 0;
178 | }
179 | if (!_.has(val, userLang) || val[userLang] == 0) {
180 | val['numLanguages'] += 1;
181 | val[userLang] = 0;
182 | }
183 | val[userLang] += diff;
184 | if (val[userLang] <= 0) {
185 | val['numLanguages'] -= 1;
186 | val[userLang] = 0;
187 | }
188 | return val;
189 | }));
190 |
191 | await Promise.all(operations);
192 | }
193 |
194 | // There are two standardized ways to represent language codes: a localized
195 | // version, which distinguishes between accents and regions and such (BCP-47)
196 | // and a generic just-the-language code (ISO-639-2). The Speech API uses BCP-47,
197 | // the Translate API uses ISO-639 Alas, they don't match exactly, especially
198 | // around the various Chinese languages, and so we must map them manually.
199 | import bcp47toISO639 from './bcp47iso639';
200 | export function nounAdded(scene: string, noun: string) {
201 | let operations = [];
202 | _.forEach(bcp47toISO639, (iso639code, bcp47code) => {
203 | if (bcp47code == 'en-US') {
204 | // This is our source language.
205 | return;
206 | }
207 | let options = {
208 | from: 'en',
209 | to: iso639code,
210 | };
211 | operations.push(translate.translate(noun, options).then(results => {
212 | let translations = results[0];
213 | let translation = Array.isArray(translations) ? translations[0] : translations;
214 | translation = translation.toLowerCase(); // For cases like German, which capitalizes nouns.
215 | return db.set(`/admin/scenes/${scene}/nouns/${bcp47code}/${translation}`, noun);
216 | }));
217 | operations.push(db.set(`summary/${scene}/${noun}`, {
218 | num_langs: 0,
219 | score: 0
220 | }));
221 | });
222 | return Promise.all(operations);
223 | }
224 |
225 |
--------------------------------------------------------------------------------
/public/scripts/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2017 Google Inc. All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | // Initialize the FirebaseUI Widget using Firebase.
17 | var ui = new firebaseui.auth.AuthUI(firebase.auth());
18 | // Keep track of the currently signed in user.
19 | var uid = null;
20 | var lang = 'English';
21 | var langCode = 'en-US';
22 | var scene = null;
23 | var micCapable = true;
24 | var gameActive = true;
25 |
26 | // Database references to manage listeners
27 | var userSceneNounsChangedRef = null;
28 | var userSceneNounsAddedRef = null;
29 | var userSceneScoreValueRef = null;
30 | var audienceScoreValueRef = null;
31 | var userSceneNounsProgressAddedRef = null;
32 | var userSceneNounsProgressRemovedRef = null;
33 |
34 | function handleSignedInUser(user) {
35 | uid = user.uid;
36 | $("a#change-lang").show();
37 | $("#user-signed-out").hide();
38 | $("#user-signed-in").show();
39 | $("#sign-out").show();
40 | $(".header-stats").show();
41 | $("span.input-toggles").show();
42 | $("footer.mic").show();
43 | initLanguages();
44 | handleLangSelection();
45 | initDisplay();
46 | };
47 |
48 | function handleSignedOutUser() {
49 | ui.start('#firebaseui-container', uiConfig);
50 |
51 | $(".header-stats").hide();
52 | $("a#change-lang").hide();
53 | $("#user-signed-out").show();
54 | $("#user-signed-in").hide();
55 | $("#sign-out").hide();
56 | $("footer.mic").hide();
57 | $("span.input-toggles").hide();
58 | };
59 |
60 | function handleLangSelection() {
61 | lang = $("#lang-picker :selected").text();
62 | langCode = $('#lang-picker').val();
63 |
64 | if (langCode) {
65 | $('#lang-display').text(lang);
66 | firebase.database().ref('users/' + uid + '/lang').set({
67 | code: langCode,
68 | name: lang
69 | });
70 | }
71 |
72 | $('#lang-picker-container').hide();
73 | $('#input-container').show();
74 | }
75 |
76 | function uploadSpeechFile(blob) {
77 | $('#start').prop('disabled', false);
78 | if (!gameActive) {
79 | alert('Round over, cannot submit more guesses.');
80 | return;
81 | }
82 | var timestamp = Date.now();
83 | var fileName = 'speech/' + uid + '.' + langCode + '.' + timestamp + '.raw';
84 | var storageRef = firebase.storage().ref().child(fileName);
85 | storageRef.put(blob).then(function(snapshot) {
86 | console.log('uploaded')
87 | firebase.database().ref('/users/' + uid + '/scenes/' + scene + '/in_progress/' + timestamp)
88 | .set(fileName);
89 | })
90 | }
91 |
92 | function initTalkBtn() {
93 | self.micCapable = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
94 |
95 | if (self.micCapable) {
96 | $('#nomic').hide();
97 |
98 | var timeoutId = 0;
99 | $('#start').on('mousedown touchstart', function() {
100 | timeoutId = setTimeout(function() {
101 | $('#listening-indicator').show();
102 | $('#btnText').text('Listening');
103 | }, 500);
104 | Fr.voice.record(false, function() {});
105 | }).on('mouseup touchend', function() {
106 | $('#listening-indicator').hide();
107 | $('#start').prop('disabled', true);
108 | $('#btnText').text('Processing');
109 | Fr.voice.export(function(f) {uploadSpeechFile(f);}, 'blob');
110 | Fr.voice.stop();
111 | clearTimeout(timeoutId);
112 | });
113 |
114 | $("#enable-mic").hide();
115 | $("#enable-keyboard").show();
116 | } else {
117 | toggleMicButton(false);
118 | }
119 |
120 | $("#enable-keyboard").click(function(e){
121 | toggleMicButton(false)
122 | });
123 |
124 | $("#enable-mic").click(function(e){
125 | toggleMicButton(true)
126 | });
127 | }
128 |
129 | function toggleMicButton(showMic) {
130 | if (showMic) {
131 | $("#mic-container").show();
132 | $("#keyboard_input").hide();
133 | $("#enable-mic").hide();
134 | $("#enable-keyboard").show();
135 | } else {
136 | $("#mic-container").hide();
137 | $("#keyboard_input").show();
138 | $("#enable-keyboard").hide();
139 |
140 | if(self.micCapable) {
141 | $("#enable-mic").show();
142 | } else {
143 | $("#enable-mic").hide();
144 | }
145 | }
146 | }
147 |
148 | function detachListener(ref) {
149 | if (ref != null) {
150 | ref.off();
151 | }
152 | }
153 |
154 | function showMetrics() {
155 | detachListener(userSceneScoreValueRef);
156 | userSceneScoreValueRef = firebase.database().ref('/users/' + uid + '/scenes/' + scene + '/score');
157 | userSceneScoreValueRef.on('value', function(snapshot) {
158 | $("#scene_score").html(snapshot.val() != null ? snapshot.val() : 0);
159 | });
160 |
161 | detachListener(audienceScoreValueRef);
162 | audienceScoreValueRef = firebase.database().ref('/total_scores/' + scene);
163 | audienceScoreValueRef.on('value', function(snapshot) {
164 | $("#audience_score").html(snapshot.val() != null ? snapshot.val() : 0);
165 | });
166 | }
167 |
168 | function showGuesses() {
169 | detachListener(userSceneNounsAddedRef);
170 | userSceneNounsAddedRef = firebase.database().ref('/users/' + uid + '/scenes/' + scene + '/nouns');
171 | userSceneNounsAddedRef.on('child_added', function(snapshot) {
172 | var noun = snapshot.key;
173 | var correctness = snapshot.val()
174 | var $pillbox = $("" + noun + "");
175 | $pillbox.addClass("chip "+ correctness);
176 | $pillbox.attr("id", "guess-" + noun);
177 | $("#activity-stream").prepend($pillbox);
178 | });
179 |
180 | detachListener(userSceneNounsChangedRef);
181 | userSceneNounsChangedRef = firebase.database().ref('/users/' + uid + '/scenes/' + scene + '/nouns');
182 | userSceneNounsChangedRef.on('child_changed', function(snapshot) {
183 | var $pillbox = $("#guess-" + snapshot.key)
184 | $pillbox.removeClass();
185 | $pillbox.addClass("chip " + snapshot.val());
186 | });
187 |
188 | detachListener(userSceneNounsProgressAddedRef);
189 | userSceneNounsProgressAddedRef = firebase.database().ref('/users/' + uid + '/scenes/' + scene + '/in_progress');
190 | userSceneNounsProgressAddedRef.on('child_added', function(snapshot, prevChildKey) {
191 | console.log('child added: ', snapshot.key)
192 | var $loader = $('' +
193 | '' +
195 | '');
196 | $loader.attr("id", "loader-" + snapshot.key);
197 | $("#activity-stream").prepend($loader);
198 | });
199 |
200 | detachListener(userSceneNounsProgressRemovedRef);
201 | userSceneNounsProgressRemovedRef = firebase.database().ref('/users/' + uid + '/scenes/' + scene + '/in_progress');
202 | userSceneNounsProgressRemovedRef.on('child_removed', function(snapshot) {
203 | $('#loader-' + snapshot.key).hide();
204 | console.log('child removed: ', snapshot.key)
205 | });
206 | }
207 |
208 | function showToast(message) {
209 | let snackbar = document.querySelector('.mdl-js-snackbar');
210 | snackbar.MaterialSnackbar.showSnackbar({message: message});
211 | }
212 |
213 | function initDisplay() {
214 | // Setup listener to show notification when scene changes and remove guesses
215 | firebase.database().ref('/admin/current_scene').on('value', function(snapshot) {
216 | if (scene !== snapshot.val()) {
217 | scene = snapshot.val();
218 | console.log('Scene changed to: ' + scene);
219 | $("#activity-stream").empty();
220 | showGuesses();
221 | showMetrics();
222 | showToast(`Current scene is "${scene}"`);
223 | }
224 | });
225 | }
226 |
227 | function addTypedGuess() {
228 | if (!gameActive) {
229 | alert('Round over, cannot submit more guesses.');
230 | return;
231 | }
232 | // Write the guessed noun (trimmed) to the list of guesses
233 | let noun = $.trim($('#noun').val().toLowerCase());
234 |
235 | console.log(noun);
236 | $('#noun').val("");
237 | // Starts with value "maybe...", gets updated to true or false when the guess has been validated.
238 | return firebase.database().ref('/users/' + uid + '/scenes/' + scene + '/nouns/' + noun).set("maybe...");
239 | }
240 |
241 | function initLanguages() {
242 | $('#lang-display').text(lang);
243 | $("#lang-picker").empty();
244 | var options = ''
245 | for (var i = 0; i < availableLangs.length; i++){
246 | var name = availableLangs[i];
247 | options += '';
250 | }
251 | $('#lang-picker').append(options);
252 |
253 | firebase.database().ref('users/' + uid + '/lang').once('value').then(function(snapshot) {
254 | if (snapshot.val()) {
255 | lang = snapshot.val().name;
256 | langCode = snapshot.val().code;
257 | $('#lang-display').text(lang);
258 | } else {
259 | $('#lang-display').text('English');
260 | }
261 | });
262 |
263 | $('#submit-lang').click(handleLangSelection);
264 | $('#change-lang').click(function() {
265 | $('#lang-picker-container').toggle();
266 | $('#input-container').toggle();
267 | });
268 | }
269 |
270 | function initApp() {
271 | firebase.database().ref('/admin/active').on('value', function(snapshot) {
272 | gameActive = snapshot.val();
273 | });
274 |
275 | $('#sign-out').click(function() {
276 | firebase.auth().signOut();
277 | });
278 |
279 | initTalkBtn();
280 |
281 | $('#guess-btn').click(addTypedGuess);
282 | $('input#noun').keypress(function (e) {
283 | if (e.which == 13) {
284 | addTypedGuess();
285 | return false;
286 | }
287 | });
288 | };
289 |
290 | $(document).ready(function() {
291 | firebase.auth().onAuthStateChanged(function(user) {
292 | if (user && user.uid == uid) {
293 | return;
294 | }
295 | user ? handleSignedInUser(user) : handleSignedOutUser();
296 | });
297 |
298 | initApp();
299 | });
300 |
--------------------------------------------------------------------------------
/functions/spec/index.spec.ts:
--------------------------------------------------------------------------------
1 | import * as sinon from 'sinon';
2 | import * as chai from 'chai';
3 | import { expect } from 'chai';
4 | import * as chaiAsPromised from 'chai-as-promised';
5 | chai.use(chaiAsPromised);
6 |
7 | import * as nock from 'nock';
8 | nock.disableNetConnect();
9 |
10 | // Use a fake database.
11 | import * as fakedb from './fake-db';
12 | fakedb.init('../src/db');
13 |
14 | // Use a fake Speech API.
15 | import * as fakespeech from './fake-speech';
16 | fakespeech.init();
17 |
18 | // Use a fake Translate API.
19 | import * as faketranslate from './fake-translate';
20 | faketranslate.init();
21 |
22 | // Use a fake Firebase configuration.
23 | import * as fakeconfig from './fake-config';
24 | fakeconfig.init();
25 |
26 | // Use a fake Firebase Admin.
27 | import * as fakeadmin from './fake-admin';
28 | fakeadmin.init();
29 |
30 | // Ready to go!
31 | import * as saythat from '../src/saythat';
32 |
33 | // Some test input data that we'll use in multiple tests.
34 | const url = 'myurl://test';
35 | const userId = '30hLypzZHnPHWrhw0pLx494fFsI2'
36 | const lang = 'nl-NL';
37 | const timestamp = '1494364778488';
38 | const filename = `speech/${userId}.${lang}.${timestamp}.raw`;
39 | const scene = 'myCoolFancyScene';
40 |
41 | describe('saythat', () => {
42 | beforeEach(async () => {
43 | faketranslate.reset();
44 | fakedb.reset();
45 | await fakedb.set('/admin/current_scene', scene);
46 | await fakedb.set(`/users/${userId}/lang/code`, lang);
47 | });
48 |
49 | describe('setDefaults', () => {
50 | it('should set the default language', async () => {
51 | await saythat.setDefaults(userId);
52 | await expect(fakedb.get(`/users/${userId}`)).to.eventually.deep.equal({
53 | lang: {
54 | name: 'English',
55 | code: 'en-US'
56 | },
57 | });
58 | });
59 | });
60 |
61 | describe('analyzeSpeech', () => {
62 | it('should parse the components of a speech filename correctly', async () => {
63 | const noun = 'appeltaart';
64 | fakespeech.setResponse([noun]);
65 | fakedb.set(`/users/${userId}/scenes/${scene}/in_progress/${timestamp}`, '...');
66 |
67 | await saythat.analyzeSpeech(url, filename);
68 | await expect(fakespeech.getLastUrl()).to.equal(url);
69 | await expect(fakespeech.getLastRequest().languageCode).to.equal(lang);
70 | await expect(fakespeech.getLastRequest().encoding).to.equal('LINEAR16');
71 | await expect(fakedb.get(`/users/${userId}/scenes/${scene}/nouns/${noun}`)).to.eventually.equal('maybe...');
72 | await expect(fakedb.has(`/users/${userId}/scenes/${scene}/in_progress/${timestamp}`)).to.equal(false);
73 | });
74 |
75 | it('should parse multiple words into multiple guesses', async () => {
76 | fakespeech.setResponse(['veel kleine appeltaartjes']);
77 |
78 | await saythat.analyzeSpeech(url, filename);
79 | await expect(fakedb.get(`/users/${userId}/scenes/${scene}/nouns/veel`)).to.eventually.equal('maybe...');
80 | await expect(fakedb.get(`/users/${userId}/scenes/${scene}/nouns/kleine`)).to.eventually.equal('maybe...');
81 | await expect(fakedb.get(`/users/${userId}/scenes/${scene}/nouns/appeltaartjes`)).to.eventually.equal('maybe...');
82 | });
83 |
84 | it('should ignore secondary transcriptions returned by the speech API', async () => {
85 | fakespeech.setResponse(['meer', 'kleine', 'appeltaartjes']);
86 |
87 | await saythat.analyzeSpeech(url, filename);
88 | await expect(fakedb.get(`/users/${userId}/scenes/${scene}/nouns/meer`)).to.eventually.equal('maybe...');
89 | await expect(fakedb.has(`/users/${userId}/scenes/${scene}/nouns/kleine`)).to.equal(false);
90 | await expect(fakedb.has(`/users/${userId}/scenes/${scene}/nouns/appeltaartjes`)).to.equal(false);
91 | });
92 |
93 | it('should always output lower-case resuls', async () => {
94 | fakespeech.setResponse(['Cantaloupe']);
95 |
96 | await saythat.analyzeSpeech(url, filename);
97 | await expect(fakedb.get(`/users/${userId}/scenes/${scene}/nouns/cantaloupe`)).to.eventually.equal('maybe...');
98 | });
99 | });
100 |
101 | describe('judgeGuessedNoun', () => {
102 | let langs = {
103 | langs: {
104 | 'nl-NL': 1,
105 | 'de-DE': 1,
106 | },
107 | num_langs: 2,
108 | score: 2,
109 | };
110 | let noun = 'taart';
111 | let english = 'pie';
112 | function insertCorrectNoun() {
113 | fakedb.set(`/admin/scenes/${scene}/nouns/${lang}/${noun}`, english);
114 | let langObj = {};
115 | langObj[noun] = english;
116 | fakedb.set(`/admin/scenes/${scene}/nouns/${lang}`, langObj);
117 | }
118 |
119 | it('should accurately determine incorrectness', async () => {
120 | await fakedb.set(`/users/${userId}/scenes/${scene}/score`, 0);
121 | await fakedb.set(`/summary/${scene}/${noun}`, langs);
122 |
123 | await saythat.judgeGuessedNoun(userId, scene, noun, false);
124 | await expect(fakedb.get(`/users/${userId}/scenes/${scene}/nouns/${noun}`)).to.eventually.equal('false');
125 | await expect(fakedb.get(`/users/${userId}/scenes/${scene}/score`)).to.eventually.equal(0);
126 | await expect(fakedb.get(`/all_guesses/${scene}/pushprefix-0`)).to.eventually.deep.equal({
127 | original: noun,
128 | correctness: "false",
129 | lang: lang,
130 | translated: null,
131 | });
132 | await expect(fakedb.get(`/summary/${scene}/${noun}`)).to.eventually.deep.equal(langs);
133 | });
134 |
135 | it('should accurately determine correctness', async () => {
136 | insertCorrectNoun();
137 | fakedb.set(`/users/${userId}/scenes/${scene}/score`, 0);
138 | await fakedb.set(`/summary/${scene}/${english}`, langs);
139 |
140 | await saythat.judgeGuessedNoun(userId, scene, noun, false);
141 | await expect(fakedb.get(`/users/${userId}/scenes/${scene}/nouns/${noun}`)).to.eventually.equal('true');
142 | await expect(fakedb.get(`/users/${userId}/scenes/${scene}/score`)).to.eventually.equal(1);
143 | await expect(fakedb.get(`/all_guesses/${scene}/pushprefix-0`)).to.eventually.deep.equal({
144 | original: noun,
145 | correctness: "true",
146 | lang: lang,
147 | translated: english,
148 | });
149 | await expect(fakedb.get(`/summary/${scene}/${english}`)).to.eventually.deep.equal({
150 | langs: {
151 | 'nl-NL': 2,
152 | 'de-DE': 1,
153 | },
154 | num_langs: 2,
155 | score: 3,
156 | });
157 | });
158 |
159 | it('should initialize the score if there wasn\'t one yet', async () => {
160 | insertCorrectNoun();
161 |
162 | await saythat.judgeGuessedNoun(userId, scene, noun, false);
163 | await expect(fakedb.get(`/users/${userId}/scenes/${scene}/score`)).to.eventually.equal(1);
164 | await expect(fakedb.get(`/summary/${scene}/${english}`)).to.eventually.deep.equal({
165 | langs: {
166 | 'nl-NL': 1,
167 | },
168 | num_langs: 1,
169 | score: 1,
170 | });
171 | });
172 |
173 | it('should not override existing language counters when adding a new one', async () => {
174 | insertCorrectNoun();
175 | await fakedb.set(`/summary/${scene}/${english}`, {
176 | langs: {
177 | 'de-DE': 1,
178 | },
179 | num_langs: 1,
180 | score: 1,
181 | });
182 |
183 | await saythat.judgeGuessedNoun(userId, scene, noun, false);
184 | await expect(fakedb.get(`/users/${userId}/scenes/${scene}/score`)).to.eventually.equal(1);
185 | await expect(fakedb.get(`/summary/${scene}/${english}`)).to.eventually.deep.equal({
186 | langs: {
187 | 'de-DE': 1,
188 | 'nl-NL': 1,
189 | },
190 | num_langs: 2,
191 | score: 2,
192 | });
193 | });
194 |
195 | it('should filter out duplicate guesses from the same user', async () => {
196 | insertCorrectNoun();
197 | fakedb.set(`/users/${userId}/scenes/${scene}/score`, 1);
198 |
199 | await saythat.judgeGuessedNoun(userId, scene, noun, true /* guessed_before */);
200 | await expect(fakedb.get(`/users/${userId}/scenes/${scene}/nouns/${noun}`)).to.eventually.equal('true');
201 | await expect(fakedb.get(`/users/${userId}/scenes/${scene}/score`)).to.eventually.equal(1);
202 | });
203 | });
204 |
205 | describe('updateCollectiveScores', () => {
206 | it('should create non-existing collective scores', async () => {
207 | await saythat.updateCollectiveScores(userId, scene, 1);
208 | await expect(fakedb.get(`/total_scores/${scene}`)).to.eventually.equal(1);
209 | await expect(fakedb.get(`/total_langs/${scene}`)).to.eventually.deep.equal({
210 | 'nl-NL': 1,
211 | 'numLanguages': 1,
212 | });
213 | });
214 |
215 | it('should increment existing collective scores', async () => {
216 | fakedb.set(`/total_scores/${scene}`, 10);
217 | fakedb.set(`/total_langs/${scene}`, {
218 | 'nl-NL': 5,
219 | 'de-DE': 5,
220 | 'numLanguages': 2,
221 | });
222 |
223 | await saythat.updateCollectiveScores(userId, scene, 1);
224 | await expect(fakedb.get(`/total_scores/${scene}`)).to.eventually.equal(11);
225 | await expect(fakedb.get(`/total_langs/${scene}`)).to.eventually.deep.equal({
226 | 'nl-NL': 6,
227 | 'de-DE': 5,
228 | 'numLanguages': 2,
229 | });
230 | });
231 | });
232 |
233 | describe('nounAdded', () => {
234 | it('should translate a single noun to all languages', async () => {
235 | faketranslate.setResponse('nl', ['kaas']);
236 | faketranslate.setResponse('fr', ['fromage']);
237 |
238 | await saythat.nounAdded(scene, 'cheese');
239 | // 44 languages, but no translation is done for English.
240 | await expect(faketranslate.getNumTranslations()).to.equal(43);
241 | await expect(fakedb.get(`/admin/scenes/${scene}/nouns/nl-NL/kaas`)).to.eventually.equal('cheese');
242 | await expect(fakedb.get(`/admin/scenes/${scene}/nouns/fr-FR/fromage`)).to.eventually.equal('cheese');
243 | await expect(fakedb.get(`summary/${scene}/cheese`)).to.eventually.deep.equal({
244 | num_langs: 0,
245 | score: 0
246 | });
247 | });
248 |
249 | it('should be able to deal with various array-formatted replies', async () => {
250 | faketranslate.setResponse('nl', ['kaas', 'kaasje', 'Kelly']);
251 | faketranslate.setResponse('fr', [['fromage', 'omelette']]);
252 |
253 | await saythat.nounAdded(scene, 'cheese');
254 | await expect(fakedb.get(`/admin/scenes/${scene}/nouns/nl-NL/kaas`)).to.eventually.equal('cheese');
255 | await expect(fakedb.get(`/admin/scenes/${scene}/nouns/fr-FR/fromage`)).to.eventually.equal('cheese');
256 | });
257 |
258 | it('should lower-case nouns', async () => {
259 | faketranslate.setResponse('de', ['Morgen']);
260 |
261 | await saythat.nounAdded(scene, 'morning');
262 | await expect(fakedb.get(`/admin/scenes/${scene}/nouns/de-DE/morgen`)).to.eventually.equal('morning');
263 | });
264 | });
265 | });
266 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/public/third_party/material.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * material-design-lite - Material Design Components in CSS, JS and HTML
3 | * @version v1.3.0
4 | * @license Apache-2.0
5 | * @copyright 2015 Google, Inc.
6 | * @link https://github.com/google/material-design-lite
7 | */
8 | !function(){"use strict";function e(e,t){if(e){if(t.element_.classList.contains(t.CssClasses_.MDL_JS_RIPPLE_EFFECT)){var s=document.createElement("span");s.classList.add(t.CssClasses_.MDL_RIPPLE_CONTAINER),s.classList.add(t.CssClasses_.MDL_JS_RIPPLE_EFFECT);var i=document.createElement("span");i.classList.add(t.CssClasses_.MDL_RIPPLE),s.appendChild(i),e.appendChild(s)}e.addEventListener("click",function(s){if("#"===e.getAttribute("href").charAt(0)){s.preventDefault();var i=e.href.split("#")[1],n=t.element_.querySelector("#"+i);t.resetTabState_(),t.resetPanelState_(),e.classList.add(t.CssClasses_.ACTIVE_CLASS),n.classList.add(t.CssClasses_.ACTIVE_CLASS)}})}}function t(e,t,s,i){function n(){var n=e.href.split("#")[1],a=i.content_.querySelector("#"+n);i.resetTabState_(t),i.resetPanelState_(s),e.classList.add(i.CssClasses_.IS_ACTIVE),a.classList.add(i.CssClasses_.IS_ACTIVE)}if(i.tabBar_.classList.contains(i.CssClasses_.JS_RIPPLE_EFFECT)){var a=document.createElement("span");a.classList.add(i.CssClasses_.RIPPLE_CONTAINER),a.classList.add(i.CssClasses_.JS_RIPPLE_EFFECT);var l=document.createElement("span");l.classList.add(i.CssClasses_.RIPPLE),a.appendChild(l),e.appendChild(a)}i.tabBar_.classList.contains(i.CssClasses_.TAB_MANUAL_SWITCH)||e.addEventListener("click",function(t){"#"===e.getAttribute("href").charAt(0)&&(t.preventDefault(),n())}),e.show=n}var s={upgradeDom:function(e,t){},upgradeElement:function(e,t){},upgradeElements:function(e){},upgradeAllRegistered:function(){},registerUpgradedCallback:function(e,t){},register:function(e){},downgradeElements:function(e){}};s=function(){function e(e,t){for(var s=0;s0&&l(t.children))}function o(t){var s="undefined"==typeof t.widget&&"undefined"==typeof t.widget,i=!0;s||(i=t.widget||t.widget);var n={classConstructor:t.constructor||t.constructor,className:t.classAsString||t.classAsString,cssClass:t.cssClass||t.cssClass,widget:i,callbacks:[]};if(c.forEach(function(e){if(e.cssClass===n.cssClass)throw new Error("The provided cssClass has already been registered: "+e.cssClass);if(e.className===n.className)throw new Error("The provided className has already been registered")}),t.constructor.prototype.hasOwnProperty(C))throw new Error("MDL component classes must not have "+C+" defined as a property.");var a=e(t.classAsString,n);a||c.push(n)}function r(t,s){var i=e(t);i&&i.callbacks.push(s)}function _(){for(var e=0;e0&&this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)&&(e.keyCode===this.Keycodes_.UP_ARROW?(e.preventDefault(),t[t.length-1].focus()):e.keyCode===this.Keycodes_.DOWN_ARROW&&(e.preventDefault(),t[0].focus()))}},d.prototype.handleItemKeyboardEvent_=function(e){if(this.element_&&this.container_){var t=this.element_.querySelectorAll("."+this.CssClasses_.ITEM+":not([disabled])");if(t&&t.length>0&&this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)){var s=Array.prototype.slice.call(t).indexOf(e.target);if(e.keyCode===this.Keycodes_.UP_ARROW)e.preventDefault(),s>0?t[s-1].focus():t[t.length-1].focus();else if(e.keyCode===this.Keycodes_.DOWN_ARROW)e.preventDefault(),t.length>s+1?t[s+1].focus():t[0].focus();else if(e.keyCode===this.Keycodes_.SPACE||e.keyCode===this.Keycodes_.ENTER){e.preventDefault();var i=new MouseEvent("mousedown");e.target.dispatchEvent(i),i=new MouseEvent("mouseup"),e.target.dispatchEvent(i),e.target.click()}else e.keyCode===this.Keycodes_.ESCAPE&&(e.preventDefault(),this.hide())}}},d.prototype.handleItemClick_=function(e){e.target.hasAttribute("disabled")?e.stopPropagation():(this.closing_=!0,window.setTimeout(function(e){this.hide(),this.closing_=!1}.bind(this),this.Constant_.CLOSE_TIMEOUT))},d.prototype.applyClip_=function(e,t){this.element_.classList.contains(this.CssClasses_.UNALIGNED)?this.element_.style.clip="":this.element_.classList.contains(this.CssClasses_.BOTTOM_RIGHT)?this.element_.style.clip="rect(0 "+t+"px 0 "+t+"px)":this.element_.classList.contains(this.CssClasses_.TOP_LEFT)?this.element_.style.clip="rect("+e+"px 0 "+e+"px 0)":this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)?this.element_.style.clip="rect("+e+"px "+t+"px "+e+"px "+t+"px)":this.element_.style.clip=""},d.prototype.removeAnimationEndListener_=function(e){e.target.classList.remove(d.prototype.CssClasses_.IS_ANIMATING)},d.prototype.addAnimationEndListener_=function(){this.element_.addEventListener("transitionend",this.removeAnimationEndListener_),this.element_.addEventListener("webkitTransitionEnd",this.removeAnimationEndListener_)},d.prototype.show=function(e){if(this.element_&&this.container_&&this.outline_){var t=this.element_.getBoundingClientRect().height,s=this.element_.getBoundingClientRect().width;this.container_.style.width=s+"px",this.container_.style.height=t+"px",this.outline_.style.width=s+"px",this.outline_.style.height=t+"px";for(var i=this.Constant_.TRANSITION_DURATION_SECONDS*this.Constant_.TRANSITION_DURATION_FRACTION,n=this.element_.querySelectorAll("."+this.CssClasses_.ITEM),a=0;a0&&this.showSnackbar(this.queuedNotifications_.shift())},C.prototype.cleanup_=function(){this.element_.classList.remove(this.cssClasses_.ACTIVE),setTimeout(function(){this.element_.setAttribute("aria-hidden","true"),this.textElement_.textContent="",Boolean(this.actionElement_.getAttribute("aria-hidden"))||(this.setActionHidden_(!0),this.actionElement_.textContent="",this.actionElement_.removeEventListener("click",this.actionHandler_)),this.actionHandler_=void 0,this.message_=void 0,this.actionText_=void 0,this.active=!1,this.checkQueue_()}.bind(this),this.Constant_.ANIMATION_LENGTH)},C.prototype.setActionHidden_=function(e){e?this.actionElement_.setAttribute("aria-hidden","true"):this.actionElement_.removeAttribute("aria-hidden")},s.register({constructor:C,classAsString:"MaterialSnackbar",cssClass:"mdl-js-snackbar",widget:!0});var u=function(e){this.element_=e,this.init()};window.MaterialSpinner=u,u.prototype.Constant_={MDL_SPINNER_LAYER_COUNT:4},u.prototype.CssClasses_={MDL_SPINNER_LAYER:"mdl-spinner__layer",MDL_SPINNER_CIRCLE_CLIPPER:"mdl-spinner__circle-clipper",MDL_SPINNER_CIRCLE:"mdl-spinner__circle",MDL_SPINNER_GAP_PATCH:"mdl-spinner__gap-patch",MDL_SPINNER_LEFT:"mdl-spinner__left",MDL_SPINNER_RIGHT:"mdl-spinner__right"},u.prototype.createLayer=function(e){var t=document.createElement("div");t.classList.add(this.CssClasses_.MDL_SPINNER_LAYER),t.classList.add(this.CssClasses_.MDL_SPINNER_LAYER+"-"+e);var s=document.createElement("div");s.classList.add(this.CssClasses_.MDL_SPINNER_CIRCLE_CLIPPER),s.classList.add(this.CssClasses_.MDL_SPINNER_LEFT);var i=document.createElement("div");i.classList.add(this.CssClasses_.MDL_SPINNER_GAP_PATCH);var n=document.createElement("div");n.classList.add(this.CssClasses_.MDL_SPINNER_CIRCLE_CLIPPER),n.classList.add(this.CssClasses_.MDL_SPINNER_RIGHT);for(var a=[s,i,n],l=0;l=this.maxRows&&e.preventDefault()},L.prototype.onFocus_=function(e){this.element_.classList.add(this.CssClasses_.IS_FOCUSED)},L.prototype.onBlur_=function(e){this.element_.classList.remove(this.CssClasses_.IS_FOCUSED)},L.prototype.onReset_=function(e){this.updateClasses_()},L.prototype.updateClasses_=function(){this.checkDisabled(),this.checkValidity(),this.checkDirty(),this.checkFocus()},L.prototype.checkDisabled=function(){this.input_.disabled?this.element_.classList.add(this.CssClasses_.IS_DISABLED):this.element_.classList.remove(this.CssClasses_.IS_DISABLED)},L.prototype.checkDisabled=L.prototype.checkDisabled,L.prototype.checkFocus=function(){Boolean(this.element_.querySelector(":focus"))?this.element_.classList.add(this.CssClasses_.IS_FOCUSED):this.element_.classList.remove(this.CssClasses_.IS_FOCUSED)},L.prototype.checkFocus=L.prototype.checkFocus,L.prototype.checkValidity=function(){this.input_.validity&&(this.input_.validity.valid?this.element_.classList.remove(this.CssClasses_.IS_INVALID):this.element_.classList.add(this.CssClasses_.IS_INVALID))},L.prototype.checkValidity=L.prototype.checkValidity,L.prototype.checkDirty=function(){this.input_.value&&this.input_.value.length>0?this.element_.classList.add(this.CssClasses_.IS_DIRTY):this.element_.classList.remove(this.CssClasses_.IS_DIRTY)},L.prototype.checkDirty=L.prototype.checkDirty,L.prototype.disable=function(){this.input_.disabled=!0,this.updateClasses_()},L.prototype.disable=L.prototype.disable,L.prototype.enable=function(){this.input_.disabled=!1,this.updateClasses_()},L.prototype.enable=L.prototype.enable,L.prototype.change=function(e){this.input_.value=e||"",this.updateClasses_()},L.prototype.change=L.prototype.change,L.prototype.init=function(){if(this.element_&&(this.label_=this.element_.querySelector("."+this.CssClasses_.LABEL),this.input_=this.element_.querySelector("."+this.CssClasses_.INPUT),this.input_)){this.input_.hasAttribute(this.Constant_.MAX_ROWS_ATTRIBUTE)&&(this.maxRows=parseInt(this.input_.getAttribute(this.Constant_.MAX_ROWS_ATTRIBUTE),10),isNaN(this.maxRows)&&(this.maxRows=this.Constant_.NO_MAX_ROWS)),this.input_.hasAttribute("placeholder")&&this.element_.classList.add(this.CssClasses_.HAS_PLACEHOLDER),this.boundUpdateClassesHandler=this.updateClasses_.bind(this),this.boundFocusHandler=this.onFocus_.bind(this),this.boundBlurHandler=this.onBlur_.bind(this),this.boundResetHandler=this.onReset_.bind(this),this.input_.addEventListener("input",this.boundUpdateClassesHandler),this.input_.addEventListener("focus",this.boundFocusHandler),this.input_.addEventListener("blur",this.boundBlurHandler),this.input_.addEventListener("reset",this.boundResetHandler),this.maxRows!==this.Constant_.NO_MAX_ROWS&&(this.boundKeyDownHandler=this.onKeyDown_.bind(this),this.input_.addEventListener("keydown",this.boundKeyDownHandler));var e=this.element_.classList.contains(this.CssClasses_.IS_INVALID);this.updateClasses_(),this.element_.classList.add(this.CssClasses_.IS_UPGRADED),e&&this.element_.classList.add(this.CssClasses_.IS_INVALID),this.input_.hasAttribute("autofocus")&&(this.element_.focus(),this.checkFocus())}},s.register({constructor:L,classAsString:"MaterialTextfield",cssClass:"mdl-js-textfield",widget:!0});var I=function(e){this.element_=e,this.init()};window.MaterialTooltip=I,I.prototype.Constant_={},I.prototype.CssClasses_={IS_ACTIVE:"is-active",BOTTOM:"mdl-tooltip--bottom",LEFT:"mdl-tooltip--left",RIGHT:"mdl-tooltip--right",TOP:"mdl-tooltip--top"},I.prototype.handleMouseEnter_=function(e){var t=e.target.getBoundingClientRect(),s=t.left+t.width/2,i=t.top+t.height/2,n=-1*(this.element_.offsetWidth/2),a=-1*(this.element_.offsetHeight/2);this.element_.classList.contains(this.CssClasses_.LEFT)||this.element_.classList.contains(this.CssClasses_.RIGHT)?(s=t.width/2,i+a<0?(this.element_.style.top="0",this.element_.style.marginTop="0"):(this.element_.style.top=i+"px",this.element_.style.marginTop=a+"px")):s+n<0?(this.element_.style.left="0",this.element_.style.marginLeft="0"):(this.element_.style.left=s+"px",this.element_.style.marginLeft=n+"px"),this.element_.classList.contains(this.CssClasses_.TOP)?this.element_.style.top=t.top-this.element_.offsetHeight-10+"px":this.element_.classList.contains(this.CssClasses_.RIGHT)?this.element_.style.left=t.left+t.width+10+"px":this.element_.classList.contains(this.CssClasses_.LEFT)?this.element_.style.left=t.left-this.element_.offsetWidth-10+"px":this.element_.style.top=t.top+t.height+10+"px",this.element_.classList.add(this.CssClasses_.IS_ACTIVE)},I.prototype.hideTooltip_=function(){this.element_.classList.remove(this.CssClasses_.IS_ACTIVE)},I.prototype.init=function(){if(this.element_){var e=this.element_.getAttribute("for")||this.element_.getAttribute("data-mdl-for");e&&(this.forElement_=document.getElementById(e)),this.forElement_&&(this.forElement_.hasAttribute("tabindex")||this.forElement_.setAttribute("tabindex","0"),this.boundMouseEnterHandler=this.handleMouseEnter_.bind(this),this.boundMouseLeaveAndScrollHandler=this.hideTooltip_.bind(this),this.forElement_.addEventListener("mouseenter",this.boundMouseEnterHandler,!1),this.forElement_.addEventListener("touchend",this.boundMouseEnterHandler,!1),this.forElement_.addEventListener("mouseleave",this.boundMouseLeaveAndScrollHandler,!1),window.addEventListener("scroll",this.boundMouseLeaveAndScrollHandler,!0),window.addEventListener("touchstart",this.boundMouseLeaveAndScrollHandler))}},s.register({constructor:I,classAsString:"MaterialTooltip",cssClass:"mdl-tooltip"});var f=function(e){this.element_=e,this.init()};window.MaterialLayout=f,f.prototype.Constant_={MAX_WIDTH:"(max-width: 1024px)",TAB_SCROLL_PIXELS:100,RESIZE_TIMEOUT:100,MENU_ICON:"",CHEVRON_LEFT:"chevron_left",CHEVRON_RIGHT:"chevron_right"},f.prototype.Keycodes_={ENTER:13,ESCAPE:27,SPACE:32},f.prototype.Mode_={STANDARD:0,SEAMED:1,WATERFALL:2,SCROLL:3},f.prototype.CssClasses_={CONTAINER:"mdl-layout__container",HEADER:"mdl-layout__header",DRAWER:"mdl-layout__drawer",CONTENT:"mdl-layout__content",DRAWER_BTN:"mdl-layout__drawer-button",ICON:"material-icons",JS_RIPPLE_EFFECT:"mdl-js-ripple-effect",RIPPLE_CONTAINER:"mdl-layout__tab-ripple-container",RIPPLE:"mdl-ripple",RIPPLE_IGNORE_EVENTS:"mdl-js-ripple-effect--ignore-events",HEADER_SEAMED:"mdl-layout__header--seamed",HEADER_WATERFALL:"mdl-layout__header--waterfall",HEADER_SCROLL:"mdl-layout__header--scroll",FIXED_HEADER:"mdl-layout--fixed-header",OBFUSCATOR:"mdl-layout__obfuscator",TAB_BAR:"mdl-layout__tab-bar",TAB_CONTAINER:"mdl-layout__tab-bar-container",TAB:"mdl-layout__tab",TAB_BAR_BUTTON:"mdl-layout__tab-bar-button",TAB_BAR_LEFT_BUTTON:"mdl-layout__tab-bar-left-button",TAB_BAR_RIGHT_BUTTON:"mdl-layout__tab-bar-right-button",TAB_MANUAL_SWITCH:"mdl-layout__tab-manual-switch",PANEL:"mdl-layout__tab-panel",HAS_DRAWER:"has-drawer",HAS_TABS:"has-tabs",HAS_SCROLLING_HEADER:"has-scrolling-header",CASTING_SHADOW:"is-casting-shadow",IS_COMPACT:"is-compact",IS_SMALL_SCREEN:"is-small-screen",IS_DRAWER_OPEN:"is-visible",IS_ACTIVE:"is-active",IS_UPGRADED:"is-upgraded",IS_ANIMATING:"is-animating",ON_LARGE_SCREEN:"mdl-layout--large-screen-only",ON_SMALL_SCREEN:"mdl-layout--small-screen-only"},f.prototype.contentScrollHandler_=function(){if(!this.header_.classList.contains(this.CssClasses_.IS_ANIMATING)){var e=!this.element_.classList.contains(this.CssClasses_.IS_SMALL_SCREEN)||this.element_.classList.contains(this.CssClasses_.FIXED_HEADER);this.content_.scrollTop>0&&!this.header_.classList.contains(this.CssClasses_.IS_COMPACT)?(this.header_.classList.add(this.CssClasses_.CASTING_SHADOW),this.header_.classList.add(this.CssClasses_.IS_COMPACT),e&&this.header_.classList.add(this.CssClasses_.IS_ANIMATING)):this.content_.scrollTop<=0&&this.header_.classList.contains(this.CssClasses_.IS_COMPACT)&&(this.header_.classList.remove(this.CssClasses_.CASTING_SHADOW),this.header_.classList.remove(this.CssClasses_.IS_COMPACT),e&&this.header_.classList.add(this.CssClasses_.IS_ANIMATING))}},f.prototype.keyboardEventHandler_=function(e){e.keyCode===this.Keycodes_.ESCAPE&&this.drawer_.classList.contains(this.CssClasses_.IS_DRAWER_OPEN)&&this.toggleDrawer()},f.prototype.screenSizeHandler_=function(){this.screenSizeMediaQuery_.matches?this.element_.classList.add(this.CssClasses_.IS_SMALL_SCREEN):(this.element_.classList.remove(this.CssClasses_.IS_SMALL_SCREEN),this.drawer_&&(this.drawer_.classList.remove(this.CssClasses_.IS_DRAWER_OPEN),this.obfuscator_.classList.remove(this.CssClasses_.IS_DRAWER_OPEN)))},f.prototype.drawerToggleHandler_=function(e){if(e&&"keydown"===e.type){if(e.keyCode!==this.Keycodes_.SPACE&&e.keyCode!==this.Keycodes_.ENTER)return;e.preventDefault()}this.toggleDrawer()},f.prototype.headerTransitionEndHandler_=function(){this.header_.classList.remove(this.CssClasses_.IS_ANIMATING)},f.prototype.headerClickHandler_=function(){this.header_.classList.contains(this.CssClasses_.IS_COMPACT)&&(this.header_.classList.remove(this.CssClasses_.IS_COMPACT),this.header_.classList.add(this.CssClasses_.IS_ANIMATING))},f.prototype.resetTabState_=function(e){for(var t=0;t0?c.classList.add(this.CssClasses_.IS_ACTIVE):c.classList.remove(this.CssClasses_.IS_ACTIVE),this.tabBar_.scrollLeft0)return;this.setFrameCount(1);var i,n,a=e.currentTarget.getBoundingClientRect();if(0===e.clientX&&0===e.clientY)i=Math.round(a.width/2),n=Math.round(a.height/2);else{var l=void 0!==e.clientX?e.clientX:e.touches[0].clientX,o=void 0!==e.clientY?e.clientY:e.touches[0].clientY;i=Math.round(l-a.left),n=Math.round(o-a.top)}this.setRippleXY(i,n),this.setRippleStyles(!0),window.requestAnimationFrame(this.animFrameHandler.bind(this))}},S.prototype.upHandler_=function(e){e&&2!==e.detail&&window.setTimeout(function(){this.rippleElement_.classList.remove(this.CssClasses_.IS_VISIBLE)}.bind(this),0)},S.prototype.init=function(){if(this.element_){var e=this.element_.classList.contains(this.CssClasses_.RIPPLE_CENTER);this.element_.classList.contains(this.CssClasses_.RIPPLE_EFFECT_IGNORE_EVENTS)||(this.rippleElement_=this.element_.querySelector("."+this.CssClasses_.RIPPLE),this.frameCount_=0,this.rippleSize_=0,this.x_=0,this.y_=0,this.ignoringMouseDown_=!1,this.boundDownHandler=this.downHandler_.bind(this),this.element_.addEventListener("mousedown",this.boundDownHandler),this.element_.addEventListener("touchstart",this.boundDownHandler),this.boundUpHandler=this.upHandler_.bind(this),this.element_.addEventListener("mouseup",this.boundUpHandler),this.element_.addEventListener("mouseleave",this.boundUpHandler),this.element_.addEventListener("touchend",this.boundUpHandler),this.element_.addEventListener("blur",this.boundUpHandler),this.getFrameCount=function(){return this.frameCount_},this.setFrameCount=function(e){this.frameCount_=e},this.getRippleElement=function(){return this.rippleElement_},this.setRippleXY=function(e,t){this.x_=e,this.y_=t},this.setRippleStyles=function(t){if(null!==this.rippleElement_){var s,i,n,a="translate("+this.x_+"px, "+this.y_+"px)";t?(i=this.Constant_.INITIAL_SCALE,n=this.Constant_.INITIAL_SIZE):(i=this.Constant_.FINAL_SCALE,n=this.rippleSize_+"px",e&&(a="translate("+this.boundWidth/2+"px, "+this.boundHeight/2+"px)")),s="translate(-50%, -50%) "+a+i,this.rippleElement_.style.webkitTransform=s,this.rippleElement_.style.msTransform=s,this.rippleElement_.style.transform=s,t?this.rippleElement_.classList.remove(this.CssClasses_.IS_ANIMATING):this.rippleElement_.classList.add(this.CssClasses_.IS_ANIMATING)}},this.animFrameHandler=function(){this.frameCount_-- >0?window.requestAnimationFrame(this.animFrameHandler.bind(this)):this.setRippleStyles(!1)})}},s.register({constructor:S,classAsString:"MaterialRipple",cssClass:"mdl-js-ripple-effect",widget:!1})}();
10 | //# sourceMappingURL=material.min.js.map
11 |
--------------------------------------------------------------------------------