Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 var Galore = Galore || {}; | 5 var STOPPED = "Stopped"; |
| 6 | 6 var RECORDING = "Recording"; |
| 7 Galore.controller = { | 7 var PAUSED_RECORDING = "Recording Paused"; |
| 8 /** @constructor */ | 8 var PAUSED_PLAYING = "Playing Paused"; |
| 9 create: function() { | 9 var PLAYING = "Playing"; |
| 10 var controller = Object.create(this); | 10 |
| 11 controller.api = chrome; | 11 var recordingState = STOPPED; |
| 12 controller.counter = 0; | 12 |
| 13 return controller; | 13 // Timestamp when current segment started. |
| 14 }, | 14 var segmentStart; |
| 15 | 15 // Segment duration accumulated before pause button was hit. |
| 16 createWindow: function() { | 16 var pausedDuration; |
| 17 chrome.storage.sync.get('settings', this.onSettingsFetched_.bind(this)); | 17 // The array of segments, with delay and action. |
| 18 }, | 18 var recordingList; |
| 19 | 19 // When this timer fires, the next segment from recordingList should be played. |
| 20 /** @private */ | 20 var playingTimer; |
| 21 onSettingsFetched_: function(items) { | 21 var currentSegmentIndex; |
| 22 var request = new XMLHttpRequest(); | 22 // A set of web Notifications - used to delete them during playback by id. |
| 23 var settings = items.settings || {}; | 23 var webNotifications = {}; |
| 24 var source = settings.data || '/data/' + this.getDataVersion_(); | 24 |
| 25 request.open('GET', source, true); | 25 var recorderButtons = [ "play", "record", "pause", "stop"]; |
| 26 request.responseType = 'text'; | 26 var recorderButtonStates = [ |
| 27 request.onload = this.onDataFetched_.bind(this, settings, request); | 27 { state: STOPPED, enabled: "play record" }, |
| 28 request.send(); | 28 { state: RECORDING, enabled: "pause stop" }, |
| 29 }, | 29 { state: PAUSED_RECORDING, enabled: "record stop" }, |
| 30 | 30 { state: PAUSED_PLAYING, enabled: "play stop" }, |
| 31 /** @private */ | 31 { state: PLAYING, enabled: "pause stop" } |
| 32 onDataFetched_: function(settings, request) { | 32 ]; |
| 33 var count = 0; | 33 |
| 34 var data = JSON.parse(request.response); | 34 function updateButtonsState() { |
|
dewittj
2014/06/10 17:10:19
This function could use a comment with all its fan
| |
| 35 recorderButtonStates.map(function(entry) { | |
| 36 if (entry.state != recordingState) | |
| 37 return; | |
| 38 var disabled = recorderButtons.slice(0); // copy | |
| 39 var enabled = entry.enabled.split(" "); | |
| 40 for (var i = 0; i < enabled.length; i++) { | |
| 41 disabled.splice(disabled.indexOf(enabled[i]), 1); | |
| 42 enabled[i] = "#" + enabled[i]; | |
| 43 } | |
| 44 for (var i = 0; i < disabled.length; i++) { | |
| 45 disabled[i] = "#" + disabled[i]; | |
| 46 } | |
| 47 getElements(disabled.join(", ")).forEach(function(element) { | |
| 48 element.setAttribute("disabled", "true") | |
| 49 }) | |
| 50 getElements(enabled.join(", ")).forEach(function(element) { | |
| 51 element.removeAttribute("disabled") | |
| 52 }) | |
| 53 }) | |
| 54 } | |
| 55 | |
| 56 | |
| 57 function setRecordingState(newState) { | |
| 58 setRecorderStatusText(newState); | |
| 59 recordingState = newState; | |
| 60 updateButtonsState(); | |
| 61 } | |
| 62 | |
| 63 function updateRecordingStats(context) { | |
| 64 var length = 0; | |
| 65 var segmentCnt = 0; | |
| 66 recordingList.slice(currentSegmentIndex).forEach(function(segment) { | |
| 67 length += segment.delay || 0; | |
| 68 segmentCnt++; | |
| 69 }) | |
| 70 updateRecordingStatsDisplay(context + ": " + (segmentCnt-1) + " segments, " + | |
| 71 Math.floor(length/1000) + " seconds."); | |
| 72 } | |
| 73 | |
| 74 function loadRecording() { | |
| 75 chrome.storage.local.get("recording", function(items) { | |
| 76 recordingList = JSON.parse(items["recording"] || "[]"); | |
| 77 setRecordingState(STOPPED); | |
| 78 updateRecordingStats("Loaded record"); | |
| 79 }) | |
| 80 } | |
| 81 | |
| 82 function finalizeRecording() { | |
| 83 chrome.storage.local.set({"recording": JSON.stringify(recordingList)}); | |
| 84 updateRecordingStats("Recorded"); | |
| 85 } | |
| 86 | |
| 87 function setPreviousSegmentDuration() { | |
| 88 var now = new Date().getTime(); | |
| 89 var delay = now - segmentStart; | |
| 90 segmentStart = now; | |
| 91 recordingList[recordingList.length - 1].delay = delay; | |
| 92 } | |
| 93 | |
| 94 function recordCreate(kind, id, options) { | |
| 95 if (recordingState != RECORDING) | |
| 96 return; | |
| 97 setPreviousSegmentDuration(); | |
| 98 recordingList.push({ type: "create", kind: kind, id: id, options: options }); | |
| 99 updateRecordingStats("Recording"); | |
| 100 } | |
| 101 | |
| 102 function recordDelete(kind, id) { | |
| 103 if (recordingState != RECORDING) | |
| 104 return; | |
| 105 setPreviousSegmentDuration(); | |
| 106 recordingList.push({ type: "delete", kind: kind, id: id }); | |
| 107 updateRecordingStats("Recording"); | |
| 108 } | |
| 109 | |
| 110 function startPlaying() { | |
| 111 if (recordingList.length < 2) | |
| 112 return false; | |
| 113 | |
| 114 setRecordingState(PLAYING); | |
| 115 | |
| 116 if (playingTimer) | |
| 117 clearTimeout(playingTimer); | |
| 118 | |
| 119 webNotifications = {}; | |
| 120 currentSegmentIndex = 0; | |
| 121 playingTimer = setTimeout(playNextSegment, | |
| 122 recordingList[currentSegmentIndex].delay); | |
| 123 updateRecordingStats("Playing"); | |
| 124 } | |
| 125 | |
| 126 function playNextSegment() { | |
| 127 currentSegmentIndex++; | |
| 128 var segment = recordingList[currentSegmentIndex]; | |
| 129 if (!segment) { | |
| 130 stopPlaying(); | |
| 131 return; | |
| 132 } | |
| 133 | |
| 134 if (segment.type == "create") { | |
| 135 createNotificationForPlay(segment.kind, segment.id, segment.options); | |
| 136 } else { // type == "delete" | |
| 137 deleteNotificaitonForPlay(segment.kind, segment.id); | |
|
dewittj
2014/06/10 17:10:19
spelling
| |
| 138 } | |
| 139 playingTimer = setTimeout(playNextSegment, | |
| 140 recordingList[currentSegmentIndex].delay); | |
| 141 segmentStart = new Date().getTime(); | |
| 142 updateRecordingStats("Playing"); | |
| 143 } | |
| 144 | |
| 145 function deleteNotificaitonForPlay(kind, id) { | |
|
dewittj
2014/06/10 17:10:19
spelling
| |
| 146 if (kind == 'web') { | |
| 147 webNotifications[id].close(); | |
| 148 } else { | |
| 149 chrome.notifications.clear(id, function(wasClosed) { | |
| 150 // nothing to do | |
| 151 }); | |
| 152 } | |
| 153 } | |
| 154 | |
| 155 function createNotificationForPlay(kind, id, options) { | |
| 156 if (kind == 'web') { | |
| 157 webNotifications[id] = createWebNotification(id, options); | |
| 158 } else { | |
| 159 var type = options.type; | |
| 160 var priority = options.priority; | |
| 161 createRichNotification(id, type, priority, options); | |
| 162 } | |
| 163 } | |
| 164 function stopPlaying() { | |
| 165 currentSegmentIndex = 0; | |
| 166 clearTimeout(playingTimer); | |
| 167 updateRecordingStats("Record"); | |
| 168 setRecordingState(STOPPED); | |
| 169 } | |
| 170 | |
| 171 function pausePlaying() { | |
| 172 clearTimeout(playingTimer); | |
| 173 pausedDuration = new Date().getTime() - segmentStart; | |
| 174 setRecordingState(PAUSED_PLAYING); | |
| 175 } | |
| 176 | |
| 177 function unpausePlaying() { | |
| 178 var remainingInSegment = | |
| 179 recordingList[currentSegmentIndex].delay - pausedDuration; | |
| 180 if (remainingInSegment < 0) | |
| 181 remainingInSegment = 0; | |
| 182 playingTimer = setTimeout(playNextSegment, remainingInSegment); | |
| 183 segmentStart = new Date().getTime() - pausedDuration; | |
| 184 } | |
| 185 | |
| 186 function onRecord() { | |
| 187 if (recordingState == STOPPED) { | |
| 188 segmentStart = new Date().getTime(); | |
| 189 pausedDuration = 0; | |
| 190 // This item is only needed to keep a duration of the delay between start | |
| 191 // and first action. | |
| 192 recordingList = [ { type:"start" } ]; | |
| 193 } else if (recordingState == PAUSED_RECORDING) { | |
| 194 segmentStart = new Date().getTime() - pausedDuration; | |
| 195 pausedDuration = 0; | |
| 196 } else { | |
| 197 return; | |
| 198 } | |
| 199 updateRecordingStats("Recording"); | |
| 200 setRecordingState(RECORDING); | |
| 201 } | |
| 202 | |
| 203 function pauseRecording() { | |
| 204 pausedDuration = new Date().getTime() - segmentStart; | |
| 205 segmentStart = 0; | |
| 206 setRecordingState(PAUSED_RECORDING); | |
| 207 } | |
| 208 | |
| 209 function onPause() { | |
| 210 if (recordingState == RECORDING) { | |
| 211 pauseRecording(); | |
| 212 } else if (recordingState == PLAYING) { | |
| 213 pausePlaying(); | |
| 214 } else { | |
| 215 return; | |
| 216 } | |
| 217 } | |
| 218 | |
| 219 function onStop() { | |
| 220 switch (recordingState) { | |
| 221 case PAUSED_RECORDING: | |
| 222 segmentStart = new Date().getTime() - pausedDuration; | |
| 223 // fall through | |
| 224 case RECORDING: | |
| 225 finalizeRecording(); | |
| 226 break; | |
| 227 case PLAYING: | |
| 228 case PAUSED_PLAYING: | |
| 229 stopPlaying(); | |
| 230 break; | |
| 231 } | |
| 232 setRecordingState(STOPPED); | |
| 233 } | |
| 234 | |
| 235 function onPlay() { | |
| 236 if (recordingState == STOPPED) { | |
| 237 if (!startPlaying()) | |
| 238 return; | |
| 239 } else if (recordingState == PAUSED_PLAYING) { | |
| 240 unpausePlaying(); | |
| 241 } | |
| 242 setRecordingState(PLAYING); | |
| 243 } | |
| 244 | |
| 245 function createWindow() { | |
| 246 chrome.storage.local.get('settings', onSettingsFetched); | |
| 247 } | |
| 248 | |
| 249 function onSettingsFetched(items) { | |
| 250 settings = items.settings || settings; | |
| 251 var request = new XMLHttpRequest(); | |
| 252 var source = '/data/data.json'; | |
| 253 request.open('GET', source, true); | |
| 254 request.responseType = 'text'; | |
| 255 request.onload = onDataFetched; | |
| 256 request.send(); | |
| 257 } | |
| 258 | |
| 259 function onDataFetched() { | |
| 260 var data = JSON.parse(this.response); | |
| 261 createAppWindow(function() { | |
| 262 // Create notification buttons. | |
| 35 data.forEach(function(section) { | 263 data.forEach(function(section) { |
| 264 var type = section.notificationType; | |
| 36 (section.notificationOptions || []).forEach(function(options) { | 265 (section.notificationOptions || []).forEach(function(options) { |
| 37 ++count; | 266 addNotificationButton(section.sectionName, |
| 38 this.fetchImages_(options, function() { | 267 options.title, |
| 39 if (--count == 0) | 268 options.iconUrl, |
| 40 this.onImagesFetched_(settings, data); | 269 function() { createNotification(type, options) }); |
| 41 }.bind(this)); | 270 }); |
| 42 }, this); | |
| 43 }, this); | |
| 44 }, | |
| 45 | |
| 46 /** @private */ | |
| 47 onImagesFetched_: function(settings, data) { | |
| 48 this.settings = settings; | |
| 49 this.view = Galore.view.create(this.settings, function() { | |
| 50 // Create buttons. | |
| 51 data.forEach(function(section) { | |
| 52 var defaults = section.globals || data[0].globals; | |
| 53 var type = section.notificationType; | |
| 54 (section.notificationOptions || []).forEach(function(options) { | |
| 55 var defaulted = this.getDefaultedOptions_(options, defaults); | |
| 56 var create = this.createNotification_.bind(this, type, defaulted); | |
| 57 this.view.addNotificationButton(section.sectionName, | |
| 58 defaulted.title, | |
| 59 defaulted.iconUrl, | |
| 60 create); | |
| 61 }, this); | |
| 62 }, this); | |
| 63 // Set the API entry point and use it to set event listeners. | |
| 64 this.api = this.getApi_(data); | |
| 65 if (this.api) | |
| 66 this.addListeners_(this.api, data[0].events); | |
| 67 // Display the completed and ready window. | |
| 68 this.view.showWindow(); | |
| 69 }.bind(this), this.onSettingsChange_.bind(this)); | |
| 70 }, | |
| 71 | |
| 72 /** @private */ | |
| 73 fetchImages_: function(options, onFetched) { | |
| 74 var count = 0; | |
| 75 var replacements = {}; | |
| 76 this.mapStrings_(options, function(string) { | |
| 77 if (string.indexOf("/images/") == 0 || string.search(/https?:\//) == 0) { | |
| 78 ++count; | |
| 79 this.fetchImage_(string, function(url) { | |
| 80 replacements[string] = url; | |
| 81 if (--count == 0) { | |
| 82 this.mapStrings_(options, function(string) { | |
| 83 return replacements[string] || string; | |
| 84 }); | |
| 85 onFetched.call(this, options); | |
| 86 } | |
| 87 }); | |
| 88 } | |
| 89 }); | 271 }); |
| 90 }, | 272 loadRecording(); |
| 91 | 273 addListeners(); |
| 92 /** @private */ | 274 showWindow(); |
| 93 fetchImage_: function(url, onFetched) { | 275 }); |
| 94 var request = new XMLHttpRequest(); | 276 } |
| 95 request.open('GET', url, true); | 277 |
| 96 request.responseType = 'blob'; | 278 function onSettingsChange(settings) { |
| 97 request.onload = function() { | 279 chrome.storage.local.set({settings: settings}); |
| 98 var url = window.URL.createObjectURL(request.response); | 280 } |
| 99 onFetched.call(this, url); | 281 |
| 100 }.bind(this); | 282 function createNotification(type, options) { |
| 101 request.send(); | 283 var id = getNextId(); |
| 102 }, | 284 var priority = Number(settings.priority || 0); |
| 103 | 285 if (type == 'web') |
| 104 /** @private */ | 286 createWebNotification(id, options); |
| 105 onSettingsChange_: function(settings) { | 287 else |
| 106 this.settings = settings; | 288 createRichNotification(id, type, priority, options); |
| 107 chrome.storage.sync.set({settings: this.settings}); | 289 } |
| 108 }, | 290 |
| 109 | 291 function createWebNotification(id, options) { |
| 110 /** @private */ | 292 var iconUrl = options.iconUrl; |
| 111 createNotification_: function(type, options) { | 293 var title = options.title; |
| 112 var id = this.getNextId_(); | 294 var message = options.message; |
| 113 var priority = Number(this.settings.priority || 0); | 295 var n = new Notification(title, { |
| 114 var expanded = this.getExpandedOptions_(options, id, type, priority); | 296 body: message, |
| 115 if (type == 'webkit') | 297 icon: iconUrl, |
| 116 this.createWebKitNotification_(expanded); | 298 tag: id |
| 117 else | 299 }); |
| 118 this.createRichNotification_(expanded, id, type, priority); | 300 n.onshow = function() { logEvent('WebNotification #' + id + ': onshow'); } |
| 119 }, | 301 n.onclick = function() { logEvent('WebNotification #' + id + ': onclick'); } |
| 120 | 302 n.onclose = function() { |
| 121 /** @private */ | 303 logEvent('WebNotification #' + id + ': onclose'); |
| 122 createWebKitNotification_: function(options) { | 304 recordDelete('web', id); |
| 123 var iconUrl = options.iconUrl; | 305 } |
| 124 var title = options.title; | 306 logCreate('Web', id, 'title: "' + title + '"'); |
| 125 var message = options.message; | 307 recordCreate('web', id, options); |
| 126 new Notification(title, { | 308 return n; |
| 127 body: message, | 309 } |
| 128 icon: iconUrl | 310 |
| 129 }); | 311 function createRichNotification(id, type, priority, options) { |
| 130 this.handleEvent_('create', '?', 'title: "' + title + '"'); | 312 options["type"] = type; |
| 131 }, | 313 options["priority"] = priority; |
| 132 | 314 chrome.notifications.create(id, options, function() { |
| 133 /** @private */ | 315 var argument1 = 'type: "' + type + '"'; |
| 134 createRichNotification_: function(options, id, type, priority) { | 316 var argument2 = 'priority: ' + priority; |
| 135 this.api.create(id, options, function() { | 317 var argument3 = 'title: "' + options.title + '"'; |
| 136 var argument1 = 'type: "' + type + '"'; | 318 logCreate('Rich', id, argument1, argument2, argument3); |
| 137 var argument2 = 'priority: ' + priority; | 319 }); |
| 138 var argument3 = 'title: "' + options.title + '"'; | 320 recordCreate('rich', id, options); |
| 139 this.handleEvent_('create', id, argument1, argument2, argument3); | 321 } |
| 140 }.bind(this)); | 322 |
| 141 }, | 323 var counter = 0; |
| 142 | 324 function getNextId() { |
| 143 /** @private */ | 325 return String(counter++); |
| 144 getNextId_: function() { | 326 } |
| 145 this.counter += 1; | 327 |
| 146 return String(this.counter); | 328 function addListeners() { |
| 147 }, | 329 chrome.notifications.onClosed.addListener(onClosed); |
| 148 | 330 chrome.notifications.onClicked.addListener(onClicked); |
| 149 /** @private */ | 331 chrome.notifications.onButtonClicked.addListener(onButtonClicked); |
| 150 getDefaultedOptions_: function(options, defaults) { | 332 } |
| 151 var defaulted = this.deepCopy_(options); | 333 |
| 152 Object.keys(defaults || {}).forEach(function (key) { | 334 function logCreate(kind, id, var_args) { |
| 153 defaulted[key] = options[key] || defaults[key]; | 335 logEvent(kind + ' Notification #' + id + ': created ' + '(' + |
| 154 }); | 336 Array.prototype.slice.call(arguments, 2).join(', ') + ')'); |
| 155 return defaulted; | 337 } |
| 156 }, | 338 |
| 157 | 339 function onClosed(id) { |
| 158 /** @private */ | 340 logEvent('Notification #' + id + ': onClosed'); |
| 159 getExpandedOptions_: function(options, id, type, priority) { | 341 recordDelete('rich', id); |
| 160 var expanded = this.deepCopy_(options); | 342 } |
| 161 return this.mapStrings_(expanded, function(string) { | 343 |
| 162 return this.getExpandedOption_(string, id, type, priority); | 344 function onClicked(id) { |
| 163 }, this); | 345 logEvent('Notification #' + id + ': onClicked'); |
| 164 }, | 346 } |
| 165 | 347 |
| 166 /** @private */ | 348 function onButtonClicked(id, index) { |
| 167 getExpandedOption_: function(option, id, type, priority) { | 349 logEvent('Notification #' + id + ': onButtonClicked, btn: ' + index); |
| 168 if (option == '$!') { | 350 } |
| 169 option = priority; // Avoids making priorities into strings. | |
| 170 } else { | |
| 171 option = option.replace(/\$#/g, id); | |
| 172 option = option.replace(/\$\?/g, type); | |
| 173 option = option.replace(/\$\!/g, priority); | |
| 174 } | |
| 175 return option; | |
| 176 }, | |
| 177 | |
| 178 /** @private */ | |
| 179 deepCopy_: function(value) { | |
| 180 var copy = value; | |
| 181 if (Array.isArray(value)) { | |
| 182 copy = value.map(this.deepCopy_, this); | |
| 183 } else if (value && typeof value === 'object') { | |
| 184 copy = {} | |
| 185 Object.keys(value).forEach(function (key) { | |
| 186 copy[key] = this.deepCopy_(value[key]); | |
| 187 }, this); | |
| 188 } | |
| 189 return copy; | |
| 190 }, | |
| 191 | |
| 192 /** @private */ | |
| 193 mapStrings_: function(value, map) { | |
| 194 var mapped = value; | |
| 195 if (typeof value === 'string') { | |
| 196 mapped = map.call(this, value); | |
| 197 mapped = (typeof mapped !== 'undefined') ? mapped : value; | |
| 198 } else if (value && typeof value == 'object') { | |
| 199 Object.keys(value).forEach(function (key) { | |
| 200 mapped[key] = this.mapStrings_(value[key], map); | |
| 201 }, this); | |
| 202 } | |
| 203 return mapped; | |
| 204 }, | |
| 205 | |
| 206 /** @private */ | |
| 207 addListeners_: function(api, events) { | |
| 208 (events || []).forEach(function(event) { | |
| 209 var listener = this.handleEvent_.bind(this, event); | |
| 210 if (api[event]) | |
| 211 api[event].addListener(listener); | |
| 212 else | |
| 213 console.log('Event ' + event + ' not defined.'); | |
| 214 }, this); | |
| 215 }, | |
| 216 | |
| 217 /** @private */ | |
| 218 handleEvent_: function(event, id, var_args) { | |
| 219 this.view.logEvent('Notification #' + id + ': ' + event + '(' + | |
| 220 Array.prototype.slice.call(arguments, 2).join(', ') + | |
| 221 ')'); | |
| 222 }, | |
| 223 | |
| 224 /** @private */ | |
| 225 getDataVersion_: function() { | |
| 226 var version = navigator.appVersion.replace(/^.* Chrome\//, ''); | |
| 227 return (version > '28.0.1500.70') ? '28.0.1500.70.json' : | |
| 228 (version > '27.0.1433.1') ? '27.0.1433.1.json' : | |
| 229 (version > '27.0.1432.2') ? '27.0.1432.2.json' : | |
| 230 '27.0.0.0.json'; | |
| 231 }, | |
| 232 | |
| 233 /** @private */ | |
| 234 getApi_: function(data) { | |
| 235 var path = data[0].api || 'notifications'; | |
| 236 var api = chrome; | |
| 237 path.split('.').forEach(function(key) { api = api && api[key]; }); | |
| 238 if (!api) | |
| 239 this.view.logError('No API found - chrome.' + path + ' is undefined'); | |
| 240 return api; | |
| 241 } | |
| 242 | |
| 243 }; | |
| OLD | NEW |