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 // This function forms 2 selector lists - one that includes enabled buttons |
| 35 // and one that includes disabled ones. Then it applies "disabled" attribute to |
| 36 // corresponding sets of buttons. |
| 37 function updateButtonsState() { |
| 38 recorderButtonStates.map(function(entry) { |
| 39 if (entry.state != recordingState) |
| 40 return; |
| 41 // Found entry with current recorder state. Now compute the sets |
| 42 // of enabled/disabled buttons. |
| 43 // Copy a list of all buttons. |
| 44 var disabled = recorderButtons.slice(0); |
| 45 // Get an array of enabled buttons for the state. |
| 46 var enabled = entry.enabled.split(" "); |
| 47 // Remove enabled buttons from disabled list, prefix them with "#" so they |
| 48 // form proper id selectors. |
| 49 for (var i = 0; i < enabled.length; i++) { |
| 50 disabled.splice(disabled.indexOf(enabled[i]), 1); |
| 51 enabled[i] = "#" + enabled[i]; |
| 52 } |
| 53 // Prefix remaining disabled ids to form proper id selectors. |
| 54 for (var i = 0; i < disabled.length; i++) { |
| 55 disabled[i] = "#" + disabled[i]; |
| 56 } |
| 57 getElements(disabled.join(", ")).forEach(function(element) { |
| 58 element.setAttribute("disabled", "true") |
| 59 }) |
| 60 getElements(enabled.join(", ")).forEach(function(element) { |
| 61 element.removeAttribute("disabled") |
| 62 }) |
| 63 }) |
| 64 } |
| 65 |
| 66 |
| 67 function setRecordingState(newState) { |
| 68 setRecorderStatusText(newState); |
| 69 recordingState = newState; |
| 70 updateButtonsState(); |
| 71 } |
| 72 |
| 73 function updateRecordingStats(context) { |
| 74 var length = 0; |
| 75 var segmentCnt = 0; |
| 76 recordingList.slice(currentSegmentIndex).forEach(function(segment) { |
| 77 length += segment.delay || 0; |
| 78 segmentCnt++; |
| 79 }) |
| 80 updateRecordingStatsDisplay(context + ": " + (segmentCnt-1) + " segments, " + |
| 81 Math.floor(length/1000) + " seconds."); |
| 82 } |
| 83 |
| 84 function loadRecording() { |
| 85 chrome.storage.local.get("recording", function(items) { |
| 86 recordingList = JSON.parse(items["recording"] || "[]"); |
| 87 setRecordingState(STOPPED); |
| 88 updateRecordingStats("Loaded record"); |
| 89 }) |
| 90 } |
| 91 |
| 92 function finalizeRecording() { |
| 93 chrome.storage.local.set({"recording": JSON.stringify(recordingList)}); |
| 94 updateRecordingStats("Recorded"); |
| 95 } |
| 96 |
| 97 function setPreviousSegmentDuration() { |
| 98 var now = new Date().getTime(); |
| 99 var delay = now - segmentStart; |
| 100 segmentStart = now; |
| 101 recordingList[recordingList.length - 1].delay = delay; |
| 102 } |
| 103 |
| 104 function recordCreate(kind, id, options) { |
| 105 if (recordingState != RECORDING) |
| 106 return; |
| 107 setPreviousSegmentDuration(); |
| 108 recordingList.push({ type: "create", kind: kind, id: id, options: options }); |
| 109 updateRecordingStats("Recording"); |
| 110 } |
| 111 |
| 112 function recordDelete(kind, id) { |
| 113 if (recordingState != RECORDING) |
| 114 return; |
| 115 setPreviousSegmentDuration(); |
| 116 recordingList.push({ type: "delete", kind: kind, id: id }); |
| 117 updateRecordingStats("Recording"); |
| 118 } |
| 119 |
| 120 function startPlaying() { |
| 121 if (recordingList.length < 2) |
| 122 return false; |
| 123 |
| 124 setRecordingState(PLAYING); |
| 125 |
| 126 if (playingTimer) |
| 127 clearTimeout(playingTimer); |
| 128 |
| 129 webNotifications = {}; |
| 130 currentSegmentIndex = 0; |
| 131 playingTimer = setTimeout(playNextSegment, |
| 132 recordingList[currentSegmentIndex].delay); |
| 133 updateRecordingStats("Playing"); |
| 134 } |
| 135 |
| 136 function playNextSegment() { |
| 137 currentSegmentIndex++; |
| 138 var segment = recordingList[currentSegmentIndex]; |
| 139 if (!segment) { |
| 140 stopPlaying(); |
| 141 return; |
| 142 } |
| 143 |
| 144 if (segment.type == "create") { |
| 145 createNotificationForPlay(segment.kind, segment.id, segment.options); |
| 146 } else { // type == "delete" |
| 147 deleteNotificationForPlay(segment.kind, segment.id); |
| 148 } |
| 149 playingTimer = setTimeout(playNextSegment, |
| 150 recordingList[currentSegmentIndex].delay); |
| 151 segmentStart = new Date().getTime(); |
| 152 updateRecordingStats("Playing"); |
| 153 } |
| 154 |
| 155 function deleteNotificationForPlay(kind, id) { |
| 156 if (kind == 'web') { |
| 157 webNotifications[id].close(); |
| 158 } else { |
| 159 chrome.notifications.clear(id, function(wasClosed) { |
| 160 // nothing to do |
| 161 }); |
| 162 } |
| 163 } |
| 164 |
| 165 function createNotificationForPlay(kind, id, options) { |
| 166 if (kind == 'web') { |
| 167 webNotifications[id] = createWebNotification(id, options); |
| 168 } else { |
| 169 var type = options.type; |
| 170 var priority = options.priority; |
| 171 createRichNotification(id, type, priority, options); |
| 172 } |
| 173 } |
| 174 function stopPlaying() { |
| 175 currentSegmentIndex = 0; |
| 176 clearTimeout(playingTimer); |
| 177 updateRecordingStats("Record"); |
| 178 setRecordingState(STOPPED); |
| 179 } |
| 180 |
| 181 function pausePlaying() { |
| 182 clearTimeout(playingTimer); |
| 183 pausedDuration = new Date().getTime() - segmentStart; |
| 184 setRecordingState(PAUSED_PLAYING); |
| 185 } |
| 186 |
| 187 function unpausePlaying() { |
| 188 var remainingInSegment = |
| 189 recordingList[currentSegmentIndex].delay - pausedDuration; |
| 190 if (remainingInSegment < 0) |
| 191 remainingInSegment = 0; |
| 192 playingTimer = setTimeout(playNextSegment, remainingInSegment); |
| 193 segmentStart = new Date().getTime() - pausedDuration; |
| 194 } |
| 195 |
| 196 function onRecord() { |
| 197 if (recordingState == STOPPED) { |
| 198 segmentStart = new Date().getTime(); |
| 199 pausedDuration = 0; |
| 200 // This item is only needed to keep a duration of the delay between start |
| 201 // and first action. |
| 202 recordingList = [ { type:"start" } ]; |
| 203 } else if (recordingState == PAUSED_RECORDING) { |
| 204 segmentStart = new Date().getTime() - pausedDuration; |
| 205 pausedDuration = 0; |
| 206 } else { |
| 207 return; |
| 208 } |
| 209 updateRecordingStats("Recording"); |
| 210 setRecordingState(RECORDING); |
| 211 } |
| 212 |
| 213 function pauseRecording() { |
| 214 pausedDuration = new Date().getTime() - segmentStart; |
| 215 segmentStart = 0; |
| 216 setRecordingState(PAUSED_RECORDING); |
| 217 } |
| 218 |
| 219 function onPause() { |
| 220 if (recordingState == RECORDING) { |
| 221 pauseRecording(); |
| 222 } else if (recordingState == PLAYING) { |
| 223 pausePlaying(); |
| 224 } else { |
| 225 return; |
| 226 } |
| 227 } |
| 228 |
| 229 function onStop() { |
| 230 switch (recordingState) { |
| 231 case PAUSED_RECORDING: |
| 232 segmentStart = new Date().getTime() - pausedDuration; |
| 233 // fall through |
| 234 case RECORDING: |
| 235 finalizeRecording(); |
| 236 break; |
| 237 case PLAYING: |
| 238 case PAUSED_PLAYING: |
| 239 stopPlaying(); |
| 240 break; |
| 241 } |
| 242 setRecordingState(STOPPED); |
| 243 } |
| 244 |
| 245 function onPlay() { |
| 246 if (recordingState == STOPPED) { |
| 247 if (!startPlaying()) |
| 248 return; |
| 249 } else if (recordingState == PAUSED_PLAYING) { |
| 250 unpausePlaying(); |
| 251 } |
| 252 setRecordingState(PLAYING); |
| 253 } |
| 254 |
| 255 function createWindow() { |
| 256 chrome.storage.local.get('settings', onSettingsFetched); |
| 257 } |
| 258 |
| 259 function onSettingsFetched(items) { |
| 260 settings = items.settings || settings; |
| 261 var request = new XMLHttpRequest(); |
| 262 var source = '/data/data.json'; |
| 263 request.open('GET', source, true); |
| 264 request.responseType = 'text'; |
| 265 request.onload = onDataFetched; |
| 266 request.send(); |
| 267 } |
| 268 |
| 269 function onDataFetched() { |
| 270 var data = JSON.parse(this.response); |
| 271 createAppWindow(function() { |
| 272 // Create notification buttons. |
35 data.forEach(function(section) { | 273 data.forEach(function(section) { |
| 274 var type = section.notificationType; |
36 (section.notificationOptions || []).forEach(function(options) { | 275 (section.notificationOptions || []).forEach(function(options) { |
37 ++count; | 276 addNotificationButton(section.sectionName, |
38 this.fetchImages_(options, function() { | 277 options.title, |
39 if (--count == 0) | 278 options.iconUrl, |
40 this.onImagesFetched_(settings, data); | 279 function() { createNotification(type, options) }); |
41 }.bind(this)); | 280 }); |
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 }); | 281 }); |
90 }, | 282 loadRecording(); |
91 | 283 addListeners(); |
92 /** @private */ | 284 showWindow(); |
93 fetchImage_: function(url, onFetched) { | 285 }); |
94 var request = new XMLHttpRequest(); | 286 } |
95 request.open('GET', url, true); | 287 |
96 request.responseType = 'blob'; | 288 function onSettingsChange(settings) { |
97 request.onload = function() { | 289 chrome.storage.local.set({settings: settings}); |
98 var url = window.URL.createObjectURL(request.response); | 290 } |
99 onFetched.call(this, url); | 291 |
100 }.bind(this); | 292 function createNotification(type, options) { |
101 request.send(); | 293 var id = getNextId(); |
102 }, | 294 var priority = Number(settings.priority || 0); |
103 | 295 if (type == 'web') |
104 /** @private */ | 296 createWebNotification(id, options); |
105 onSettingsChange_: function(settings) { | 297 else |
106 this.settings = settings; | 298 createRichNotification(id, type, priority, options); |
107 chrome.storage.sync.set({settings: this.settings}); | 299 } |
108 }, | 300 |
109 | 301 function createWebNotification(id, options) { |
110 /** @private */ | 302 var iconUrl = options.iconUrl; |
111 createNotification_: function(type, options) { | 303 var title = options.title; |
112 var id = this.getNextId_(); | 304 var message = options.message; |
113 var priority = Number(this.settings.priority || 0); | 305 var n = new Notification(title, { |
114 var expanded = this.getExpandedOptions_(options, id, type, priority); | 306 body: message, |
115 if (type == 'webkit') | 307 icon: iconUrl, |
116 this.createWebKitNotification_(expanded); | 308 tag: id |
117 else | 309 }); |
118 this.createRichNotification_(expanded, id, type, priority); | 310 n.onshow = function() { logEvent('WebNotification #' + id + ': onshow'); } |
119 }, | 311 n.onclick = function() { logEvent('WebNotification #' + id + ': onclick'); } |
120 | 312 n.onclose = function() { |
121 /** @private */ | 313 logEvent('WebNotification #' + id + ': onclose'); |
122 createWebKitNotification_: function(options) { | 314 recordDelete('web', id); |
123 var iconUrl = options.iconUrl; | 315 } |
124 var title = options.title; | 316 logCreate('Web', id, 'title: "' + title + '"'); |
125 var message = options.message; | 317 recordCreate('web', id, options); |
126 new Notification(title, { | 318 return n; |
127 body: message, | 319 } |
128 icon: iconUrl | 320 |
129 }); | 321 function createRichNotification(id, type, priority, options) { |
130 this.handleEvent_('create', '?', 'title: "' + title + '"'); | 322 options["type"] = type; |
131 }, | 323 options["priority"] = priority; |
132 | 324 chrome.notifications.create(id, options, function() { |
133 /** @private */ | 325 var argument1 = 'type: "' + type + '"'; |
134 createRichNotification_: function(options, id, type, priority) { | 326 var argument2 = 'priority: ' + priority; |
135 this.api.create(id, options, function() { | 327 var argument3 = 'title: "' + options.title + '"'; |
136 var argument1 = 'type: "' + type + '"'; | 328 logCreate('Rich', id, argument1, argument2, argument3); |
137 var argument2 = 'priority: ' + priority; | 329 }); |
138 var argument3 = 'title: "' + options.title + '"'; | 330 recordCreate('rich', id, options); |
139 this.handleEvent_('create', id, argument1, argument2, argument3); | 331 } |
140 }.bind(this)); | 332 |
141 }, | 333 var counter = 0; |
142 | 334 function getNextId() { |
143 /** @private */ | 335 return String(counter++); |
144 getNextId_: function() { | 336 } |
145 this.counter += 1; | 337 |
146 return String(this.counter); | 338 function addListeners() { |
147 }, | 339 chrome.notifications.onClosed.addListener(onClosed); |
148 | 340 chrome.notifications.onClicked.addListener(onClicked); |
149 /** @private */ | 341 chrome.notifications.onButtonClicked.addListener(onButtonClicked); |
150 getDefaultedOptions_: function(options, defaults) { | 342 } |
151 var defaulted = this.deepCopy_(options); | 343 |
152 Object.keys(defaults || {}).forEach(function (key) { | 344 function logCreate(kind, id, var_args) { |
153 defaulted[key] = options[key] || defaults[key]; | 345 logEvent(kind + ' Notification #' + id + ': created ' + '(' + |
154 }); | 346 Array.prototype.slice.call(arguments, 2).join(', ') + ')'); |
155 return defaulted; | 347 } |
156 }, | 348 |
157 | 349 function onClosed(id) { |
158 /** @private */ | 350 logEvent('Notification #' + id + ': onClosed'); |
159 getExpandedOptions_: function(options, id, type, priority) { | 351 recordDelete('rich', id); |
160 var expanded = this.deepCopy_(options); | 352 } |
161 return this.mapStrings_(expanded, function(string) { | 353 |
162 return this.getExpandedOption_(string, id, type, priority); | 354 function onClicked(id) { |
163 }, this); | 355 logEvent('Notification #' + id + ': onClicked'); |
164 }, | 356 } |
165 | 357 |
166 /** @private */ | 358 function onButtonClicked(id, index) { |
167 getExpandedOption_: function(option, id, type, priority) { | 359 logEvent('Notification #' + id + ': onButtonClicked, btn: ' + index); |
168 if (option == '$!') { | 360 } |
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 |