1 | 1 patch for repository /home/josip/bin/tahoe-tmp: |
---|
2 | |
---|
3 | Sat Jul 10 20:57:04 CEST 2010 josip.lisec@gmail.com |
---|
4 | * add-music-player |
---|
5 | |
---|
6 | New patches: |
---|
7 | |
---|
8 | [add-music-player |
---|
9 | josip.lisec@gmail.com**20100710185704 |
---|
10 | Ignore-this: c29dc0709640abd2e33cfd119b2681f |
---|
11 | ] { |
---|
12 | adddir ./contrib/musicplayer |
---|
13 | addfile ./contrib/musicplayer/INSTALL |
---|
14 | hunk ./contrib/musicplayer/INSTALL 1 |
---|
15 | +=== Installing Music Player for Tahoe (codename 'Daaw') === |
---|
16 | + |
---|
17 | +== Maths and Systems Theory quiz == |
---|
18 | +If you already have a 'build' directory, feel free to skip this step. |
---|
19 | + |
---|
20 | +To build player's code you'll have to do a not-so-simple |
---|
21 | +operation of computing file dependencies, compressing variable names in JavaScript |
---|
22 | +code and stitching them all into one file. |
---|
23 | + |
---|
24 | +I strongly hope that you took advanced Maths, Systems Theory |
---|
25 | +and computing related courses. |
---|
26 | + |
---|
27 | +Just in case you haven't, you can type in next line into your shell: |
---|
28 | + $ python manage.py build |
---|
29 | + running build |
---|
30 | + Calculating dependencies... |
---|
31 | + Compressing <something>... |
---|
32 | + ... |
---|
33 | + Done! |
---|
34 | + |
---|
35 | +Bravo, you're done! (just make sure you have a 'build' directory) |
---|
36 | + |
---|
37 | +(And if you're one of those who prefer to do it by-hand (and keyboard), |
---|
38 | +this file isn't a place for you.) |
---|
39 | + |
---|
40 | +== Battle for Configuration File == |
---|
41 | +Player's configuration file is a real beast on its own, |
---|
42 | +and in order to edit it we must prepare ourselves really good, |
---|
43 | +otherwise, we're doomed (actually, only you are )! |
---|
44 | + |
---|
45 | +Read next few steps carefully, the beast is just around the corner! |
---|
46 | + |
---|
47 | +1. Create two dirnodes on your Tahoe-LAFS server, one will be used for storing |
---|
48 | + all your music files and the other one for syncing settings between multiple |
---|
49 | + computers. |
---|
50 | + |
---|
51 | + Just in case you've forgotten how to create Tahoe dirnodes, run this from your |
---|
52 | + shell: |
---|
53 | + $ tahoe mkdir music |
---|
54 | + <top secret no.1> |
---|
55 | + $ tahoe mkdir settings |
---|
56 | + <top secret no.2> |
---|
57 | + |
---|
58 | + (make sure Tahoe-LAFS is running on your computer before issuing those commands) |
---|
59 | + |
---|
60 | +2. Take a big breath, as we're about to open example configuration file! |
---|
61 | + |
---|
62 | +3. Yep, now open the 'config.example.json' file in your favourite text editor. |
---|
63 | + Now quickly, we have to replace her evil genes with a good ones, |
---|
64 | + find following line in her DNA sequence: |
---|
65 | + |
---|
66 | + "music_cap": "<bad gene no.1>", |
---|
67 | + "settings_cap": "<bad gene no.2>" |
---|
68 | + |
---|
69 | + and quickly replace <bad gene no.1> with <top secret no.1> as well as <bad gene no.2> |
---|
70 | + with <top secret no.2>. |
---|
71 | + |
---|
72 | + If you're still here, congrats! |
---|
73 | + |
---|
74 | + (The truth about <top secret>s is that your Tahoe-LAFS installation actually |
---|
75 | + knows how to re-sequence DNA of living beings, and we don't want others to |
---|
76 | + find out about that and use it in evil purposes, don't we?) |
---|
77 | + |
---|
78 | + Now save the new genes under name of 'config.json'. |
---|
79 | + |
---|
80 | +== The Critical Step == |
---|
81 | +After we've conquered the beast of configuration file we're ready to |
---|
82 | +upload the player to the Tahoe! |
---|
83 | + |
---|
84 | +To do that, just copy the 'build' directory to 'public_html' directory of your |
---|
85 | +Tahoe storage node (usually ~/.tahoe). |
---|
86 | +Note that 'public_html' directory is probably missing, |
---|
87 | +so you'll have to create it on your own. |
---|
88 | + |
---|
89 | + $ mkdir -p ~/.tahoe/public_html/musicplayer |
---|
90 | + $ cp -r build/ ~/.tahoe/public_html/musicplayer |
---|
91 | + |
---|
92 | +WARNING: If you don't perform next step exactly as |
---|
93 | +you're instructed, the whole process could fail and you'll |
---|
94 | +have to start all over! |
---|
95 | + |
---|
96 | +Now, stand up, and with evident excitement on your face, |
---|
97 | +say the following phrase: |
---|
98 | + "Yay! It's working!" |
---|
99 | + |
---|
100 | +== Fin == |
---|
101 | +You can now upload your music to the <top secret no.1> dirnode and |
---|
102 | +launch music player by typing this URL into your web browser: |
---|
103 | + http://localhost:3456/static/musicplayer |
---|
104 | + |
---|
105 | +If it appears that something isn't working, it probably means |
---|
106 | +that you haven't read 'The Critical Step' carefully enough. |
---|
107 | + |
---|
108 | +We hope you're going to enjoy your music even more with Music Player for Tahoe-LAFS! |
---|
109 | addfile ./contrib/musicplayer/manage.py |
---|
110 | hunk ./contrib/musicplayer/manage.py 1 |
---|
111 | +#!/usr/bin/env python |
---|
112 | +# -*- coding: utf-8 -*- |
---|
113 | + |
---|
114 | +import os, shutil, sys, subprocess, re |
---|
115 | +from time import sleep |
---|
116 | +from setuptools import setup |
---|
117 | +from setuptools import Command |
---|
118 | + |
---|
119 | +CLOSURE_COMPILER_PATH = 'tools/closure-compiler-20100514/compiler.jar' |
---|
120 | + |
---|
121 | +class JSDepsBuilder: |
---|
122 | + """ |
---|
123 | + Looks for |
---|
124 | + //#require "file.js" |
---|
125 | + and |
---|
126 | + //#require <file.js> |
---|
127 | + lines in JavaScript files and creates a file with all the required files. |
---|
128 | + """ |
---|
129 | + requires_re = re.compile('^//#require ["|\<](.+)["|\>]$', re.M) |
---|
130 | + |
---|
131 | + def __init__(self, root_directory): |
---|
132 | + self.files = {} |
---|
133 | + self.included = [] |
---|
134 | + self.root = root_directory |
---|
135 | + |
---|
136 | + self.scan() |
---|
137 | + |
---|
138 | + def scan(self): |
---|
139 | + for (dirname, dirs, files) in os.walk(self.root): |
---|
140 | + for filename in files: |
---|
141 | + if filename.endswith('.js'): |
---|
142 | + self.detect_requires(os.path.join(dirname, filename)) |
---|
143 | + |
---|
144 | + def detect_requires(self, path): |
---|
145 | + reqs = [] |
---|
146 | + script_file = open(path, 'r') |
---|
147 | + script = script_file.read() |
---|
148 | + script_file.close() |
---|
149 | + |
---|
150 | + reqs = re.findall(JSDepsBuilder.requires_re, script) |
---|
151 | + for i in range(len(reqs)): |
---|
152 | + req_path = os.path.join(self.root, reqs[i]) |
---|
153 | + reqs[i] = req_path |
---|
154 | + if not os.path.isdir(req_path) and not req_path.endswith('.js'): |
---|
155 | + reqs[i] += '.js' |
---|
156 | + |
---|
157 | + #if len(reqs): |
---|
158 | + # print '%s depends on:' % os.path.basename(path) |
---|
159 | + # print '\t', '\n\t'.join(reqs) |
---|
160 | + |
---|
161 | + self.files[path] = reqs |
---|
162 | + |
---|
163 | + def parse(self, path): |
---|
164 | + if path in self.included: |
---|
165 | + return '' |
---|
166 | + if not path.endswith('.js'): |
---|
167 | + # TODO: If path points to a directory, require all the files within that directory. |
---|
168 | + return '' |
---|
169 | + |
---|
170 | + def insert_code(match): |
---|
171 | + req_path = os.path.join(self.root, match.group(1)) |
---|
172 | + if not req_path in self.included: |
---|
173 | + if not os.path.isfile(req_path): |
---|
174 | + raise Exception('%s requires non existing file: %s' % (path, req_path)) |
---|
175 | + |
---|
176 | + return self.parse(req_path) |
---|
177 | + |
---|
178 | + script_file = open(path, 'r') |
---|
179 | + script = script_file.read() |
---|
180 | + script_file.close() |
---|
181 | + script = re.sub(JSDepsBuilder.requires_re, insert_code, script) |
---|
182 | + self.included.append(path) |
---|
183 | + |
---|
184 | + return script |
---|
185 | + |
---|
186 | + def write_to_file(self, filename, root_file = 'Application.js'): |
---|
187 | + output = open(filename, 'w+') |
---|
188 | + self.included = [] |
---|
189 | + output.write(self.parse(os.path.join(self.root, root_file))) |
---|
190 | + output.close() |
---|
191 | + |
---|
192 | + def print_script_tags(self, root_file = 'Application.js'): |
---|
193 | + self.included = [] |
---|
194 | + self.parse(os.path.join(self.root, root_file)) |
---|
195 | + |
---|
196 | + for filename in self.included: |
---|
197 | + print '<script src="%s" type="text/javascript" charset="utf-8"></script>' % filename |
---|
198 | + |
---|
199 | +class Build(Command): |
---|
200 | + description = 'builds whole application into build directory' |
---|
201 | + user_options = [ |
---|
202 | + ('compilation-level=', 'c', 'compilation level for Google\'s Closure compiler.'), |
---|
203 | + ] |
---|
204 | + |
---|
205 | + def initialize_options(self): |
---|
206 | + self.compilation_level = 'SIMPLE_OPTIMIZATIONS' |
---|
207 | + |
---|
208 | + def finalize_options(self): |
---|
209 | + compilation_levels = [ |
---|
210 | + 'SIMPLE_OPTIMIZATIONS', 'WHITESPACE_ONLY', 'ADVANCED_OPTIMIZATIONS', 'NONE' |
---|
211 | + ] |
---|
212 | + |
---|
213 | + self.compilation_level = self.compilation_level.upper() |
---|
214 | + if not self.compilation_level in compilation_levels: |
---|
215 | + self.compilation_level = compilation_levels[0] |
---|
216 | + |
---|
217 | + def run(self): |
---|
218 | + if os.path.isdir('build'): |
---|
219 | + shutil.rmtree('build') |
---|
220 | + |
---|
221 | + shutil.copytree('src/resources', 'build/resources') |
---|
222 | + shutil.copytree('src/plugins', 'build/plugins') |
---|
223 | + shutil.copy('src/config.example.json', 'build/') |
---|
224 | + shutil.copy('src/index.html', 'build/') |
---|
225 | + |
---|
226 | + shutil.copytree('src/libs/vendor/soundmanager/swf', 'build/resources/flash') |
---|
227 | + shutil.copy('src/libs/vendor/persist-js/persist.swf', 'build/resources/flash') |
---|
228 | + |
---|
229 | + os.makedirs('build/js/libs') |
---|
230 | + os.makedirs('build/js/workers') |
---|
231 | + shutil.copy('src/libs/vendor/browser-couch/js/worker-map-reducer.js', 'build/js/workers/map-reducer.js') |
---|
232 | + |
---|
233 | + print 'Calculating dependencies...' |
---|
234 | + appjs = JSDepsBuilder('src/') |
---|
235 | + appjs.write_to_file('build/app.js') |
---|
236 | + self._compress('build/js/app.js', ['build/app.js']) |
---|
237 | + os.remove('build/app.js') |
---|
238 | + |
---|
239 | + # Libraries used by web workers |
---|
240 | + self._compile_js('src/libs', 'build/js/workers/env.js', files = [ |
---|
241 | + 'vendor/mootools-1.2.4-core-server.js', |
---|
242 | + 'vendor/mootools-1.2.4-request.js', |
---|
243 | + |
---|
244 | + 'util/util.js', |
---|
245 | + 'util/BinaryFile.js', |
---|
246 | + 'util/ID3.js', |
---|
247 | + 'util/ID3v2.js', |
---|
248 | + 'util/ID3v1.js', |
---|
249 | + 'TahoeObject.js' |
---|
250 | + ]) |
---|
251 | + |
---|
252 | + # Compressing the workers themselves |
---|
253 | + self._compile_js('src/workers', 'build/js/workers/', join = False) |
---|
254 | + |
---|
255 | + print 'Done!' |
---|
256 | + |
---|
257 | + def _compile_js(self, source, output_file, files = None, join = True): |
---|
258 | + js_files = files |
---|
259 | + if not js_files: |
---|
260 | + js_files = [] |
---|
261 | + for filename in os.listdir(source): |
---|
262 | + if filename.endswith('.js'): |
---|
263 | + js_files.append(os.path.join(source, filename)) |
---|
264 | + |
---|
265 | + js_files.sort() |
---|
266 | + else: |
---|
267 | + js_files = [os.path.join(source, path) for path in files] |
---|
268 | + |
---|
269 | + if join: |
---|
270 | + self._compress(output_file, js_files) |
---|
271 | + else: |
---|
272 | + for js_file in js_files: |
---|
273 | + self._compress(output_file + os.path.basename(js_file), [js_file]) |
---|
274 | + |
---|
275 | + def _compress(self, output_file, files): |
---|
276 | + print 'Compressing %s...' % output_file |
---|
277 | + |
---|
278 | + if self.compilation_level == 'NONE': |
---|
279 | + output_file = open(output_file, 'a') |
---|
280 | + for filename in files: |
---|
281 | + f = open(filename) |
---|
282 | + output_file.write(f.read()) |
---|
283 | + output_file.write('\n') |
---|
284 | + f.close() |
---|
285 | + |
---|
286 | + output_file.close() |
---|
287 | + else: |
---|
288 | + args = [ |
---|
289 | + 'java', |
---|
290 | + '-jar', CLOSURE_COMPILER_PATH, |
---|
291 | + '--warning_level', 'QUIET', |
---|
292 | + '--compilation_level', self.compilation_level, |
---|
293 | + '--js_output_file', output_file] |
---|
294 | + |
---|
295 | + for filename in files: |
---|
296 | + args.append('--js') |
---|
297 | + args.append(filename) |
---|
298 | + |
---|
299 | + subprocess.call(args) |
---|
300 | + |
---|
301 | +class Watch(Build): |
---|
302 | + description = 'watches src directory for changes and runs build command when they occur' |
---|
303 | + |
---|
304 | + def run(self): |
---|
305 | + self.dirs = {} |
---|
306 | + |
---|
307 | + while True: |
---|
308 | + if self._watch_dir(): |
---|
309 | + print 'Watching for changes...' |
---|
310 | + sleep(5) |
---|
311 | + |
---|
312 | + def _watch_dir(self): |
---|
313 | + should_build = False |
---|
314 | + for (root, dirs, files) in os.walk('src'): |
---|
315 | + for file in files: |
---|
316 | + path = root + '/' + file |
---|
317 | + mtime = os.stat(path).st_mtime |
---|
318 | + |
---|
319 | + if not path in self.dirs: |
---|
320 | + self.dirs[path] = 0 |
---|
321 | + |
---|
322 | + if self.dirs[path] != mtime: |
---|
323 | + should_build = True |
---|
324 | + self.dirs[path] = mtime |
---|
325 | + print '\t* ' + path |
---|
326 | + |
---|
327 | + if should_build: |
---|
328 | + Build.run(self) |
---|
329 | + return True |
---|
330 | + else: |
---|
331 | + return False |
---|
332 | + |
---|
333 | + |
---|
334 | +class Package(Build): |
---|
335 | + description = 'builds application and creates a .tar.gz archive' |
---|
336 | + user_options = [] |
---|
337 | + |
---|
338 | + def initalize_options(self): |
---|
339 | + pass |
---|
340 | + def finalize_options(self): |
---|
341 | + pass |
---|
342 | + |
---|
343 | + def run(self): |
---|
344 | + Build.run(self) |
---|
345 | + |
---|
346 | + |
---|
347 | +class Install(Command): |
---|
348 | + description = 'copies application to storage node\'s public_html and writes configuration files' |
---|
349 | + user_options = [] |
---|
350 | + |
---|
351 | + def initalize_options(self): |
---|
352 | + pass |
---|
353 | + def finalize_options(self): |
---|
354 | + pass |
---|
355 | + def run(self): |
---|
356 | + pass |
---|
357 | + |
---|
358 | +class Docs(Command): |
---|
359 | + description = 'generate documentation' |
---|
360 | + user_options = [] |
---|
361 | + |
---|
362 | + def initialize_options(self): |
---|
363 | + pass |
---|
364 | + def finalize_options(self): |
---|
365 | + pass |
---|
366 | + |
---|
367 | + def run(self): |
---|
368 | + if os.path.isdir('docs'): |
---|
369 | + shutil.rmtree('docs') |
---|
370 | + |
---|
371 | + args = ['pdoc', '-o', 'docs'] |
---|
372 | + |
---|
373 | + root_dirs = [ |
---|
374 | + 'src/', 'src/libs', 'src/libs/ui', 'src/libs/db', |
---|
375 | + 'src/libs/util', 'src/controllers', 'src/doctemplates' |
---|
376 | + ] |
---|
377 | + for root_dir in root_dirs: |
---|
378 | + for filename in os.listdir(root_dir): |
---|
379 | + if filename.endswith('.js'): |
---|
380 | + args.append(os.path.join(root_dir, filename)) |
---|
381 | + |
---|
382 | + subprocess.call(args) |
---|
383 | + |
---|
384 | +setup( |
---|
385 | + name = 'tahoe-music-player', |
---|
386 | + cmdclass = { |
---|
387 | + 'build': Build, |
---|
388 | + 'install': Install, |
---|
389 | + 'watch': Watch, |
---|
390 | + 'docs': Docs |
---|
391 | + } |
---|
392 | +) |
---|
393 | adddir ./contrib/musicplayer/src |
---|
394 | addfile ./contrib/musicplayer/src/Application.js |
---|
395 | hunk ./contrib/musicplayer/src/Application.js 1 |
---|
396 | +//#require "libs/vendor/mootools-1.2.4-core-ui.js" |
---|
397 | +//#require "libs/vendor/mootools-1.2.4.4-more.js" |
---|
398 | + |
---|
399 | +/** |
---|
400 | + * da |
---|
401 | + * |
---|
402 | + * The root namespace. Shorthand for '[Daaw](http://en.wikipedia.org/wiki/Lake_Tahoe#Native_people)'. |
---|
403 | + * |
---|
404 | + **/ |
---|
405 | +if(typeof da === "undefined") |
---|
406 | + this.da = {}; |
---|
407 | + |
---|
408 | +//#require "libs/db/PersistStorage.js" |
---|
409 | +//#require "libs/db/DocumentTemplate.js" |
---|
410 | +//#require "libs/util/Goal.js" |
---|
411 | + |
---|
412 | +(function () { |
---|
413 | +var BrowserCouch = da.db.BrowserCouch, |
---|
414 | + PersistStorage = da.db.PersistStorage, |
---|
415 | + Goal = da.util.Goal; |
---|
416 | + |
---|
417 | +/** section: Controllers |
---|
418 | + * class da.app |
---|
419 | + * |
---|
420 | + * The main controller. All methods are public. |
---|
421 | + **/ |
---|
422 | +da.app = { |
---|
423 | + /** |
---|
424 | + * da.app.caps -> Object |
---|
425 | + * Object with `music` and `settings` properties, ie. the contents of `config.json` file. |
---|
426 | + **/ |
---|
427 | + caps: {}, |
---|
428 | + |
---|
429 | + initialize: function () { |
---|
430 | + this.startup = new Goal({ |
---|
431 | + checkpoints: ["domready", "settings_db", "caps", "data_db", "soundmanager"], |
---|
432 | + onFinish: this.ready.bind(this) |
---|
433 | + }); |
---|
434 | + |
---|
435 | + BrowserCouch.get("settings", function (db) { |
---|
436 | + da.db.SETTINGS = db; |
---|
437 | + if(!db.getLength()) |
---|
438 | + this.loadInitialSettings(); |
---|
439 | + else { |
---|
440 | + this.startup.checkpoint("settings_db"); |
---|
441 | + this.getCaps(); |
---|
442 | + } |
---|
443 | + }.bind(this), new PersistStorage("tahoemp_settings")); |
---|
444 | + |
---|
445 | + BrowserCouch.get("data", function (db) { |
---|
446 | + da.db.DEFAULT = db; |
---|
447 | + this.startup.checkpoint("data_db"); |
---|
448 | + }.bind(this), new PersistStorage("tahoemp_data")); |
---|
449 | + }, |
---|
450 | + |
---|
451 | + loadInitialSettings: function () { |
---|
452 | + new Request.JSON({ |
---|
453 | + url: "config.json", |
---|
454 | + noCache: true, |
---|
455 | + |
---|
456 | + onSuccess: function (data) { |
---|
457 | + da.db.SETTINGS.put([ |
---|
458 | + {id: "music_cap", type: "Setting", group_id: "caps", value: data.music_cap}, |
---|
459 | + {id: "settings_cap", type: "Setting", group_id: "caps", value: data.settings_cap} |
---|
460 | + ], function () { |
---|
461 | + this.startup.checkpoint("settings_db"); |
---|
462 | + |
---|
463 | + this.caps.music = data.music_cap, |
---|
464 | + this.caps.settings = data.settings_cap; |
---|
465 | + |
---|
466 | + this.startup.checkpoint("caps"); |
---|
467 | + }.bind(this)); |
---|
468 | + }.bind(this), |
---|
469 | + |
---|
470 | + onFailure: function () { |
---|
471 | + alert("You're missing a config.json file! See docs on how to set it up."); |
---|
472 | + var showSettings = function () { |
---|
473 | + da.controller.Settings.showGroup("caps"); |
---|
474 | + }; |
---|
475 | + |
---|
476 | + if(da.controller.Settings) |
---|
477 | + showSettings(); |
---|
478 | + else |
---|
479 | + da.app.addEvent("ready.controller.Settings", showSettings); |
---|
480 | + } |
---|
481 | + }).get() |
---|
482 | + }, |
---|
483 | + |
---|
484 | + getCaps: function () { |
---|
485 | + // We can't use DocumentTemplate.Setting here as the class |
---|
486 | + // is usually instantiated after the call to this function. |
---|
487 | + da.db.SETTINGS.view({ |
---|
488 | + id: "caps", |
---|
489 | + |
---|
490 | + map: function (doc, emit) { |
---|
491 | + if(doc && doc.type === "Setting" && doc.group_id === "caps") |
---|
492 | + emit(doc.id, doc.value); |
---|
493 | + }, |
---|
494 | + |
---|
495 | + finished: function (result) { |
---|
496 | + this.caps.music = result.getRow("music_cap"); |
---|
497 | + this.caps.settings = result.getRow("settings_cap"); |
---|
498 | + if(!this.caps.music.length || !this.caps.music.length) |
---|
499 | + this.loadInitialSettings(); |
---|
500 | + else |
---|
501 | + this.startup.checkpoint("caps"); |
---|
502 | + }.bind(this), |
---|
503 | + |
---|
504 | + updated: function (result) { |
---|
505 | + if(result.findRow("music_cap") !== -1) { |
---|
506 | + this.caps.music = result.getRow("music_cap"); |
---|
507 | + da.controller.CollectionScanner.scan(this.caps.music); |
---|
508 | + } |
---|
509 | + if(result.findRow("settings_cap") !== -1) |
---|
510 | + this.caps.settings = result.getRow("settings_cap") |
---|
511 | + }.bind(this) |
---|
512 | + }); |
---|
513 | + }, |
---|
514 | + |
---|
515 | + /** |
---|
516 | + * da.app.ready() -> undefined |
---|
517 | + * fires ready |
---|
518 | + * |
---|
519 | + * Called when all necessary components are initialized. |
---|
520 | + **/ |
---|
521 | + ready: function () { |
---|
522 | + $("loader").destroy(); |
---|
523 | + $("panes").setStyle("display", "block"); |
---|
524 | + |
---|
525 | + if(da.db.DEFAULT.getLength() === 0) |
---|
526 | + da.controller.CollectionScanner.scan(); |
---|
527 | + |
---|
528 | + this.fireEvent("ready"); |
---|
529 | + } |
---|
530 | +}; |
---|
531 | +$extend(da.app, new Events()); |
---|
532 | + |
---|
533 | +da.app.initialize(); |
---|
534 | + |
---|
535 | +window.addEvent("domready", function () { |
---|
536 | + da.app.startup.checkpoint("domready"); |
---|
537 | +}); |
---|
538 | + |
---|
539 | +})(); |
---|
540 | + |
---|
541 | +//#require <doctemplates/doctemplates.js> |
---|
542 | +//#require <controllers/controllers.js> |
---|
543 | addfile ./contrib/musicplayer/src/config.example.json |
---|
544 | hunk ./contrib/musicplayer/src/config.example.json 1 |
---|
545 | +{ |
---|
546 | + "music_cap": "URI:DIR2:yz6mfqhmnog7jti65rblzwrdxe:7pyqgnikbn4iklmcst6n7hwgmkoim24dfxfm3y4374oi755yhyta", |
---|
547 | + "settings_cap": "URI:DIR2:i564xjoawurnbrkaevyzdufqzi:mqnvdbqzia3euvorf2dwte6jdb6hnmwlxa4i7syw63kly4ubndda" |
---|
548 | +} |
---|
549 | adddir ./contrib/musicplayer/src/controllers |
---|
550 | addfile ./contrib/musicplayer/src/controllers/CollectionScanner.js |
---|
551 | hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 1 |
---|
552 | +//#require "libs/util/Goal.js" |
---|
553 | +//#require "doctemplates/Song.js" |
---|
554 | +//#require "doctemplates/Artist.js" |
---|
555 | +//#require "doctemplates/Album.js" |
---|
556 | + |
---|
557 | +(function () { |
---|
558 | +var DocumentTemplate = da.db.DocumentTemplate, |
---|
559 | + Song = DocumentTemplate.Song, |
---|
560 | + Artist = DocumentTemplate.Artist, |
---|
561 | + Album = DocumentTemplate.Album, |
---|
562 | + Goal = da.util.Goal; |
---|
563 | + |
---|
564 | +/** section: Controllers |
---|
565 | + * class CollectionScanner |
---|
566 | + * |
---|
567 | + * Controller which operates with [[Scanner]] and [[Indexer]] WebWorkers. |
---|
568 | + * |
---|
569 | + * #### Notes |
---|
570 | + * This is private class. |
---|
571 | + * Public interface is provided via [[da.controller.CollectionScanner]]. |
---|
572 | + **/ |
---|
573 | +var CollectionScanner = new Class({ |
---|
574 | + /** |
---|
575 | + * new CollectionScanner() |
---|
576 | + * |
---|
577 | + * Starts a new scan using [[Application.caps.music]] as root directory. |
---|
578 | + **/ |
---|
579 | + initialize: function (root) { |
---|
580 | + this.indexer = new Worker("js/workers/indexer.js"); |
---|
581 | + this.indexer.onmessage = this.onIndexerMessage.bind(this); |
---|
582 | + |
---|
583 | + this.scanner = new Worker("js/workers/scanner.js"); |
---|
584 | + this.scanner.onmessage = this.onScannerMessage.bind(this); |
---|
585 | + |
---|
586 | + this.scanner.postMessage(root || da.app.caps.music); |
---|
587 | + |
---|
588 | + this.finished = false; |
---|
589 | + |
---|
590 | + this._goal = new Goal({ |
---|
591 | + checkpoints: ["scanner", "indexer"], |
---|
592 | + onFinish: function () { |
---|
593 | + this.finished = true; |
---|
594 | + this.terminate() |
---|
595 | + }.bind(this) |
---|
596 | + }) |
---|
597 | + }, |
---|
598 | + |
---|
599 | + /** |
---|
600 | + * CollectionScanner#finished -> true | false |
---|
601 | + **/ |
---|
602 | + finished: false, |
---|
603 | + |
---|
604 | + /** |
---|
605 | + * CollectionScanner#terminate() -> undefined |
---|
606 | + * |
---|
607 | + * Instantly kills both workers. |
---|
608 | + **/ |
---|
609 | + terminate: function () { |
---|
610 | + this.indexer.terminate(); |
---|
611 | + this.scanner.terminate(); |
---|
612 | + }, |
---|
613 | + |
---|
614 | + onScannerMessage: function (event) { |
---|
615 | + var cap = event.data; |
---|
616 | + if(cap === "**FINISHED**") { |
---|
617 | + this._goal.checkpoint("scanner"); |
---|
618 | + return; // this.scanner.terminate(); |
---|
619 | + } |
---|
620 | + |
---|
621 | + if(da.db.DEFAULT.views.Song.view.findRow(cap) === -1) |
---|
622 | + this.indexer.postMessage(cap); |
---|
623 | + }, |
---|
624 | + |
---|
625 | + onIndexerMessage: function (event) { |
---|
626 | + if(event.data === "**FINISHED**") { |
---|
627 | + this._goal.checkpoint("indexer"); |
---|
628 | + return; //this.indexer.terminate(); |
---|
629 | + } |
---|
630 | + |
---|
631 | + // Lots of async stuff is going on, a short summary would look something like: |
---|
632 | + // 1. find or create artist with given name and save its id |
---|
633 | + // to artist_id. |
---|
634 | + // 2. look for an album with given artist_id (afterCheckpoint.artist) |
---|
635 | + // 3. save the album data. |
---|
636 | + // 4. look for song with given id and save the new data. |
---|
637 | + |
---|
638 | + var tags = event.data, |
---|
639 | + album_id, artist_id, |
---|
640 | + links = new Goal({ |
---|
641 | + checkpoints: ["artist", "album"], |
---|
642 | + onFinish: function () { |
---|
643 | + Song.findOrCreate({ |
---|
644 | + properties: {id: tags.id}, |
---|
645 | + onSuccess: function (song) { |
---|
646 | + song.update({ |
---|
647 | + title: tags.title, |
---|
648 | + track: tags.track, |
---|
649 | + year: tags.year, |
---|
650 | + lyrics: tags.lyrics, |
---|
651 | + genre: tags.genere, |
---|
652 | + artist_id: artist_id, |
---|
653 | + album_id: album_id |
---|
654 | + }); |
---|
655 | + } |
---|
656 | + }); |
---|
657 | + }, |
---|
658 | + |
---|
659 | + afterCheckpoint: { |
---|
660 | + artist: function () { |
---|
661 | + Album.findOrCreate({ |
---|
662 | + properties: {artist_id: artist_id, title: tags.album}, |
---|
663 | + onSuccess: function (album, wasCreated) { |
---|
664 | + album_id = album.id; |
---|
665 | + if(wasCreated) |
---|
666 | + album.save(function () { links.checkpoint("album"); }) |
---|
667 | + else |
---|
668 | + links.checkpoint("album"); |
---|
669 | + } |
---|
670 | + }); |
---|
671 | + } |
---|
672 | + } |
---|
673 | + }); |
---|
674 | + |
---|
675 | + Artist.findOrCreate({ |
---|
676 | + properties: {title: tags.artist}, |
---|
677 | + onSuccess: function (artist, was_created) { |
---|
678 | + artist_id = artist.id; |
---|
679 | + if(was_created) |
---|
680 | + artist.save(function () { links.checkpoint("artist"); }); |
---|
681 | + else |
---|
682 | + links.checkpoint("artist"); |
---|
683 | + } |
---|
684 | + }); |
---|
685 | + } |
---|
686 | +}); |
---|
687 | + |
---|
688 | +var SCANNER; |
---|
689 | +/** |
---|
690 | + * da.controller.CollectionScanner |
---|
691 | + **/ |
---|
692 | +da.controller.CollectionScanner = { |
---|
693 | + /** |
---|
694 | + * da.controller.CollectionScanner.scan() -> undefined |
---|
695 | + * Starts scanning music directory |
---|
696 | + * |
---|
697 | + * Part of public API. |
---|
698 | + **/ |
---|
699 | + scan: function (cap) { |
---|
700 | + if(!SCANNER || (SCANNER && SCANNER.finished)) |
---|
701 | + SCANNER = new CollectionScanner(cap); |
---|
702 | + }, |
---|
703 | + |
---|
704 | + /** |
---|
705 | + * da.controller.CollectionScanner.isScanning() -> true | false |
---|
706 | + * |
---|
707 | + * Part of public API. |
---|
708 | + **/ |
---|
709 | + isScanning: function () { |
---|
710 | + return SCANNER && !SCANNER.finished; |
---|
711 | + } |
---|
712 | +}; |
---|
713 | + |
---|
714 | +da.app.fireEvent("ready.controller.CollectionScanner", [], 1); |
---|
715 | +})(); |
---|
716 | addfile ./contrib/musicplayer/src/controllers/Navigation.js |
---|
717 | hunk ./contrib/musicplayer/src/controllers/Navigation.js 1 |
---|
718 | +//#require "libs/ui/Menu.js" |
---|
719 | +(function () { |
---|
720 | +var Menu = da.ui.Menu; |
---|
721 | + |
---|
722 | +/** section: Controllers |
---|
723 | + * class NavigationColumnContainer |
---|
724 | + * |
---|
725 | + * Class for managing column views. |
---|
726 | + * |
---|
727 | + * #### Notes |
---|
728 | + * This class is private. |
---|
729 | + * Public interface is accessible via [[da.controller.Navigation]]. |
---|
730 | + **/ |
---|
731 | + |
---|
732 | +var NavigationColumnContainer = new Class({ |
---|
733 | + /** |
---|
734 | + * new NavigationColumnContainer(options) |
---|
735 | + * - options.columnName (String): name of the column. |
---|
736 | + * - options.container (Element): container element. |
---|
737 | + * - options.header (Element): header element. |
---|
738 | + * - options.menu (UI.Menu): [[UI.Menu]] instance. |
---|
739 | + * |
---|
740 | + * Renders column and adds self to the [[da.controller.Navigation.activeColumns]]. |
---|
741 | + **/ |
---|
742 | + |
---|
743 | + /** |
---|
744 | + * NavigationColumnContainer#column_name -> String |
---|
745 | + * Name of the column. |
---|
746 | + **/ |
---|
747 | + |
---|
748 | + /** |
---|
749 | + * NavigationColumnContainer#column -> NavigationColumn |
---|
750 | + * `column` here represents the list itself. |
---|
751 | + **/ |
---|
752 | + |
---|
753 | + /** |
---|
754 | + * NavigationColumnContainer#parent_column -> NavigationColumnContainer |
---|
755 | + * Usually column which created _this_ one. Visually, its the one to the left of _this_ one. |
---|
756 | + **/ |
---|
757 | + |
---|
758 | + /** |
---|
759 | + * NavigationColumnContainer#header -> Element |
---|
760 | + * Header element. It's an `a` tag with an `span` element. |
---|
761 | + * `a` tag has `column_header`, while `span` tag has `column_title` CSS class. |
---|
762 | + **/ |
---|
763 | + |
---|
764 | + /** |
---|
765 | + * NavigationColumnContainer#menu -> UI.Menu |
---|
766 | + * Container's [[UI.Menu]]. It can be also accesed with: |
---|
767 | + * |
---|
768 | + * this.header.retrieve("menu") |
---|
769 | + **/ |
---|
770 | + |
---|
771 | + /** |
---|
772 | + * NavigationColumnContainer#_el -> Element |
---|
773 | + * [[Element]] of the actual container. Has `column_container` CSS class. |
---|
774 | + **/ |
---|
775 | + initialize: function (options) { |
---|
776 | + this.column_name = options.columnName; |
---|
777 | + this.parent_column = Navigation.activeColumns[Navigation.activeColumns.push(this) - 2]; |
---|
778 | + |
---|
779 | + if(!(this._el = options.container)) |
---|
780 | + this.createContainer(); |
---|
781 | + |
---|
782 | + if(!(this.header = options.header)) |
---|
783 | + this.createHeader(); |
---|
784 | + |
---|
785 | + this.column = new Navigation.columns[this.column_name]({ |
---|
786 | + filter: options.filter, |
---|
787 | + parentElement: this._el |
---|
788 | + }); |
---|
789 | + Navigation.adjustColumnSize(this.column); |
---|
790 | + |
---|
791 | + if(!(this.menu = options.menu)) |
---|
792 | + this.createMenu(); |
---|
793 | + |
---|
794 | + if(this.column.constructor.filters && this.column.constructor.filters.length) |
---|
795 | + this.column.addEvent("click", this.listItemClick.bind(this)); |
---|
796 | + |
---|
797 | + var first_item = this.column._el.getElement(".column_item"); |
---|
798 | + if(first_item) |
---|
799 | + first_item.focus(); |
---|
800 | + }, |
---|
801 | + |
---|
802 | + /** |
---|
803 | + * NavigationColumnContainer#createContainer() -> this |
---|
804 | + * |
---|
805 | + * Creates container element in `navigation_pane` [[Element]]. |
---|
806 | + **/ |
---|
807 | + createContainer: function () { |
---|
808 | + $("navigation_pane").grab(this._el = new Element("div", { |
---|
809 | + id: this.column_name + "_column_container", |
---|
810 | + "class": "column_container no_selection" |
---|
811 | + })); |
---|
812 | + |
---|
813 | + return this; |
---|
814 | + }, |
---|
815 | + |
---|
816 | + /** |
---|
817 | + * NavigationColumnContainer#createHeader() -> this |
---|
818 | + * |
---|
819 | + * Creates header element and attaches click event. Element is added to [[NavigationColumnContainer#toElement]]. |
---|
820 | + **/ |
---|
821 | + createHeader: function () { |
---|
822 | + this.header = new Element("a", { |
---|
823 | + "class": "column_header", |
---|
824 | + href: "#" |
---|
825 | + }); |
---|
826 | + |
---|
827 | + this.header.addEvent("click", function (e) { |
---|
828 | + var menu = this.retrieve("menu"); |
---|
829 | + if(menu) |
---|
830 | + menu.show(e); |
---|
831 | + }); |
---|
832 | + |
---|
833 | + this._el.grab(this.header.grab(new Element("span", { |
---|
834 | + html: this.column_name, |
---|
835 | + "class": "column_title" |
---|
836 | + }))); |
---|
837 | + |
---|
838 | + return this; |
---|
839 | + }, |
---|
840 | + |
---|
841 | + /** |
---|
842 | + * NavigationColumnContainer#createMenu() -> this | false |
---|
843 | + * |
---|
844 | + * Creates menu for current column if it has filters. |
---|
845 | + * [[da.ui.Menu]] instance is stored to `header` element with `menu` key. |
---|
846 | + **/ |
---|
847 | + createMenu: function () { |
---|
848 | + var filters = this.column.constructor.filters, |
---|
849 | + items = {}; |
---|
850 | + |
---|
851 | + if(!filters || !filters.length) |
---|
852 | + return false; |
---|
853 | + |
---|
854 | + items[this.column_name] = {html: this.column.constructor.title, "class": "checked", href: "#"}; |
---|
855 | + for(var n = 0, m = filters.length; n < m; n++) |
---|
856 | + items[filters[n]] = {html: filters[n], href: "#"}; |
---|
857 | + |
---|
858 | + this.menu = new Menu({ |
---|
859 | + items: items |
---|
860 | + }); |
---|
861 | + |
---|
862 | + this.menu._el.addClass("navigation_menu"); |
---|
863 | + this.header.store("menu", this.menu); |
---|
864 | + |
---|
865 | + this.menu.addEvent("show", function () { |
---|
866 | + var header = this.header; |
---|
867 | + header.addClass("active"); |
---|
868 | + header.retrieve("menu")._el.style.width = header.getWidth() + "px"; |
---|
869 | + }.bind(this)); |
---|
870 | + |
---|
871 | + this.menu.addEvent("hide", function () { |
---|
872 | + this.header.removeClass("active"); |
---|
873 | + }.bind(this)); |
---|
874 | + |
---|
875 | + if(filters && filters.length) |
---|
876 | + this.menu.addEvent("click", this.menuItemClick.bind(this.parent_column || this)); |
---|
877 | + |
---|
878 | + return this; |
---|
879 | + }, |
---|
880 | + |
---|
881 | + /** |
---|
882 | + * NavigationColumnContainer#menuItemClick(filterName, event, element) -> undefined |
---|
883 | + * - filterName (String): id of the menu item. |
---|
884 | + * - event (Event): DOM event. |
---|
885 | + * - element (Element): clicked `Element`. |
---|
886 | + * |
---|
887 | + * Function called on menu click. If `filterName` is name of an actual filter then |
---|
888 | + * list in current column is replaced with a new one (provided by that filter). |
---|
889 | + **/ |
---|
890 | + menuItemClick: function (filter_name, event, element) { |
---|
891 | + if(!Navigation.columns[filter_name]) |
---|
892 | + return; |
---|
893 | + |
---|
894 | + var parent = this.filter_column._el, |
---|
895 | + header = this.filter_column.header, |
---|
896 | + menu = this.filter_column.menu; |
---|
897 | + |
---|
898 | + // we need to keep the menu and header, since |
---|
899 | + // all we need to do is to replace the list |
---|
900 | + this.filter_column.menu = null; |
---|
901 | + this.filter_column._el = null; |
---|
902 | + this.filter_column.destroy(); |
---|
903 | + |
---|
904 | + this.filter_column = new NavigationColumnContainer({ |
---|
905 | + columnName: filter_name, |
---|
906 | + filter: this.filter_column.column.options.filter, |
---|
907 | + container: parent, |
---|
908 | + header: header, |
---|
909 | + menu: menu |
---|
910 | + }); |
---|
911 | + |
---|
912 | + if(menu.last_clicked) |
---|
913 | + menu.last_clicked.removeClass("checked"); |
---|
914 | + element.addClass("checked"); |
---|
915 | + |
---|
916 | + header.getElement(".column_title").empty().appendText(filter_name); |
---|
917 | + }, |
---|
918 | + |
---|
919 | + /** |
---|
920 | + * NavigationColumnContainer#listItemClick(item) -> undefined |
---|
921 | + * - item (Object): clicked item. |
---|
922 | + * |
---|
923 | + * Creates a new column after this one with applied filter. |
---|
924 | + **/ |
---|
925 | + listItemClick: function (item) { |
---|
926 | + if(this.filter_column) |
---|
927 | + this.filter_column.destroy(); |
---|
928 | + |
---|
929 | + this.filter_column = new NavigationColumnContainer({ |
---|
930 | + columnName: this.column.constructor.filters[0], |
---|
931 | + filter: this.column.createFilter(item) |
---|
932 | + }); |
---|
933 | + }, |
---|
934 | + |
---|
935 | + /** |
---|
936 | + * NavigationColumnContainer#destroy() -> this |
---|
937 | + * |
---|
938 | + * Destroys this column (including menu). |
---|
939 | + * Removes itself from [[da.controller.Navigation.activeColumns]]. |
---|
940 | + **/ |
---|
941 | + destroy: function () { |
---|
942 | + if(this.filter_column) { |
---|
943 | + this.filter_column.destroy(); |
---|
944 | + delete this.filter_column; |
---|
945 | + } |
---|
946 | + if(this.menu) { |
---|
947 | + this.menu.destroy(); |
---|
948 | + delete this.menu; |
---|
949 | + } |
---|
950 | + this.column.destroy(); |
---|
951 | + delete this.column; |
---|
952 | + if(this._el) { |
---|
953 | + this._el.destroy(); |
---|
954 | + delete this._el; |
---|
955 | + } |
---|
956 | + |
---|
957 | + var index = Navigation.activeColumns.indexOf(this); |
---|
958 | + delete Navigation.activeColumns[index]; |
---|
959 | + Navigation.activeColumns = Navigation.activeColumns.clean(); |
---|
960 | + |
---|
961 | + return this; |
---|
962 | + }, |
---|
963 | + |
---|
964 | + /** |
---|
965 | + * NavigationColumnContainer#toElement() -> Element |
---|
966 | + **/ |
---|
967 | + toElement: function () { |
---|
968 | + return this._el; |
---|
969 | + } |
---|
970 | +}); |
---|
971 | + |
---|
972 | +/** section: Controllers |
---|
973 | + * da.controller.Navigation |
---|
974 | + **/ |
---|
975 | +var Navigation = { |
---|
976 | + // This is not really a class, but PDoc refuses to generate docs otherwise. |
---|
977 | + /** |
---|
978 | + * da.controller.Navigation.columns |
---|
979 | + * |
---|
980 | + * Contains all known columns. |
---|
981 | + * |
---|
982 | + * #### Notes |
---|
983 | + * Use [[da.controller.Navigation.registerColumn]] to add new ones, |
---|
984 | + * *do not* add them manually. |
---|
985 | + **/ |
---|
986 | + columns: {}, |
---|
987 | + |
---|
988 | + /** |
---|
989 | + * da.controller.Navigation.activeColumns -> [NavigationColumnContainer, ...] |
---|
990 | + * |
---|
991 | + * Array of currently active columns. |
---|
992 | + * The first column is always [[da.controller.Navigation.columns.Root]]. |
---|
993 | + **/ |
---|
994 | + activeColumns: [], |
---|
995 | + |
---|
996 | + initialize: function () { |
---|
997 | + var root_column = new NavigationColumnContainer({columnName: "Root"}); |
---|
998 | + root_column.menu.removeItem("Root"); |
---|
999 | + root_column.menu.addItems({ |
---|
1000 | + separator_1: Menu.separator, |
---|
1001 | + search: {html: "Search", href:"#"}, |
---|
1002 | + settings: {html: "Settings", href:"#", events: {click: da.controller.Settings.show}}, |
---|
1003 | + help: {html: "Help", href:"#"} |
---|
1004 | + }); |
---|
1005 | + |
---|
1006 | + var artists_column = new NavigationColumnContainer({ |
---|
1007 | + columnName: "Artists", |
---|
1008 | + menu: root_column.menu |
---|
1009 | + }); |
---|
1010 | + artists_column.header.store("menu", root_column.menu); |
---|
1011 | + root_column.filter_column = artists_column; |
---|
1012 | + root_column.header = artists_column.header; |
---|
1013 | + |
---|
1014 | + this._header_height = root_column.header.getHeight(); |
---|
1015 | + window.addEvent("resize", function () { |
---|
1016 | + var columns = Navigation.activeColumns, |
---|
1017 | + n = columns.length, |
---|
1018 | + windowHeight = window.getHeight(); |
---|
1019 | + |
---|
1020 | + while(n--) |
---|
1021 | + columns[n].column._el.setStyle("height", windowHeight - this._header_height); |
---|
1022 | + }.bind(this)); |
---|
1023 | + |
---|
1024 | + window.fireEvent("resize"); |
---|
1025 | + }, |
---|
1026 | + |
---|
1027 | + /** |
---|
1028 | + * da.controller.Navigation.adjustColumnSize(column) -> undefined |
---|
1029 | + * - column (da.ui.NavigationColumn): column which needs size adjustment. |
---|
1030 | + * |
---|
1031 | + * Adjusts column's height to window. |
---|
1032 | + **/ |
---|
1033 | + adjustColumnSize: function (column) { |
---|
1034 | + column._el.setStyle("height", window.getHeight() - this._header_height); |
---|
1035 | + }, |
---|
1036 | + |
---|
1037 | + /** |
---|
1038 | + * da.controller.Navigation.registerColumn(name, filters, column) -> undefined |
---|
1039 | + * - name (String): name of the column. |
---|
1040 | + * - filters (Array): names of the columns which can accept filter created (with [[da.ui.NavigationColumn#createFilter]]) by this one. |
---|
1041 | + * - column (da.ui.NavigationColumn): column class. |
---|
1042 | + * |
---|
1043 | + * `name` (renamed to `title`) and `filters` will be added to `column` as static methods. |
---|
1044 | + **/ |
---|
1045 | + registerColumn: function (name, filters, col) { |
---|
1046 | + col.extend({ |
---|
1047 | + title: name, |
---|
1048 | + filters: filters || [] |
---|
1049 | + }); |
---|
1050 | + |
---|
1051 | + this.columns[name] = col; |
---|
1052 | + if(name !== "Root") |
---|
1053 | + this.columns.Root.filters.push(name); |
---|
1054 | + } |
---|
1055 | +}; |
---|
1056 | + |
---|
1057 | +da.controller.Navigation = Navigation; |
---|
1058 | +da.app.addEvent("ready", function () { |
---|
1059 | + Navigation.initialize(); |
---|
1060 | +}); |
---|
1061 | + |
---|
1062 | +//#require "controllers/default_columns.js" |
---|
1063 | + |
---|
1064 | +da.app.fireEvent("ready.controller.Navigation", [], 1); |
---|
1065 | +})(); |
---|
1066 | addfile ./contrib/musicplayer/src/controllers/Player.js |
---|
1067 | hunk ./contrib/musicplayer/src/controllers/Player.js 1 |
---|
1068 | +//#require "libs/vendor/soundmanager/script/soundmanager2.js" |
---|
1069 | + |
---|
1070 | +(function () { |
---|
1071 | +/** section: Controllers |
---|
1072 | + * class Player |
---|
1073 | + * |
---|
1074 | + * Player interface and playlist managment class. |
---|
1075 | + * |
---|
1076 | + * #### Notes |
---|
1077 | + * This class is private. |
---|
1078 | + * Public interface is provided through [[da.controller.Player]]. |
---|
1079 | + **/ |
---|
1080 | +var Player = { |
---|
1081 | + /** |
---|
1082 | + * new Player() |
---|
1083 | + * Sets up soundManager2 and initializes player's interface. |
---|
1084 | + **/ |
---|
1085 | + initialize: function () { |
---|
1086 | + var path = location.protocol + "//" + location.host + location.pathname; |
---|
1087 | + $extend(soundManager, { |
---|
1088 | + useHTML5Audio: false, |
---|
1089 | + url: path + 'resources/flash/', |
---|
1090 | + debugMode: false, |
---|
1091 | + debugFlash: false |
---|
1092 | + }); |
---|
1093 | + |
---|
1094 | + soundManager.onready(function () { |
---|
1095 | + da.app.startup.checkpoint("soundmanager"); |
---|
1096 | + }); |
---|
1097 | + } |
---|
1098 | +}; |
---|
1099 | + |
---|
1100 | +Player.initialize(); |
---|
1101 | + |
---|
1102 | +/** |
---|
1103 | + * da.controller.Player |
---|
1104 | + **/ |
---|
1105 | +da.controller.Player = { |
---|
1106 | + /** |
---|
1107 | + * da.controller.Player.play([uri]) -> Boolean |
---|
1108 | + * - uri (String): location of the audio. |
---|
1109 | + * |
---|
1110 | + * If `uri` is omitted and there is paused playback, then the paused |
---|
1111 | + * file will resume playing. |
---|
1112 | + **/ |
---|
1113 | + play: function (uri) { return false }, |
---|
1114 | + |
---|
1115 | + /** |
---|
1116 | + * da.controller.Player.pause() -> Boolean |
---|
1117 | + * |
---|
1118 | + * Pauses the playback (if any). |
---|
1119 | + **/ |
---|
1120 | + pause: function () { return false }, |
---|
1121 | + |
---|
1122 | + /** |
---|
1123 | + * da.controller.Player.queue(uri) -> Boolean |
---|
1124 | + * - uri (String): location of the audio file. |
---|
1125 | + * |
---|
1126 | + * Adds file to the play queue and plays it as soon as currently playing |
---|
1127 | + * file finishes playing (if any). |
---|
1128 | + **/ |
---|
1129 | + queue: function (uri) { return false } |
---|
1130 | +}; |
---|
1131 | + |
---|
1132 | +da.app.fireEvent("ready.controller.Player", [], 1); |
---|
1133 | + |
---|
1134 | +})(); |
---|
1135 | addfile ./contrib/musicplayer/src/controllers/Settings.js |
---|
1136 | hunk ./contrib/musicplayer/src/controllers/Settings.js 1 |
---|
1137 | +//#require "doctemplates/Setting.js" |
---|
1138 | +//#require "libs/ui/NavigationColumn.js" |
---|
1139 | +//#require "libs/ui/Dialog.js" |
---|
1140 | + |
---|
1141 | +(function () { |
---|
1142 | +/** section: Controllers |
---|
1143 | + * class Settings |
---|
1144 | + * |
---|
1145 | + * #### Notes |
---|
1146 | + * This is private class. |
---|
1147 | + * Public interface is accessible via [[da.controller.Settings]]. |
---|
1148 | + **/ |
---|
1149 | + |
---|
1150 | +var Dialog = da.ui.Dialog, |
---|
1151 | + Setting = da.db.DocumentTemplate.Setting; |
---|
1152 | + |
---|
1153 | +var GROUPS = [{ |
---|
1154 | + id: "caps", |
---|
1155 | + title: "Caps", |
---|
1156 | + description: "Tahoe caps for your music and configuration files." |
---|
1157 | + }, { |
---|
1158 | + id: "lastfm", |
---|
1159 | + title: "Last.fm", |
---|
1160 | + description: 'Share the music your are listening to with the world via <a href="http://last.fm" target="_blank">Last.fm</a>.' |
---|
1161 | + } |
---|
1162 | +]; |
---|
1163 | + |
---|
1164 | +// Renderers are used render interface elements for each setting (input boxes, checkboxes etc.) |
---|
1165 | +// Settings and renderers are bound together via "representAs" property which |
---|
1166 | +// defaults to "text" for each setting. |
---|
1167 | +// All renderer has to do is to renturn a DIV element with "setting_box" CSS class |
---|
1168 | +// which contains an element with "setting_<setting name>" element. |
---|
1169 | +// That same element will be passed to the matching serializer. |
---|
1170 | + |
---|
1171 | +var RENDERERS = { |
---|
1172 | + _label: function (setting, details) { |
---|
1173 | + var container = new Element("div", { |
---|
1174 | + "class": "setting_box" |
---|
1175 | + }); |
---|
1176 | + return container.grab(new Element("label", { |
---|
1177 | + text: details.title + ":", |
---|
1178 | + "for": "setting_" + setting.id |
---|
1179 | + })); |
---|
1180 | + }, |
---|
1181 | + |
---|
1182 | + text: function (setting, details) { |
---|
1183 | + return this._label(setting, details).grab(new Element("input", { |
---|
1184 | + type: "text", |
---|
1185 | + id: "setting_" + setting.id, |
---|
1186 | + value: setting.get("value") |
---|
1187 | + })); |
---|
1188 | + }, |
---|
1189 | + |
---|
1190 | + password: function (setting, details) { |
---|
1191 | + var text = this.text(setting, details); |
---|
1192 | + text.getElement("input").type = "password"; |
---|
1193 | + return text; |
---|
1194 | + }, |
---|
1195 | + |
---|
1196 | + checkbox: function (setting, details) { |
---|
1197 | + var control = this._label(setting, details); |
---|
1198 | + control.getElement("label").empty().grab(new Element("input", { |
---|
1199 | + id: "setting_" + setting.id, |
---|
1200 | + type: "checkbox" |
---|
1201 | + })); |
---|
1202 | + control.getElement("input").checked = setting.get("value"); |
---|
1203 | + control.grab(new Element("label", { |
---|
1204 | + text: details.title, |
---|
1205 | + "class": "no_indent", |
---|
1206 | + "for": "setting_" + setting.id |
---|
1207 | + })); |
---|
1208 | + return control; |
---|
1209 | + } |
---|
1210 | +}; |
---|
1211 | +RENDERERS.numeric = RENDERERS.text; |
---|
1212 | + |
---|
1213 | +// Serializers do the opposite job of the one that renderers do, |
---|
1214 | +// they take an element and return its value. |
---|
1215 | +var SERIALIZERS = { |
---|
1216 | + text: function (input) { |
---|
1217 | + return input.value; |
---|
1218 | + }, |
---|
1219 | + |
---|
1220 | + password: function (input) { |
---|
1221 | + return input.value; |
---|
1222 | + }, |
---|
1223 | + |
---|
1224 | + numeric: function (input) { |
---|
1225 | + return +input.value; |
---|
1226 | + }, |
---|
1227 | + |
---|
1228 | + checkbox: function (input) { |
---|
1229 | + return input.checked; |
---|
1230 | + } |
---|
1231 | +}; |
---|
1232 | + |
---|
1233 | +var Settings = { |
---|
1234 | + initialize: function () { |
---|
1235 | + this.dialog = new Dialog({ |
---|
1236 | + title: "Settings", |
---|
1237 | + html: new Element("div", {id: "settings"}), |
---|
1238 | + hideOnOutsideClick: false |
---|
1239 | + }); |
---|
1240 | + this._el = $("settings"); |
---|
1241 | + this.column = new GroupsColumn({ |
---|
1242 | + parentElement: this._el |
---|
1243 | + }); |
---|
1244 | + |
---|
1245 | + var select_message = new Element("div", { |
---|
1246 | + html: "Click on a group on the left.", |
---|
1247 | + "class": "message" |
---|
1248 | + }); |
---|
1249 | + this._controls = new Element("div", {id: "settings_controls"}); |
---|
1250 | + this._controls.grab(select_message); |
---|
1251 | + this._el.grab(this._controls); |
---|
1252 | + |
---|
1253 | + this.initialized = true; |
---|
1254 | + }, |
---|
1255 | + |
---|
1256 | + /** |
---|
1257 | + * Settings.show() -> this |
---|
1258 | + * Shows the settings panel. |
---|
1259 | + **/ |
---|
1260 | + show: function () { |
---|
1261 | + this.dialog.show(); |
---|
1262 | + if(!this._adjusted_height) { |
---|
1263 | + this._title_height = this._el.getElement(".dialog_title").getHeight(); |
---|
1264 | + this.column.toElement().setStyle("height", 300 - this._title_height); |
---|
1265 | + this._controls.style.height = (300 - this._title_height) + "px"; |
---|
1266 | + this._adjusted_height = true; |
---|
1267 | + } |
---|
1268 | + |
---|
1269 | + return this; |
---|
1270 | + }, |
---|
1271 | + |
---|
1272 | + /** |
---|
1273 | + * Settings.hide() -> this |
---|
1274 | + * Hides the settings panel. |
---|
1275 | + **/ |
---|
1276 | + hide: function () { |
---|
1277 | + this.dialog.hide(); |
---|
1278 | + return this; |
---|
1279 | + }, |
---|
1280 | + |
---|
1281 | + /** |
---|
1282 | + * Settings.renderGroup(groupName) -> this |
---|
1283 | + * - groupName (String) name of the settings group whose panel |
---|
1284 | + * is about to be rendered. |
---|
1285 | + **/ |
---|
1286 | + renderGroup: function (group) { |
---|
1287 | + Setting.find({ |
---|
1288 | + properties: {group_id: group.id}, |
---|
1289 | + onSuccess: function (settings) { |
---|
1290 | + Settings.renderSettings(group.value, settings); |
---|
1291 | + } |
---|
1292 | + }); |
---|
1293 | + }, |
---|
1294 | + |
---|
1295 | + /** |
---|
1296 | + * Settings.renderSettings(settings) -> false | this |
---|
1297 | + * - settings ([Settin]): settings for which controls need to be rendered. |
---|
1298 | + * |
---|
1299 | + * Calls the rendering functions for each setting. |
---|
1300 | + * |
---|
1301 | + **/ |
---|
1302 | + renderSettings: function (group, settings) { |
---|
1303 | + if(!settings.length) |
---|
1304 | + return false; |
---|
1305 | + if(this._controls) |
---|
1306 | + this._controls.empty(); |
---|
1307 | + |
---|
1308 | + settings.sort(positionSort); |
---|
1309 | + var container = new Element("div"), |
---|
1310 | + header = new Element("p", { |
---|
1311 | + html: group.description, |
---|
1312 | + "class": "settings_header" |
---|
1313 | + }), |
---|
1314 | + footer = new Element("div", {"class": "settings_footer no_selection"}), |
---|
1315 | + apply_button = new Element("input", { |
---|
1316 | + type: "button", |
---|
1317 | + value: "Apply", |
---|
1318 | + id: "save_settings", |
---|
1319 | + events: {click: function () { Settings.save() }} |
---|
1320 | + }), |
---|
1321 | + revert_button = new Element("input", { |
---|
1322 | + type: "button", |
---|
1323 | + value: "Revert", |
---|
1324 | + id: "revert_settings", |
---|
1325 | + events: {click: function () { Settings.renderSettings(group, settings) }} |
---|
1326 | + }), |
---|
1327 | + settings_el = new Element("form"); |
---|
1328 | + |
---|
1329 | + container.grab(header); |
---|
1330 | + |
---|
1331 | + var n = settings.length, setting, details; |
---|
1332 | + while(n--) { |
---|
1333 | + setting = settings[n]; |
---|
1334 | + details = Setting.getDetails(setting.id); |
---|
1335 | + RENDERERS[details.representAs](setting, details).inject(settings_el, "top"); |
---|
1336 | + } |
---|
1337 | + |
---|
1338 | + footer.adopt(revert_button, apply_button); |
---|
1339 | + container.adopt(settings_el, footer); |
---|
1340 | + this._controls.grab(container); |
---|
1341 | + return this; |
---|
1342 | + }, |
---|
1343 | + |
---|
1344 | + save: function () { |
---|
1345 | + var settings = this.serialize(); |
---|
1346 | + for(var id in settings) |
---|
1347 | + Setting.findFirst({ |
---|
1348 | + properties: {id: id}, |
---|
1349 | + onSuccess: function (setting) { |
---|
1350 | + setting.update({value: settings[id]}); |
---|
1351 | + } |
---|
1352 | + }); |
---|
1353 | + }, |
---|
1354 | + |
---|
1355 | + serialize: function () { |
---|
1356 | + var values = this._controls.getElement("form").getElements("input[id^=setting_]"), |
---|
1357 | + serialized = {}, |
---|
1358 | + // in combo with el.id.slice is approx. x10 faster |
---|
1359 | + // than el.id.split("setting_")[1] |
---|
1360 | + setting_l = "setting_".length, |
---|
1361 | + n = values.length; |
---|
1362 | + |
---|
1363 | + while(n--) { |
---|
1364 | + var el = values[n], |
---|
1365 | + setting_name = el.id.slice(setting_l), |
---|
1366 | + details = Setting.getDetails(setting_name); |
---|
1367 | + serialized[setting_name] = SERIALIZERS[details.representAs](el); |
---|
1368 | + } |
---|
1369 | + |
---|
1370 | + return serialized; |
---|
1371 | + } |
---|
1372 | +}; |
---|
1373 | +$extend(Settings, new Events()); |
---|
1374 | + |
---|
1375 | +function positionSort(a, b) { |
---|
1376 | + a = Setting.getDetails(a.id).position; |
---|
1377 | + b = Setting.getDetails(b.id).position; |
---|
1378 | + |
---|
1379 | + return (a < b) ? -1 : ((a > b) ? 1 : 0); |
---|
1380 | +} |
---|
1381 | + |
---|
1382 | +var GroupsColumn = new Class({ |
---|
1383 | + Extends: da.ui.NavigationColumn, |
---|
1384 | + |
---|
1385 | + view: null, |
---|
1386 | + |
---|
1387 | + initialize: function (options) { |
---|
1388 | + options.totalCount = GROUPS.length; |
---|
1389 | + this.parent(options); |
---|
1390 | + |
---|
1391 | + this.addEvent("click", function (item) { |
---|
1392 | + Settings.renderGroup(item); |
---|
1393 | + }); |
---|
1394 | + }, |
---|
1395 | + |
---|
1396 | + getItem: function (n) { |
---|
1397 | + var group = GROUPS[n]; |
---|
1398 | + return {id: group.id, value: group}; |
---|
1399 | + } |
---|
1400 | +}); |
---|
1401 | + |
---|
1402 | +/** |
---|
1403 | + * da.controller.Settings |
---|
1404 | + * |
---|
1405 | + * Public interface of the settings controller. |
---|
1406 | + **/ |
---|
1407 | +da.controller.Settings = { |
---|
1408 | + /** |
---|
1409 | + * da.controller.Settings.registerGroup(config) -> this |
---|
1410 | + * - config.id (String): name of group. |
---|
1411 | + * - config.title (String): human-friendly name of the group. |
---|
1412 | + * - config.description (String): brief explanation of what this group is for. |
---|
1413 | + * The description will be displayed at the top of settings dialog. |
---|
1414 | + **/ |
---|
1415 | + registerGroup: function (config) { |
---|
1416 | + GROUPS[name] = config; |
---|
1417 | + return this; |
---|
1418 | + }, |
---|
1419 | + |
---|
1420 | + /** |
---|
1421 | + * da.controller.Settings.addRenderer(name, renderer) -> this |
---|
1422 | + * - name (String): name of the renderer. [[da.db.DocumentTemplate.Setting]] uses this in `representAs` property. |
---|
1423 | + * - renderer (Function): function which renderes specific setting. |
---|
1424 | + * |
---|
1425 | + * As first argument `renderer` function takes [[Setting]] object, |
---|
1426 | + * while the second one is the result of [[da.db.DocumentTemplate.Setting.getDetails]]. |
---|
1427 | + * |
---|
1428 | + * The function *must* return an [[Element]] with `setting_box` CSS class name. |
---|
1429 | + * The very same element *must* contain another element with `setting_<setting id>` id property. |
---|
1430 | + * That element will be passed to the serializer function. |
---|
1431 | + * |
---|
1432 | + * #### Default renderers |
---|
1433 | + * * `text` |
---|
1434 | + * * `numeric` (same as `text`, the only difference is in serializer) |
---|
1435 | + * * `password` |
---|
1436 | + * * `checkbox` |
---|
1437 | + **/ |
---|
1438 | + addRenderer: function (name, fn) { |
---|
1439 | + if(!(name in RENDERERS)) |
---|
1440 | + RENDERERS[name] = fn; |
---|
1441 | + |
---|
1442 | + return this; |
---|
1443 | + }, |
---|
1444 | + |
---|
1445 | + /** |
---|
1446 | + * da.controller.Settings.addSerializer(name, serializer) -> this |
---|
1447 | + * - name (String): name of the serializer. Usually the same name used by matching renderer. |
---|
1448 | + * - serializer (Function): function which returns value stored by rendered UI controls. |
---|
1449 | + * Function takes exactly one argument, the `setting_<setting id>` element. |
---|
1450 | + **/ |
---|
1451 | + addSerializer: function (name, serializer) { |
---|
1452 | + if(!(name in SERIALIZERS)) |
---|
1453 | + SERIALIZERS[name] = serializer; |
---|
1454 | + |
---|
1455 | + return this; |
---|
1456 | + }, |
---|
1457 | + |
---|
1458 | + /** |
---|
1459 | + * da.controller.Settings.show() -> undefined |
---|
1460 | + * |
---|
1461 | + * Shows the settings dialog. |
---|
1462 | + **/ |
---|
1463 | + show: function () { |
---|
1464 | + if(!Settings.initialized) |
---|
1465 | + Settings.initialize(); |
---|
1466 | + |
---|
1467 | + Settings.show(); |
---|
1468 | + }, |
---|
1469 | + |
---|
1470 | + /** |
---|
1471 | + * da.controller.Settings.hide() -> undefined |
---|
1472 | + * |
---|
1473 | + * Hides the settings dialog. |
---|
1474 | + * Changes to the settings are not automatically saved when dialog |
---|
1475 | + * is dismissed. |
---|
1476 | + **/ |
---|
1477 | + hide: function () { |
---|
1478 | + Settings.hide(); |
---|
1479 | + }, |
---|
1480 | + |
---|
1481 | + /** |
---|
1482 | + * da.controller.Settings.showGroup(group) -> undefined |
---|
1483 | + * - group (String): group's id. |
---|
1484 | + **/ |
---|
1485 | + showGroup: function (group) { |
---|
1486 | + this.show(); |
---|
1487 | + var n = GROUPS.length; |
---|
1488 | + while(n--) |
---|
1489 | + if(GROUPS[n].id === group) |
---|
1490 | + break; |
---|
1491 | + |
---|
1492 | + Settings.renderGroup({id: group, value: GROUPS[n+1]}); |
---|
1493 | + } |
---|
1494 | +}; |
---|
1495 | + |
---|
1496 | +da.app.fireEvent("ready.controller.Settings", [], 1); |
---|
1497 | +})(); |
---|
1498 | + |
---|
1499 | addfile ./contrib/musicplayer/src/controllers/controllers.js |
---|
1500 | hunk ./contrib/musicplayer/src/controllers/controllers.js 1 |
---|
1501 | +/** |
---|
1502 | + * == Controllers == |
---|
1503 | + * |
---|
1504 | + * Controller classes control "background" jobs and user interface. |
---|
1505 | + **/ |
---|
1506 | + |
---|
1507 | +/** section: Controllers |
---|
1508 | + * da.controller |
---|
1509 | + **/ |
---|
1510 | +if(typeof da.controller === "undefined") |
---|
1511 | + da.controller = {}; |
---|
1512 | + |
---|
1513 | +//#require "controllers/Navigation.js" |
---|
1514 | +//#require "controllers/Player.js" |
---|
1515 | +//#require "controllers/Settings.js" |
---|
1516 | +//#require "controllers/CollectionScanner.js" |
---|
1517 | addfile ./contrib/musicplayer/src/controllers/default_columns.js |
---|
1518 | hunk ./contrib/musicplayer/src/controllers/default_columns.js 1 |
---|
1519 | +//#require "libs/ui/NavigationColumn.js" |
---|
1520 | hunk ./contrib/musicplayer/src/controllers/default_columns.js 3 |
---|
1521 | +(function () { |
---|
1522 | +var Navigation = da.controller.Navigation, |
---|
1523 | + NavigationColumn = da.ui.NavigationColumn; |
---|
1524 | + |
---|
1525 | +/** section: Controller |
---|
1526 | + * class da.controller.Navigation.columns.Root < da.ui.NavigationColumn |
---|
1527 | + * filters: All filters provided by other columns. |
---|
1528 | + * |
---|
1529 | + * The root column which provides root menu. |
---|
1530 | + * To access the root menu use: |
---|
1531 | + * |
---|
1532 | + * da.controller.Navigation.columns.Root.menu |
---|
1533 | + **/ |
---|
1534 | +Navigation.registerColumn("Root", [], new Class({ |
---|
1535 | + Extends: NavigationColumn, |
---|
1536 | + |
---|
1537 | + title: "Root", |
---|
1538 | + view: null, |
---|
1539 | + |
---|
1540 | + initialize: function (options) { |
---|
1541 | + this._data = Navigation.columns.Root.filters; |
---|
1542 | + this.parent($extend(options, { |
---|
1543 | + totalCount: 0 // this._data.length |
---|
1544 | + })); |
---|
1545 | + this.render(); |
---|
1546 | + this.options.parentElement.style.display = "none"; |
---|
1547 | + }, |
---|
1548 | + |
---|
1549 | + getItem: function (index) { |
---|
1550 | + return {id: index, key: index, value: {title: this._data[index]}}; |
---|
1551 | + } |
---|
1552 | +})); |
---|
1553 | + |
---|
1554 | +/** |
---|
1555 | + * class da.controller.Navigation.columns.Artists < da.ui.NavigationColumn |
---|
1556 | + * filters: [[da.controller.Navigation.columns.Albums]], [[da.controller.Navigation.columns.Songs]] |
---|
1557 | + * |
---|
1558 | + * Displays artists. |
---|
1559 | + **/ |
---|
1560 | +var the_regex = /^the\s*/i; |
---|
1561 | +Navigation.registerColumn("Artists", ["Albums", "Songs"], new Class({ |
---|
1562 | + Extends: NavigationColumn, |
---|
1563 | + |
---|
1564 | + view: { |
---|
1565 | + id: "artists_column", |
---|
1566 | + map: function (doc, emit) { |
---|
1567 | + // If there are no documents in the DB this function |
---|
1568 | + // will be called with "undefined" as first argument |
---|
1569 | + if(!doc) return; |
---|
1570 | + |
---|
1571 | + if(doc.type === "Artist") |
---|
1572 | + emit(doc.id, { |
---|
1573 | + title: doc.title |
---|
1574 | + }); |
---|
1575 | + } |
---|
1576 | + }, |
---|
1577 | + |
---|
1578 | + createFilter: function (item) { |
---|
1579 | + return {artist_id: item.id}; |
---|
1580 | + }, |
---|
1581 | + |
---|
1582 | + compareFunction: function (a, b) { |
---|
1583 | + a = a && a.value.title ? a.value.title.split(the_regex).slice(-1) : a; |
---|
1584 | + b = b && b.value.title ? b.value.title.split(the_regex).slice(-1) : b; |
---|
1585 | + |
---|
1586 | + if(a < b) return -1; |
---|
1587 | + if(a > b) return 1; |
---|
1588 | + return 0; |
---|
1589 | + } |
---|
1590 | +})); |
---|
1591 | + |
---|
1592 | +/** |
---|
1593 | + * class da.controller.Navigation.columns.Albums < da.ui.NavigationColumn |
---|
1594 | + * filters: [[da.controller.Navigation.columns.Songs]] |
---|
1595 | + * |
---|
1596 | + * Displays albums. |
---|
1597 | + **/ |
---|
1598 | +Navigation.registerColumn("Albums", ["Songs"], new Class({ |
---|
1599 | + Extends: NavigationColumn, |
---|
1600 | + |
---|
1601 | + // We can't reuse "Album" view because of #_passesFilter(). |
---|
1602 | + view: { |
---|
1603 | + id: "albums_column", |
---|
1604 | + |
---|
1605 | + map: function (doc, emit) { |
---|
1606 | + if(!doc || !this._passesFilter(doc)) return; |
---|
1607 | + |
---|
1608 | + if(doc.type === "Album") |
---|
1609 | + emit(doc.id, { |
---|
1610 | + title: doc.title |
---|
1611 | + }); |
---|
1612 | + } |
---|
1613 | + }, |
---|
1614 | + |
---|
1615 | + createFilter: function (item) { |
---|
1616 | + return {album_id: item.id}; |
---|
1617 | + } |
---|
1618 | +})); |
---|
1619 | + |
---|
1620 | +/** |
---|
1621 | + * class da.controller.Navigation.columns.Songs < da.ui.NavigationColumn |
---|
1622 | + * filters: none |
---|
1623 | + * |
---|
1624 | + * Displays songs. |
---|
1625 | + **/ |
---|
1626 | +Navigation.registerColumn("Songs", [], new Class({ |
---|
1627 | + Extends: NavigationColumn, |
---|
1628 | + |
---|
1629 | + initialize: function (options) { |
---|
1630 | + this.parent(options); |
---|
1631 | + |
---|
1632 | + this._el.style.width = "300px"; |
---|
1633 | + |
---|
1634 | + this.addEvent("click", function (item) { |
---|
1635 | + if(this.sound) |
---|
1636 | + soundManager.stop(item.id); |
---|
1637 | + |
---|
1638 | + this.sound = soundManager.createSound({ |
---|
1639 | + id: item.id, |
---|
1640 | + url: "/uri/" + encodeURIComponent(item.id), |
---|
1641 | + autoLoad: true, |
---|
1642 | + onload: function () { |
---|
1643 | + this.play(); |
---|
1644 | + } |
---|
1645 | + }); |
---|
1646 | + }.bind(this)); |
---|
1647 | + }, |
---|
1648 | + |
---|
1649 | + view: { |
---|
1650 | + id: "songs_column", |
---|
1651 | + map: function (doc, emit) { |
---|
1652 | + if(!doc || !this._passesFilter(doc)) return; |
---|
1653 | + |
---|
1654 | + if(doc.type === "Song" && doc.title) |
---|
1655 | + emit(doc.title, { |
---|
1656 | + title: doc.title, |
---|
1657 | + subtitle: doc.track, |
---|
1658 | + track: doc.track |
---|
1659 | + }); |
---|
1660 | + } |
---|
1661 | + }, |
---|
1662 | + |
---|
1663 | + renderItem: function (index) { |
---|
1664 | + var item = this.getItem(index).value, |
---|
1665 | + el = new Element("a", {href: "#", title: item.title}); |
---|
1666 | + |
---|
1667 | + el.grab(new Element("span", {html: item.title, "class": "title"})); |
---|
1668 | + |
---|
1669 | + return el; |
---|
1670 | + }, |
---|
1671 | + |
---|
1672 | + compareFunction: function (a, b) { |
---|
1673 | + a = a && a.value ? a.value.track : a; |
---|
1674 | + b = b && b.value ? b.value.track : b; |
---|
1675 | + |
---|
1676 | + if(a < b) return -1; |
---|
1677 | + if(a > b) return 1; |
---|
1678 | + return 0; |
---|
1679 | + } |
---|
1680 | +})); |
---|
1681 | + |
---|
1682 | +})(); |
---|
1683 | adddir ./contrib/musicplayer/src/doctemplates |
---|
1684 | addfile ./contrib/musicplayer/src/doctemplates/Album.js |
---|
1685 | hunk ./contrib/musicplayer/src/doctemplates/Album.js 1 |
---|
1686 | +//#require "libs/db/DocumentTemplate.js" |
---|
1687 | +/** |
---|
1688 | + * class da.db.DocumentTemplate.Album < da.db.DocumentTemplate |
---|
1689 | + * hasMany: [[da.db.DocumentTemplate.Song]] |
---|
1690 | + * belongsTo: [[da.db.DocumentTemplate.Artist]] |
---|
1691 | + * |
---|
1692 | + * #### Standard properties |
---|
1693 | + * * `title` - name of the album |
---|
1694 | + **/ |
---|
1695 | + |
---|
1696 | +(function () { |
---|
1697 | +var DocumentTemplate = da.db.DocumentTemplate; |
---|
1698 | + |
---|
1699 | +DocumentTemplate.registerType("Album", new Class({ |
---|
1700 | + Extends: DocumentTemplate, |
---|
1701 | + |
---|
1702 | + hasMany: { |
---|
1703 | + songs: "Song" |
---|
1704 | + }, |
---|
1705 | + |
---|
1706 | + belongsTo: { |
---|
1707 | + artist: "Artist" |
---|
1708 | + } |
---|
1709 | +})); |
---|
1710 | + |
---|
1711 | +})(); |
---|
1712 | addfile ./contrib/musicplayer/src/doctemplates/Artist.js |
---|
1713 | hunk ./contrib/musicplayer/src/doctemplates/Artist.js 1 |
---|
1714 | +//#require "libs/db/DocumentTemplate.js" |
---|
1715 | +/** |
---|
1716 | + * class da.db.DocumentTemplate.Artist < da.db.DocumentTemplate |
---|
1717 | + * hasMany: [[da.db.DocumentTemplate.Song]] |
---|
1718 | + * belongsTo: [[da.db.DocumentTemplate.Artist]] |
---|
1719 | + * |
---|
1720 | + * #### Standard properties |
---|
1721 | + * * `title` - name of the artist |
---|
1722 | + * |
---|
1723 | + **/ |
---|
1724 | +(function () { |
---|
1725 | +var DocumentTemplate = da.db.DocumentTemplate; |
---|
1726 | + |
---|
1727 | +DocumentTemplate.registerType("Artist", new Class({ |
---|
1728 | + Extends: DocumentTemplate, |
---|
1729 | + |
---|
1730 | + hasMany: { |
---|
1731 | + songs: "Song" |
---|
1732 | + }, |
---|
1733 | + |
---|
1734 | + belongsTo: { |
---|
1735 | + artist: "Artist" |
---|
1736 | + } |
---|
1737 | +})); |
---|
1738 | + |
---|
1739 | +})(); |
---|
1740 | addfile ./contrib/musicplayer/src/doctemplates/Setting.js |
---|
1741 | hunk ./contrib/musicplayer/src/doctemplates/Setting.js 1 |
---|
1742 | +(function () { |
---|
1743 | +var DocumentTemplate = da.db.DocumentTemplate, |
---|
1744 | + // We are separating the actual setting values from |
---|
1745 | + // information needed to display the UI controls. |
---|
1746 | + SETTINGS = {}; |
---|
1747 | + |
---|
1748 | +/** |
---|
1749 | + * class da.db.DocumentTemplate.Setting < da.db.DocumentTemplate |
---|
1750 | + * |
---|
1751 | + * Class for represeting settings. |
---|
1752 | + * |
---|
1753 | + * #### Example |
---|
1754 | + * da.db.DocumentTemplate.Setting.register({ |
---|
1755 | + * id: "volume", |
---|
1756 | + * group_id: "general", |
---|
1757 | + * representAs: "Number", |
---|
1758 | + * |
---|
1759 | + * title: "Volume", |
---|
1760 | + * help: "Configure the volume", |
---|
1761 | + * value: 64 |
---|
1762 | + * }); |
---|
1763 | + **/ |
---|
1764 | + |
---|
1765 | +var Setting = new Class({ |
---|
1766 | + Extends: DocumentTemplate |
---|
1767 | +}); |
---|
1768 | +DocumentTemplate.registerType("Setting", da.db.SETTINGS, Setting); |
---|
1769 | + |
---|
1770 | +Setting.extend({ |
---|
1771 | + /** |
---|
1772 | + * da.db.DocumentTemplate.Setting.register(template) -> undefined |
---|
1773 | + * - template.id (String): ID of the setting. |
---|
1774 | + * - template.group_id (String | Number): ID of the group to which setting belongs to. |
---|
1775 | + * - template.representAs (String): type of the data this setting represents. ex. `text`, `password`. |
---|
1776 | + * - template.title (String): human-friendly name of the setting. |
---|
1777 | + * - template.help (String): a semi-long description of what this setting is used for. |
---|
1778 | + * - template.value (String | Number | Object): default value. |
---|
1779 | + * - template.hidden (Boolean): if `true`, the setting will not be displayed in settings dialog. |
---|
1780 | + * Defaults to `false`. |
---|
1781 | + * - template.position (Number): position in the list. |
---|
1782 | + * |
---|
1783 | + * For list of possible `template.representAs` values see [[Settings.addRenderer]] for details. |
---|
1784 | + **/ |
---|
1785 | + register: function (template) { |
---|
1786 | + SETTINGS[template.id] = { |
---|
1787 | + title: template.title, |
---|
1788 | + help: template.help, |
---|
1789 | + representAs: template.representAs || "text", |
---|
1790 | + position: typeof template.position === "number" ? template.position : -1 |
---|
1791 | + }; |
---|
1792 | + |
---|
1793 | + this.findOrCreate({ |
---|
1794 | + properties: {id: template.id}, |
---|
1795 | + onSuccess: function (doc, was_created) { |
---|
1796 | + if(was_created) |
---|
1797 | + doc.update({ |
---|
1798 | + group_id: template.group_id, |
---|
1799 | + value: template.value |
---|
1800 | + }); |
---|
1801 | + } |
---|
1802 | + }); |
---|
1803 | + }, |
---|
1804 | + |
---|
1805 | + /** |
---|
1806 | + * da.db.DocumentTemplate.Setting.findInGroup(group, callback) -> undefined |
---|
1807 | + * - group (String | Number): ID of the group. |
---|
1808 | + * - callback (Function): function called with all found settings. |
---|
1809 | + **/ |
---|
1810 | + findInGroup: function (group, callback) { |
---|
1811 | + this.find({ |
---|
1812 | + properties: {group_id: group}, |
---|
1813 | + onSuccess: callback, |
---|
1814 | + onFailure: callback |
---|
1815 | + }); |
---|
1816 | + }, |
---|
1817 | + |
---|
1818 | + /** |
---|
1819 | + * da.db.DocumentTemplate.Setting.getDetails(id) -> Object |
---|
1820 | + * - id (String | Number): id of the setting. |
---|
1821 | + * |
---|
1822 | + * Returns presentation-related details about the given setting. |
---|
1823 | + * These details include `title`, `help` and `data` properties given to [[da.db.DocumentTemplate.Setting.register]]. |
---|
1824 | + **/ |
---|
1825 | + getDetails: function (id) { |
---|
1826 | + return SETTINGS[id]; |
---|
1827 | + } |
---|
1828 | +}); |
---|
1829 | + |
---|
1830 | +Setting.register({ |
---|
1831 | + id: "music_cap", |
---|
1832 | + group_id: "caps", |
---|
1833 | + representAs: "text", |
---|
1834 | + title: "Music cap", |
---|
1835 | + help: "Tahoe cap for the root dirnode in which all your music files are.", |
---|
1836 | + value: "" |
---|
1837 | +}); |
---|
1838 | + |
---|
1839 | +Setting.register({ |
---|
1840 | + id: "settings_cap", |
---|
1841 | + group_id: "caps", |
---|
1842 | + representAs: "text", |
---|
1843 | + title: "Settings cap", |
---|
1844 | + help: "Tahoe read-write cap to the dirnode in which settings will be kept.", |
---|
1845 | + value: "" |
---|
1846 | +}); |
---|
1847 | + |
---|
1848 | +Setting.register({ |
---|
1849 | + id: "lastfm_enabled", |
---|
1850 | + group_id: "lastfm", |
---|
1851 | + representAs: "checkbox", |
---|
1852 | + title: "Enable Last.fm scrobbler", |
---|
1853 | + help: "Enable this if you whish to share music your are listening to with others.", |
---|
1854 | + value: false, |
---|
1855 | + position: 0 |
---|
1856 | +}); |
---|
1857 | + |
---|
1858 | +Setting.register({ |
---|
1859 | + id: "lastfm_username", |
---|
1860 | + group_id: "lastfm", |
---|
1861 | + representAs: "text", |
---|
1862 | + title: "Username", |
---|
1863 | + help: "Type in your Last.fm username.", |
---|
1864 | + value: "", |
---|
1865 | + position: 1 |
---|
1866 | +}); |
---|
1867 | + |
---|
1868 | +Setting.register({ |
---|
1869 | + id: "lastfm_password", |
---|
1870 | + group_id: "lastfm", |
---|
1871 | + representAs: "password", |
---|
1872 | + title: "Password", |
---|
1873 | + help: "Write down your Last.fm password.", |
---|
1874 | + value: "", |
---|
1875 | + position: 2 |
---|
1876 | +}); |
---|
1877 | + |
---|
1878 | +})(); |
---|
1879 | addfile ./contrib/musicplayer/src/doctemplates/Song.js |
---|
1880 | hunk ./contrib/musicplayer/src/doctemplates/Song.js 1 |
---|
1881 | +//#require "libs/db/DocumentTemplate.js" |
---|
1882 | + |
---|
1883 | +(function () { |
---|
1884 | +var DocumentTemplate = da.db.DocumentTemplate; |
---|
1885 | + |
---|
1886 | +/** |
---|
1887 | + * class da.db.DocumentTemplate.Song < da.db.DocumentTemplate |
---|
1888 | + * belongsTo: [[da.db.DocumentTemplate.Artist]], [[da.db.DocumentTemplate.Album]] |
---|
1889 | + * |
---|
1890 | + * #### Standard properties |
---|
1891 | + * * `id` - Read-only cap of the file |
---|
1892 | + * * `title` - name of the song |
---|
1893 | + * * `track` - track number |
---|
1894 | + * * `year` - year in which track was published |
---|
1895 | + * * `lyrics` - lyrics of the song |
---|
1896 | + * * `artist_id` - id of an [[da.db.DocumentTemplate.Artist]] |
---|
1897 | + * * `album_id` - id of an [[da.db.DocumentTemplate.Album]] |
---|
1898 | + * |
---|
1899 | + **/ |
---|
1900 | + |
---|
1901 | +// Defined by ID3 specs: |
---|
1902 | +// http://www.id3.org/id3v2.3.0#head-129376727ebe5309c1de1888987d070288d7c7e7 |
---|
1903 | +var GENRES = [ |
---|
1904 | + "Blues","Classic Rock","Country","Dance","Disco","Funk","Grunge","Hip-Hop","Jazz", |
---|
1905 | + "Metal","New Age","Oldies","Other","Pop","R&B","Rap","Reggae","Rock","Techno", |
---|
1906 | + "Industrial","Alternative","Ska","Death Metal","Pranks","Soundtrack","Euro-Techno", |
---|
1907 | + "Ambient","Trip-Hop","Vocal","Jazz+Funk","Fusion","Trance","Classical","Instrumental", |
---|
1908 | + "Acid","House","Game","Sound Clip","Gospel","Noise","AlternRock","Bass","Soul","Punk", |
---|
1909 | + "Space","Meditative","Instrumental Pop","Instrumental Rock","Ethnic","Gothic", |
---|
1910 | + "Darkwave","Techno-Industrial","Electronic","Pop-Folk","Eurodance","Dream", |
---|
1911 | + "Southern Rock","Comedy","Cult","Gangsta","Top 40","Christian Rap","Pop/Funk", |
---|
1912 | + "Jungle","Native American","Cabaret","New Wave","Psychadelic","Rave","Showtunes", |
---|
1913 | + "Trailer","Lo-Fi","Tribal","Acid Punk","Acid Jazz","Polka","Retro","Musical", |
---|
1914 | + "Rock & Roll","Hard Rock","Folk","Folk-Rock","National Folk","Swing","Fast Fusion", |
---|
1915 | + "Bebob","Latin","Revival","Celtic","Bluegrass","Avantgarde","Gothic Rock", |
---|
1916 | + "Progressive Rock","Psychedelic Rock","Symphonic Rock","Slow Rock","Big Band", |
---|
1917 | + "Chorus","Easy Listening","Acoustic","Humour","Speech","Chanson","Opera","Chamber Music", |
---|
1918 | + "Sonata","Symphony","Booty Bass","Primus","Porn Groove","Satire","Slow Jam","Club","Tango", |
---|
1919 | + "Samba","Folklore","Ballad","Power Ballad","Rhythmic Soul","Freestyle","Duet","Punk Rock", |
---|
1920 | + "Drum Solo","A capella","Euro-House","Dance Hall" |
---|
1921 | +]; |
---|
1922 | + |
---|
1923 | +DocumentTemplate.registerType("Song", new Class({ |
---|
1924 | + Extends: DocumentTemplate, |
---|
1925 | + |
---|
1926 | + belongsTo: { |
---|
1927 | + artist: "Artist", |
---|
1928 | + album: "Album" |
---|
1929 | + }, |
---|
1930 | + |
---|
1931 | + /** |
---|
1932 | + * da.db.DocumentTemplate.Song#getGenre() -> String |
---|
1933 | + * Returns human-friendly name of the genre. |
---|
1934 | + **/ |
---|
1935 | + getGenre: function () { |
---|
1936 | + return GENRES[this.get("genere")]; |
---|
1937 | + } |
---|
1938 | +})); |
---|
1939 | + |
---|
1940 | +})(); |
---|
1941 | addfile ./contrib/musicplayer/src/doctemplates/doctemplates.js |
---|
1942 | hunk ./contrib/musicplayer/src/doctemplates/doctemplates.js 1 |
---|
1943 | +/** |
---|
1944 | + * == DocumentTemplates == |
---|
1945 | + * |
---|
1946 | + * Database document templates. |
---|
1947 | + * |
---|
1948 | + **/ |
---|
1949 | + |
---|
1950 | +//#require "doctemplates/Setting.js" |
---|
1951 | +//#require "doctemplates/Artist.js" |
---|
1952 | +//#require "doctemplates/Album.js" |
---|
1953 | +//#require "doctemplates/Song.js" |
---|
1954 | addfile ./contrib/musicplayer/src/index.html |
---|
1955 | hunk ./contrib/musicplayer/src/index.html 1 |
---|
1956 | +<!DOCTYPE html> |
---|
1957 | +<html> |
---|
1958 | + <head> |
---|
1959 | + <title>Music Player for Tahoe-LAFS</title> |
---|
1960 | + |
---|
1961 | + <link rel="stylesheet" href="resources/css/reset.css" type="text/css" media="screen" charset="utf-8"/> |
---|
1962 | + <link rel="stylesheet" href="resources/css/text.css" type="text/css" media="screen" charset="utf-8"/> |
---|
1963 | + <link rel="stylesheet" href="resources/css/app.css" type="text/css" media="screen" charset="utf-8"/> |
---|
1964 | + |
---|
1965 | + <script src="js/app.js" type="text/javascript" charset="utf-8"></script> |
---|
1966 | + </head> |
---|
1967 | + <body> |
---|
1968 | + <div id="loader">Loading...</div> |
---|
1969 | + <div id="panes" style="display:none"> |
---|
1970 | + <div id="navigation_pane"></div> |
---|
1971 | + <div id="player_pane"></div> |
---|
1972 | + </div> |
---|
1973 | + </body> |
---|
1974 | +</html> |
---|
1975 | addfile ./contrib/musicplayer/src/index_devel.html |
---|
1976 | hunk ./contrib/musicplayer/src/index_devel.html 1 |
---|
1977 | +<!DOCTYPE html> |
---|
1978 | +<html> |
---|
1979 | + <head> |
---|
1980 | + <title>Music Player for Tahoe-LAFS</title> |
---|
1981 | + |
---|
1982 | + <link rel="stylesheet" href="resources/css/reset.css" type="text/css" media="screen" charset="utf-8"/> |
---|
1983 | + <link rel="stylesheet" href="resources/css/text.css" type="text/css" media="screen" charset="utf-8"/> |
---|
1984 | + <link rel="stylesheet" href="resources/css/app.css" type="text/css" media="screen" charset="utf-8"/> |
---|
1985 | + |
---|
1986 | + <script src="libs/vendor/mootools-1.2.4-core-ui.js" type="text/javascript" charset="utf-8"></script> |
---|
1987 | + <script src="libs/vendor/mootools-1.2.4.4-more.js" type="text/javascript" charset="utf-8"></script> |
---|
1988 | + <script src="libs/vendor/persist-js/src/persist.js" type="text/javascript" charset="utf-8"></script> |
---|
1989 | + <script src="libs/vendor/soundmanager/script/soundmanager2.js" type="text/javascript" charset="utf-8"></script> |
---|
1990 | + |
---|
1991 | + <script type="text/javascript" charset="utf-8"> |
---|
1992 | + this.da = {}; |
---|
1993 | + </script> |
---|
1994 | + |
---|
1995 | + <script src="libs/db/db.js" type="text/javascript" charset="utf-8"></script> |
---|
1996 | + <script src="libs/db/PersistStorage.js" type="text/javascript" charset="utf-8"></script> |
---|
1997 | + <script src="libs/db/BrowserCouch.js" type="text/javascript" charset="utf-8"></script> |
---|
1998 | + <script src="libs/vendor/Math.uuid.js" type="text/javascript" charset="utf-8"></script> |
---|
1999 | + <script src="libs/util/util.js" type="text/javascript" charset="utf-8"></script> |
---|
2000 | + <script src="libs/db/DocumentTemplate.js" type="text/javascript" charset="utf-8"></script> |
---|
2001 | + <script src="libs/util/Goal.js" type="text/javascript" charset="utf-8"></script> |
---|
2002 | + <script src="libs/util/BinaryFile.js" type="text/javascript" charset="utf-8"></script> |
---|
2003 | + <script src="libs/util/ID3.js" type="text/javascript" charset="utf-8"></script> |
---|
2004 | + <script src="libs/util/ID3v2.js" type="text/javascript" charset="utf-8"></script> |
---|
2005 | + <script src="libs/util/ID3v1.js" type="text/javascript" charset="utf-8"></script> |
---|
2006 | + |
---|
2007 | + <script src="Application.js" type="text/javascript" charset="utf-8"></script> |
---|
2008 | + |
---|
2009 | + <script src="doctemplates/Setting.js" type="text/javascript" charset="utf-8"></script> |
---|
2010 | + <script src="doctemplates/Artist.js" type="text/javascript" charset="utf-8"></script> |
---|
2011 | + <script src="doctemplates/Album.js" type="text/javascript" charset="utf-8"></script> |
---|
2012 | + <script src="doctemplates/Song.js" type="text/javascript" charset="utf-8"></script> |
---|
2013 | + |
---|
2014 | + <script src="libs/ui/ui.js" type="text/javascript" charset="utf-8"></script> |
---|
2015 | + <script src="libs/ui/Column.js" type="text/javascript" charset="utf-8"></script> |
---|
2016 | + <script src="libs/ui/NavigationColumn.js" type="text/javascript" charset="utf-8"></script> |
---|
2017 | + <script src="libs/ui/Menu.js" type="text/javascript" charset="utf-8"></script> |
---|
2018 | + <script src="libs/ui/Dialog.js" type="text/javascript" charset="utf-8"></script> |
---|
2019 | + |
---|
2020 | + <script src="controllers/controllers.js" type="text/javascript" charset="utf-8"></script> |
---|
2021 | + <script src="controllers/Navigation.js" type="text/javascript" charset="utf-8"></script> |
---|
2022 | + <script src="controllers/default_columns.js" type="text/javascript" charset="utf-8"></script> |
---|
2023 | + <script src="controllers/Player.js" type="text/javascript" charset="utf-8"></script> |
---|
2024 | + <script src="controllers/Settings.js" type="text/javascript" charset="utf-8"></script> |
---|
2025 | + <script src="controllers/CollectionScanner.js" type="text/javascript" charset="utf-8"></script> |
---|
2026 | + </head> |
---|
2027 | + <body> |
---|
2028 | + <div id="loader">Loading...</div> |
---|
2029 | + <div id="panes" style="display:none"> |
---|
2030 | + <div id="navigation_pane"></div> |
---|
2031 | + <div id="player_pane"></div> |
---|
2032 | + </div> |
---|
2033 | + </body> |
---|
2034 | +</html> |
---|
2035 | adddir ./contrib/musicplayer/src/libs |
---|
2036 | addfile ./contrib/musicplayer/src/libs/TahoeObject.js |
---|
2037 | hunk ./contrib/musicplayer/src/libs/TahoeObject.js 1 |
---|
2038 | +(function () { |
---|
2039 | +/** |
---|
2040 | + * == Tahoe == |
---|
2041 | + * |
---|
2042 | + * Classes and utility methods for working with Tahoe's [web API](http://tahoe-lafs.org/source/tahoe/trunk/docs/frontends/webapi.txt). |
---|
2043 | + **/ |
---|
2044 | +var CACHE = {}; |
---|
2045 | + |
---|
2046 | +/** section: Tahoe |
---|
2047 | + * class TahoeObject |
---|
2048 | + * |
---|
2049 | + * Abstract class representing any Tahoe object - either file or directory. |
---|
2050 | + **/ |
---|
2051 | +var TahoeObject = new Class({ |
---|
2052 | + /** |
---|
2053 | + * new TahoeObject(cap[, meta]) |
---|
2054 | + * - cap (String): cap of the object. |
---|
2055 | + * - meta (Object): metadata about the object. |
---|
2056 | + **/ |
---|
2057 | + initialize: function (uri, meta) { |
---|
2058 | + this.uri = uri; |
---|
2059 | + CACHE[uri] = this; |
---|
2060 | + this._fetched = false; |
---|
2061 | + |
---|
2062 | + if(meta) |
---|
2063 | + this.applyMeta(meta); |
---|
2064 | + }, |
---|
2065 | + |
---|
2066 | + /** |
---|
2067 | + * TahoeObject#applyMeta(meta) -> this |
---|
2068 | + * - meta (Object): metadata about the object. |
---|
2069 | + * |
---|
2070 | + * Applies the metadata to current object. If `meta` contains information |
---|
2071 | + * of child items, new [[TahoeObject]] instances will be created for those |
---|
2072 | + * as well. |
---|
2073 | + **/ |
---|
2074 | + applyMeta: function (meta) { |
---|
2075 | + this.type = meta[0]; |
---|
2076 | + var old_children = meta[1].children || {}, |
---|
2077 | + children = []; |
---|
2078 | + |
---|
2079 | + for(var child_name in old_children) { |
---|
2080 | + var child = old_children[child_name]; |
---|
2081 | + child[1].objectName = child_name; |
---|
2082 | + //child[1].type = child[0]; |
---|
2083 | + |
---|
2084 | + if(CACHE[child[1].ro_uri]) |
---|
2085 | + children.push(CACHE[child[1].ro_uri]) |
---|
2086 | + else |
---|
2087 | + children.push(new TahoeObject(child[1].ro_uri, child)); |
---|
2088 | + } |
---|
2089 | + |
---|
2090 | + meta[1].children = children; |
---|
2091 | + $extend(this, meta[1]); |
---|
2092 | + |
---|
2093 | + return this; |
---|
2094 | + }, |
---|
2095 | + |
---|
2096 | + /** |
---|
2097 | + * TahoeObject#get([onSuccess][, onFailure]) -> this |
---|
2098 | + * - onSuccess (Funcion): called if request succeeds. First argument is `this`. |
---|
2099 | + * - onFailure (Function): called if request fails. |
---|
2100 | + * |
---|
2101 | + * Requests metadata about `this` object. |
---|
2102 | + **/ |
---|
2103 | + get: function (success, failure) { |
---|
2104 | + if(this._fetched) { |
---|
2105 | + (success||$empty)(this); |
---|
2106 | + return this; |
---|
2107 | + } |
---|
2108 | + this._fetched = true; |
---|
2109 | + |
---|
2110 | + new Request.JSON({ |
---|
2111 | + url: "/uri/" + encodeURIComponent(this.uri), |
---|
2112 | + |
---|
2113 | + onSuccess: function (data) { |
---|
2114 | + this.applyMeta(data); |
---|
2115 | + (success||$empty)(this); |
---|
2116 | + }.bind(this), |
---|
2117 | + |
---|
2118 | + onFailure: failure || $empty |
---|
2119 | + }).get({t: "json"}); |
---|
2120 | + |
---|
2121 | + return this; |
---|
2122 | + }, |
---|
2123 | + |
---|
2124 | + /** |
---|
2125 | + * TahoeObject#directories() -> [TahoeObject...] |
---|
2126 | + * Returns an [[Array]] of all child directories. |
---|
2127 | + **/ |
---|
2128 | + directories: function () { |
---|
2129 | + var children = this.children, |
---|
2130 | + n = children.length, |
---|
2131 | + result = []; |
---|
2132 | + |
---|
2133 | + while(n--) |
---|
2134 | + if(children[n].type === "dirnode") |
---|
2135 | + result.push(children[n]); |
---|
2136 | + |
---|
2137 | + return result; |
---|
2138 | + }, |
---|
2139 | + |
---|
2140 | + /** |
---|
2141 | + * TahoeObject#files() -> [TahoeObject...] |
---|
2142 | + * Returns an [[Array]] of all child files. |
---|
2143 | + **/ |
---|
2144 | + files: function () { |
---|
2145 | + var children = this.children, |
---|
2146 | + n = children.length, |
---|
2147 | + result = []; |
---|
2148 | + |
---|
2149 | + while(n--) |
---|
2150 | + if(children[n].type === "filenode") |
---|
2151 | + result.push(children[n]); |
---|
2152 | + |
---|
2153 | + return result; |
---|
2154 | + } |
---|
2155 | +}); |
---|
2156 | +window.TahoeObject = TahoeObject; |
---|
2157 | + |
---|
2158 | +})(); |
---|
2159 | adddir ./contrib/musicplayer/src/libs/db |
---|
2160 | addfile ./contrib/musicplayer/src/libs/db/BrowserCouch.js |
---|
2161 | hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 1 |
---|
2162 | +/* ***** BEGIN LICENSE BLOCK ***** |
---|
2163 | + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 |
---|
2164 | + * |
---|
2165 | + * The contents of this file are subject to the Mozilla Public License Version |
---|
2166 | + * 1.1 (the "License"); you may not use this file except in compliance with |
---|
2167 | + * the License. You may obtain a copy of the License at |
---|
2168 | + * http://www.mozilla.org/MPL/ |
---|
2169 | + * |
---|
2170 | + * Software distributed under the License is distributed on an "AS IS" basis, |
---|
2171 | + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License |
---|
2172 | + * for the specific language governing rights and limitations under the |
---|
2173 | + * License. |
---|
2174 | + * |
---|
2175 | + * The Original Code is Ubiquity. |
---|
2176 | + * |
---|
2177 | + * The Initial Developer of the Original Code is Mozilla. |
---|
2178 | + * Portions created by the Initial Developer are Copyright (C) 2007 |
---|
2179 | + * the Initial Developer. All Rights Reserved. |
---|
2180 | + * |
---|
2181 | + * Contributor(s): |
---|
2182 | + * Atul Varma <atul@gmozilla.com> |
---|
2183 | + * |
---|
2184 | + * Alternatively, the contents of this file may be used under the terms of |
---|
2185 | + * either the GNU General Public License Version 2 or later (the "GPL"), or |
---|
2186 | + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), |
---|
2187 | + * in which case the provisions of the GPL or the LGPL are applicable instead |
---|
2188 | + * of those above. If you wish to allow use of your version of this file only |
---|
2189 | + * under the terms of either the GPL or the LGPL, and not to allow others to |
---|
2190 | + * use your version of this file under the terms of the MPL, indicate your |
---|
2191 | + * decision by deleting the provisions above and replace them with the notice |
---|
2192 | + * and other provisions required by the GPL or the LGPL. If you do not delete |
---|
2193 | + * the provisions above, a recipient may use your version of this file under |
---|
2194 | + * the terms of any one of the MPL, the GPL or the LGPL. |
---|
2195 | + * |
---|
2196 | + * ***** END LICENSE BLOCK ***** */ |
---|
2197 | + |
---|
2198 | +/* |
---|
2199 | + * TODO: Update license block - we've done some changes: |
---|
2200 | + * - optimised loops - faster map proccess |
---|
2201 | + * - removed LocalStorage and FakeStorage - using PersistStorage instead |
---|
2202 | + * - removed ModuleLoader - it's safe to assume JSON is available - speed optimisation |
---|
2203 | + * - fixed mapping proccess - if no documents were emitted finish function wouldn't be called |
---|
2204 | + * - added live views - map/reduce is only performed on updated documents |
---|
2205 | + */ |
---|
2206 | + |
---|
2207 | +//#require <libs/db/db.js> |
---|
2208 | + |
---|
2209 | +(function () { |
---|
2210 | + |
---|
2211 | +/** section: Database |
---|
2212 | + * class da.db.BrowserCouch |
---|
2213 | + * |
---|
2214 | + * Map/Reduce framework for browser. |
---|
2215 | + * |
---|
2216 | + * #### MapReducer Implementations |
---|
2217 | + * MapReducer is a generic interface for any map-reduce |
---|
2218 | + * implementation. Any object implementing this interface will need |
---|
2219 | + * to be able to work asynchronously, passing back control to the |
---|
2220 | + * client at a given interval, so that the client has the ability to |
---|
2221 | + * pause/cancel or report progress on the calculation if needed. |
---|
2222 | + **/ |
---|
2223 | +var BrowserCouch = { |
---|
2224 | + /** |
---|
2225 | + * da.db.BrowserCouch.get(name, callback[, storage]) -> DB |
---|
2226 | + * - name (String): name of the database. |
---|
2227 | + * - callback (Function): called when database is initialized. |
---|
2228 | + * - storage (Function): instance of storage class. |
---|
2229 | + * Defaults to [[da.db.PersistStorage]]. |
---|
2230 | + **/ |
---|
2231 | + get: function BC_get(name, cb, storage) { |
---|
2232 | + if (!storage) |
---|
2233 | + storage = new da.db.PersistStorage(name); |
---|
2234 | + |
---|
2235 | + new DB({ |
---|
2236 | + name: name, |
---|
2237 | + storage: storage, |
---|
2238 | + dict: new Dictionary(), |
---|
2239 | + onReady: cb |
---|
2240 | + }); |
---|
2241 | + } |
---|
2242 | +}; |
---|
2243 | + |
---|
2244 | +/** |
---|
2245 | + * class da.db.BrowserCouch.Dictionary |
---|
2246 | + * |
---|
2247 | + * Internal representation of the database. |
---|
2248 | + **/ |
---|
2249 | +/** |
---|
2250 | + * new da.db.BrowserCouch.Dictionary([object]) |
---|
2251 | + * - object (Object): initial values |
---|
2252 | + **/ |
---|
2253 | +function Dictionary (object) { |
---|
2254 | + /** |
---|
2255 | + * da.db.BrowserCouch.Dictionary#dict -> Object |
---|
2256 | + * The dictionary itself. |
---|
2257 | + **/ |
---|
2258 | + this.dict = {}; |
---|
2259 | + /** |
---|
2260 | + * da.db.BrowserCouch.Dictionary#keys -> Array |
---|
2261 | + * |
---|
2262 | + * Use this property to determine the number of items |
---|
2263 | + * in the dictionary. |
---|
2264 | + * |
---|
2265 | + * (new Dictionary({h: 1, e: 1, l: 2, o: 1})).keys.length |
---|
2266 | + * // => 4 |
---|
2267 | + * |
---|
2268 | + **/ |
---|
2269 | + this.keys = []; |
---|
2270 | + |
---|
2271 | + if(object) |
---|
2272 | + this.unpickle(object); |
---|
2273 | + |
---|
2274 | + return this; |
---|
2275 | +} |
---|
2276 | + |
---|
2277 | +Dictionary.prototype = { |
---|
2278 | + /** |
---|
2279 | + * da.db.BrowserCouch.Dictionary#has(key) -> true | false |
---|
2280 | + **/ |
---|
2281 | + has: function (key) { |
---|
2282 | + return (key in this.dict); |
---|
2283 | + }, |
---|
2284 | + |
---|
2285 | + /* |
---|
2286 | + getKeys: function () { |
---|
2287 | + return this.keys; |
---|
2288 | + }, |
---|
2289 | + |
---|
2290 | + get: function (key) { |
---|
2291 | + return this.dict[key]; |
---|
2292 | + }, |
---|
2293 | + */ |
---|
2294 | + |
---|
2295 | + /** |
---|
2296 | + * da.db.BrowserCouch.Dictionary#set(key, value) -> undefined |
---|
2297 | + **/ |
---|
2298 | + set: function (key, value) { |
---|
2299 | + if(!(key in this.dict)) |
---|
2300 | + this.keys.push(key); |
---|
2301 | + |
---|
2302 | + this.dict[key] = value; |
---|
2303 | + }, |
---|
2304 | + |
---|
2305 | + /** |
---|
2306 | + * da.db.BrowserCouch.Dictionary#setDocs(docs) -> undefined |
---|
2307 | + * - docs ([Object, ...]): array of objects whose `id` property |
---|
2308 | + * will be used as key. |
---|
2309 | + * |
---|
2310 | + * Use this method whenever you have to add more then one |
---|
2311 | + * item to the dictionary as it provides better perofrmance over |
---|
2312 | + * calling [[da.db.BrowserCouch.Dictionary#set]] over and over. |
---|
2313 | + **/ |
---|
2314 | + setDocs: function (docs) { |
---|
2315 | + var n = docs.length, |
---|
2316 | + newKeys = []; |
---|
2317 | + |
---|
2318 | + while(n--) { |
---|
2319 | + var doc = docs[n], id = doc.id; |
---|
2320 | + if(!(id in this.dict) && newKeys.indexOf(id) === -1) |
---|
2321 | + newKeys.push(id); |
---|
2322 | + |
---|
2323 | + this.dict[id] = doc; |
---|
2324 | + } |
---|
2325 | + |
---|
2326 | + this.keys = this.keys.concat(newKeys); |
---|
2327 | + }, |
---|
2328 | + |
---|
2329 | + /** |
---|
2330 | + * da.db.BrowserCouch.Dictionary#remove(key) -> undefined |
---|
2331 | + **/ |
---|
2332 | + remove: function (key) { |
---|
2333 | + delete this.dict[key]; |
---|
2334 | + |
---|
2335 | + var keys = this.keys, |
---|
2336 | + index = keys.indexOf(key), |
---|
2337 | + keysLength = keys.length; |
---|
2338 | + |
---|
2339 | + if(index === 0) |
---|
2340 | + return this.keys.shift(); |
---|
2341 | + if(index === length - 1) |
---|
2342 | + return this.keys = keys.slice(0, -1); |
---|
2343 | + |
---|
2344 | + this.keys = keys.slice(0, index).concat(keys.slice(index + 1, keysLength)); |
---|
2345 | + }, |
---|
2346 | + |
---|
2347 | + /** |
---|
2348 | + * da.db.BrowserCouch.Dictionary#clear() -> undefined |
---|
2349 | + **/ |
---|
2350 | + clear: function () { |
---|
2351 | + this.dict = {}; |
---|
2352 | + this.keys = []; |
---|
2353 | + }, |
---|
2354 | + |
---|
2355 | + /** |
---|
2356 | + * da.db.BrowserCouch.Dictionary#unpickle(object) -> undefined |
---|
2357 | + * - object (Object): `object`'s properties will be replaced with current ones. |
---|
2358 | + **/ |
---|
2359 | + unpickle: function (obj) { |
---|
2360 | + if(!obj) |
---|
2361 | + return; |
---|
2362 | + |
---|
2363 | + this.dict = obj; |
---|
2364 | + this._regenerateKeys(); |
---|
2365 | + }, |
---|
2366 | + |
---|
2367 | + _regenerateKeys: function () { |
---|
2368 | + var keys = [], |
---|
2369 | + dict = this.dict; |
---|
2370 | + |
---|
2371 | + for(var key in dict) |
---|
2372 | + keys.push(key); |
---|
2373 | + |
---|
2374 | + this.keys = keys; |
---|
2375 | + } |
---|
2376 | +}; |
---|
2377 | + |
---|
2378 | +/** section: Database |
---|
2379 | + * class DB |
---|
2380 | + * |
---|
2381 | + * da.db.BrowserCouch database instance. |
---|
2382 | + **/ |
---|
2383 | +var DB = new Class({ |
---|
2384 | + Implements: [Events, Options], |
---|
2385 | + |
---|
2386 | + options: {}, |
---|
2387 | + /** |
---|
2388 | + * new DB(options) |
---|
2389 | + * - options.name (String) |
---|
2390 | + * - options.storage (Object) |
---|
2391 | + * - options.dict (Dictionary) |
---|
2392 | + * - options.onReady (Function) |
---|
2393 | + * fires ready |
---|
2394 | + **/ |
---|
2395 | + initialize: function (options) { |
---|
2396 | + this.setOptions(options); |
---|
2397 | + |
---|
2398 | + this.name = "BrowserCouch_DB_" + this.options.name; |
---|
2399 | + this.dict = this.options.dict; |
---|
2400 | + this.storage = this.options.storage; |
---|
2401 | + this.views = {}; |
---|
2402 | + |
---|
2403 | + this.storage.get(this.name, function (obj) { |
---|
2404 | + this.dict.unpickle(obj); |
---|
2405 | + this.fireEvent("ready", [this]); |
---|
2406 | + }.bind(this)); |
---|
2407 | + |
---|
2408 | + this.addEvent("store", function (docs) { |
---|
2409 | + var views = this.views, |
---|
2410 | + dict = new Dictionary(); |
---|
2411 | + |
---|
2412 | + if($type(docs) === "array") |
---|
2413 | + dict.setDocs(docs); |
---|
2414 | + else |
---|
2415 | + dict.set(docs.id, docs); |
---|
2416 | + |
---|
2417 | + for(var view_name in views) |
---|
2418 | + this.view(views[view_name].options, dict); |
---|
2419 | + }.bind(this), true); |
---|
2420 | + }, |
---|
2421 | + |
---|
2422 | + /** |
---|
2423 | + * DB#commitToStorage(callback) -> undefined |
---|
2424 | + **/ |
---|
2425 | + commitToStorage: function (callback) { |
---|
2426 | + if(!callback) |
---|
2427 | + callback = $empty; |
---|
2428 | + |
---|
2429 | + this.storage.put(this.name, this.dict.dict, callback); |
---|
2430 | + }, |
---|
2431 | + |
---|
2432 | + /** |
---|
2433 | + * DB#wipe(callback) -> undefined |
---|
2434 | + **/ |
---|
2435 | + wipe: function wipe(cb) { |
---|
2436 | + this.dict.clear(); |
---|
2437 | + this.commitToStorage(cb); |
---|
2438 | + this.views = {}; |
---|
2439 | + }, |
---|
2440 | + |
---|
2441 | + /** |
---|
2442 | + * DB#get(id, callback) -> undefined |
---|
2443 | + * - id (String): id of the document. |
---|
2444 | + **/ |
---|
2445 | + get: function get(id, cb) { |
---|
2446 | + if(this.dict.has(id)) |
---|
2447 | + cb(this.dict.dict[id]); |
---|
2448 | + else |
---|
2449 | + cb(null); |
---|
2450 | + }, |
---|
2451 | + |
---|
2452 | + /** |
---|
2453 | + * DB#getLength() -> Number |
---|
2454 | + * Size of the database - number of documents. |
---|
2455 | + **/ |
---|
2456 | + getLength: function () { |
---|
2457 | + return this.dict.keys.length; |
---|
2458 | + }, |
---|
2459 | + |
---|
2460 | + /** |
---|
2461 | + * DB#put(document, callback) -> undefined |
---|
2462 | + * DB#put(documents, callback) -> undefined |
---|
2463 | + * - document.id (String | Number): remember to set this property |
---|
2464 | + * - documents (Array): array of documents. |
---|
2465 | + * fires store |
---|
2466 | + **/ |
---|
2467 | + put: function (doc, cb) { |
---|
2468 | + if ($type(doc) === "array") { |
---|
2469 | + this.dict.setDocs(doc); |
---|
2470 | + //var n = doc.length, _doc; |
---|
2471 | + //while(n--) { |
---|
2472 | + // _doc = doc[n]; |
---|
2473 | + // this.dict.set(_doc.id, _doc); |
---|
2474 | + //} |
---|
2475 | + } else |
---|
2476 | + this.dict.set(doc.id, doc); |
---|
2477 | + |
---|
2478 | + this.commitToStorage(cb); |
---|
2479 | + this.fireEvent("store", [doc]); |
---|
2480 | + }, |
---|
2481 | + |
---|
2482 | + /** |
---|
2483 | + * DB#view(options[, _dict]) -> this |
---|
2484 | + * - options.id (String): name of the view. Optional for temporary views. |
---|
2485 | + * - options.map (Function): mapping function. First argument is the document, |
---|
2486 | + * while second argument is `emit` function. |
---|
2487 | + * - options.reduce (Function): reduce function. |
---|
2488 | + * - options.finished (Function): called once map/reduce process finishes. |
---|
2489 | + * - options.updated (Function): called on each update to the view. |
---|
2490 | + * First argument is a view with only new/changed documents. |
---|
2491 | + * - options.progress (Function): called between pauses. |
---|
2492 | + * - options.chunkSize (Number): number of documents to be processed at once. |
---|
2493 | + * Defaults to 50. |
---|
2494 | + * - options.mapReducer (Object): MapReducer to be used. |
---|
2495 | + * Defaults to [[da.db.SingleThreadedMapReducer]]. |
---|
2496 | + * - options.temporary (Boolean): if enabled, new updates won't be reported. |
---|
2497 | + * (`options.updated` won't be called at all) |
---|
2498 | + * - _dict (Dictionary): objects on which proccess will be performed. |
---|
2499 | + * Defaults to current database. |
---|
2500 | + **/ |
---|
2501 | + view: function DB_view(options, dict) { |
---|
2502 | + if(!options.id && !options.temporary) |
---|
2503 | + return false; |
---|
2504 | + if(!options.map) |
---|
2505 | + return false; |
---|
2506 | + if(!options.finished) |
---|
2507 | + return false; |
---|
2508 | + |
---|
2509 | + if(typeof options.temporary === "undefined") |
---|
2510 | + options.temporary = false; |
---|
2511 | + if(options.updated && !this.views[options.id]) |
---|
2512 | + this.addEvent("updated." + options.id, options.updated); |
---|
2513 | + if(!options.mapReducer) |
---|
2514 | + options.mapReducer = SingleThreadedMapReducer; |
---|
2515 | + if(!options.progress) |
---|
2516 | + options.progress = defaultProgress; |
---|
2517 | + if(!options.chunkSize) |
---|
2518 | + options.chunkSize = DEFAULT_CHUNK_SIZE; |
---|
2519 | + |
---|
2520 | + var onReduce = function onReduce (rows) { |
---|
2521 | + this._updateView(options, new ReduceView(rows), rows); |
---|
2522 | + }.bind(this); |
---|
2523 | + |
---|
2524 | + var onMap = function (mapResult) { |
---|
2525 | + if(!options.reduce) |
---|
2526 | + this._updateView(options, new MapView(mapResult), mapResult); |
---|
2527 | + else |
---|
2528 | + options.mapReducer.reduce( |
---|
2529 | + options.reduce, mapResult, options.progress, options.chunkSize, onReduce |
---|
2530 | + ); |
---|
2531 | + }.bind(this); |
---|
2532 | + |
---|
2533 | + options.mapReducer.map( |
---|
2534 | + options.map, |
---|
2535 | + dict || this.dict, |
---|
2536 | + options.progress, |
---|
2537 | + options.chunkSize, |
---|
2538 | + onMap |
---|
2539 | + ); |
---|
2540 | + |
---|
2541 | + return this; |
---|
2542 | + }, |
---|
2543 | + |
---|
2544 | + _updateView: function (options, view, rows) { |
---|
2545 | + if(options.temporary) |
---|
2546 | + return options.finished(view); |
---|
2547 | + |
---|
2548 | + var id = options.id; |
---|
2549 | + if(!this.views[id]) { |
---|
2550 | + this.views[id] = { |
---|
2551 | + options: options, |
---|
2552 | + view: view |
---|
2553 | + }; |
---|
2554 | + options.finished(view); |
---|
2555 | + } else { |
---|
2556 | + if(!view.rows.length) |
---|
2557 | + return this; |
---|
2558 | + |
---|
2559 | + if(options.reduce) { |
---|
2560 | + var full_view = this.views[id].view.rows.concat(view.rows), |
---|
2561 | + rereduce = {}, |
---|
2562 | + reduce = options.reduce, |
---|
2563 | + n = full_view.length; |
---|
2564 | + |
---|
2565 | + while(n--) { |
---|
2566 | + var row = full_view[n], |
---|
2567 | + key = row.key; |
---|
2568 | + if(!rereduce[key]) |
---|
2569 | + rereduce[key] = [row.value]; |
---|
2570 | + else |
---|
2571 | + rereduce[key].push(row.value); |
---|
2572 | + } |
---|
2573 | + |
---|
2574 | + rows = []; |
---|
2575 | + for(var key in rereduce) |
---|
2576 | + rows.push({ |
---|
2577 | + key: key, |
---|
2578 | + value: reduce(null, rereduce[key], true) |
---|
2579 | + }); |
---|
2580 | + } |
---|
2581 | + |
---|
2582 | + this.views[id].view._include(rows); |
---|
2583 | + this.fireEvent("updated." + id, [view]); |
---|
2584 | + } |
---|
2585 | + |
---|
2586 | + return this; |
---|
2587 | + }, |
---|
2588 | + |
---|
2589 | + /** |
---|
2590 | + * DB#killView(id) -> this |
---|
2591 | + * - id (String): name of the view. |
---|
2592 | + **/ |
---|
2593 | + killView: function (id) { |
---|
2594 | + delete this.views[id]; |
---|
2595 | + return this; |
---|
2596 | + } |
---|
2597 | +}); |
---|
2598 | + |
---|
2599 | +// Maximum number of items to process before giving the UI a chance |
---|
2600 | +// to breathe. |
---|
2601 | +var DEFAULT_CHUNK_SIZE = 1000; |
---|
2602 | + |
---|
2603 | +// If no progress callback is given, we'll automatically give the |
---|
2604 | +// UI a chance to breathe for this many milliseconds before continuing |
---|
2605 | +// processing. |
---|
2606 | +var DEFAULT_UI_BREATHE_TIME = 50; |
---|
2607 | + |
---|
2608 | +function defaultProgress(phase, percent, resume) { |
---|
2609 | + window.setTimeout(resume, DEFAULT_UI_BREATHE_TIME); |
---|
2610 | +} |
---|
2611 | + |
---|
2612 | +/** |
---|
2613 | + * class ReduceView |
---|
2614 | + * Represents the result of map/reduce process. |
---|
2615 | + **/ |
---|
2616 | +/** |
---|
2617 | + * new ReduceView(rows) -> this |
---|
2618 | + * - rows (Array): value returned by reducer. |
---|
2619 | + **/ |
---|
2620 | +function ReduceView(rows) { |
---|
2621 | + /** |
---|
2622 | + * ReduceView#rows -> Array |
---|
2623 | + * Result of the reduce process. |
---|
2624 | + **/ |
---|
2625 | + this.rows = []; |
---|
2626 | + var keys = []; |
---|
2627 | + |
---|
2628 | + this._include = function (newRows) { |
---|
2629 | + var n = newRows.length; |
---|
2630 | + |
---|
2631 | + while(n--) { |
---|
2632 | + var row = newRows[n]; |
---|
2633 | + if(keys.indexOf(row.key) === -1) { |
---|
2634 | + this.rows.push(row); |
---|
2635 | + keys.push(row.key); |
---|
2636 | + } else { |
---|
2637 | + this.rows[this.findRow(row.key)] = newRows[n]; |
---|
2638 | + } |
---|
2639 | + } |
---|
2640 | + |
---|
2641 | + this.rows.sort(keySort); |
---|
2642 | + }; |
---|
2643 | + |
---|
2644 | + /** |
---|
2645 | + * ReduceView#findRow(key) -> Number |
---|
2646 | + * - key (String): key of the row. |
---|
2647 | + * |
---|
2648 | + * Returns position of the row in [[ReduceView#rows]]. |
---|
2649 | + **/ |
---|
2650 | + this.findRow = function (key) { |
---|
2651 | + return findRowInReducedView(key, rows); |
---|
2652 | + }; |
---|
2653 | + |
---|
2654 | + /** |
---|
2655 | + * ReduceView#getRow(key) -> row |
---|
2656 | + * - key (String): key of the row. |
---|
2657 | + **/ |
---|
2658 | + this.getRow = function (key) { |
---|
2659 | + var row = this.rows[findRowInReducedView(key, rows)]; |
---|
2660 | + return row ? row.value : undefined; |
---|
2661 | + }; |
---|
2662 | + |
---|
2663 | + this._include(rows); |
---|
2664 | + return this; |
---|
2665 | +} |
---|
2666 | + |
---|
2667 | +function keySort (a, b) { |
---|
2668 | + a = a.key; |
---|
2669 | + b = b.key |
---|
2670 | + if(a < b) return -1; |
---|
2671 | + if(a > b) return 1; |
---|
2672 | + return 0; |
---|
2673 | +} |
---|
2674 | + |
---|
2675 | +function findRowInReducedView (key, rows) { |
---|
2676 | + if(rows.length > 1) { |
---|
2677 | + var midpoint = Math.floor(rows.length / 2); |
---|
2678 | + var row = rows[midpoint]; |
---|
2679 | + if(key < row.key) |
---|
2680 | + return findRowInReducedView(key, rows.slice(0, midpoint)); |
---|
2681 | + if(key > row.key) |
---|
2682 | + return midpoint + findRowInReducedView(key, rows.slice(midpoint)); |
---|
2683 | + return row.key === key ? midpoint : -1; |
---|
2684 | + } |
---|
2685 | + |
---|
2686 | + return rows[0].key === key ? 0 : -1; |
---|
2687 | +} |
---|
2688 | + |
---|
2689 | +/** |
---|
2690 | + * class MapView |
---|
2691 | + * Represents the result of map/reduce process. |
---|
2692 | + **/ |
---|
2693 | +/** |
---|
2694 | + * new MapView(rows) -> this |
---|
2695 | + * - rows (Array): value returned by mapper. |
---|
2696 | + **/ |
---|
2697 | +function MapView (mapResult) { |
---|
2698 | + /** |
---|
2699 | + * MapView#rows -> Object |
---|
2700 | + * Result of the mapping process. |
---|
2701 | + **/ |
---|
2702 | + this.rows = []; |
---|
2703 | + var keyRows = []; |
---|
2704 | + |
---|
2705 | + this._include = function (mapResult) { |
---|
2706 | + var mapKeys = mapResult.keys, |
---|
2707 | + mapDict = mapResult.dict; |
---|
2708 | + |
---|
2709 | + for(var i = 0, ii = mapKeys.length; i < ii; i++) { |
---|
2710 | + var key = mapKeys[i], |
---|
2711 | + ki = this.findRow(key), |
---|
2712 | + has_key = ki !== -1, |
---|
2713 | + item = mapDict[key], |
---|
2714 | + j = item.keys.length, |
---|
2715 | + newRows = new Array(j); |
---|
2716 | + |
---|
2717 | + if(has_key && this.rows[ki]) { |
---|
2718 | + this.rows[ki].value = item.values.shift(); |
---|
2719 | + item.keys.shift(); |
---|
2720 | + j--; |
---|
2721 | + } //else |
---|
2722 | + //keyRows.push({key: key, pos: this.rows.length}); |
---|
2723 | + |
---|
2724 | + while(j--) |
---|
2725 | + newRows[j] = { |
---|
2726 | + id: item.keys[j], |
---|
2727 | + key: key, |
---|
2728 | + value: item.values[j] |
---|
2729 | + }; |
---|
2730 | + |
---|
2731 | + if(has_key) |
---|
2732 | + newRows.shift(); |
---|
2733 | + this.rows = this.rows.concat(newRows); |
---|
2734 | + } |
---|
2735 | + |
---|
2736 | + this.rows.sort(idSort); |
---|
2737 | + |
---|
2738 | + var keys = []; |
---|
2739 | + keyRows = []; |
---|
2740 | + for(var n = 0, m = this.rows.length; n < m; n++) { |
---|
2741 | + var key = this.rows[n].key; |
---|
2742 | + if(keys.indexOf(key) === -1) |
---|
2743 | + keyRows.push({ |
---|
2744 | + key: key, |
---|
2745 | + pos: keys.push(key) - 1 |
---|
2746 | + }); |
---|
2747 | + } |
---|
2748 | + |
---|
2749 | + //delete keys; |
---|
2750 | + }; |
---|
2751 | + |
---|
2752 | + /** |
---|
2753 | + * MapView#findRow(key) -> Number |
---|
2754 | + * - key (String): key of the row. |
---|
2755 | + * |
---|
2756 | + * Returns position of the row in [[MapView#rows]]. |
---|
2757 | + **/ |
---|
2758 | + this.findRow = function MV_findRow (key) { |
---|
2759 | + return findRowInMappedView(key, keyRows); |
---|
2760 | + }; |
---|
2761 | + |
---|
2762 | + /** |
---|
2763 | + * MapView#getRow(key) -> row |
---|
2764 | + * - key (String): key of the row. |
---|
2765 | + * |
---|
2766 | + * Returns row's value, ie. it's a shortcut for: |
---|
2767 | + * this.rows[this.findRow(key)].value |
---|
2768 | + **/ |
---|
2769 | + this.getRow = function MV_findRow (key) { |
---|
2770 | + var row = this.rows[findRowInMappedView(key, keyRows)]; |
---|
2771 | + return row ? row.value : undefined; |
---|
2772 | + }; |
---|
2773 | + |
---|
2774 | + this._include(mapResult); |
---|
2775 | + |
---|
2776 | + return this; |
---|
2777 | +} |
---|
2778 | + |
---|
2779 | +function idSort (a, b) { |
---|
2780 | + a = a.id; |
---|
2781 | + b = b.id; |
---|
2782 | + |
---|
2783 | + if(a < b) return -1; |
---|
2784 | + if(a > b) return 1; |
---|
2785 | + return 0; |
---|
2786 | +} |
---|
2787 | + |
---|
2788 | +function findRowInMappedView (key, keyRows) { |
---|
2789 | + if (keyRows.length > 1) { |
---|
2790 | + var midpoint = Math.floor(keyRows.length / 2); |
---|
2791 | + var keyRow = keyRows[midpoint]; |
---|
2792 | + if (key < keyRow.key) |
---|
2793 | + return findRowInMappedView(key, keyRows.slice(0, midpoint)); |
---|
2794 | + if (key > keyRow.key) |
---|
2795 | + return findRowInMappedView(key, keyRows.slice(midpoint)); |
---|
2796 | + return keyRow ? keyRow.pos : -1; |
---|
2797 | + } else |
---|
2798 | + return (keyRows[0] && keyRows[0].key === key) ? keyRows[0].pos : -1; |
---|
2799 | +} |
---|
2800 | + |
---|
2801 | +/** section: Database |
---|
2802 | + * class WebWorkerMapReducer |
---|
2803 | + * |
---|
2804 | + * A MapReducer that uses [Web Workers](https://developer.mozilla.org/En/Using_DOM_workers) |
---|
2805 | + * for its implementation, allowing the client to take advantage of |
---|
2806 | + * multiple processor cores and potentially decouple the map-reduce |
---|
2807 | + * calculation from the user interface. |
---|
2808 | + * |
---|
2809 | + * The script run by spawned Web Workers is [[MapReduceWorker]]. |
---|
2810 | + **/ |
---|
2811 | + |
---|
2812 | +/** |
---|
2813 | + * new WebWorkerMapReducer(numWorkers[, worker]) |
---|
2814 | + * - numWorkers (Number): number of workers. |
---|
2815 | + * - worker (Object): reference to Web worker implementation. Defaults to `window.Worker`. |
---|
2816 | + **/ |
---|
2817 | +function WebWorkerMapReducer(numWorkers, Worker) { |
---|
2818 | + if (!Worker) |
---|
2819 | + Worker = window.Worker; |
---|
2820 | + |
---|
2821 | + var pool = []; |
---|
2822 | + |
---|
2823 | + function MapWorker(id) { |
---|
2824 | + var worker = new Worker('js/workers/map-reducer.js'); |
---|
2825 | + var onDone; |
---|
2826 | + |
---|
2827 | + worker.onmessage = function(event) { |
---|
2828 | + onDone(event.data); |
---|
2829 | + }; |
---|
2830 | + |
---|
2831 | + this.id = id; |
---|
2832 | + this.map = function MW_map(map, dict, cb) { |
---|
2833 | + onDone = cb; |
---|
2834 | + worker.postMessage({map: map.toString(), dict: dict}); |
---|
2835 | + }; |
---|
2836 | + } |
---|
2837 | + |
---|
2838 | + for (var i = 0; i < numWorkers; i++) |
---|
2839 | + pool.push(new MapWorker(i)); |
---|
2840 | + |
---|
2841 | + this.map = function WWMR_map(map, dict, progress, chunkSize, finished) { |
---|
2842 | + var keys = dict.keys, |
---|
2843 | + size = keys.length, |
---|
2844 | + workersDone = 0, |
---|
2845 | + mapDict = {}; |
---|
2846 | + |
---|
2847 | + function getNextChunk() { |
---|
2848 | + if (keys.length) { |
---|
2849 | + var chunkKeys = keys.slice(0, chunkSize), |
---|
2850 | + chunk = {}, |
---|
2851 | + n = chunkKeys.length; |
---|
2852 | + |
---|
2853 | + keys = keys.slice(chunkSize); |
---|
2854 | + var key; |
---|
2855 | + while(n--) { |
---|
2856 | + key = chunkKeys[n]; |
---|
2857 | + chunk[key] = dict.dict[key]; |
---|
2858 | + } |
---|
2859 | + return chunk; |
---|
2860 | +// for (var i = 0, ii = chunkKeys.length; i < ii; i++) |
---|
2861 | +// chunk[chunkKeys[i]] = dict.dict[chunkKeys[i]]; |
---|
2862 | + |
---|
2863 | + } else |
---|
2864 | + return null; |
---|
2865 | + } |
---|
2866 | + |
---|
2867 | + function nextJob(mapWorker) { |
---|
2868 | + var chunk = getNextChunk(); |
---|
2869 | + if (chunk) { |
---|
2870 | + mapWorker.map( |
---|
2871 | + map, |
---|
2872 | + chunk, |
---|
2873 | + function jobDone(aMapDict) { |
---|
2874 | + for (var name in aMapDict) |
---|
2875 | + if (name in mapDict) { |
---|
2876 | + var item = mapDict[name]; |
---|
2877 | + item.keys = item.keys.concat(aMapDict[name].keys); |
---|
2878 | + item.values = item.values.concat(aMapDict[name].values); |
---|
2879 | + } else |
---|
2880 | + mapDict[name] = aMapDict[name]; |
---|
2881 | + |
---|
2882 | + if (keys.length) |
---|
2883 | + progress("map", |
---|
2884 | + (size - keys.length) / size, |
---|
2885 | + function() { nextJob(mapWorker); }); |
---|
2886 | + else |
---|
2887 | + workerDone(); |
---|
2888 | + }); |
---|
2889 | + } else |
---|
2890 | + workerDone(); |
---|
2891 | + } |
---|
2892 | + |
---|
2893 | + function workerDone() { |
---|
2894 | + workersDone += 1; |
---|
2895 | + if (workersDone == numWorkers) |
---|
2896 | + allWorkersDone(); |
---|
2897 | + } |
---|
2898 | + |
---|
2899 | + function allWorkersDone() { |
---|
2900 | + var mapKeys = []; |
---|
2901 | + for (var name in mapDict) |
---|
2902 | + mapKeys.push(name); |
---|
2903 | + mapKeys.sort(); |
---|
2904 | + finished({dict: mapDict, keys: mapKeys}); |
---|
2905 | + } |
---|
2906 | + |
---|
2907 | + for (var i = 0; i < numWorkers; i++) |
---|
2908 | + nextJob(pool[i]); |
---|
2909 | + }; |
---|
2910 | + |
---|
2911 | + // TODO: Actually implement our own reduce() method here instead |
---|
2912 | + // of delegating to the single-threaded version. |
---|
2913 | + this.reduce = SingleThreadedMapReducer.reduce; |
---|
2914 | +}; |
---|
2915 | + |
---|
2916 | +/** section: Database |
---|
2917 | + * da.db.SingleThreadedMapReducer |
---|
2918 | + * |
---|
2919 | + * A MapReducer that works on the current thread. |
---|
2920 | + **/ |
---|
2921 | +var SingleThreadedMapReducer = { |
---|
2922 | + /** |
---|
2923 | + * da.db.SingleThreadedMapReducer.map(map, dict, progress, chunkSize, finished) -> undefined |
---|
2924 | + * - map (Function): mapping function. |
---|
2925 | + * - dict (Object): database documents. |
---|
2926 | + * - progress (Function): progress reporting function. Called with `"map"` as first argument. |
---|
2927 | + * - chunkSize (Number): number of documents to map at once. |
---|
2928 | + * - finished (Function): called once map proccess finishes. |
---|
2929 | + **/ |
---|
2930 | + map: function STMR_map(map, dict, progress, |
---|
2931 | + chunkSize, finished) { |
---|
2932 | + var mapDict = {}, |
---|
2933 | + keys = dict.keys, |
---|
2934 | + currDoc; |
---|
2935 | + |
---|
2936 | + function emit(key, value) { |
---|
2937 | + // TODO: This assumes that the key will always be |
---|
2938 | + // an indexable value. We may have to hash the value, |
---|
2939 | + // though, if it's e.g. an Object. |
---|
2940 | + var item = mapDict[key]; |
---|
2941 | + if (!item) |
---|
2942 | + item = mapDict[key] = {keys: [], values: []}; |
---|
2943 | + item.keys.push(currDoc.id); |
---|
2944 | + item.values.push(value); |
---|
2945 | + } |
---|
2946 | + |
---|
2947 | + var i = 0; |
---|
2948 | + |
---|
2949 | + function continueMap() { |
---|
2950 | + var iAtStart = i, keysLength = keys.length; |
---|
2951 | + |
---|
2952 | + if(keysLength > 0) |
---|
2953 | + do { |
---|
2954 | + currDoc = dict.dict[keys[i]]; |
---|
2955 | + map(currDoc, emit); |
---|
2956 | + i++; |
---|
2957 | + } while (i - iAtStart < chunkSize && i < keysLength); |
---|
2958 | + |
---|
2959 | + if (i == keys.length) { |
---|
2960 | + var mapKeys = []; |
---|
2961 | + for (var name in mapDict) |
---|
2962 | + mapKeys.push(name); |
---|
2963 | + mapKeys.sort(); |
---|
2964 | + finished({dict: mapDict, keys: mapKeys}); |
---|
2965 | + } else |
---|
2966 | + progress("map", i / keysLength, continueMap); |
---|
2967 | + } |
---|
2968 | + |
---|
2969 | + continueMap(); |
---|
2970 | + }, |
---|
2971 | + |
---|
2972 | + /** |
---|
2973 | + * da.db.SingleThreadedMapReducer.reduce(reduce, mapResult, progress, chunkSize, finished) -> undefined |
---|
2974 | + * - reduce (Function): reduce function. |
---|
2975 | + * - mapResult (Object): Object returned by [[da.db.SingleThreadedMapReducer.map]]. |
---|
2976 | + * - progress (Function): progress reportiong function. Called with `"reduce"` as first argument. |
---|
2977 | + * - chunkSize (Number): number of documents to process at once. |
---|
2978 | + * - finished (Function): called when reduce process finishes. |
---|
2979 | + * - rereduce (Boolean | Object): object which will be passed to `reduce` during the rereduce process. |
---|
2980 | + * |
---|
2981 | + * Please refer to [CouchDB's docs on map and reduce functions](http://wiki.apache.org/couchdb/Introduction_to_CouchDB_views#Basics) |
---|
2982 | + * for more detailed usage details. |
---|
2983 | + **/ |
---|
2984 | + reduce: function STMR_reduce(reduce, mapResult, progress, |
---|
2985 | + chunkSize, finished, rereduce) { |
---|
2986 | + var rows = [], |
---|
2987 | + mapDict = mapResult.dict, |
---|
2988 | + mapKeys = mapResult.keys, |
---|
2989 | + i = 0; |
---|
2990 | + rereduce = rereduce || {}; |
---|
2991 | + |
---|
2992 | + function continueReduce() { |
---|
2993 | + var iAtStart = i; |
---|
2994 | + |
---|
2995 | + do { |
---|
2996 | + var key = mapKeys[i], |
---|
2997 | + item = mapDict[key] |
---|
2998 | + |
---|
2999 | + rows.push({ |
---|
3000 | + key: key, |
---|
3001 | + value: reduce(key, item.values, false) |
---|
3002 | + }); |
---|
3003 | + |
---|
3004 | + i++; |
---|
3005 | + } while (i - iAtStart < chunkSize && |
---|
3006 | + i < mapKeys.length) |
---|
3007 | + |
---|
3008 | + if (i == mapKeys.length) { |
---|
3009 | + finished(rows); |
---|
3010 | + } else |
---|
3011 | + progress("reduce", i / mapKeys.length, continueReduce); |
---|
3012 | + } |
---|
3013 | + |
---|
3014 | + continueReduce(); |
---|
3015 | + } |
---|
3016 | +}; |
---|
3017 | + |
---|
3018 | +da.db.BrowserCouch = BrowserCouch; |
---|
3019 | +da.db.BrowserCouch.Dictionary = Dictionary; |
---|
3020 | +da.db.SingleThreadedMapReducer = SingleThreadedMapReducer; |
---|
3021 | +da.db.WebWorkerMapReducer = WebWorkerMapReducer; |
---|
3022 | + |
---|
3023 | +})(); |
---|
3024 | addfile ./contrib/musicplayer/src/libs/db/DocumentTemplate.js |
---|
3025 | hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 1 |
---|
3026 | +//#require "libs/db/db.js" |
---|
3027 | +//#require "libs/db/BrowserCouch.js" |
---|
3028 | +//#require "libs/vendor/Math.uuid.js" |
---|
3029 | +//#require "libs/util/util.js" |
---|
3030 | + |
---|
3031 | +(function () { |
---|
3032 | +/** section: Database |
---|
3033 | + * class da.db.DocumentTemplate |
---|
3034 | + * implements Events |
---|
3035 | + * |
---|
3036 | + * Abstract class for manufacturing document templates. (ie. Model from MVC) |
---|
3037 | + **/ |
---|
3038 | +var DocumentTemplate = new Class({ |
---|
3039 | + Implements: Events, |
---|
3040 | + |
---|
3041 | + /** |
---|
3042 | + * da.db.DocumentTemplate#belongsTo -> Object |
---|
3043 | + * |
---|
3044 | + * Provides belongs-to-many relationsip found in may ORM libraries. |
---|
3045 | + * |
---|
3046 | + * #### Example |
---|
3047 | + * da.db.DocumentTemplate.registerType("Artist", new Class({ |
---|
3048 | + * Extends: da.db.DocumentTemplate |
---|
3049 | + * })); |
---|
3050 | + * |
---|
3051 | + * var queen = new da.db.DocumentTemplate.Artist({ |
---|
3052 | + * id: 0, |
---|
3053 | + * title: "Queen" |
---|
3054 | + * }); |
---|
3055 | + * |
---|
3056 | + * da.db.DocumentTemplate.registerType("Song", new Class({ |
---|
3057 | + * Extends: da.db.DocumentTemplate, |
---|
3058 | + * belongsTo: { |
---|
3059 | + * artist: "Artist" // -> artist_id property will be used to create a new Artist |
---|
3060 | + * } |
---|
3061 | + * })); |
---|
3062 | + * |
---|
3063 | + * var yeah = new da.db.DocumentTemplate.Song({ |
---|
3064 | + * artist_id: queen.id, |
---|
3065 | + * album_id: 5, |
---|
3066 | + * title: "Yeah" |
---|
3067 | + * }); |
---|
3068 | + * |
---|
3069 | + * yeah.get("artist", function (artist) { |
---|
3070 | + * console.log("Yeah by " + artist.get("title")); |
---|
3071 | + * }); |
---|
3072 | + * |
---|
3073 | + **/ |
---|
3074 | + belongsTo: {}, |
---|
3075 | + |
---|
3076 | + /** |
---|
3077 | + * da.db.DocumentTemplate#hasMany -> Object |
---|
3078 | + * |
---|
3079 | + * Provides has-many relationship between database documents. |
---|
3080 | + * |
---|
3081 | + * #### Example |
---|
3082 | + * If we defined `da.db.DocumentTemplate.Artist` in [[da.db.DocumentTemplate#belongsTo]] like: |
---|
3083 | + * |
---|
3084 | + * da.db.DocumentTemplate.registerType("Artist", new Class({ |
---|
3085 | + * Extends: da.db.DocumentTemplate, |
---|
3086 | + * hasMany: { |
---|
3087 | + * songs: ["Song", "artist_id"] |
---|
3088 | + * } |
---|
3089 | + * })); |
---|
3090 | + * |
---|
3091 | + * And assumed that `"artist_id"` is the name of the property which holds id of an `Artist`, |
---|
3092 | + * while `"Song"` represents the type of the document. |
---|
3093 | + * |
---|
3094 | + * Then we can obtain all the songs by given a artist with: |
---|
3095 | + * |
---|
3096 | + * queen.get("songs", function (songs) { |
---|
3097 | + * console.log("Queen songs:") |
---|
3098 | + * for(var n = 0, m = songs.length; n < m; n++) |
---|
3099 | + * console.log(songs[n].get("title")); |
---|
3100 | + * }); |
---|
3101 | + **/ |
---|
3102 | + hasMany: {}, |
---|
3103 | + |
---|
3104 | + /** |
---|
3105 | + * new da.db.DocumentTemplate(properties[, events]) |
---|
3106 | + * - properties (Object): document's properties. |
---|
3107 | + * - events (Object): default events. |
---|
3108 | + **/ |
---|
3109 | + initialize: function (properties, events) { |
---|
3110 | + this.doc = properties; |
---|
3111 | + if(!this.doc.id) |
---|
3112 | + this.doc.id = Math.uuid(); |
---|
3113 | + |
---|
3114 | + this.id = this.doc.id; |
---|
3115 | + this.doc.type = this.constructor.type; |
---|
3116 | + if(!this.constructor.db) |
---|
3117 | + this.constructor.db = function () { |
---|
3118 | + return Application.db |
---|
3119 | + }; |
---|
3120 | + |
---|
3121 | + // Time delay is set so class can finish initialization |
---|
3122 | + this.addEvents(events); |
---|
3123 | + this.fireEvent("create", [this], 1); |
---|
3124 | + }, |
---|
3125 | + |
---|
3126 | + /** |
---|
3127 | + * da.db.DocumentTemplate#id -> "id of the document" |
---|
3128 | + * |
---|
3129 | + * Shortcut for [[da.db.DocumentTemplate#get]]`("id")`. |
---|
3130 | + **/ |
---|
3131 | + id: null, |
---|
3132 | + |
---|
3133 | + /** |
---|
3134 | + * da.db.DocumentTemplate#get(key[, callback]) -> Object | false | this |
---|
3135 | + * - key (String): name of the property. |
---|
3136 | + * - callback (Function): needed only if `key` points to an property defined by an relationship. |
---|
3137 | + **/ |
---|
3138 | + get: function (key, callback) { |
---|
3139 | + if(key in this.doc) |
---|
3140 | + return this.doc[key]; |
---|
3141 | + |
---|
3142 | + if(!callback) |
---|
3143 | + return false; |
---|
3144 | + |
---|
3145 | + if(key in this.belongsTo) { |
---|
3146 | + var cache_key = "_belongs_to_" + key, |
---|
3147 | + cached = this[cache_key]; |
---|
3148 | + |
---|
3149 | + if(cached && cached.id === this.doc[key + "_id"]) |
---|
3150 | + return callback(cached); |
---|
3151 | + |
---|
3152 | + if(!this.doc[key + "_id"]) |
---|
3153 | + return callback(null); |
---|
3154 | + |
---|
3155 | + DocumentTemplate.find({ |
---|
3156 | + properties: { |
---|
3157 | + id: this.doc[key + "_id"], |
---|
3158 | + type: this.belongsTo[key] |
---|
3159 | + }, |
---|
3160 | + |
---|
3161 | + onSuccess: function (doc) { |
---|
3162 | + this[cache_key] = doc; |
---|
3163 | + callback(doc); |
---|
3164 | + }.bind(this), |
---|
3165 | + |
---|
3166 | + onFailure: callback |
---|
3167 | + }, this.constructor.db()); |
---|
3168 | + } else if(key in this.hasMany) { |
---|
3169 | + var relation = this.hasMany[key], |
---|
3170 | + props = {type: relation[0]}; |
---|
3171 | + |
---|
3172 | + props[relation[1]] = this.id; |
---|
3173 | + |
---|
3174 | + DocumentTemplate.find({ |
---|
3175 | + properties: props, |
---|
3176 | + onSuccess: callback, |
---|
3177 | + onFailure: callback |
---|
3178 | + }, DocumentTemplate[relation[0]].db()); |
---|
3179 | + } |
---|
3180 | + |
---|
3181 | + return this; |
---|
3182 | + }, |
---|
3183 | + |
---|
3184 | + /** |
---|
3185 | + * da.db.DocumentTemplate#set(properties) -> this |
---|
3186 | + * da.db.DocumentTemplate#set(key, value) -> this |
---|
3187 | + * - properties (Object): updated properties. |
---|
3188 | + * fires propertyChange |
---|
3189 | + **/ |
---|
3190 | + set: function (properties) { |
---|
3191 | + if(arguments.length == 2) { |
---|
3192 | + var key = properties; |
---|
3193 | + properties = {}; |
---|
3194 | + properties[key] = arguments[1]; |
---|
3195 | + } |
---|
3196 | + |
---|
3197 | + $extend(this.doc, properties); |
---|
3198 | + this.fireEvent("propertyChange", [properties, this]); |
---|
3199 | + |
---|
3200 | + return this; |
---|
3201 | + }, |
---|
3202 | + |
---|
3203 | + /** |
---|
3204 | + * da.db.DocumentTemplate#remove(property) -> this |
---|
3205 | + * - property (String): property to be removed. |
---|
3206 | + * fires propertyRemove |
---|
3207 | + **/ |
---|
3208 | + remove: function (property) { |
---|
3209 | + if(property !== "_id") |
---|
3210 | + delete this.doc[property]; |
---|
3211 | + |
---|
3212 | + this.fireEvent("propertyRemove", [property, this]); |
---|
3213 | + return this; |
---|
3214 | + }, |
---|
3215 | + |
---|
3216 | + /** |
---|
3217 | + * da.db.DocumentTemplate#save([callback]) -> this |
---|
3218 | + * - callback (Function): function called after `save` event. |
---|
3219 | + * fires save |
---|
3220 | + **/ |
---|
3221 | + save: function (callback) { |
---|
3222 | + this.constructor.db().put(this.doc, function () { |
---|
3223 | + this.fireEvent("save", [this]); |
---|
3224 | + if(callback) |
---|
3225 | + callback(this); |
---|
3226 | + }.bind(this)); |
---|
3227 | + |
---|
3228 | + return this; |
---|
3229 | + }, |
---|
3230 | + |
---|
3231 | + /** |
---|
3232 | + * da.db.DocumentTemplate#update(properties[, cb]) -> this |
---|
3233 | + * - properties (Object): new properties. |
---|
3234 | + * - callback (Function): called after `save`. |
---|
3235 | + * |
---|
3236 | + * Calls [[da.db.DocumentTemplate#set]] and [[da.db.DocumentTemplate#save]]. |
---|
3237 | + **/ |
---|
3238 | + update: function (properties, cb) { |
---|
3239 | + this.set(properties); |
---|
3240 | + this.save(cb); |
---|
3241 | + return this; |
---|
3242 | + }, |
---|
3243 | + |
---|
3244 | + /** |
---|
3245 | + * da.db.DocumentTemplate#destroy([callback]) -> this |
---|
3246 | + * - callback (Function): function called after `destroy` event. |
---|
3247 | + * |
---|
3248 | + * Removes all document's properties except for `id` and adds `_deleted` property. |
---|
3249 | + **/ |
---|
3250 | + destroy: function (callback) { |
---|
3251 | + this.doc = {id: this.id, _deleted: true}; |
---|
3252 | + this.constructor.db().put(this.doc, function () { |
---|
3253 | + this.fireEvent("destroy", [this]); |
---|
3254 | + if(callback) |
---|
3255 | + callback(this); |
---|
3256 | + }); |
---|
3257 | + |
---|
3258 | + return this; |
---|
3259 | + } |
---|
3260 | +}); |
---|
3261 | + |
---|
3262 | +DocumentTemplate.extend({ |
---|
3263 | + /** |
---|
3264 | + * da.db.DocumentTemplate.find(options[, db]) -> undefined |
---|
3265 | + * - options.properties (String | Object | Function): properties document must have or an function which checks document's properties. |
---|
3266 | + * If `String` is provided, it's assumed that it represents document's `id`. |
---|
3267 | + * - options.onSuccess (Function): function called once document is found. |
---|
3268 | + * - options.onFailure (Function): function called if no documents are found. |
---|
3269 | + * - options.onlyFirst (Bool): gives back only first result. |
---|
3270 | + * - db (BrowserCouch): if not provided, `Application.db` is used. |
---|
3271 | + **/ |
---|
3272 | + find: function (options, db) { |
---|
3273 | + if(!options.onSuccess) |
---|
3274 | + return false; |
---|
3275 | + if(!options.onFailure) |
---|
3276 | + options.onFailure = $empty; |
---|
3277 | + if(typeof options.properties === "string") |
---|
3278 | + options.properties = {id: options.properties} |
---|
3279 | + |
---|
3280 | + var map_fn, props = options.properties; |
---|
3281 | + if(typeof properties === "function") |
---|
3282 | + map_fn = function (doc, emit) { |
---|
3283 | + if(doc && !doc._deleted && props(doc)) |
---|
3284 | + emit(doc.id, doc); |
---|
3285 | + }; |
---|
3286 | + else |
---|
3287 | + map_fn = function (doc, emit) { |
---|
3288 | + if(doc && !doc._deleted && Hash.containsAll(doc, props)) |
---|
3289 | + emit(doc.id, doc); |
---|
3290 | + }; |
---|
3291 | + |
---|
3292 | + (db || da.db.DEFAULT).view({ |
---|
3293 | + temporary: true, |
---|
3294 | + map: map_fn, |
---|
3295 | + finished: function (result) { |
---|
3296 | + if(!result.rows.length) |
---|
3297 | + return options.onFailure(); |
---|
3298 | + |
---|
3299 | + var n = result.rows.length; |
---|
3300 | + while(n--) { |
---|
3301 | + var row = result.rows[n].value, |
---|
3302 | + type = DocumentTemplate[row.type]; |
---|
3303 | + |
---|
3304 | + result.rows[n] = type ? new type(row) : row; |
---|
3305 | + } |
---|
3306 | + |
---|
3307 | + options.onSuccess(options.onlyFirst ? result.rows[0] : result.rows); |
---|
3308 | + } |
---|
3309 | + }); |
---|
3310 | + }, |
---|
3311 | + |
---|
3312 | + /** |
---|
3313 | + * da.db.DocumentTemplate.findFirst(options[, db]) -> undefined |
---|
3314 | + * - options (Object): same options as in [[da.db.DocumentTemplate.find]] apply here. |
---|
3315 | + **/ |
---|
3316 | + findFirst: function (options, db) { |
---|
3317 | + options.onlyFirst = true; |
---|
3318 | + this.find(options, db); |
---|
3319 | + }, |
---|
3320 | + |
---|
3321 | + /** |
---|
3322 | + * da.db.DocumentTemplate.findOrCreate(options[, db]) -> undefined |
---|
3323 | + * - options (Object): same options as in [[da.db.DocumentTemplate.find]] apply here. |
---|
3324 | + * - options.properties.type (String): must be set to the desired [[da.db.DocumentTemplate]] type. |
---|
3325 | + **/ |
---|
3326 | + findOrCreate: function (options, db) { |
---|
3327 | + options.onSuccess = options.onSuccess || $empty; |
---|
3328 | + options.onFailure = function () { |
---|
3329 | + options.onSuccess(new DocumentTemplate[options.properties.type](options.properties), true); |
---|
3330 | + }; |
---|
3331 | + this.findFirst(options, db); |
---|
3332 | + }, |
---|
3333 | + |
---|
3334 | + /** |
---|
3335 | + * da.db.DocumentTemplate.registerType(typeName[, db = Application.db], template) -> da.db.DocumentTemplate |
---|
3336 | + * - typeName (String): name of the type. ex.: `Car`, `Chocolate` etc. |
---|
3337 | + * - db (BrowserCouch): database to be used. |
---|
3338 | + * - template (da.db.DocumentTemplate): the actual [[da.db.DocumentTemplate]] [[Class]]. |
---|
3339 | + * |
---|
3340 | + * New classes are accessible from `da.db.DocumentTemplate.<typeName>`. |
---|
3341 | + **/ |
---|
3342 | + registerType: function (type, db, template) { |
---|
3343 | + if(arguments.length === 2) { |
---|
3344 | + template = db; |
---|
3345 | + db = null; |
---|
3346 | + } |
---|
3347 | + |
---|
3348 | + template.type = type; |
---|
3349 | + // This is a function so we can |
---|
3350 | + // return a reference to the original instance |
---|
3351 | + // of DB, otherwise, due to MooTools' inheritance |
---|
3352 | + // we would get a new copy. |
---|
3353 | + if(db) |
---|
3354 | + template.db = function () { return db }; |
---|
3355 | + else |
---|
3356 | + template.db = function () { return da.db.DEFAULT }; |
---|
3357 | + |
---|
3358 | + template.find = function (options) { |
---|
3359 | + options.properties.type = type; |
---|
3360 | + DocumentTemplate.find(options, db); |
---|
3361 | + }; |
---|
3362 | + |
---|
3363 | + template.findFirst = function (options) { |
---|
3364 | + options.properties.type = type; |
---|
3365 | + DocumentTemplate.findFirst(options, db); |
---|
3366 | + }; |
---|
3367 | + |
---|
3368 | + template.create = function (properties, callback) { |
---|
3369 | + return (new template(properties)).save(callback); |
---|
3370 | + }; |
---|
3371 | + |
---|
3372 | + template.findOrCreate = function (options) { |
---|
3373 | + options.properties.type = type; |
---|
3374 | + DocumentTemplate.findOrCreate(options, db); |
---|
3375 | + }; |
---|
3376 | + |
---|
3377 | + template.db().view({ |
---|
3378 | + id: type, |
---|
3379 | + map: function (doc, emit) { |
---|
3380 | + if(doc && doc.type === type) |
---|
3381 | + emit(doc.id, doc); |
---|
3382 | + }, |
---|
3383 | + finished: $empty |
---|
3384 | + }); |
---|
3385 | + |
---|
3386 | + DocumentTemplate[type] = template; |
---|
3387 | + return template; |
---|
3388 | + } |
---|
3389 | +}); |
---|
3390 | + |
---|
3391 | +da.db.DocumentTemplate = DocumentTemplate; |
---|
3392 | + |
---|
3393 | +})(); |
---|
3394 | addfile ./contrib/musicplayer/src/libs/db/PersistStorage.js |
---|
3395 | hunk ./contrib/musicplayer/src/libs/db/PersistStorage.js 1 |
---|
3396 | +//#require "libs/vendor/persist-js/src/persist.js" |
---|
3397 | +//#require "libs/db/db.js" |
---|
3398 | + |
---|
3399 | +(function () { |
---|
3400 | +/** section: Database |
---|
3401 | + * class da.db.PersistStorage |
---|
3402 | + * |
---|
3403 | + * Interface between PersistJS and BrowserCouch. |
---|
3404 | + **/ |
---|
3405 | +/* |
---|
3406 | + * new da.db.PersistStorage(database_name) |
---|
3407 | + * - database_name (String): name of the database. |
---|
3408 | + **/ |
---|
3409 | +function PersistStorage (db_name) { |
---|
3410 | + var storage = new Persist.Store(db_name || "tahoemp"); |
---|
3411 | + |
---|
3412 | + /** |
---|
3413 | + * da.db.PersistStorage#get(key, callback) -> undefined |
---|
3414 | + * - key (String): name of the property |
---|
3415 | + * - callback (Function): will be called once data is fetched, |
---|
3416 | + * which will be passed as first argument. |
---|
3417 | + **/ |
---|
3418 | + this.get = function (key, cb) { |
---|
3419 | + storage.get(key, function (ok, value) { |
---|
3420 | + cb(value ? JSON.parse(value) : null, ok); |
---|
3421 | + }); |
---|
3422 | + }; |
---|
3423 | + |
---|
3424 | + /** |
---|
3425 | + * da.db.PersistStorage#put(key, value[, callback]) -> undefined |
---|
3426 | + * - key (String): name of the property. |
---|
3427 | + * - value (Object): value of the property. |
---|
3428 | + * - callback (Function): will be called once data is saved. |
---|
3429 | + **/ |
---|
3430 | + this.put = function (key, value, cb) { |
---|
3431 | + storage.set(key, JSON.stringify(value)); |
---|
3432 | + if(cb) cb(); |
---|
3433 | + }; |
---|
3434 | + |
---|
3435 | + return this; |
---|
3436 | +} |
---|
3437 | + |
---|
3438 | +da.db.PersistStorage = PersistStorage; |
---|
3439 | +})(); |
---|
3440 | addfile ./contrib/musicplayer/src/libs/db/db.js |
---|
3441 | hunk ./contrib/musicplayer/src/libs/db/db.js 1 |
---|
3442 | +/** |
---|
3443 | + * == Database == |
---|
3444 | + * |
---|
3445 | + * Map/Reduce, storage and model APIs. |
---|
3446 | + **/ |
---|
3447 | + |
---|
3448 | +/** section: Database |
---|
3449 | + * da.db |
---|
3450 | + **/ |
---|
3451 | +if(typeof da.db === "undefined") |
---|
3452 | + da.db = {}; |
---|
3453 | adddir ./contrib/musicplayer/src/libs/ui |
---|
3454 | addfile ./contrib/musicplayer/src/libs/ui/Column.js |
---|
3455 | hunk ./contrib/musicplayer/src/libs/ui/Column.js 1 |
---|
3456 | +//#require "libs/ui/ui.js" |
---|
3457 | + |
---|
3458 | +/** section: UserInterface |
---|
3459 | + * class da.ui.Column |
---|
3460 | + * implements Events, Options |
---|
3461 | + * |
---|
3462 | + * Widget which can efficiently display large amounts of items in a list. |
---|
3463 | + **/ |
---|
3464 | +da.ui.Column = new Class({ |
---|
3465 | + Implements: [Events, Options], |
---|
3466 | + |
---|
3467 | + options: { |
---|
3468 | + id: undefined, |
---|
3469 | + rowHeight: 30, |
---|
3470 | + totalCount: 0, |
---|
3471 | + renderTimeout: 120, |
---|
3472 | + itemClassNames: "column_item" |
---|
3473 | + }, |
---|
3474 | + /** |
---|
3475 | + * new da.ui.Column(options) |
---|
3476 | + * - options.id (String): desired ID of the column's DIV element, `_column` will be appended. |
---|
3477 | + * if ommited, random one will be generated. |
---|
3478 | + * - options.rowHeight (Number): height of an row. Defaults to 30. |
---|
3479 | + * - options.totalCount (Number): number of items this column has to show in total. |
---|
3480 | + * - options.itemClassNames (String): CSS class names added to each item. Defaults to `column_item`. |
---|
3481 | + * - options.renderTimeout (Number): milliseconds to wait during the scroll before rendering |
---|
3482 | + * items. Defaults to 120. |
---|
3483 | + * |
---|
3484 | + * Creates a new Column. |
---|
3485 | + * |
---|
3486 | + * ##### Notes |
---|
3487 | + * When resizing (height) of the column use [[Element#set]] function provided by MooTools |
---|
3488 | + * which properly fires `resize` event. |
---|
3489 | + * |
---|
3490 | + * column._el.set("height", window.getHeight()); |
---|
3491 | + * |
---|
3492 | + **/ |
---|
3493 | + initialize: function (options) { |
---|
3494 | + this.setOptions(options); |
---|
3495 | + if(!this.options.id) |
---|
3496 | + this.options.id = "column_" + Math.uuid(5); |
---|
3497 | + |
---|
3498 | + this._populated = false; |
---|
3499 | + // #_rendered will contain keys of items which have been rendered. |
---|
3500 | + // What is a key is up to particular implementation. |
---|
3501 | + this._rendered = []; |
---|
3502 | + |
---|
3503 | + this._el = new Element("div", { |
---|
3504 | + id: options.id, |
---|
3505 | + 'class': 'column', |
---|
3506 | + styles: { |
---|
3507 | + overflowX: "hidden", |
---|
3508 | + overflowY: "auto", |
---|
3509 | + position: "relative" |
---|
3510 | + } |
---|
3511 | + }); |
---|
3512 | + |
---|
3513 | + // weight is used to force the browser |
---|
3514 | + // to show scrollbar with right proportions. |
---|
3515 | + this._weight = new Element("div", { |
---|
3516 | + styles: { |
---|
3517 | + position: "absolute", |
---|
3518 | + top: 0, |
---|
3519 | + left: 0, |
---|
3520 | + width: 1, |
---|
3521 | + height: 1 |
---|
3522 | + } |
---|
3523 | + }); |
---|
3524 | + this._weight.injectBottom(this._el); |
---|
3525 | + |
---|
3526 | + // scroll event is fired for even smallest changes |
---|
3527 | + // of scrollbars positions, since rendering items can be |
---|
3528 | + // expensive a small timeout will be set in order to save |
---|
3529 | + // some bandwidth - the negative side is that flicker is seen |
---|
3530 | + // while scrolling. |
---|
3531 | + var scroll_timer = null, |
---|
3532 | + timeout = this.options.renderTimeout, |
---|
3533 | + timeout_fn = this.render.bind(this); |
---|
3534 | + |
---|
3535 | + this._el.addEvent("scroll", function () { |
---|
3536 | + clearTimeout(scroll_timer); |
---|
3537 | + scroll_timer = setTimeout(scroll_timer, timeout); |
---|
3538 | + }); |
---|
3539 | + |
---|
3540 | + // We're caching lists' height so we won't have to |
---|
3541 | + // ask for it in every #render() - which can be quite expensiv. |
---|
3542 | + this._el.addEvent("resize", function () { |
---|
3543 | + this._el_height = this._el.getHeight(); |
---|
3544 | + }.bind(this)); |
---|
3545 | + }, |
---|
3546 | + |
---|
3547 | + /** |
---|
3548 | + * da.ui.Column#render() -> this | false |
---|
3549 | + * |
---|
3550 | + * Renders all of items which are in current viewport in a batch. |
---|
3551 | + * |
---|
3552 | + * Returns `false` if all of items have already been rendered. |
---|
3553 | + * |
---|
3554 | + * Items are rendered in groups of (`div` tags with `column_items_box` CSS class). |
---|
3555 | + * The number of items is determined by number of items which can fit in viewport + five |
---|
3556 | + * items before and 10 items after current viewport. |
---|
3557 | + * Each item has CSS classes defined in `options.itemClassNames` and have a `column_index` |
---|
3558 | + * property stored. |
---|
3559 | + **/ |
---|
3560 | + render: function () { |
---|
3561 | + if(!this._populated) |
---|
3562 | + this.populate(); |
---|
3563 | + if(this._rendered.length === this.options.totalCount + 1) |
---|
3564 | + return false; |
---|
3565 | + |
---|
3566 | + // We're pre-fetching previous 5 and next 10 items |
---|
3567 | + // which are outside of current viewport |
---|
3568 | + var total_count = this.options.totalCount, |
---|
3569 | + ids = this.getVisibleIndexes(), |
---|
3570 | + n = Math.max(0, ids[0] - 6), |
---|
3571 | + m = Math.max(Math.min(ids[1] + 10, total_count), total_count), |
---|
3572 | + box = new Element("div", {"class": "column_items_box"}), |
---|
3573 | + item_class = this.options.itemClassNames, |
---|
3574 | + first_rendered = null; |
---|
3575 | + |
---|
3576 | + for( ; n < m; n++) { |
---|
3577 | + if(!this._rendered.contains(n)) { |
---|
3578 | + // First item in viewport could be already rendered |
---|
3579 | + // this helps minimizing amount of DOM nodes that will be inserted |
---|
3580 | + if(first_rendered === null) |
---|
3581 | + first_rendered = n; |
---|
3582 | + |
---|
3583 | + this.renderItem(n).addClass(item_class).store("column_index", n).injectBottom(box); |
---|
3584 | + this._rendered.push(n); |
---|
3585 | + } |
---|
3586 | + } |
---|
3587 | + |
---|
3588 | + if(first_rendered !== null) { |
---|
3589 | + var coords = this.getBoxCoords(first_rendered); |
---|
3590 | + box.setStyles({ |
---|
3591 | + position: "absolute", |
---|
3592 | + top: coords[0], |
---|
3593 | + left: coords[1] |
---|
3594 | + }).injectBottom(this._el); |
---|
3595 | + } |
---|
3596 | + |
---|
3597 | + return this; |
---|
3598 | + }, |
---|
3599 | + |
---|
3600 | + /** |
---|
3601 | + * da.ui.Column#populate() -> this |
---|
3602 | + * fires resize |
---|
3603 | + * |
---|
3604 | + * Positiones weight element and fires `resize` event. This method should ignore `_populated` property. |
---|
3605 | + **/ |
---|
3606 | + populate: function () { |
---|
3607 | + var o = this.options; |
---|
3608 | + this._populated = true; |
---|
3609 | + this._weight.setStyle("top", o.rowHeight * o.totalCount /*+ o.rowHeight*/); |
---|
3610 | + this._el.fireEvent("resize"); |
---|
3611 | + |
---|
3612 | + return this; |
---|
3613 | + }, |
---|
3614 | + |
---|
3615 | + /** |
---|
3616 | + * da.ui.Column#rerender() -> this |
---|
3617 | + **/ |
---|
3618 | + rerender: function () { |
---|
3619 | + var weight = this._weight; |
---|
3620 | + this._el.empty(); |
---|
3621 | + this._el.grab(weight); |
---|
3622 | + |
---|
3623 | + this._rendered = []; |
---|
3624 | + this._populated = false; |
---|
3625 | + return this.render(); |
---|
3626 | + }, |
---|
3627 | + /** |
---|
3628 | + * da.ui.Column#updateTotalCount(totalCount) -> this | false |
---|
3629 | + * - totalCount (Number): total number of items this column is going to display |
---|
3630 | + * |
---|
3631 | + * Provides means to update `totalCount` option after column has already been rendered/initialized. |
---|
3632 | + **/ |
---|
3633 | + updateTotalCount: function (total_count) { |
---|
3634 | + this.options.totalCount = total_count; |
---|
3635 | + return this.populate(); |
---|
3636 | + }, |
---|
3637 | + |
---|
3638 | + /** |
---|
3639 | + * da.ui.Column#renderItem(index) -> Element |
---|
3640 | + * - index (Object): could be a String or Number, internal representation of data. |
---|
3641 | + * |
---|
3642 | + * Constructs and returns new Element without adding it to the `document`. |
---|
3643 | + **/ |
---|
3644 | + renderItem: function(index) { |
---|
3645 | + console.warn("Column.renderItem(index) should be overwritten", this); |
---|
3646 | + return new Element("div", {html: index}); |
---|
3647 | + }, |
---|
3648 | + |
---|
3649 | + /** |
---|
3650 | + * da.ui.Column#getBoxCoords(index) -> [Number, Number] |
---|
3651 | + * - index (Number): index of the first item in a box. |
---|
3652 | + * |
---|
3653 | + * Returns X and Y coordinates at which item with given `index` should be rendered at. |
---|
3654 | + **/ |
---|
3655 | + getBoxCoords: function(index) { |
---|
3656 | + return [this.options.rowHeight * index, 0]; |
---|
3657 | + }, |
---|
3658 | + |
---|
3659 | + /** |
---|
3660 | + * da.ui.Column#getVisibleIndexes() -> Array |
---|
3661 | + * |
---|
3662 | + * Returns an array with indexes of first and last item in visible portion of list. |
---|
3663 | + **/ |
---|
3664 | + getVisibleIndexes: function () { |
---|
3665 | + // Math.round() and Math.ceil() are used in such combination |
---|
3666 | + // to include items which could be only partially in viewport |
---|
3667 | + var rh = this.options.rowHeight, |
---|
3668 | + per_viewport = Math.round(this._el_height / rh), |
---|
3669 | + first = Math.ceil(this._el.getScroll().y / rh); |
---|
3670 | + if(first > 0) first--; |
---|
3671 | + |
---|
3672 | + return [first, first + per_viewport]; |
---|
3673 | + }, |
---|
3674 | + |
---|
3675 | + /** |
---|
3676 | + * da.ui.Column#injectBottom(element) -> this |
---|
3677 | + * - element (Element): element to which column should be appended. |
---|
3678 | + * |
---|
3679 | + * Injects column at the bottom of provided element. |
---|
3680 | + **/ |
---|
3681 | + injectBottom: function(el) { |
---|
3682 | + this._el.injectBottom(el); |
---|
3683 | + return this; |
---|
3684 | + }, |
---|
3685 | + |
---|
3686 | + /** |
---|
3687 | + * da.ui.Column#destory() -> this |
---|
3688 | + * |
---|
3689 | + * Removes column from DOM. |
---|
3690 | + **/ |
---|
3691 | + destroy: function (dispose) { |
---|
3692 | + this._el.destroy(); |
---|
3693 | + delete this._el; |
---|
3694 | + return this; |
---|
3695 | + }, |
---|
3696 | + |
---|
3697 | + /** |
---|
3698 | + * da.ui.Column#toElement() -> Element |
---|
3699 | + **/ |
---|
3700 | + toElement: function () { |
---|
3701 | + return this._el; |
---|
3702 | + } |
---|
3703 | +}); |
---|
3704 | addfile ./contrib/musicplayer/src/libs/ui/Dialog.js |
---|
3705 | hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 1 |
---|
3706 | +//#require "libs/ui/ui.js" |
---|
3707 | + |
---|
3708 | +/** section: UserInterface |
---|
3709 | + * class da.ui.Dialog |
---|
3710 | + * |
---|
3711 | + * Class for working with interface dialogs. |
---|
3712 | + **/ |
---|
3713 | +da.ui.Dialog = new Class({ |
---|
3714 | + Implements: [Events, Options], |
---|
3715 | + |
---|
3716 | + options: { |
---|
3717 | + title: null, |
---|
3718 | + hideOnOutsideClick: true, |
---|
3719 | + show: false |
---|
3720 | + }, |
---|
3721 | + |
---|
3722 | + /** |
---|
3723 | + * new da.ui.Dialog(options) |
---|
3724 | + * - options.title (String): title of the dialog. optional. |
---|
3725 | + * - options.hideOnOutsideClick (Boolean): if `true`, the dialog will be hidden when |
---|
3726 | + * click outside the dialog element (ie. on the dimmed portion of screen) occurs. |
---|
3727 | + * - options.show (Boolean): if `true` the dialog will be shown immediately as it's created. |
---|
3728 | + * Defaults to `false`. |
---|
3729 | + * - options.html (Element): contents of the. |
---|
3730 | + * |
---|
3731 | + * To the `options.html` element `dialog` CSS class name will be added and |
---|
3732 | + * the element will be wrapped into a `div` with `dialog_wrapper` CSS class name. |
---|
3733 | + * |
---|
3734 | + * If `options.title` is provided, the title element will be injected at the top of |
---|
3735 | + * `options.html` and will be given `dialog_title` CSS class name. |
---|
3736 | + * |
---|
3737 | + * #### Notes |
---|
3738 | + * All dialogs are hidden by default, use [[Dialog.show]] to show them immediately |
---|
3739 | + * after they are created method. |
---|
3740 | + * |
---|
3741 | + * #### Example |
---|
3742 | + * new da.ui.Dialog({ |
---|
3743 | + * title: "What's your name?" |
---|
3744 | + * html: new Element("div", { |
---|
3745 | + * html: "Hello!" |
---|
3746 | + * }), |
---|
3747 | + * show: true |
---|
3748 | + * }); |
---|
3749 | + * |
---|
3750 | + **/ |
---|
3751 | + initialize: function (options) { |
---|
3752 | + this.setOptions(options); |
---|
3753 | + if(!this.options.html) |
---|
3754 | + throw "options.html must be provided when creating an Dialog"; |
---|
3755 | + |
---|
3756 | + this._el = new Element("div", { |
---|
3757 | + "class": "dialog_wrapper" |
---|
3758 | + }); |
---|
3759 | + if(!this.options.show) |
---|
3760 | + this._el.style.display = "none"; |
---|
3761 | + |
---|
3762 | + if(this.options.title) |
---|
3763 | + if(typeof this.options.title === "string") |
---|
3764 | + (new Element("h2", { |
---|
3765 | + html: this.options.title, |
---|
3766 | + "class": "dialog_title no_selection" |
---|
3767 | + })).inject(this.options.html, "top"); |
---|
3768 | + else if($type(this.options.title) === "element") |
---|
3769 | + this.options.title.inject(this.options.html, "top"); |
---|
3770 | + |
---|
3771 | + if(this.options.hideOnOutsideClick) |
---|
3772 | + this._el.addEvent("click", this.hide.bind(this)); |
---|
3773 | + |
---|
3774 | + this._el.grab(options.html.addClass("dialog")); |
---|
3775 | + document.body.grab(this._el); |
---|
3776 | + }, |
---|
3777 | + |
---|
3778 | + /** |
---|
3779 | + * da.ui.Dialog#show() -> this |
---|
3780 | + * fires show |
---|
3781 | + **/ |
---|
3782 | + show: function () { |
---|
3783 | + this._el.show(); |
---|
3784 | + this.fireEvent("show", [this]); |
---|
3785 | + return this; |
---|
3786 | + }, |
---|
3787 | + |
---|
3788 | + /** |
---|
3789 | + * da.ui.Dialog#hide(event) -> this |
---|
3790 | + * fires hide |
---|
3791 | + **/ |
---|
3792 | + hide: function (event) { |
---|
3793 | + if(event && event.target !== this._el) |
---|
3794 | + return this; |
---|
3795 | + |
---|
3796 | + this._el.hide(); |
---|
3797 | + this.fireEvent("hide", [this]); |
---|
3798 | + return this; |
---|
3799 | + }, |
---|
3800 | + |
---|
3801 | + /** |
---|
3802 | + * da.ui.Dialog#destroy() -> this |
---|
3803 | + **/ |
---|
3804 | + destory: function () { |
---|
3805 | + this.options.html.destroy(); |
---|
3806 | + this._el.destroy(); |
---|
3807 | + return this; |
---|
3808 | + } |
---|
3809 | +}); |
---|
3810 | + |
---|
3811 | addfile ./contrib/musicplayer/src/libs/ui/Menu.js |
---|
3812 | hunk ./contrib/musicplayer/src/libs/ui/Menu.js 1 |
---|
3813 | +//#require "libs/ui/ui.js" |
---|
3814 | + |
---|
3815 | +(function () { |
---|
3816 | +var VISIBLE_MENU; |
---|
3817 | + |
---|
3818 | +/** section: UserInterface |
---|
3819 | + * class da.ui.Menu |
---|
3820 | + * implements Events, Options |
---|
3821 | + * |
---|
3822 | + * Lightweight menu class. |
---|
3823 | + * |
---|
3824 | + * #### Example |
---|
3825 | + * |
---|
3826 | + * var file_menu = new da.ui.Menu({ |
---|
3827 | + * items: { |
---|
3828 | + * neu: {html: "New", href: "#"}, |
---|
3829 | + * neu_tpl: {html: "New from template", href: "#"}, |
---|
3830 | + * open: {html: "Open", href: "#"}, |
---|
3831 | + * |
---|
3832 | + * _sep1: da.ui.Menu.separator, |
---|
3833 | + * |
---|
3834 | + * close: {html: "Close", href: "#"}, |
---|
3835 | + * save: {html: "Save", href: "#"}, |
---|
3836 | + * save_all: {html: "Save all", href: "#", "class": "disabled"}, |
---|
3837 | + * |
---|
3838 | + * _sep2: da.ui.Menu.separator, |
---|
3839 | + * |
---|
3840 | + * quit: {html: "Quit", href: "#", onClick: function () { |
---|
3841 | + * confirm("Are you sure?") |
---|
3842 | + * }} |
---|
3843 | + * }, |
---|
3844 | + * |
---|
3845 | + * position: { |
---|
3846 | + * position: "topLeft" |
---|
3847 | + * }, |
---|
3848 | + * |
---|
3849 | + * onClick: function (key, event, element) { |
---|
3850 | + * console.log("knock knock", key); |
---|
3851 | + * } |
---|
3852 | + * }); |
---|
3853 | + * |
---|
3854 | + * file_menu.show(); |
---|
3855 | + * |
---|
3856 | + * Values of properties in `items` are actually second arguments for MooTools' |
---|
3857 | + * `new Element()` and therefore provide great customization ability. |
---|
3858 | + * |
---|
3859 | + * `position` property will be passed to MooTools' `Element.position()` method, |
---|
3860 | + * and defaults to `bottomRight`. |
---|
3861 | + * |
---|
3862 | + * #### Events |
---|
3863 | + * - `click` - arguments: key of the clicked item, clicked element |
---|
3864 | + * - `show` |
---|
3865 | + * - `hide` |
---|
3866 | + * |
---|
3867 | + * #### Notes |
---|
3868 | + * `href` attribute is added to all items in order to enable |
---|
3869 | + * keyboard navigation with tab key. |
---|
3870 | + * |
---|
3871 | + * #### See also |
---|
3872 | + * * [MooTools Element class](http://mootools.net/docs/core/Element/Element#Element:constructor) |
---|
3873 | + **/ |
---|
3874 | + |
---|
3875 | +da.ui.Menu = new Class({ |
---|
3876 | + Implements: [Events, Options], |
---|
3877 | + |
---|
3878 | + options: { |
---|
3879 | + items: {}, |
---|
3880 | + position: { |
---|
3881 | + position: "bottomLeft" |
---|
3882 | + } |
---|
3883 | + }, |
---|
3884 | + |
---|
3885 | + /** |
---|
3886 | + * da.ui.Menu#last_clicked -> Element |
---|
3887 | + * |
---|
3888 | + * Last clicked menu item. |
---|
3889 | + **/ |
---|
3890 | + last_clicked: null, |
---|
3891 | + |
---|
3892 | + /** |
---|
3893 | + * new da.ui.Menu([options = {}]) |
---|
3894 | + * - options.items (Object): menu items. |
---|
3895 | + * - options.position (Object): menu positioning parameters. |
---|
3896 | + **/ |
---|
3897 | + initialize: function (options) { |
---|
3898 | + this.setOptions(options); |
---|
3899 | + |
---|
3900 | + this._el = (new Element("ul")).addClass("menu").addClass("no_selection"); |
---|
3901 | + this._el.style.display = "none"; |
---|
3902 | + this._el.addEvent("click:relay(.menu_item a)", this.click.bind(this)); |
---|
3903 | + //this._el.injectBottom(document.body); |
---|
3904 | + |
---|
3905 | + this.render(); |
---|
3906 | + }, |
---|
3907 | + |
---|
3908 | + /** |
---|
3909 | + * da.ui.Menu#render() -> this |
---|
3910 | + * |
---|
3911 | + * Renders the menu items and adds them to the document. |
---|
3912 | + * Menu element is an `ul` tag appeded to the bottom of `document.body` and has `menu` CSS class. |
---|
3913 | + **/ |
---|
3914 | + render: function () { |
---|
3915 | + var items = this.options.items; |
---|
3916 | + this._el.dispose().empty(); |
---|
3917 | + |
---|
3918 | + for(var id in items) |
---|
3919 | + this._el.grab(this.renderItem(id)); |
---|
3920 | + |
---|
3921 | + document.body.grab(this._el); |
---|
3922 | + return this; |
---|
3923 | + }, |
---|
3924 | + |
---|
3925 | + /** |
---|
3926 | + * da.ui.Menu#renderItem(id) -> Element |
---|
3927 | + * - id (String): id of the menu item. |
---|
3928 | + * |
---|
3929 | + * Renders item without attaching it to DOM. |
---|
3930 | + * Item is a `li` tag with `menu_item` CSS class. `li` tag contains an `a` tag with the item's text. |
---|
3931 | + * Each `li` tag also has a `menu_key` property set, which can be retrived with: |
---|
3932 | + * |
---|
3933 | + * menu.toElement().getItems('.menu_item').retrieve("menu_key") |
---|
3934 | + * |
---|
3935 | + * If the item was defined with function than those tag names might not be used, |
---|
3936 | + * but CSS class names are guaranteed to be there in both cases. |
---|
3937 | + **/ |
---|
3938 | + renderItem: function (id) { |
---|
3939 | + var options = this.options.items[id], el; |
---|
3940 | + |
---|
3941 | + if(typeof options === "function") |
---|
3942 | + el = options(this).addClass("menu_item"); |
---|
3943 | + else |
---|
3944 | + el = new Element("li").grab(new Element("a", options)); |
---|
3945 | + |
---|
3946 | + return el.addClass("menu_item").store("menu_key", id); |
---|
3947 | + }, |
---|
3948 | + |
---|
3949 | + /** |
---|
3950 | + * da.ui.Menu#addItems(items) -> this |
---|
3951 | + * - items (Object): key-value pairs of items to be added to the menu. |
---|
3952 | + * |
---|
3953 | + * Adds items to the bottom of menu and renders them. |
---|
3954 | + **/ |
---|
3955 | + addItems: function (items) { |
---|
3956 | + $extend(this.options.items, items); |
---|
3957 | + return this.render(); |
---|
3958 | + }, |
---|
3959 | + |
---|
3960 | + /** |
---|
3961 | + * da.ui.Menu#addItem(id, value) -> this |
---|
3962 | + * - id (String): id of the item. |
---|
3963 | + * - value (Object | Function): options for [[Element]] class or function which will render the item. |
---|
3964 | + * |
---|
3965 | + * If `value` is an [[Object]] then it will be passed as second argument to MooTools's [[Element]] class. |
---|
3966 | + * If `value` is an [[Function]] then it has return an [[Element]], |
---|
3967 | + * first argument of the function is id of the item that needs to be rendered. |
---|
3968 | + **/ |
---|
3969 | + addItem: function (id, value) { |
---|
3970 | + this.options.items[id] = value; |
---|
3971 | + this._el.grab(this.renderItem(id)); |
---|
3972 | + return this; |
---|
3973 | + }, |
---|
3974 | + |
---|
3975 | + /** |
---|
3976 | + * da.ui.Menu#removeItem(id) -> this |
---|
3977 | + * - id (String): id of the item. |
---|
3978 | + * |
---|
3979 | + * Removes an item from the menu. |
---|
3980 | + **/ |
---|
3981 | + removeItem: function (id) { |
---|
3982 | + delete this.options.items[id]; |
---|
3983 | + return this.render(); |
---|
3984 | + }, |
---|
3985 | + |
---|
3986 | + /** |
---|
3987 | + * da.ui.Menu#addSeparator() -> this |
---|
3988 | + * |
---|
3989 | + * Adds separator to the menu. |
---|
3990 | + **/ |
---|
3991 | + addSeparator: function () { |
---|
3992 | + return this.addItem("separator_" + Math.uuid(3), da.ui.Menu.separator); |
---|
3993 | + }, |
---|
3994 | + |
---|
3995 | + /** |
---|
3996 | + * da.ui.Menu#click(event, element) -> this |
---|
3997 | + * - event (Event): DOM event or `null`. |
---|
3998 | + * - element (Element): list item which was clicked. |
---|
3999 | + * fires: click |
---|
4000 | + **/ |
---|
4001 | + click: function (event, element) { |
---|
4002 | + this.hide(); |
---|
4003 | + |
---|
4004 | + if(!element.className.contains("menu_item")) |
---|
4005 | + element = element.getParent(".menu_item"); |
---|
4006 | + if(!element) |
---|
4007 | + return this; |
---|
4008 | + |
---|
4009 | + this.fireEvent("click", [element.retrieve("menu_key"), event, element]); |
---|
4010 | + this.last_clicked = element; |
---|
4011 | + |
---|
4012 | + return this; |
---|
4013 | + }, |
---|
4014 | + |
---|
4015 | + /** |
---|
4016 | + * da.ui.Menu#show([event]) -> this |
---|
4017 | + * - event (Event): click or some other DOM event with coordinates. |
---|
4018 | + * fires show |
---|
4019 | + * |
---|
4020 | + * Shows the menu. If event is present than menus location will be adjusted according to |
---|
4021 | + * event's coordinates and position option. |
---|
4022 | + * In case the menu is already visible, it will be hidden. |
---|
4023 | + **/ |
---|
4024 | + show: function (event) { |
---|
4025 | + if(VISIBLE_MENU) { |
---|
4026 | + if(VISIBLE_MENU == this) |
---|
4027 | + return this.hide(); |
---|
4028 | + else |
---|
4029 | + VISIBLE_MENU.hide(); |
---|
4030 | + } |
---|
4031 | + |
---|
4032 | + VISIBLE_MENU = this; |
---|
4033 | + |
---|
4034 | + if(event) |
---|
4035 | + event.stop(); |
---|
4036 | + |
---|
4037 | + if(event && event.target) |
---|
4038 | + this._el.position($extend({ |
---|
4039 | + relativeTo: event.target |
---|
4040 | + }, this.options.position)); |
---|
4041 | + |
---|
4042 | + this._el.style.zIndex = 5; |
---|
4043 | + this._el.style.display = "block"; |
---|
4044 | + this._el.focus(); |
---|
4045 | + |
---|
4046 | + this.fireEvent("show"); |
---|
4047 | + |
---|
4048 | + return this; |
---|
4049 | + }, |
---|
4050 | + |
---|
4051 | + /** |
---|
4052 | + * da.ui.Menu#hide() -> this |
---|
4053 | + * fires hide |
---|
4054 | + * |
---|
4055 | + * Hides the menu. |
---|
4056 | + **/ |
---|
4057 | + hide: function () { |
---|
4058 | + if(this._el.style.display === "none") |
---|
4059 | + return this; |
---|
4060 | + |
---|
4061 | + VISIBLE_MENU = null; |
---|
4062 | + this._el.style.display = "none"; |
---|
4063 | + this.fireEvent("hide"); |
---|
4064 | + |
---|
4065 | + return this; |
---|
4066 | + }, |
---|
4067 | + |
---|
4068 | + /** |
---|
4069 | + * da.ui.Menu#destroy() -> this |
---|
4070 | + * |
---|
4071 | + * Destroys the menu. |
---|
4072 | + **/ |
---|
4073 | + destroy: function () { |
---|
4074 | + this._el.destroy(); |
---|
4075 | + delete this._el; |
---|
4076 | + return this; |
---|
4077 | + }, |
---|
4078 | + |
---|
4079 | + /** |
---|
4080 | + * da.ui.Menu#toElement() -> Element |
---|
4081 | + * |
---|
4082 | + * Returns menu element. |
---|
4083 | + **/ |
---|
4084 | + toElement: function () { |
---|
4085 | + return this._el; |
---|
4086 | + } |
---|
4087 | +}); |
---|
4088 | + |
---|
4089 | +/** |
---|
4090 | + * da.ui.Menu.separator -> Object |
---|
4091 | + * |
---|
4092 | + * Use this object as a separator. |
---|
4093 | + **/ |
---|
4094 | +da.ui.Menu.separator = { |
---|
4095 | + "class": "menu_separator", |
---|
4096 | + html: "<hr/>", |
---|
4097 | + onClick: function (event) { |
---|
4098 | + if(event) |
---|
4099 | + event.stop(); |
---|
4100 | + } |
---|
4101 | +}; |
---|
4102 | + |
---|
4103 | +// Hides the menu if click happened somewhere outside of the menu. |
---|
4104 | +window.addEvent("click", function (e) { |
---|
4105 | + var target = e.target; |
---|
4106 | + if(VISIBLE_MENU && (!target || !$(target).getParents().contains(VISIBLE_MENU._el))) |
---|
4107 | + VISIBLE_MENU.hide(); |
---|
4108 | +}); |
---|
4109 | + |
---|
4110 | +})(); |
---|
4111 | addfile ./contrib/musicplayer/src/libs/ui/NavigationColumn.js |
---|
4112 | hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 1 |
---|
4113 | +//#require "libs/ui/Column.js" |
---|
4114 | +//#require "libs/util/util.js" |
---|
4115 | + |
---|
4116 | +/** section: UserInterface |
---|
4117 | + * class da.ui.NavigationColumn < da.ui.Column |
---|
4118 | + * |
---|
4119 | + * Extends Column class to provide common implementation of a navigation column. |
---|
4120 | + **/ |
---|
4121 | +da.ui.NavigationColumn = new Class({ |
---|
4122 | + Extends: da.ui.Column, |
---|
4123 | + |
---|
4124 | + /** |
---|
4125 | + * da.ui.NavigationColumn#view -> {map: $empty, finished: $empty} |
---|
4126 | + * |
---|
4127 | + * Use this object to pass arguments to `Application.db.view()`. |
---|
4128 | + * |
---|
4129 | + * If `view.finished` is left empty, it will be replaced with function which will |
---|
4130 | + * render the list as soon as map/reduce proccess finishes. |
---|
4131 | + **/ |
---|
4132 | + view: { |
---|
4133 | + map: function (doc, emit) { |
---|
4134 | + if(!this._passesFilter(doc)) |
---|
4135 | + return false; |
---|
4136 | + |
---|
4137 | + emit(doc.id, { |
---|
4138 | + title: doc.title || doc.id |
---|
4139 | + }); |
---|
4140 | + }, |
---|
4141 | + |
---|
4142 | + finished: $empty |
---|
4143 | + }, |
---|
4144 | + |
---|
4145 | + options: { |
---|
4146 | + filter: null, |
---|
4147 | + killView: true |
---|
4148 | + }, |
---|
4149 | + |
---|
4150 | + /** |
---|
4151 | + * new da.ui.NavigationColumn([options]) |
---|
4152 | + * - options.filter (Object | Function): filtering object or function. |
---|
4153 | + * - options.db (BrowserCouch): [[BrowserCouch]] database to use for views. |
---|
4154 | + * Defaults to `Application.db`. |
---|
4155 | + * |
---|
4156 | + * If `filter` is provided than it will be applied during the map/reduce proccess. |
---|
4157 | + * If it's an [[Object]] than only documents with same properties as those |
---|
4158 | + * in `filter` will be considered, and if it's an [[Function]], |
---|
4159 | + * than it *must* return `true` if document should be passed to |
---|
4160 | + * any aditional filters, or `false` if the document should be discarded. |
---|
4161 | + * First argument of the `filter` function will be the document itself. |
---|
4162 | + * |
---|
4163 | + * If the column lacks map/reduce view but `total_count` is present, [[da.ui.NavigationColumn#render]] will be called. |
---|
4164 | + * |
---|
4165 | + * All other options are the same as for [[da.ui.Column]]. |
---|
4166 | + **/ |
---|
4167 | + initialize: function (options) { |
---|
4168 | + this.parent(options); |
---|
4169 | + this._el.addClass("navigation_column"); |
---|
4170 | + |
---|
4171 | + // Small speed-hack |
---|
4172 | + if(!this.options.filter) |
---|
4173 | + this._passesFilter = $lambda(true); |
---|
4174 | + |
---|
4175 | + this._el.addEvent("click:relay(.column_item)", this.click.bind(this)); |
---|
4176 | + |
---|
4177 | + if(this.view) { |
---|
4178 | + this.view.map = this.view.map.bind(this); |
---|
4179 | + if(!this.view.finished || this.view.finished === $empty) |
---|
4180 | + this.view.finished = this.mapReduceFinished.bind(this); |
---|
4181 | + else |
---|
4182 | + this.view.finished = this.view.finished.bind(this); |
---|
4183 | + |
---|
4184 | + if(this.view.reduce) |
---|
4185 | + this.view.reduce = this.view.reduced.bind(this); |
---|
4186 | + if(!this.view.updated && !this.view.temporary) |
---|
4187 | + this.view.updated = this.mapReduceUpdated; |
---|
4188 | + if(this.view.updated) |
---|
4189 | + this.view.updated = this.view.updated.bind(this); |
---|
4190 | + |
---|
4191 | + (options.db || da.db.DEFAULT).view(this.view); |
---|
4192 | + } else if(this.options.totalCount) { |
---|
4193 | + this.injectBottom(this.options.parentElement || document.body); |
---|
4194 | + this.render(); |
---|
4195 | + } |
---|
4196 | + }, |
---|
4197 | + |
---|
4198 | + /** |
---|
4199 | + * da.ui.NavigationColumn#mapReduceFinished(values) -> this |
---|
4200 | + * - values (Object): an object with result rows and `findRow` function. |
---|
4201 | + * |
---|
4202 | + * Function called when map/reduce proccess finishes, if not specified otherwise in view. |
---|
4203 | + * This function will provide [[da.ui.NavigationColumn#getItem]], update `total_count` option and render the column. |
---|
4204 | + **/ |
---|
4205 | + mapReduceFinished: function (values) { |
---|
4206 | + // BrowserCouch's findRow() needs rows to be sorted by id. |
---|
4207 | + this._rows = $A(values.rows); |
---|
4208 | + this._rows.sort(this.compareFunction); |
---|
4209 | + |
---|
4210 | + this.updateTotalCount(values.rows.length); |
---|
4211 | + this.injectBottom(this.options.parentElement || document.body); |
---|
4212 | + return this.render(); |
---|
4213 | + }, |
---|
4214 | + |
---|
4215 | + /** |
---|
4216 | + * da.ui.NavigationColumn#mapReduceUpdated(values) -> this |
---|
4217 | + * - values (Object): rows returned by map/reduce process. |
---|
4218 | + * |
---|
4219 | + * Note that this will have to re-render the whole column, as it's possible |
---|
4220 | + * that one of the new documents should be rendered in the middle of already |
---|
4221 | + * rendered ones (due to sorting). |
---|
4222 | + **/ |
---|
4223 | + mapReduceUpdated: function (values) { |
---|
4224 | + this._rows = $A(da.db.DEFAULT.views[this.view.id].view.rows); |
---|
4225 | + this._rows.sort(this.compareFunction); |
---|
4226 | + this.options.totalCount = this._rows.length; |
---|
4227 | + return this.rerender(); |
---|
4228 | + }, |
---|
4229 | + |
---|
4230 | + /** |
---|
4231 | + * da.ui.NavigationColumn#getItem(index) -> Object |
---|
4232 | + * - index (Number): index number of the item in the list. |
---|
4233 | + **/ |
---|
4234 | + getItem: function (index) { |
---|
4235 | + return this._rows[index]; |
---|
4236 | + }, |
---|
4237 | + |
---|
4238 | + /** |
---|
4239 | + * da.ui.NavigationColumn#renderItem(index) -> Element |
---|
4240 | + * - index (Number): position of the item that needs to be rendered. |
---|
4241 | + * |
---|
4242 | + * This function relies on `title`, `subtitle` and `icon` properties from emitted documents. |
---|
4243 | + **/ |
---|
4244 | + renderItem: function (index) { |
---|
4245 | + var item = this.getItem(index).value, |
---|
4246 | + el = new Element("a", {href: "#", title: item.title}); |
---|
4247 | + |
---|
4248 | + if(item.icon) |
---|
4249 | + el.grab(new Element("img", {src: item.icon})); |
---|
4250 | + if(item.title) |
---|
4251 | + el.grab(new Element("span", {html: item.title, "class": "title"})); |
---|
4252 | + if(item.subtitle) |
---|
4253 | + el.grab(new Element("span", {html: item.subtitle, "class": "subtitle"})); |
---|
4254 | + |
---|
4255 | + return el; |
---|
4256 | + }, |
---|
4257 | + |
---|
4258 | + /** |
---|
4259 | + * da.ui.NavigationColumn#createFilter(item) -> Object | Function |
---|
4260 | + * - item (Object): one of the rendered objects, usually clicked one. |
---|
4261 | + * |
---|
4262 | + * Returns an object with properties which will be required from |
---|
4263 | + * on columns "below" this one. |
---|
4264 | + * |
---|
4265 | + * If function is returned, than returned function will be called |
---|
4266 | + * by Map/Reduce proccess on column "below" and should return `true`/`false` |
---|
4267 | + * depending if the document meets criteria. |
---|
4268 | + * |
---|
4269 | + * #### Examples |
---|
4270 | + * |
---|
4271 | + * function createFilter (item) { |
---|
4272 | + * return {artist_id: item.id}; |
---|
4273 | + * } |
---|
4274 | + * |
---|
4275 | + **/ |
---|
4276 | + createFilter: function (item) { |
---|
4277 | + return {}; |
---|
4278 | + }, |
---|
4279 | + |
---|
4280 | + click: function (event, el) { |
---|
4281 | + var item = this.getItem(el.retrieve("column_index")); |
---|
4282 | + if(this._active_el) |
---|
4283 | + this._active_el.removeClass("active_column_item"); |
---|
4284 | + |
---|
4285 | + this._active_el = el.addClass("active_column_item"); |
---|
4286 | + this.fireEvent("click", [item, event, el]); |
---|
4287 | + |
---|
4288 | + return item; |
---|
4289 | + }, |
---|
4290 | + |
---|
4291 | + /** |
---|
4292 | + * da.ui.NavigationColumn#compareFunction(a, b) -> Number |
---|
4293 | + * - a (Object): first document. |
---|
4294 | + * - b (Object): second document. |
---|
4295 | + * |
---|
4296 | + * Function used for sorting items returned by map/reduce proccess. Compares documents by their `title` property. |
---|
4297 | + * |
---|
4298 | + * [See meanings of return values](https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/sort#Description). |
---|
4299 | + **/ |
---|
4300 | + compareFunction: function (a, b) { |
---|
4301 | + a = a.value.title; |
---|
4302 | + b = b.value.title; |
---|
4303 | + |
---|
4304 | + if(a < b) return -1; |
---|
4305 | + if(a > b) return 1; |
---|
4306 | + return 0; |
---|
4307 | + }, |
---|
4308 | + |
---|
4309 | + destroy: function () { |
---|
4310 | + this.parent(); |
---|
4311 | + if(this.view) |
---|
4312 | + if(this.options.killView) |
---|
4313 | + (this.options.db || da.db.DEFAULT).killView(this.view.id); |
---|
4314 | + else |
---|
4315 | + (this.options.db || da.db.DEFAULT).removeEvent("update." + this.view.id, this.view.updated); |
---|
4316 | + }, |
---|
4317 | + |
---|
4318 | + _passesFilter: function (doc) { |
---|
4319 | + var filter = this.options.filter; |
---|
4320 | + if(!filter) |
---|
4321 | + return false; |
---|
4322 | + |
---|
4323 | + return (typeof(filter) === "object") ? Hash.containsAll(doc, filter) : filter(doc); |
---|
4324 | + } |
---|
4325 | +}); |
---|
4326 | addfile ./contrib/musicplayer/src/libs/ui/ui.js |
---|
4327 | hunk ./contrib/musicplayer/src/libs/ui/ui.js 1 |
---|
4328 | +/** |
---|
4329 | + * == UserInterface == |
---|
4330 | + * |
---|
4331 | + * Common UI classes like [[Column]] and [[Menu]]. |
---|
4332 | + **/ |
---|
4333 | + |
---|
4334 | +/** section: UserInterface |
---|
4335 | + * da.ui |
---|
4336 | + **/ |
---|
4337 | +da.ui = {}; |
---|
4338 | adddir ./contrib/musicplayer/src/libs/util |
---|
4339 | addfile ./contrib/musicplayer/src/libs/util/BinaryFile.js |
---|
4340 | hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 1 |
---|
4341 | +/* |
---|
4342 | + * Binary Ajax 0.2 |
---|
4343 | + * |
---|
4344 | + * Copyright (c) 2008 Jacob Seidelin, cupboy@gmail.com, http://blog.nihilogic.dk/ |
---|
4345 | + * Copyright (c) 2010 Josip Lisec <josiplisec@gmail.com> |
---|
4346 | + * MIT License [http://www.opensource.org/licenses/mit-license.php] |
---|
4347 | + * |
---|
4348 | + * Adoption for MooTools, da.util.BinaryFile#unpack(), da.util.BinaryFile#getBitsAt() and Request.Binary |
---|
4349 | + * were added by Josip Lisec. |
---|
4350 | + */ |
---|
4351 | + |
---|
4352 | +(function () { |
---|
4353 | +/** section: Utilities |
---|
4354 | + * class da.util.BinaryFile |
---|
4355 | + * |
---|
4356 | + * Class containing methods for working with files as binary data. |
---|
4357 | + **/ |
---|
4358 | +var BinaryFile = new Class({ |
---|
4359 | + /** |
---|
4360 | + * new da.util.BinaryFile(data[, options]) |
---|
4361 | + * - data (String): the binary data. |
---|
4362 | + * - options.offset (Number): initial offset. |
---|
4363 | + * - options.length (Number): length of the data. |
---|
4364 | + * - options.bigEndian (Boolean): defaults to `false`. |
---|
4365 | + **/ |
---|
4366 | + initialize: function (data, options) { |
---|
4367 | + options = options || {}; |
---|
4368 | + this.data = data; |
---|
4369 | + this.offset = options.offset || 0; |
---|
4370 | + this.length = options.length || 0; |
---|
4371 | + this.bigEndian = options.bigEndian || false; |
---|
4372 | + |
---|
4373 | + if(typeof data === "string") { |
---|
4374 | + this.length = this.length || data.length; |
---|
4375 | + } else { |
---|
4376 | + // In this case we're probably dealing with IE, |
---|
4377 | + // and in order for this to work, VisualBasic-script magic is needed, |
---|
4378 | + // for which we don't have enough of mana. |
---|
4379 | + throw Exception("This browser is not supported"); |
---|
4380 | + } |
---|
4381 | + }, |
---|
4382 | + |
---|
4383 | + /** |
---|
4384 | + * da.util.BinaryFile#getByteAt(offset) -> Number |
---|
4385 | + **/ |
---|
4386 | + getByteAt: function (offset) { |
---|
4387 | + return this.data.charCodeAt(offset + this.offset) & 0xFF; |
---|
4388 | + }, |
---|
4389 | + |
---|
4390 | + /** |
---|
4391 | + * da.util.BinaryFile#getSByteAt(offset) -> Number |
---|
4392 | + **/ |
---|
4393 | + getSByteAt: function(iOffset) { |
---|
4394 | + var iByte = this.getByteAt(iOffset); |
---|
4395 | + return iByte > 127 ? iByte - 256 : iByte; |
---|
4396 | + }, |
---|
4397 | + |
---|
4398 | + /** |
---|
4399 | + * da.util.BinaryFile#getShortAt(offset) -> Number |
---|
4400 | + **/ |
---|
4401 | + getShortAt: function(iOffset) { |
---|
4402 | + var iShort = this.bigEndian ? |
---|
4403 | + (this.getByteAt(iOffset) << 8) + this.getByteAt(iOffset + 1) |
---|
4404 | + : (this.getByteAt(iOffset + 1) << 8) + this.getByteAt(iOffset) |
---|
4405 | + |
---|
4406 | + return iShort < 0 ? iShort + 65536 : iShort; |
---|
4407 | + }, |
---|
4408 | + |
---|
4409 | + /** |
---|
4410 | + * da.util.BinaryFile#getSShortAt(offset) -> Number |
---|
4411 | + **/ |
---|
4412 | + getSShortAt: function(iOffset) { |
---|
4413 | + var iUShort = this.getShortAt(iOffset); |
---|
4414 | + return iUShort > 32767 ? iUShort - 65536 : iUShort; |
---|
4415 | + }, |
---|
4416 | + |
---|
4417 | + /** |
---|
4418 | + * da.util.BinaryFile#getLongAt(offset) -> Number |
---|
4419 | + **/ |
---|
4420 | + getLongAt: function(iOffset) { |
---|
4421 | + var iByte1 = this.getByteAt(iOffset), |
---|
4422 | + iByte2 = this.getByteAt(iOffset + 1), |
---|
4423 | + iByte3 = this.getByteAt(iOffset + 2), |
---|
4424 | + iByte4 = this.getByteAt(iOffset + 3); |
---|
4425 | + |
---|
4426 | + var iLong = this.bigEndian ? |
---|
4427 | + (((((iByte1 << 8) + iByte2) << 8) + iByte3) << 8) + iByte4 |
---|
4428 | + : (((((iByte4 << 8) + iByte3) << 8) + iByte2) << 8) + iByte1; |
---|
4429 | + if (iLong < 0) iLong += 4294967296; |
---|
4430 | + return iLong; |
---|
4431 | + }, |
---|
4432 | + |
---|
4433 | + /** |
---|
4434 | + * da.util.BinaryFile#getSLongAt(offset) -> Number |
---|
4435 | + **/ |
---|
4436 | + getSLongAt: function(iOffset) { |
---|
4437 | + var iULong = this.getLongAt(iOffset); |
---|
4438 | + return iULong > 2147483647 ? iULong - 4294967296 : iULong; |
---|
4439 | + }, |
---|
4440 | + |
---|
4441 | + /** |
---|
4442 | + * da.util.BinaryFile#getStringAt(offset, length) -> String |
---|
4443 | + **/ |
---|
4444 | + getStringAt: function(offset, length) { |
---|
4445 | + var str = new Array(length); |
---|
4446 | + length += offset; |
---|
4447 | + |
---|
4448 | + for(var i = 0; offset < length; offset++, i++) |
---|
4449 | + str[i] = String.fromCharCode(this.getByteAt(offset)); |
---|
4450 | + |
---|
4451 | + return str.join(""); |
---|
4452 | + }, |
---|
4453 | + |
---|
4454 | + /** |
---|
4455 | + * da.util.BinaryFile#getCharAt(offset) -> String |
---|
4456 | + * - offset (Number): position of the character. |
---|
4457 | + **/ |
---|
4458 | + getCharAt: function(iOffset) { |
---|
4459 | + return String.fromCharCode(this.getByteAt(iOffset)); |
---|
4460 | + }, |
---|
4461 | + |
---|
4462 | + /** |
---|
4463 | + * da.util.BinaryFile#getBitsAt(offset[, length]) -> Array |
---|
4464 | + * - offset (Number): position of character. |
---|
4465 | + * - length (Number): number of bits, if result has less, zeors will be appended at the begging. |
---|
4466 | + * |
---|
4467 | + * Returns an array with bit values. |
---|
4468 | + * |
---|
4469 | + * #### Example |
---|
4470 | + * (new da.util.BinaryFile("2")).getBitsAt(0, 8) |
---|
4471 | + * // -> [0, 0, 1, 1, 0, 0, 1, 0] |
---|
4472 | + * |
---|
4473 | + **/ |
---|
4474 | + getBitsAt: function (offset, padding) { |
---|
4475 | + var bits = this.getByteAt(offset).toString(2); |
---|
4476 | + padding = padding || 8; |
---|
4477 | + if(padding && bits.length < padding) { |
---|
4478 | + var delta = padding - bits.length; |
---|
4479 | + padding = []; |
---|
4480 | + while(delta--) padding.push(0); |
---|
4481 | + bits = padding.concat(bits).join(""); |
---|
4482 | + } |
---|
4483 | + |
---|
4484 | + var n = bits.length, |
---|
4485 | + result = new Array(n); |
---|
4486 | + |
---|
4487 | + while(n--) |
---|
4488 | + result[n] = +bits[n]; |
---|
4489 | + |
---|
4490 | + return result; |
---|
4491 | + }, |
---|
4492 | + |
---|
4493 | + /** |
---|
4494 | + * da.util.BinaryFile#getBitsFromStringAt(offset, length) -> Array |
---|
4495 | + * - offset (Number): position of the first character. |
---|
4496 | + * - length (Number): length of the string. |
---|
4497 | + * |
---|
4498 | + * Returns an array with return values of [[da.util.BinaryFile#getBitsAt]]. |
---|
4499 | + **/ |
---|
4500 | + getBitsFromStringAt: function (offset, length) { |
---|
4501 | + var bits = new Array(length); |
---|
4502 | + length += offset; |
---|
4503 | + |
---|
4504 | + for(var i = 0; offset < length; offset++, i++) |
---|
4505 | + bits[i] = this.getBitsAt(offset); |
---|
4506 | + |
---|
4507 | + return bits; |
---|
4508 | + }, |
---|
4509 | + |
---|
4510 | + /** |
---|
4511 | + * da.util.BinaryFile#toEncodedString() -> String |
---|
4512 | + * Returns URI encoded value of data. |
---|
4513 | + * |
---|
4514 | + * We're not using from/toBase64 because `btoa()`/`atob()` functions can't convert everything to/from Base64 encoding, |
---|
4515 | + * `encodeUriComponent()` method seems to be more reliable. |
---|
4516 | + **/ |
---|
4517 | + toEncodedString: function() { |
---|
4518 | + return encodeURIComponent(this.data); |
---|
4519 | + }, |
---|
4520 | + |
---|
4521 | + /** |
---|
4522 | + * da.util.BinaryFile#unpack(format) -> Array |
---|
4523 | + * - format (String): String according to which data will be unpacked. |
---|
4524 | + * |
---|
4525 | + * This method is using format similar to the one used in Python, and does exactly the same job, |
---|
4526 | + * mapping C types to JavaScript ones. |
---|
4527 | + * |
---|
4528 | + * |
---|
4529 | + * #### Code mapping |
---|
4530 | + * <table cellspacing="3px"> |
---|
4531 | + * <thead style="border-bottom:3px double #ddd"> |
---|
4532 | + * <tr><td>Code</td><td>C type</td><td>Returns</td><td>Function</td></tr> |
---|
4533 | + * </thead> |
---|
4534 | + * <tbody> |
---|
4535 | + * <tr> |
---|
4536 | + * <td>b</td> |
---|
4537 | + * <td><code>_Bool</code></td> |
---|
4538 | + * <td>[[Boolean]]</td> |
---|
4539 | + * <td></td> |
---|
4540 | + * </tr> |
---|
4541 | + * <tr> |
---|
4542 | + * <td>c</td> |
---|
4543 | + * <td><code>char</code></td> |
---|
4544 | + * <td>[[String]]</td> |
---|
4545 | + * <td>String with one character</td> |
---|
4546 | + * </tr> |
---|
4547 | + * <tr> |
---|
4548 | + * <td>h</td> |
---|
4549 | + * <td><code>short</code></td> |
---|
4550 | + * <td>[[Number]]</td> |
---|
4551 | + * <td></td> |
---|
4552 | + * </tr> |
---|
4553 | + * <tr> |
---|
4554 | + * <td>i</td> |
---|
4555 | + * <td><code>int</code></td> |
---|
4556 | + * <td>[[Number]]</td> |
---|
4557 | + * <td></td> |
---|
4558 | + * </tr> |
---|
4559 | + * <tr> |
---|
4560 | + * <td>l</td> |
---|
4561 | + * <td><code>long</code></td> |
---|
4562 | + * <td>[[Number]]</td> |
---|
4563 | + * <td></td> |
---|
4564 | + * </tr> |
---|
4565 | + * <tr> |
---|
4566 | + * <td>s</td> |
---|
4567 | + * <td><code>char[]</code></td> |
---|
4568 | + * <td>[[String]]</td> |
---|
4569 | + * <td></td> |
---|
4570 | + * </tr> |
---|
4571 | + * <tr> |
---|
4572 | + * <td>S</td> |
---|
4573 | + * <td><code>char[]</code></td> |
---|
4574 | + * <td>[[String]]</td> |
---|
4575 | + * <td>String with removed whitespace (including <code>\0</code> chars)</td> |
---|
4576 | + * </tr> |
---|
4577 | + * <tr> |
---|
4578 | + * <td>t</td> |
---|
4579 | + * <td><code>int</code></td> |
---|
4580 | + * <td>[[Array]]</td> |
---|
4581 | + * <td>Returns an array with bit values</td> |
---|
4582 | + * </tr> |
---|
4583 | + * <tr> |
---|
4584 | + * <td>T</td> |
---|
4585 | + * <td><code>char</code></td> |
---|
4586 | + * <td>[[Array]]</td> |
---|
4587 | + * <td>Returns an array of arrays with bit values.</td> |
---|
4588 | + * </tr> |
---|
4589 | + * <tr> |
---|
4590 | + * <td>x</td> |
---|
4591 | + * <td><code>/</code></td> |
---|
4592 | + * <td>[[String]]</td> |
---|
4593 | + * <td>Padding byte</td> |
---|
4594 | + * </tr> |
---|
4595 | + * </tbody> |
---|
4596 | + * </table> |
---|
4597 | + * |
---|
4598 | + * |
---|
4599 | + * #### External resources |
---|
4600 | + * * [Python implementation of `unpack`](http://docs.python.org/library/struct.html#format-strings) |
---|
4601 | + **/ |
---|
4602 | + _unpack_format: /(\d+\w|\w)/g, |
---|
4603 | + _whitespace: /\s./g, |
---|
4604 | + unpack: function (format) { |
---|
4605 | + format = format.replace(this._whitespace, ""); |
---|
4606 | + var pairs = format.match(this._unpack_format), |
---|
4607 | + n = pairs.length, |
---|
4608 | + result = []; |
---|
4609 | + |
---|
4610 | + if(!pairs.length) |
---|
4611 | + return pairs; |
---|
4612 | + |
---|
4613 | + var offset = 0; |
---|
4614 | + for(var n = 0, m = pairs.length; n < m; n++) { |
---|
4615 | + var pair = pairs[n], |
---|
4616 | + code = pair.slice(-1), |
---|
4617 | + repeat = +pair.slice(0, pair.length - 1) || 1; |
---|
4618 | + |
---|
4619 | + switch(code) { |
---|
4620 | + case 'b': |
---|
4621 | + while(repeat--) |
---|
4622 | + result.push(this.getByteAt(offset++) === 1); |
---|
4623 | + break; |
---|
4624 | + case 'c': |
---|
4625 | + while(repeat--) |
---|
4626 | + result.push(this.getCharAt(offset++)); |
---|
4627 | + break; |
---|
4628 | + case 'h': |
---|
4629 | + while(repeat--) { |
---|
4630 | + result.push(this.getShortAt(offset)); |
---|
4631 | + offset += 2; |
---|
4632 | + } |
---|
4633 | + break; |
---|
4634 | + case 'i': |
---|
4635 | + while(repeat--) |
---|
4636 | + result.push(this.getByteAt(offset++)); |
---|
4637 | + break; |
---|
4638 | + case 'l': |
---|
4639 | + while(repeat--) { |
---|
4640 | + result.push(this.getLongAt(offset)); |
---|
4641 | + offset += 4; |
---|
4642 | + } |
---|
4643 | + break; |
---|
4644 | + case 's': |
---|
4645 | + result.push(this.getStringAt(offset, repeat)); |
---|
4646 | + offset += repeat; |
---|
4647 | + break; |
---|
4648 | + case 'S': |
---|
4649 | + result.push(this.getStringAt(offset, repeat).strip()); |
---|
4650 | + offset += repeat; |
---|
4651 | + break; |
---|
4652 | + case 't': |
---|
4653 | + while(repeat--) |
---|
4654 | + result.push(this.getBitsAt(offset++, 2)); |
---|
4655 | + break; |
---|
4656 | + case 'T': |
---|
4657 | + result.push(this.getBitsFromStringAt(offset, repeat)); |
---|
4658 | + offset += repeat; |
---|
4659 | + break; |
---|
4660 | + case 'x': |
---|
4661 | + offset += repeat; |
---|
4662 | + break; |
---|
4663 | + default: |
---|
4664 | + throw new Exception("Unknow code is being used (" + code + ")."); |
---|
4665 | + } |
---|
4666 | + } |
---|
4667 | + |
---|
4668 | + return result; |
---|
4669 | + } |
---|
4670 | +}); |
---|
4671 | + |
---|
4672 | +BinaryFile.extend({ |
---|
4673 | + /** |
---|
4674 | + * da.util.BinaryFile.fromEncodedString(data) -> da.util.BinaryFile |
---|
4675 | + * - data (String): URI encoded string. |
---|
4676 | + **/ |
---|
4677 | + fromEncodedString: function(encoded_str) { |
---|
4678 | + return new BinaryFile(decodeURIComponent(encoded_str)); |
---|
4679 | + } |
---|
4680 | +}); |
---|
4681 | + |
---|
4682 | +da.util.BinaryFile = BinaryFile; |
---|
4683 | + |
---|
4684 | +/** section: Utilities |
---|
4685 | + * class Request |
---|
4686 | + * |
---|
4687 | + * MooTools Request class |
---|
4688 | + **/ |
---|
4689 | + |
---|
4690 | +/** section: Utilities |
---|
4691 | + * class Request.Binary < Request |
---|
4692 | + * |
---|
4693 | + * Class for receiving binary data over XMLHTTPRequest. |
---|
4694 | + * If server supports setting Range header, then only minimal data will be downloaded. |
---|
4695 | + * |
---|
4696 | + * This works in two phases, if a range option is set then a HEAD request is performed to get the |
---|
4697 | + * total length of the file and to see if server supports `Range` HTTP header. |
---|
4698 | + * If server supports `Range` header than only requested range is asked from server in another HTTP GET request, |
---|
4699 | + * otherwise the whole file is downloaded and sliced to desired range. |
---|
4700 | + * |
---|
4701 | + **/ |
---|
4702 | +Request.Binary = new Class({ |
---|
4703 | + Extends: Request, |
---|
4704 | + |
---|
4705 | + /** |
---|
4706 | + * Request.Binary#acceptsRange -> String |
---|
4707 | + * Indicates if server supports HTTP requests with `Range` header. |
---|
4708 | + **/ |
---|
4709 | + acceptsRange: false, |
---|
4710 | + options: { |
---|
4711 | + range: null |
---|
4712 | + }, |
---|
4713 | + |
---|
4714 | + /** |
---|
4715 | + * new Request.Binary(options) |
---|
4716 | + * - options (Object): all of the [Request](http://mootools.net/docs/core/Request/Request) options can be used. |
---|
4717 | + * - options.range (Object): array with starting position and length. `[0, 100]`. |
---|
4718 | + * If first element is negative, starting position will be calculated from end of the file. |
---|
4719 | + * - options.bigEndian (Boolean) |
---|
4720 | + * fires request, complete, success, failure, cancel |
---|
4721 | + * |
---|
4722 | + * Functions attached to `success` event will receive response in form of [[da.util.BinaryFile]] as their first argument. |
---|
4723 | + **/ |
---|
4724 | + initialize: function (options) { |
---|
4725 | + this.parent($extend(options, { |
---|
4726 | + method: "GET" |
---|
4727 | + })); |
---|
4728 | + |
---|
4729 | + this.headRequest = new Request({ |
---|
4730 | + url: options.url, |
---|
4731 | + method: "HEAD", |
---|
4732 | + emulation: false, |
---|
4733 | + evalResponse: false, |
---|
4734 | + onSuccess: this.onHeadSuccess.bind(this) |
---|
4735 | + }); |
---|
4736 | + }, |
---|
4737 | + |
---|
4738 | + onHeadSuccess: function () { |
---|
4739 | + this.acceptsRange = this.headRequest.getHeader("Accept-Ranges") === "bytes"; |
---|
4740 | + |
---|
4741 | + var range = this.options.range; |
---|
4742 | + if(range[0] < 0) |
---|
4743 | + range[0] += +this.headRequest.getHeader("Content-Length"); |
---|
4744 | + range[1] = range[0] + range[1] - 1; |
---|
4745 | + this.options.range = range; |
---|
4746 | + |
---|
4747 | + if(this.headRequest.isSuccess()) |
---|
4748 | + this.send(this._send_options || {}); |
---|
4749 | + }, |
---|
4750 | + |
---|
4751 | + success: function (text) { |
---|
4752 | + var range = this.options.range; |
---|
4753 | + this.response.binary = new BinaryFile(text, { |
---|
4754 | + offset: range && !this.acceptsRange ? range[0] : 0, |
---|
4755 | + length: range ? range[1] - range[0] + 1 : 0, |
---|
4756 | + bigEndian: this.options.bigEndian |
---|
4757 | + }); |
---|
4758 | + this.onSuccess(this.response.binary); |
---|
4759 | + }, |
---|
4760 | + |
---|
4761 | + send: function (options) { |
---|
4762 | + if(this.headRequest.running || this.running) |
---|
4763 | + return this; |
---|
4764 | + |
---|
4765 | + if(!this.headRequest.isSuccess()) { |
---|
4766 | + this._send_options = options; |
---|
4767 | + this.headRequest.send(); |
---|
4768 | + return this; |
---|
4769 | + } |
---|
4770 | + |
---|
4771 | + if(typeof this.xhr.overrideMimeType === "function") |
---|
4772 | + this.xhr.overrideMimeType("text/plain; charset=x-user-defined"); |
---|
4773 | + |
---|
4774 | + this.setHeader("If-Modified-Since", "Sat, 1 Jan 1970 00:00:00 GMT"); |
---|
4775 | + var range = this.options.range; |
---|
4776 | + if(range && this.acceptsRange) |
---|
4777 | + this.setHeader("Range", "bytes=" + range[0] + "-" + range[1]); |
---|
4778 | + |
---|
4779 | + return this.parent(options); |
---|
4780 | + } |
---|
4781 | +}); |
---|
4782 | + |
---|
4783 | +})(); |
---|
4784 | addfile ./contrib/musicplayer/src/libs/util/Goal.js |
---|
4785 | hunk ./contrib/musicplayer/src/libs/util/Goal.js 1 |
---|
4786 | +/** section: Utilities |
---|
4787 | + * class da.util.Goal |
---|
4788 | + * implements Events, Options |
---|
4789 | + * |
---|
4790 | + * A helper class which makes it easier to manage async nature of JS. |
---|
4791 | + * An Goal consists of several checkpoints, which, in order to complete the goal have to be reached. |
---|
4792 | + * |
---|
4793 | + * #### Examples |
---|
4794 | + * |
---|
4795 | + * var travel_the_world = new da.util.Goal({ |
---|
4796 | + * checkpoints: ["Nicosia", "Vienna", "Berlin", "Paris", "London", "Reykjavik"], |
---|
4797 | + * |
---|
4798 | + * onCheckpoint: function (city) { |
---|
4799 | + * console.log("Hello from " + name + "!"); |
---|
4800 | + * }, |
---|
4801 | + * |
---|
4802 | + * onFinish: function () { |
---|
4803 | + * console.log("Yay!"); |
---|
4804 | + * }, |
---|
4805 | + * |
---|
4806 | + * afterCheckpoint: { |
---|
4807 | + * Paris: function () { |
---|
4808 | + * consle.log("Aww..."); |
---|
4809 | + * } |
---|
4810 | + * } |
---|
4811 | + * }); |
---|
4812 | + * |
---|
4813 | + * travel_the_world.checkpoint("Nicosia"); |
---|
4814 | + * // -> "Hello from Nicosia!" |
---|
4815 | + * travel_the_world.checkpoint("Berlin"); |
---|
4816 | + * // -> "Hello from Berlin!" |
---|
4817 | + * travel_the_world.checkpoint("Paris"); |
---|
4818 | + * // -> "Hello from Paris!" |
---|
4819 | + * // -> "Aww..." |
---|
4820 | + * travel_the_world.checkpoint("London"); |
---|
4821 | + * // -> "Hello from London!" |
---|
4822 | + * travel_the_world.checkpoint("Reykyavik"); |
---|
4823 | + * // -> "Hello from Paris!" |
---|
4824 | + * travel_the_world.checkpoint("Vienna"); |
---|
4825 | + * // -> "Hello from Vienna!" |
---|
4826 | + * // -> "Yay!" |
---|
4827 | + * |
---|
4828 | + **/ |
---|
4829 | +da.util.Goal = new Class({ |
---|
4830 | + Implements: [Events, Options], |
---|
4831 | + |
---|
4832 | + options: { |
---|
4833 | + checkpoints: [], |
---|
4834 | + afterCheckpoint: {} |
---|
4835 | + }, |
---|
4836 | + /** |
---|
4837 | + * da.util.Goal#finished -> Boolean |
---|
4838 | + * |
---|
4839 | + * Indicates if all checkpoints have been reached. |
---|
4840 | + **/ |
---|
4841 | + finished: false, |
---|
4842 | + |
---|
4843 | + /** |
---|
4844 | + * new da.util.Goal([options]) |
---|
4845 | + * - options.checkpoints (Array): list of checkpoints needed for goal to finish. |
---|
4846 | + * - options.onFinish (Function): called once all checkpoints are reached. |
---|
4847 | + * - options.onCheckpoint (Function): called after each checkpoint. |
---|
4848 | + * - options.afterCheckpoint (Object): object keys represent checkpoints whose functions will be called after respective checkpoint. |
---|
4849 | + **/ |
---|
4850 | + initialize: function (options) { |
---|
4851 | + this.setOptions(options); |
---|
4852 | + this.completedCheckpoints = []; |
---|
4853 | + }, |
---|
4854 | + |
---|
4855 | + /** |
---|
4856 | + * da.util.Goal#checkpoint(name) -> undefined | false |
---|
4857 | + * - name (String): name of the checkpoint. |
---|
4858 | + * fires checkpoint, finish |
---|
4859 | + * |
---|
4860 | + * Registers that checkpoint has been reached; |
---|
4861 | + **/ |
---|
4862 | + checkpoint: function (name) { |
---|
4863 | + if(!this.options.checkpoints.contains(name)) |
---|
4864 | + return false; |
---|
4865 | + if(this.completedCheckpoints.contains(name)) |
---|
4866 | + return false; |
---|
4867 | + |
---|
4868 | + this.completedCheckpoints.push(name); |
---|
4869 | + this.fireEvent("checkpoint", [name, this.completedCheckpoints]); |
---|
4870 | + |
---|
4871 | + if(this.options.afterCheckpoint[name]) |
---|
4872 | + this.options.afterCheckpoint[name](this.completedCheckpoints); |
---|
4873 | + |
---|
4874 | + if(this.completedCheckpoints.containsAll(this.options.checkpoints)) |
---|
4875 | + this.finish(); |
---|
4876 | + }, |
---|
4877 | + |
---|
4878 | + finish: function () { |
---|
4879 | + this.finished = true; |
---|
4880 | + this.fireEvent("finish"); |
---|
4881 | + } |
---|
4882 | +}); |
---|
4883 | addfile ./contrib/musicplayer/src/libs/util/ID3.js |
---|
4884 | hunk ./contrib/musicplayer/src/libs/util/ID3.js 1 |
---|
4885 | +/** |
---|
4886 | + * == ID3 == |
---|
4887 | + * |
---|
4888 | + * ID3 parsers and common interface. |
---|
4889 | + **/ |
---|
4890 | + |
---|
4891 | +/** section: ID3 |
---|
4892 | + * class da.util.ID3 |
---|
4893 | + * |
---|
4894 | + * Class for extracting ID3 metadata from music files. Provides an interface to ID3v1 and ID3v2 parsers. |
---|
4895 | + * The reason why ID3 v1 and v2 parsers are implemented separately is due to idea that parsers for other |
---|
4896 | + * formats (OGG Comments, especially) can be later implemented with ease. |
---|
4897 | +**/ |
---|
4898 | +da.util.ID3 = new Class({ |
---|
4899 | + Implements: Options, |
---|
4900 | + |
---|
4901 | + options: { |
---|
4902 | + url: null, |
---|
4903 | + onSuccess: $empty, |
---|
4904 | + onFailure: $empty |
---|
4905 | + }, |
---|
4906 | + |
---|
4907 | + /** |
---|
4908 | + * da.util.ID3#parsers -> Array |
---|
4909 | + * List of parsers with which the file will be tested. Defaults to ID3v2 and ID3v1 parsers. |
---|
4910 | + **/ |
---|
4911 | + parsers: [], |
---|
4912 | + |
---|
4913 | + /** |
---|
4914 | + * da.util.ID3#parser -> Object |
---|
4915 | + * |
---|
4916 | + * Instance of the parser in use. |
---|
4917 | + **/ |
---|
4918 | + parser: null, |
---|
4919 | + |
---|
4920 | + /** |
---|
4921 | + * new da.util.ID3(options) |
---|
4922 | + * - options.url (String): URL of the MP3 file. |
---|
4923 | + * - options.onSuccess (Function): called with found tags once they are parsed. |
---|
4924 | + * - options.onFailure (Function): called if none of available parsers know how to extract tags. |
---|
4925 | + * |
---|
4926 | + **/ |
---|
4927 | + initialize: function (options) { |
---|
4928 | + this.setOptions(options); |
---|
4929 | + this.parsers = $A(da.util.ID3.parsers); |
---|
4930 | + this._getFile(this.parsers[0]); |
---|
4931 | + }, |
---|
4932 | + |
---|
4933 | + _getFile: function (parser) { |
---|
4934 | + if(!parser) |
---|
4935 | + return this.options.onFailure(); |
---|
4936 | + |
---|
4937 | + this.request = new Request.Binary({ |
---|
4938 | + url: this.options.url, |
---|
4939 | + range: parser.range, |
---|
4940 | + onSuccess: this._onFileFetched.bind(this) |
---|
4941 | + }); |
---|
4942 | + |
---|
4943 | + this.request.send(); |
---|
4944 | + }, |
---|
4945 | + |
---|
4946 | + _onFileFetched: function (data) { |
---|
4947 | + if(this.parsers[0] && this.parsers[0].test(data)) |
---|
4948 | + this.parser = (new this.parsers[0](data, this.options, this.request)); |
---|
4949 | + else |
---|
4950 | + this._getFile(this.parsers.shift()); |
---|
4951 | + } |
---|
4952 | +}); |
---|
4953 | + |
---|
4954 | +/** |
---|
4955 | + * da.util.ID3.parsers -> Array |
---|
4956 | + * Array with all known parsers. |
---|
4957 | + **/ |
---|
4958 | + |
---|
4959 | +da.util.ID3.parsers = []; |
---|
4960 | + |
---|
4961 | +//#require "libs/util/ID3v2.js" |
---|
4962 | +//#require "libs/util/ID3v1.js" |
---|
4963 | addfile ./contrib/musicplayer/src/libs/util/ID3v1.js |
---|
4964 | hunk ./contrib/musicplayer/src/libs/util/ID3v1.js 1 |
---|
4965 | +//#require "libs/util/BinaryFile.js" |
---|
4966 | + |
---|
4967 | +/** section: ID3 |
---|
4968 | + * class da.util.ID3v1Parser |
---|
4969 | + * |
---|
4970 | + * ID3 v1 parser based on [ID3 v1 specification](http://mpgedit.org/mpgedit/mpeg_format/mpeghdr.htm#MPEGTAG). |
---|
4971 | + * |
---|
4972 | + * #### Notes |
---|
4973 | + * All of these methods are private. |
---|
4974 | + **/ |
---|
4975 | + |
---|
4976 | +(function () { |
---|
4977 | +var CACHE = {}; |
---|
4978 | + |
---|
4979 | +var ID3v1Parser = new Class({ |
---|
4980 | + /** |
---|
4981 | + * new da.util.ID3v1Parser(data, options) |
---|
4982 | + * - data (da.util.BinaryFile): ID3 tag. |
---|
4983 | + * - options.url (String): URL of the file. |
---|
4984 | + * - options.onSuccess (Function): function called once tags are parsed. |
---|
4985 | + **/ |
---|
4986 | + initialize: function (data, options) { |
---|
4987 | + this.data = data; |
---|
4988 | + this.options = options; |
---|
4989 | + if(!this.options.url) |
---|
4990 | + this.options.url = Math.uuid(); |
---|
4991 | + |
---|
4992 | + if(CACHE[options.url]) |
---|
4993 | + options.onSuccess(CACHE[options.url]); |
---|
4994 | + else |
---|
4995 | + this.parse(); |
---|
4996 | + }, |
---|
4997 | + |
---|
4998 | + /** |
---|
4999 | + * da.util.ID3v1Parser#parse() -> undefined |
---|
5000 | + * Extracts the tags from file. |
---|
5001 | + **/ |
---|
5002 | + parse: function () { |
---|
5003 | + // 29x - comment |
---|
5004 | + this.tags = this.data.unpack("xxx30S30S30S4S29x2i").associate([ |
---|
5005 | + "title", "artist", "album", "year", "track", "genre" |
---|
5006 | + ]); |
---|
5007 | + this.tags.year = +this.tags.year; |
---|
5008 | + if(isNaN(this.tags.year)) |
---|
5009 | + this.tags.year = 0; |
---|
5010 | + |
---|
5011 | + this.options.onSuccess(CACHE[this.options.url] = this.tags); |
---|
5012 | + } |
---|
5013 | +}); |
---|
5014 | + |
---|
5015 | +ID3v1Parser.extend({ |
---|
5016 | + /** |
---|
5017 | + * da.util.ID3v1Parser.range -> [-128, 128] |
---|
5018 | + * Range in which ID3 tag is positioned. -128 indicates that it's last 128 bytes. |
---|
5019 | + **/ |
---|
5020 | + range: [-128, 128], |
---|
5021 | + |
---|
5022 | + /** |
---|
5023 | + * da.util.ID3v1Parser.test(data) -> Boolean |
---|
5024 | + * - data (da.util.BinaryFile): data that needs to be tested. |
---|
5025 | + * |
---|
5026 | + * Checks if first three characters equal to `TAG`, as per ID3 v1 specification. |
---|
5027 | + **/ |
---|
5028 | + test: function (data) { |
---|
5029 | + return data.getStringAt(0, 3) === "TAG"; |
---|
5030 | + } |
---|
5031 | +}); |
---|
5032 | + |
---|
5033 | +da.util.ID3v1Parser = ID3v1Parser; |
---|
5034 | +da.util.ID3.parsers.push(ID3v1Parser); |
---|
5035 | +})(); |
---|
5036 | addfile ./contrib/musicplayer/src/libs/util/ID3v2.js |
---|
5037 | hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 1 |
---|
5038 | +//#require "libs/util/BinaryFile.js" |
---|
5039 | +/** section: ID3 |
---|
5040 | + * class da.util.ID3v2Parser |
---|
5041 | + * |
---|
5042 | + * ID3 v2 parser implementation based on [Mutagen](http://code.google.com/p/mutagen) and |
---|
5043 | + * [ruby-mp3info](http://ruby-mp3info.rubyforge.org) libraries. |
---|
5044 | + * |
---|
5045 | + * #### Known frames |
---|
5046 | + * This is the list of frames that this implementation by default can parse - only those that are needed to get |
---|
5047 | + * the basic information about song. Others can be added via da.util.ID3v2Parser.addFrameParser. |
---|
5048 | + * |
---|
5049 | + * * TRCK |
---|
5050 | + * * TIT1 |
---|
5051 | + * * TIT2 |
---|
5052 | + * * TIT3 |
---|
5053 | + * * TPE1 |
---|
5054 | + * * TPE2 |
---|
5055 | + * * TALB |
---|
5056 | + * * TYER |
---|
5057 | + * * TIME |
---|
5058 | + * * TCON |
---|
5059 | + * * USLT |
---|
5060 | + * * WOAR |
---|
5061 | + * * WXXX |
---|
5062 | + * |
---|
5063 | + * As well as their equivalents in ID3 v2.2 specification. |
---|
5064 | + * |
---|
5065 | + * #### Notes |
---|
5066 | + * All methods except for `addFrameParser` are private. |
---|
5067 | + * |
---|
5068 | + * #### External resources |
---|
5069 | + * * [ID3v2.4 specification](http://www.id3.org/id3v2.4.0-structure) |
---|
5070 | + * * [ID3v2.4 native frames](http://www.id3.org/id3v2.4.0-frames) |
---|
5071 | + * * [ID3v2.3 specification](http://www.id3.org/id3v2.3.0) |
---|
5072 | + * * [ID3v2.2 specification](http://www.id3.org/id3v2-00) -- obsolete |
---|
5073 | + **/ |
---|
5074 | + |
---|
5075 | +(function () { |
---|
5076 | +/** section: ID3 |
---|
5077 | + * da.util.ID3v2Parser.frameTypes |
---|
5078 | + * |
---|
5079 | + * Contains know ID3v2 frame types. |
---|
5080 | + **/ |
---|
5081 | +var BinaryFile = da.util.BinaryFile, |
---|
5082 | + CACHE = [], |
---|
5083 | +FrameType = { |
---|
5084 | + /** |
---|
5085 | + * da.util.ID3v2Parser.frameTypes.text(offset, size) -> String |
---|
5086 | + **/ |
---|
5087 | + text: function (offset, size) { |
---|
5088 | + var d = this.data; |
---|
5089 | + if(d.getByteAt(offset) === 1) { |
---|
5090 | + // Unicode is being used, and we're trying to detect Unicode BOM. |
---|
5091 | + // (we don't actually care if it's little or big endian) |
---|
5092 | + if(d.getByteAt(offset + 1) + d.getByteAt(offset + 2) === 255 + 254) { |
---|
5093 | + offset += 2; |
---|
5094 | + size -= 2; |
---|
5095 | + } |
---|
5096 | + } |
---|
5097 | + |
---|
5098 | + return d.getStringAt(offset + 1, size - 1).strip(); |
---|
5099 | + }, |
---|
5100 | + |
---|
5101 | + /** |
---|
5102 | + * da.util.ID3v2Parser.frameTypes.textNumeric(offset, size) -> String |
---|
5103 | + **/ |
---|
5104 | + textNumeric: function(offset, size) { |
---|
5105 | + return +FrameType.text.call(this, offset, size); |
---|
5106 | + }, |
---|
5107 | + |
---|
5108 | + /** |
---|
5109 | + * da.util.ID3v2Parser.frameTypes.link(offset, size) -> String |
---|
5110 | + **/ |
---|
5111 | + link: function (offset, size) { |
---|
5112 | + return this.data.getStringAt(offset, size).strip(); |
---|
5113 | + }, |
---|
5114 | + |
---|
5115 | + /** |
---|
5116 | + * da.util.ID3v2Parser.frameTypes.userLink(offset, size) -> String |
---|
5117 | + **/ |
---|
5118 | + userLink: function (offset, size) { |
---|
5119 | + var str = this.data.getStringAt(offset, size); |
---|
5120 | + return str.slice(str.lastIndexOf("\0") + 1); |
---|
5121 | + }, |
---|
5122 | + |
---|
5123 | + /** |
---|
5124 | + * da.util.ID3v2Parser.frameTypes.unsyncedLyrics(offset, size) -> String |
---|
5125 | + **/ |
---|
5126 | + unsyncedLyrics: function (offset, size) { |
---|
5127 | + var is_utf8 = this.data.getByteAt(offset) === 1, |
---|
5128 | + lang = this.data.getStringAt(offset += 1, 3); |
---|
5129 | + |
---|
5130 | + return this.data.getStringAt(offset += 3, size - 4).strip(); |
---|
5131 | + }, |
---|
5132 | + |
---|
5133 | + ignore: $empty |
---|
5134 | +}, |
---|
5135 | +FRAMES = { |
---|
5136 | + // ID3v2.4 tags |
---|
5137 | + SEEK: $empty, |
---|
5138 | + |
---|
5139 | + // ID3v2.3 tags |
---|
5140 | + TRCK: function (offset, size) { |
---|
5141 | + var data = FrameType.text.call(this, offset, size); |
---|
5142 | + return +data.split("/")[0] |
---|
5143 | + }, |
---|
5144 | + TIT1: FrameType.text, |
---|
5145 | + TIT2: FrameType.text, |
---|
5146 | + TIT3: FrameType.text, |
---|
5147 | + TPE1: FrameType.text, |
---|
5148 | + TPE2: FrameType.text, |
---|
5149 | + TALB: FrameType.text, |
---|
5150 | + TYER: FrameType.textNumeric, |
---|
5151 | + TIME: $empty, |
---|
5152 | + TCON: function (offset, size) { |
---|
5153 | + // Genre, can be either "(123)Genre", "(123)" or "Genre". |
---|
5154 | + var data = FrameType.text.call(this, offset, size); |
---|
5155 | + return +((data.match(/^\(\d+\)/) || " ")[0].slice(1, -1)); |
---|
5156 | + }, |
---|
5157 | + USLT: FrameType.unsyncedLyrics, |
---|
5158 | + WOAR: FrameType.link, |
---|
5159 | + WXXX: FrameType.userLink |
---|
5160 | +}; |
---|
5161 | + |
---|
5162 | +// ID3v2.2 tags (the structure is the same as in later versions, but they use different names) |
---|
5163 | +$extend(FRAMES, { |
---|
5164 | + UFI: FRAMES.UFID, |
---|
5165 | + TT1: FRAMES.TIT1, |
---|
5166 | + TT2: FRAMES.TIT2, |
---|
5167 | + TT3: FRAMES.TIT3, |
---|
5168 | + TP1: FRAMES.TPE1, |
---|
5169 | + TP2: FRAMES.TPE2, |
---|
5170 | + TP3: FRAMES.TPE3, |
---|
5171 | + TP4: FRAMES.TPE4, |
---|
5172 | + TAL: FRAMES.TALB, |
---|
5173 | + TRK: FRAMES.TRCK, |
---|
5174 | + TYE: FRAMES.TYER, |
---|
5175 | + TPB: FRAMES.TPUB, |
---|
5176 | + ULT: FRAMES.USLT, |
---|
5177 | + WAR: FRAMES.WOAR, |
---|
5178 | + WXX: FRAMES.WXXX |
---|
5179 | +}); |
---|
5180 | + |
---|
5181 | +var ID3v2Parser = new Class({ |
---|
5182 | + /** |
---|
5183 | + * new da.util.ID3v2Parser(data, options, request) |
---|
5184 | + * - data (BinaryFile): tag. |
---|
5185 | + * - options.onSuccess (Function): function which will be called once tag is parsed. |
---|
5186 | + * - request (Request.Binary): original HTTP request object. |
---|
5187 | + **/ |
---|
5188 | + initialize: function (data, options, request) { |
---|
5189 | + this.options = options; |
---|
5190 | + |
---|
5191 | + this.data = data; |
---|
5192 | + this.data.bigEndian = true; |
---|
5193 | + |
---|
5194 | + this.header = {}; |
---|
5195 | + this.frames = {}; |
---|
5196 | + |
---|
5197 | + this._request = request; |
---|
5198 | + |
---|
5199 | + if(CACHE[options.url]) |
---|
5200 | + options.onSuccess(CACHE[options.url]); |
---|
5201 | + else |
---|
5202 | + this.parse(); |
---|
5203 | + }, |
---|
5204 | + |
---|
5205 | + /** |
---|
5206 | + * da.util.ID3v2Parser#parse() -> undefined |
---|
5207 | + * Parses the tag. If size of tag exceeds current data (and it usually does) |
---|
5208 | + * another HTTP GET request is issued to get the rest of the file. |
---|
5209 | + **/ |
---|
5210 | + /** |
---|
5211 | + * da.util.ID3v2Parser#header -> {majorVersion: 0, minorVersion: 0, flags: 0, size: 0} |
---|
5212 | + * Parsed ID3 header. |
---|
5213 | + **/ |
---|
5214 | + /** |
---|
5215 | + * da.util.ID3v2Parser#version -> 2.2 | 2.3 | 2.4 |
---|
5216 | + **/ |
---|
5217 | + parse: function () { |
---|
5218 | + this.header = this.data.unpack("xxx2ii4s").associate([ |
---|
5219 | + 'majorVersion', 'minorVersion', "flags", "size" |
---|
5220 | + ]); |
---|
5221 | + this.version = 2 + (this.header.majorVersion/10) + this.header.minorVersion; |
---|
5222 | + this.header.size = this.unsync(this.header.size) + 10; |
---|
5223 | + |
---|
5224 | + this.parseFlags(); |
---|
5225 | + |
---|
5226 | + if(this.data.length >= this.header.size) |
---|
5227 | + return this.parseFrames(); |
---|
5228 | + |
---|
5229 | + this._request.options.range = [0, this.header.size]; |
---|
5230 | + // Removing event listeners which were added by ID3 |
---|
5231 | + this._request.removeEvents('success'); |
---|
5232 | + this._request.addEvent('success', function (data) { |
---|
5233 | + this.data = data; |
---|
5234 | + this.parseFrames(); |
---|
5235 | + }.bind(this)); |
---|
5236 | + this._request.send(); |
---|
5237 | + }, |
---|
5238 | + |
---|
5239 | + /** |
---|
5240 | + * da.util.ID3v2Parser#parseFlags() -> undefined |
---|
5241 | + * Parses header flags. |
---|
5242 | + **/ |
---|
5243 | + /** |
---|
5244 | + * da.util.ID3v2Parser#flags -> {unsync_all: false, extended: false, experimental: false, footer: false} |
---|
5245 | + * Header flags. |
---|
5246 | + **/ |
---|
5247 | + parseFlags: function () { |
---|
5248 | + var flags = this.header.flags; |
---|
5249 | + this.flags = { |
---|
5250 | + unsync_all: flags & 0x80, |
---|
5251 | + extended: flags & 0x40, |
---|
5252 | + experimental: flags & 0x20, |
---|
5253 | + footer: flags & 0x10 |
---|
5254 | + }; |
---|
5255 | + }, |
---|
5256 | + |
---|
5257 | + /** |
---|
5258 | + * da.util.ID3v2Parser#parseFrames() -> undefined |
---|
5259 | + * Calls proper function for parsing frames depending on tag's version. |
---|
5260 | + **/ |
---|
5261 | + parseFrames: function () { |
---|
5262 | + if(this.version >= 2.3) |
---|
5263 | + this.parseFrames_23(); |
---|
5264 | + else |
---|
5265 | + this.parseFrames_22(); |
---|
5266 | + |
---|
5267 | + CACHE[this.options.url] = this.frames; |
---|
5268 | + this.options.onSuccess(this.simplify(), this.frames); |
---|
5269 | + }, |
---|
5270 | + |
---|
5271 | + /** |
---|
5272 | + * da.util.ID3v2Parser#parseFrames_23() -> undefined |
---|
5273 | + * Parses ID3 frames from ID3 v2.3 and newer. |
---|
5274 | + **/ |
---|
5275 | + parseFrames_23: function () { |
---|
5276 | + if(this.version >= 2.4 && this.flags.unsync_all) |
---|
5277 | + this.data.data = this.unsync(0, this.header.size); |
---|
5278 | + |
---|
5279 | + var offset = 10, |
---|
5280 | + ext_header_size = this.data.getStringAt(offset, 4), |
---|
5281 | + tag_size = this.header.size; |
---|
5282 | + |
---|
5283 | + // Some tagging software is apparently know for setting |
---|
5284 | + // "extended header present" flag but then ommiting it from the file, |
---|
5285 | + // which means that ext_header_size will be equal to name of a frame. |
---|
5286 | + if(this.flags.extended && !FRAMES[ext_header_size]) { |
---|
5287 | + if(this.version >= 2.4) |
---|
5288 | + ext_header_size = this.unsync(ext_header_size) - 4; |
---|
5289 | + else |
---|
5290 | + ext_header_size = this.data.getLongAt(10); |
---|
5291 | + |
---|
5292 | + offset += ext_header_size; |
---|
5293 | + } |
---|
5294 | + |
---|
5295 | + while(offset < tag_size) { |
---|
5296 | + var foffset = offset, |
---|
5297 | + frame_name = this.data.getStringAt(foffset, 4), |
---|
5298 | + frame_size = this.unsync(foffset += 4, 4), |
---|
5299 | + frame_flags = [this.data.getByteAt(foffset += 4), this.data.getByteAt(foffset += 1)]; |
---|
5300 | + foffset++; // frame_flags |
---|
5301 | + |
---|
5302 | + if(!frame_size) |
---|
5303 | + break; |
---|
5304 | + |
---|
5305 | + if(FRAMES[frame_name] && frame_size) |
---|
5306 | + this.frames[frame_name] = FRAMES[frame_name].call(this, foffset, frame_size); |
---|
5307 | + |
---|
5308 | + //console.log(frame_name, this.frames[frame_name], [foffset, frame_size]); |
---|
5309 | + offset += frame_size + 10; |
---|
5310 | + } |
---|
5311 | + }, |
---|
5312 | + |
---|
5313 | + /** |
---|
5314 | + * da.util.ID3v2Parser#parseFrames_22() -> undefined |
---|
5315 | + * Parses ID3 frames from ID3 v2.2 tags. |
---|
5316 | + **/ |
---|
5317 | + parseFrames_22: function () { |
---|
5318 | + var offset = 10, |
---|
5319 | + tag_size = this.header.size; |
---|
5320 | + |
---|
5321 | + while(offset < tag_size) { |
---|
5322 | + var foffset = offset, |
---|
5323 | + frame_name = this.data.getStringAt(foffset, 3), |
---|
5324 | + frame_size = (new BinaryFile( |
---|
5325 | + "\0" + this.data.getStringAt(foffset += 3, 3), |
---|
5326 | + {bigEndian:true} |
---|
5327 | + )).getLongAt(0); |
---|
5328 | + foffset += 3; |
---|
5329 | + |
---|
5330 | + if(!frame_size) |
---|
5331 | + break; |
---|
5332 | + |
---|
5333 | + if(FRAMES[frame_name] && frame_size) |
---|
5334 | + this.frames[frame_name] = FRAMES[frame_name].call(this, foffset, frame_size); |
---|
5335 | + |
---|
5336 | + //console.log(frame_name, this.frames[frame_name], [foffset, frame_size]); |
---|
5337 | + offset += frame_size + 6; |
---|
5338 | + } |
---|
5339 | + }, |
---|
5340 | + |
---|
5341 | + /** |
---|
5342 | + * da.util.ID3v2Parser#unsync(offset, length[, bits = 7]) -> Number |
---|
5343 | + * da.util.ID3v2Parser#unsync(string) -> Number |
---|
5344 | + * - offset (Number): offset from which so start unsyncing. |
---|
5345 | + * - length (Number): length string to unsync. |
---|
5346 | + * - bits (Number): number of bits used. |
---|
5347 | + * - string (String): String to unsync. |
---|
5348 | + * |
---|
5349 | + * Performs unsyncing process defined in ID3 specification. |
---|
5350 | + **/ |
---|
5351 | + unsync: function (offset, length, bits) { |
---|
5352 | + bits = bits || 7; |
---|
5353 | + var mask = (1 << bits) - 1, |
---|
5354 | + bytes = [], |
---|
5355 | + numeric_value = 0, |
---|
5356 | + data = this.data; |
---|
5357 | + |
---|
5358 | + if(typeof offset === "string") { |
---|
5359 | + data = new BinaryFile(offset, {bigEndian: true}); |
---|
5360 | + length = offset.length; |
---|
5361 | + offset = 0; |
---|
5362 | + } |
---|
5363 | + |
---|
5364 | + if(length) { |
---|
5365 | + for(var n = offset, m = offset + length; n < m; n++) |
---|
5366 | + bytes.push(data.getByteAt(n) & mask); |
---|
5367 | + |
---|
5368 | + bytes.reverse(); |
---|
5369 | + } else { |
---|
5370 | + var value = data.getByteAt(offset); |
---|
5371 | + while(value) { |
---|
5372 | + bytes.push(value & mask); |
---|
5373 | + value >>= 8; |
---|
5374 | + } |
---|
5375 | + } |
---|
5376 | + |
---|
5377 | + for(var n = 0, i = 0, m = bytes.length * bits; n < m; n+=bits, i++) |
---|
5378 | + numeric_value += bytes[i] << n; |
---|
5379 | + |
---|
5380 | + return numeric_value; |
---|
5381 | + }, |
---|
5382 | + |
---|
5383 | + /** |
---|
5384 | + * da.util.ID3v2Parser#simplify() -> Object |
---|
5385 | + * |
---|
5386 | + * Returns humanised version of data parsed from frames. |
---|
5387 | + * Returned object contains these values (in brackets are used frames or default values): |
---|
5388 | + * |
---|
5389 | + * * title (`TIT2`, `TT2`, `"Unknown"`) |
---|
5390 | + * * album (`TALB`, `TAL`, `"Unknown"`) |
---|
5391 | + * * artist (`TPE2`, `TPE1`, `TP2`, `TP1`, `"Unknown"`) |
---|
5392 | + * * track (`TRCK`, `TRK`, `0`) |
---|
5393 | + * * year (`TYER`, `TYE`, `0`) |
---|
5394 | + * * genre (`TCON`, `TCO`, `0`) |
---|
5395 | + * * lyrics (`USLT`, `ULT`, _empty string_) |
---|
5396 | + * * links: official (`WOAR`, `WXXX`, `WAR`, `WXXX`, _empty string_) |
---|
5397 | + **/ |
---|
5398 | + simplify: function () { |
---|
5399 | + var f = this.frames; |
---|
5400 | + return !f || !$H(f).getKeys().length ? {} : { |
---|
5401 | + title: f.TIT2 || f.TT2 || "Unknown", |
---|
5402 | + album: f.TALB || f.TAL || "Unknown", |
---|
5403 | + artist: f.TPE2 || f.TPE1 || f.TP2 || f.TP1 || "Unknown", |
---|
5404 | + track: f.TRCK || f.TRK || 0, |
---|
5405 | + year: f.TYER || f.TYE || 0, |
---|
5406 | + genre: f.TCON || f.TCO || 0, |
---|
5407 | + lyrics: f.USLT || f.ULT || "", |
---|
5408 | + links: { |
---|
5409 | + official: f.WOAR || f.WXXX || f.WAR || f.WXX || "" |
---|
5410 | + } |
---|
5411 | + }; |
---|
5412 | + } |
---|
5413 | +}); |
---|
5414 | + |
---|
5415 | +ID3v2Parser.extend({ |
---|
5416 | + /** |
---|
5417 | + * da.util.ID3v2Parser.range -> [0, 14] |
---|
5418 | + * |
---|
5419 | + * Default position of ID3v2 header, including extended header. |
---|
5420 | + **/ |
---|
5421 | + range: [0, 10 + 4], |
---|
5422 | + |
---|
5423 | + /** |
---|
5424 | + * da.util.ID3v2Parser.test(data) -> Boolean |
---|
5425 | + * - data (BinaryFile): the tag. |
---|
5426 | + * |
---|
5427 | + * Checks if data begins with `ID3` and major version is less than 5. |
---|
5428 | + **/ |
---|
5429 | + test: function (data) { |
---|
5430 | + return data.getStringAt(0, 3) === "ID3" && data.getByteAt(3) <= 4; |
---|
5431 | + }, |
---|
5432 | + |
---|
5433 | + /** |
---|
5434 | + * da.util.ID3v2Parser.addFrameParser(frameName, fn) -> da.util.ID3v2Parser |
---|
5435 | + * - frameName (String): name of the frame. |
---|
5436 | + * - fn (Function): function which will parse the data. |
---|
5437 | + * |
---|
5438 | + * Use this method to add your own ID3v2 frame parsers. You can access this as `da.util.ID3v2Parser.addFrameParser`. |
---|
5439 | + * |
---|
5440 | + * `fn` will be called with following arguments: |
---|
5441 | + * * offset - position at frame appears in data |
---|
5442 | + * * size - size of the frame, including header |
---|
5443 | + * |
---|
5444 | + * |
---|
5445 | + * `this` keyword inside `fn` will refer to instance of ID3v2. |
---|
5446 | + **/ |
---|
5447 | + addFrameParser: function (name, fn) { |
---|
5448 | + FRAMES[name] = fn; |
---|
5449 | + return this; |
---|
5450 | + } |
---|
5451 | +}); |
---|
5452 | + |
---|
5453 | +ID3v2Parser.frameTypes = FrameType; |
---|
5454 | +da.util.ID3v2Parser = ID3v2Parser; |
---|
5455 | +da.util.ID3.parsers.push(ID3v2Parser); |
---|
5456 | + |
---|
5457 | +})(); |
---|
5458 | addfile ./contrib/musicplayer/src/libs/util/util.js |
---|
5459 | hunk ./contrib/musicplayer/src/libs/util/util.js 1 |
---|
5460 | +/** |
---|
5461 | + * == Utilities == |
---|
5462 | + * Utility classes and extensions to Native objects. |
---|
5463 | + **/ |
---|
5464 | + |
---|
5465 | +/** |
---|
5466 | + * da.util |
---|
5467 | + **/ |
---|
5468 | +if(typeof da.util === "undefined") |
---|
5469 | + da.util = {}; |
---|
5470 | + |
---|
5471 | +(function () { |
---|
5472 | + |
---|
5473 | +/** section: Utilities |
---|
5474 | + * class String |
---|
5475 | + * |
---|
5476 | + * #### External resources |
---|
5477 | + * * [MooTools String docs](http://mootools.net/docs/core/Native/String) |
---|
5478 | + **/ |
---|
5479 | +var NULL_BYTE = /\0/g, |
---|
5480 | + INTERPOL_VAR = /\{(\w+)\}/g; |
---|
5481 | + |
---|
5482 | +String.implement({ |
---|
5483 | + /** |
---|
5484 | + * String.strip(@string) -> String |
---|
5485 | + * |
---|
5486 | + * Removes \0's from string. |
---|
5487 | + **/ |
---|
5488 | + strip: function () { |
---|
5489 | + return this.replace(NULL_BYTE, ""); |
---|
5490 | + }, |
---|
5491 | + |
---|
5492 | + /** |
---|
5493 | + * String.interpolate(@string, data) -> String |
---|
5494 | + * - data (Object | Array): object or an array with data. |
---|
5495 | + * |
---|
5496 | + * Interpolates string with data. |
---|
5497 | + * |
---|
5498 | + * #### Example |
---|
5499 | + * |
---|
5500 | + * "{0}/{1}%".interpolate([10, 100]) |
---|
5501 | + * // -> "10/100%" |
---|
5502 | + * |
---|
5503 | + * "Hi {name}! You've got {new_mail} new messages.".interpolate({name: "John", new_mail: 10}) |
---|
5504 | + * // -> "Hi John! You've got 10 new messages." |
---|
5505 | + * |
---|
5506 | + **/ |
---|
5507 | + interpolate: function (data) { |
---|
5508 | + if(!data) |
---|
5509 | + return this.toString(); // otherwise typeof result === "object". |
---|
5510 | + |
---|
5511 | + return this.replace(INTERPOL_VAR, function (match, property) { |
---|
5512 | + var value = data[property]; |
---|
5513 | + return typeof value === "undefined" ? "{" + property + "}" : value; |
---|
5514 | + }); |
---|
5515 | + } |
---|
5516 | +}); |
---|
5517 | + |
---|
5518 | +/** section: Utilities |
---|
5519 | + * class Array |
---|
5520 | + * |
---|
5521 | + * #### External resources |
---|
5522 | + * * [MooTools Array docs](http://mootools.net/docs/core/Native/Array) |
---|
5523 | + * * [MDC Array specification](https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Array) |
---|
5524 | + **/ |
---|
5525 | +Array.implement({ |
---|
5526 | + /** |
---|
5527 | + * Array.zip(@array...) -> Array |
---|
5528 | + * |
---|
5529 | + * Returns an array whose n-th element contains n-th element from each argument. |
---|
5530 | + * |
---|
5531 | + * #### Example |
---|
5532 | + * Array.zip([1,2,3], [1,2,3]) |
---|
5533 | + * // -> [[1, 1], [2, 2], [3, 3]] |
---|
5534 | + * |
---|
5535 | + * #### See also |
---|
5536 | + * * [Python's `zip` function](http://docs.python.org/library/functions.html?highlight=zip#zip) |
---|
5537 | + **/ |
---|
5538 | + zip: function () { |
---|
5539 | + var n = this.length, |
---|
5540 | + args = [this].concat($A(arguments)); |
---|
5541 | + args_length = args.length, |
---|
5542 | + zipped = new Array(n); |
---|
5543 | + |
---|
5544 | + while(n--) { |
---|
5545 | + zipped[n] = new Array(args_length); |
---|
5546 | + var m = args_length; |
---|
5547 | + while(m--) |
---|
5548 | + zipped[n][m] = args[m][n]; |
---|
5549 | + } |
---|
5550 | + |
---|
5551 | + return zipped; |
---|
5552 | + }, |
---|
5553 | + |
---|
5554 | + /** |
---|
5555 | + * Array.containsAll(@array, otherArray) -> Boolean |
---|
5556 | + * - otherArray (Array): array which has to contain all of the defined items. |
---|
5557 | + * |
---|
5558 | + * Checks if this array contains all of those provided in otherArray. |
---|
5559 | + **/ |
---|
5560 | + containsAll: function (other) { |
---|
5561 | + var n = other.length; |
---|
5562 | + |
---|
5563 | + while(n--) |
---|
5564 | + if(!this.contains(other[n])) |
---|
5565 | + return false; |
---|
5566 | + |
---|
5567 | + return true; |
---|
5568 | + } |
---|
5569 | +}); |
---|
5570 | + |
---|
5571 | +/** section: Utilities |
---|
5572 | + * class Hash |
---|
5573 | + * |
---|
5574 | + * #### External resources |
---|
5575 | + * * [MooTools Hash docs](http://mootools.net/docs/core/Native/Hash) |
---|
5576 | + **/ |
---|
5577 | + |
---|
5578 | +Hash.implement({ |
---|
5579 | + /** |
---|
5580 | + * Hash.containsAll(@hash, otherHash) -> Boolean |
---|
5581 | + * - otherHash (Hash | Object): hash which has to contain all of the defined properties. |
---|
5582 | + * |
---|
5583 | + * Checks if all properties from this hash are present in otherHash. |
---|
5584 | + **/ |
---|
5585 | + containsAll: function (otherHash) { |
---|
5586 | + for(var key in otherHash) |
---|
5587 | + if(otherHash.hasOwnProperty(key) && otherHash[key] !== this[key]) |
---|
5588 | + return false; |
---|
5589 | + |
---|
5590 | + return true; |
---|
5591 | + } |
---|
5592 | +}) |
---|
5593 | + |
---|
5594 | +})(); |
---|
5595 | adddir ./contrib/musicplayer/src/resources |
---|
5596 | adddir ./contrib/musicplayer/src/resources/css |
---|
5597 | addfile ./contrib/musicplayer/src/resources/css/app.css |
---|
5598 | hunk ./contrib/musicplayer/src/resources/css/app.css 1 |
---|
5599 | +/*** Global styles ***/ |
---|
5600 | +@font-face { |
---|
5601 | + font-family: Junction; |
---|
5602 | + font-style: normal; |
---|
5603 | + font-weight: normal; |
---|
5604 | + src: local('Junction'), url('resources/fonts/Junction.ttf') format('truetype'); |
---|
5605 | +} |
---|
5606 | + |
---|
5607 | +body { |
---|
5608 | + font-family: 'Liberation Sans', 'Helvetica Neue', Helvetica, sans-serif; |
---|
5609 | + overflow: hidden; |
---|
5610 | +} |
---|
5611 | + |
---|
5612 | +a { |
---|
5613 | + text-decoration: none; |
---|
5614 | + color: inherit; |
---|
5615 | +} |
---|
5616 | + |
---|
5617 | +input[type="text"], input[type="password"] { |
---|
5618 | + border: 1px solid #ddd; |
---|
5619 | + border-top: 1px solid #c0c0c0; |
---|
5620 | + background: #fff; |
---|
5621 | + padding: 2px; |
---|
5622 | +} |
---|
5623 | + |
---|
5624 | +input:focus, input:active { |
---|
5625 | + border-color: #33519d; |
---|
5626 | + -webkit-box-shadow: #33519d 0 0 5px; |
---|
5627 | + -moz-box-shadow: #33519d 0 0 5px; |
---|
5628 | + -o-box-shadow: #33519d 0 0 5px; |
---|
5629 | + box-shadow: #33519d 0 0 5px; |
---|
5630 | +} |
---|
5631 | + |
---|
5632 | +input[type="button"], input[type="submit"], button { |
---|
5633 | + background: #ddd; |
---|
5634 | + border: 1px transparent; |
---|
5635 | + border-bottom: 1px solid #c0c0c0; |
---|
5636 | + padding: 2px 7px; |
---|
5637 | + color: #000; |
---|
5638 | + text-shadow: #fff 0 1px 0; |
---|
5639 | + |
---|
5640 | + -webkit-border-radius: 4px; |
---|
5641 | + -moz-border-radius: 4px; |
---|
5642 | + -o-border-radius: 4px; |
---|
5643 | + border-radius: 4px; |
---|
5644 | +} |
---|
5645 | + |
---|
5646 | +input[type="button"]:active, input[type="submit"]:active, button:active { |
---|
5647 | + border-top: 1px solid #1e2128; |
---|
5648 | + border-bottom: 0; |
---|
5649 | + background: #33519d !important; |
---|
5650 | + color: #fff; |
---|
5651 | + text-shadow: #000 0 1px 1px; |
---|
5652 | +} |
---|
5653 | + |
---|
5654 | +.no_selection { |
---|
5655 | + -webkit-user-select: none; |
---|
5656 | + -moz-user-select: none; |
---|
5657 | + -o-user-select: none; |
---|
5658 | + user-select: none; |
---|
5659 | + cursor: default; |
---|
5660 | +} |
---|
5661 | + |
---|
5662 | +/*** Dialogs ***/ |
---|
5663 | +.dialog_wrapper { |
---|
5664 | + width: 100%; |
---|
5665 | + height: 100%; |
---|
5666 | + background: rgba(0, 0, 0, 0.2); |
---|
5667 | + overflow: hidden; |
---|
5668 | + position: fixed; |
---|
5669 | + top: 0; |
---|
5670 | + left: 0; |
---|
5671 | + z-index: 2; |
---|
5672 | +} |
---|
5673 | + |
---|
5674 | +.dialog { |
---|
5675 | + margin: 50px auto 0 auto; |
---|
5676 | + background: #fff; |
---|
5677 | + border: 1px solid #ddd; |
---|
5678 | + |
---|
5679 | + -webkit-box-shadow: rgba(0, 0, 0, 0.4) 0 10px 40px; |
---|
5680 | + -moz-box-shadow: rgba(0, 0, 0, 0.4) 0 10px 40px; |
---|
5681 | + -o-box-shadow: rgba(0, 0, 0, 0.4) 0 10px 40px; |
---|
5682 | + box-shadow: rgba(0, 0, 0, 0.4) 0 10px 40px; |
---|
5683 | +} |
---|
5684 | + |
---|
5685 | +.dialog_title { |
---|
5686 | + margin: 0; |
---|
5687 | + padding: 5px; |
---|
5688 | + text-indent: 10px; |
---|
5689 | + font-size: 1.3em; |
---|
5690 | + color: #fff; |
---|
5691 | + background: #2f343e; |
---|
5692 | + border-bottom: 1px solid #1e2128; |
---|
5693 | + text-shadow: #1e2128 0 1px 0; |
---|
5694 | +} |
---|
5695 | + |
---|
5696 | +#loader { |
---|
5697 | + font-size: 2em; |
---|
5698 | + width: 100%; |
---|
5699 | + height: 100%; |
---|
5700 | + text-align: center; |
---|
5701 | + padding: 50px 0 0 0; |
---|
5702 | +} |
---|
5703 | + |
---|
5704 | +/*** Navigation columns ***/ |
---|
5705 | +.column_container { |
---|
5706 | + float: left; |
---|
5707 | + min-width: 200px; |
---|
5708 | + margin-right: 1px; |
---|
5709 | +} |
---|
5710 | + |
---|
5711 | +.column_container .column_header { |
---|
5712 | + display: block; |
---|
5713 | + width: inherit; |
---|
5714 | + text-align: center; |
---|
5715 | + font-size: 1.2em; |
---|
5716 | + cursor: default; |
---|
5717 | + padding: 2px 0; |
---|
5718 | + background: #2f343e; |
---|
5719 | + color: #fff; |
---|
5720 | + text-shadow: #1e2128 0 1px 0; |
---|
5721 | + border-right: 1px solid #1e2128; |
---|
5722 | + border-bottom: 1px solid #1e2128; |
---|
5723 | +} |
---|
5724 | + |
---|
5725 | +.column_container .column_header span { |
---|
5726 | + display: block; |
---|
5727 | + vertical-align: middle; |
---|
5728 | + text-overflow: ellipsis; |
---|
5729 | + width: 100%; |
---|
5730 | +} |
---|
5731 | + |
---|
5732 | +.column_container .column_header:active, .column_container .column_header:focus, .column_header.active { |
---|
5733 | + background-color: #1e2128; |
---|
5734 | + padding: 3px 0 1px 0; |
---|
5735 | + outline: 0; |
---|
5736 | +} |
---|
5737 | + |
---|
5738 | +.column_header.active { |
---|
5739 | + |
---|
5740 | +} |
---|
5741 | + |
---|
5742 | +.column_container .navigation_column { |
---|
5743 | + border-right: 1px solid #ddd; |
---|
5744 | +} |
---|
5745 | + |
---|
5746 | +.column_container .navigation_column:last { |
---|
5747 | + border-right: 5px solid #ddd; |
---|
5748 | +} |
---|
5749 | + |
---|
5750 | +.navigation_column { |
---|
5751 | + width: 100%; |
---|
5752 | + background: #fff url(../images/column_background.png) 0 0 repeat; |
---|
5753 | +/* background-attachment: fixed; */ |
---|
5754 | + z-index: 1; |
---|
5755 | +} |
---|
5756 | + |
---|
5757 | +.navigation_column .column_items_box { |
---|
5758 | + width: inherit; |
---|
5759 | +} |
---|
5760 | + |
---|
5761 | +.navigation_column .column_item { |
---|
5762 | + display: block; |
---|
5763 | + height: 20px; |
---|
5764 | + padding: 5px 0; |
---|
5765 | + width: inherit; |
---|
5766 | + overflow: hidden; |
---|
5767 | + text-overflow: ellipsis; |
---|
5768 | + text-indent: 5px; |
---|
5769 | + white-space: nowrap; |
---|
5770 | +} |
---|
5771 | + |
---|
5772 | +.navigation_column a.column_item { |
---|
5773 | + display: block; |
---|
5774 | + cursor: default; |
---|
5775 | +} |
---|
5776 | + |
---|
5777 | +.navigation_column .column_item img { |
---|
5778 | + display: none; |
---|
5779 | +} |
---|
5780 | + |
---|
5781 | +.navigation_column .column_item span { |
---|
5782 | + /*display: block;*/ |
---|
5783 | + vertical-align: middle; |
---|
5784 | +} |
---|
5785 | + |
---|
5786 | +.navigation_column .column_item span.subtitle { |
---|
5787 | + opacity: 0.5; |
---|
5788 | + font-size: 0.9em; |
---|
5789 | + margin-left: 5px; |
---|
5790 | + vertical-align: bottom; |
---|
5791 | +} |
---|
5792 | + |
---|
5793 | +.navigation_column .column_item_with_icon span { |
---|
5794 | + margin-left: 20px; |
---|
5795 | +} |
---|
5796 | + |
---|
5797 | +.navigation_column .active_column_item, .menu_item:hover, .navigation_column .column_item:focus, .menu_item a:focus { |
---|
5798 | + background: #33519d !important; |
---|
5799 | + text-shadow: #000 0 1px 0; |
---|
5800 | + color: #fff !important; |
---|
5801 | + outline: 0 !important; |
---|
5802 | +} |
---|
5803 | + |
---|
5804 | +/*** Menus ***/ |
---|
5805 | +.menu { |
---|
5806 | + display: block; |
---|
5807 | + text-indent: 0; |
---|
5808 | + margin: 0 0 0 -1px; |
---|
5809 | + padding: 3px 0; |
---|
5810 | + position: fixed; |
---|
5811 | + background: #fff; |
---|
5812 | + color: #000; |
---|
5813 | + min-width: 100px; |
---|
5814 | + overflow: hidden; |
---|
5815 | + text-overflow: ellipsis; |
---|
5816 | + white-space: nowrap; |
---|
5817 | + list-style: none; |
---|
5818 | + cursor: default; |
---|
5819 | + z-index: 5; |
---|
5820 | + border: 1px solid #ddd; |
---|
5821 | + |
---|
5822 | + -webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px; |
---|
5823 | + -moz-box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px; |
---|
5824 | + -o-box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px; |
---|
5825 | + box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px; |
---|
5826 | + |
---|
5827 | + -webkit-border-radius: 3px; |
---|
5828 | + -moz-border-radius: 3px; |
---|
5829 | + -o-border-radius: 3px; |
---|
5830 | + border-radius: 3px; |
---|
5831 | +} |
---|
5832 | + |
---|
5833 | +.menu_item { |
---|
5834 | + margin: 0; |
---|
5835 | +} |
---|
5836 | + |
---|
5837 | +.menu_item a { |
---|
5838 | + display: block; |
---|
5839 | + padding: 2px 0; |
---|
5840 | + text-indent: 15px; |
---|
5841 | + color: inherit; |
---|
5842 | + text-decoration: none; |
---|
5843 | + cursor: default; |
---|
5844 | +} |
---|
5845 | + |
---|
5846 | +.menu_item .menu_separator { |
---|
5847 | + margin: 2px auto; |
---|
5848 | + background: #fff !important; |
---|
5849 | + padding: 0; |
---|
5850 | + height: 1px; |
---|
5851 | +} |
---|
5852 | + |
---|
5853 | +.menu_item hr { |
---|
5854 | + margin: auto; |
---|
5855 | + padding: 0; |
---|
5856 | + height: 1px; |
---|
5857 | + color: #ddd; |
---|
5858 | + width: 95%; |
---|
5859 | +} |
---|
5860 | + |
---|
5861 | +.menu_item.checked a:before { |
---|
5862 | + content: " ✔ "; |
---|
5863 | +} |
---|
5864 | + |
---|
5865 | +.navigation_menu { |
---|
5866 | + border-top: 0; |
---|
5867 | + -webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px; |
---|
5868 | + -moz-box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px; |
---|
5869 | + -o-box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px; |
---|
5870 | + box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px; |
---|
5871 | +} |
---|
5872 | + |
---|
5873 | +/*** Settings ***/ |
---|
5874 | +#settings { |
---|
5875 | + width: 600px; |
---|
5876 | + height: 300px; |
---|
5877 | +} |
---|
5878 | + |
---|
5879 | +#settings .navigation_column { |
---|
5880 | + border-right: 1px solid #c0c0c0; |
---|
5881 | + width: 150px; |
---|
5882 | + float: left; |
---|
5883 | +} |
---|
5884 | + |
---|
5885 | +#settings_controls { |
---|
5886 | + width: 449px; |
---|
5887 | + height: inherit; |
---|
5888 | + float: right; |
---|
5889 | + background: #f3f3f3; |
---|
5890 | +} |
---|
5891 | + |
---|
5892 | +#settings_controls .message { |
---|
5893 | + text-align: center; |
---|
5894 | + font-size: 2em; |
---|
5895 | + color: #ddd; |
---|
5896 | + margin-top: 70px; |
---|
5897 | +} |
---|
5898 | + |
---|
5899 | +#settings_controls .settings_header { |
---|
5900 | + padding: 10px; |
---|
5901 | + border-bottom: 1px solid #c0c0c0; |
---|
5902 | + text-shadow: #fff 0 1px 0; |
---|
5903 | + margin: 0; |
---|
5904 | +} |
---|
5905 | + |
---|
5906 | +#settings_controls .settings_header a { |
---|
5907 | + color: #00f; |
---|
5908 | + text-decoration: underline; |
---|
5909 | +} |
---|
5910 | + |
---|
5911 | +#settings_controls form { |
---|
5912 | + background: #fff; |
---|
5913 | + padding: 20px 0; |
---|
5914 | +} |
---|
5915 | + |
---|
5916 | +#settings_controls .setting_box { |
---|
5917 | + padding: 2px 10px; |
---|
5918 | + width: inherit; |
---|
5919 | +} |
---|
5920 | + |
---|
5921 | +#settings_controls .setting_box label { |
---|
5922 | + width: 150px; |
---|
5923 | + text-align: right; |
---|
5924 | + display: inline-block; |
---|
5925 | +} |
---|
5926 | + |
---|
5927 | +#settings_controls .setting_box label.no_indent { |
---|
5928 | + width: auto; |
---|
5929 | + text-align: left; |
---|
5930 | +} |
---|
5931 | + |
---|
5932 | +#settings_controls .settings_footer { |
---|
5933 | + border-top: 1px solid #c0c0c0; |
---|
5934 | + text-align: right; |
---|
5935 | + padding: 5px; |
---|
5936 | +} |
---|
5937 | + |
---|
5938 | + |
---|
5939 | +#save_settings { |
---|
5940 | + font-weight: bold; |
---|
5941 | + padding-top: 4px; |
---|
5942 | + padding-bottom: 4px; |
---|
5943 | +} |
---|
5944 | + |
---|
5945 | +#revert_settings { |
---|
5946 | + float: left; |
---|
5947 | + background: transparent; |
---|
5948 | + border-bottom: 1px transparent; |
---|
5949 | +} |
---|
5950 | adddir ./contrib/musicplayer/src/resources/images |
---|
5951 | addfile ./contrib/musicplayer/src/resources/images/column_background.png |
---|
5952 | binary ./contrib/musicplayer/src/resources/images/column_background.png |
---|
5953 | oldhex |
---|
5954 | * |
---|
5955 | newhex |
---|
5956 | *89504e470d0a1a0a0000000d49484452000000010000003c0802000000289347ad0000033b6943 |
---|
5957 | *43504943432050726f66696c650000780185944b68d4501486ff8c2982b482a8b51694e0428bb4 |
---|
5958 | *253ed08a50db69b5d6917118fbd022c83473671a4d333199191f884841dcf95a8a1b1f888b2ae2 |
---|
5959 | *42ba5070a50b9142eb6b510471a52288423752c6ff26ed4c2a562f24f972ce7fcfeb8600550f53 |
---|
5960 | *8e63453460d8cebbc9aea876e8f080b6780255a84135b85286e7b42712fb7da6563ee7afe9b750 |
---|
5961 | *a465b249c68af51d98f8b46df5fd4b8f62efeb9ef6fa9ef9fa796f352e13028a46eb8a6cc05b25 |
---|
5962 | *0f06bc57f2c9bc93a7e68864632895263be446b727d941be415e9a0df16088d3c23380aa366a72 |
---|
5963 | *86e3324e6405b9a5686465cc51b26ea74d9b3c25ed69cf18a686fd467ec859d0c6958f01ad6b80 |
---|
5964 | *452f2bb6010f18bd0bac5a5fb135d4012bfb81b12d15dbcfa43f1fa576dccb6cd9ec8753aaa3ac |
---|
5965 | *e943a9f4733db0f81a3073b554fa75ab549ab9cd1cace3996514dca2af6561ca2b20a837d8cdf8 |
---|
5966 | *c9395a88839e7c550b709373ec5f02c42e00d73f021b1e00cb1f03891aa0670722e7d96e70e5c5 |
---|
5967 | *29ce05e8c839a75d333b94d736ebfa76ad9d472bb46edb686ed45296a5f92e4f738527dca24837 |
---|
5968 | *63d82a70cefe5ac67bb5b07b0ff2c9fe22e784b77b96959174aa93b34433dbfa92169dbbc98de4 |
---|
5969 | *7b19734f37b981d754c6ddd31bb0b2d1cc77f7041ce9b3ad38cf456a2275f660fc0099f115d5c9 |
---|
5970 | *47e53c248f78c58332a66f3f96da9720d7d39e3c9e8b494d2df7b69d19eae1194956ac33431df1 |
---|
5971 | *597eed169232ef3a6aee3896ffcdb3b6c8731c8605011336ef363424d185289ae0c0450e197a4c |
---|
5972 | *2a4c5aa55fd06ac2c3f1bf2a2d24cabb2c2abaf0997b3efb7b4ea0c0dd327e1fa2718c34962368 |
---|
5973 | *fa3bfd9bfe46bfa9dfd1bf5ea92f34543c23ee51d318bffc9d716566598d8c1bd428e3cb9a82f8 |
---|
5974 | *06ab6da7d74296d6615e414f5e59df14ae2e635fa92f7b3499435c8c4f87ba14a14c4d18643cd9 |
---|
5975 | *b5ecbe48b6f826fc7c73d9169a1eb52fce3ea9abe47aa38e1d99ac7e71365c0d6bffb3ab60d2b2 |
---|
5976 | *abf0e48d902e3c6ba1ae5537a9dd6a8bba039aba4b6d535bd54ebeed54f79777f47256264eb26e |
---|
5977 | *97d5a7d8838dd3f4564eba325b04ff167e31fc2f75095bb8a6a1c97f68c2cd654c4bf88ee0f61f |
---|
5978 | *7748f92ffc0d0185150d7c4b3b3b000000097048597300000b1300000b1301009a9c180000001a |
---|
5979 | *49444154081d63f8ffff3f13030303ddf1dbafffe86e27d09f0052e10654d7b720ec0000000049 |
---|
5980 | *454e44ae426082 |
---|
5981 | adddir ./contrib/musicplayer/src/workers |
---|
5982 | addfile ./contrib/musicplayer/src/workers/indexer.js |
---|
5983 | hunk ./contrib/musicplayer/src/workers/indexer.js 1 |
---|
5984 | +/** |
---|
5985 | + * == Workers == |
---|
5986 | + * |
---|
5987 | + * Web Workers used to dispach computation-heavy work into background. |
---|
5988 | + **/ |
---|
5989 | + |
---|
5990 | +/** section: Workers, related to: CollectionScanner |
---|
5991 | + * Indexer |
---|
5992 | + * |
---|
5993 | + * This Worker is responsible for fetching MP3 files and then |
---|
5994 | + * extracting ID3 metadata, which could grately slowup the interface. |
---|
5995 | + * |
---|
5996 | + * Messages sent to this worker have to contain only a read-cap to |
---|
5997 | + * an MP3 file stored in Tahoe (without /uri/ prefix). |
---|
5998 | + * |
---|
5999 | + * Messages sent from this worker are objects returned by ID3 parser. |
---|
6000 | + * |
---|
6001 | + **/ |
---|
6002 | + |
---|
6003 | +var window = this, |
---|
6004 | + document = {}, |
---|
6005 | + queue = 0; |
---|
6006 | + |
---|
6007 | +this.da = {}; |
---|
6008 | +importScripts("env.js"); |
---|
6009 | + |
---|
6010 | +/** |
---|
6011 | + * Indexer.onMessage(event) -> undefined |
---|
6012 | + * - event (Event): DOM event. |
---|
6013 | + * - event.data (String): Tahoe URI cap for an file. |
---|
6014 | + * |
---|
6015 | + * When tags are parsed, `postMessage` is called. |
---|
6016 | + **/ |
---|
6017 | +onmessage = function (event) { |
---|
6018 | + var cap = event.data, |
---|
6019 | + uri = "/uri/" + encodeURIComponent(cap); |
---|
6020 | + |
---|
6021 | + queue++; |
---|
6022 | + new da.util.ID3({ |
---|
6023 | + url: uri, |
---|
6024 | + onSuccess: function (tags) { |
---|
6025 | + // To avoid duplication, we're using id property (which is mandatary) to store |
---|
6026 | + // read-cap, which is probably already "more unique" than Math.uuid() |
---|
6027 | + if(tags && typeof tags.title !== "undefined" && typeof tags.artist !== "undefined") { |
---|
6028 | + tags.id = cap; |
---|
6029 | + postMessage(tags); |
---|
6030 | + } |
---|
6031 | + |
---|
6032 | + // Not all files are reporeted instantly so it might |
---|
6033 | + // take some time for scanner.js/CollectionScanner.js to |
---|
6034 | + // report the files, maximum delay we're allowing here |
---|
6035 | + // for new files to come in is one minute. |
---|
6036 | + if(!--queue) |
---|
6037 | + setTimeout(checkQueue, 1000*60*1); |
---|
6038 | + }, |
---|
6039 | + onFailure: function () { |
---|
6040 | + if(!--queue) |
---|
6041 | + setTimeout(checkQueue, 1000*60*1); |
---|
6042 | + } |
---|
6043 | + }); |
---|
6044 | +}; |
---|
6045 | + |
---|
6046 | +function checkQueue() { |
---|
6047 | + if(!queue) |
---|
6048 | + postMessage("**FINISHED**"); |
---|
6049 | +} |
---|
6050 | + |
---|
6051 | addfile ./contrib/musicplayer/src/workers/scanner.js |
---|
6052 | hunk ./contrib/musicplayer/src/workers/scanner.js 1 |
---|
6053 | +/** section: Workers |
---|
6054 | + * Scanner |
---|
6055 | + * |
---|
6056 | + * Scanner worker recursively scans the given root direcory for any type of files. |
---|
6057 | + * Messages sent to this worker should contain a directory cap (without `/uri/` part). |
---|
6058 | + * Messages sent from this worker are strings with read-only caps for each found file. |
---|
6059 | + **/ |
---|
6060 | + |
---|
6061 | +var window = this, |
---|
6062 | + document = {}, |
---|
6063 | + queue = 0; |
---|
6064 | + |
---|
6065 | +this.da = {}; |
---|
6066 | +importScripts("env.js"); |
---|
6067 | + |
---|
6068 | +/** |
---|
6069 | + * Scanner.scan(object) -> undefined |
---|
6070 | + * - object (TahoeObject): an Tahoe object. |
---|
6071 | + * |
---|
6072 | + * Traverses the `object` until it finds a file, whose cap is then reported to main thread via `postMessage`. |
---|
6073 | + **/ |
---|
6074 | +function scan (obj) { |
---|
6075 | + queue++; |
---|
6076 | + obj.get(function () { |
---|
6077 | + queue--; |
---|
6078 | + |
---|
6079 | + if(obj.type === "filenode") |
---|
6080 | + return postMessage(obj.uri); |
---|
6081 | + |
---|
6082 | + var n = obj.children.length; |
---|
6083 | + while(n--) { |
---|
6084 | + var child = obj.children[n]; |
---|
6085 | + |
---|
6086 | + if(child.type === "filenode") |
---|
6087 | + postMessage(child.ro_uri); |
---|
6088 | + else |
---|
6089 | + scan(child); |
---|
6090 | + } |
---|
6091 | + |
---|
6092 | + if(!queue) |
---|
6093 | + postMessage("**FINISHED**"); |
---|
6094 | + }); |
---|
6095 | +} |
---|
6096 | + |
---|
6097 | +/** |
---|
6098 | + * Scanner.onmessage(event) -> undefined |
---|
6099 | + * - event.data (String): Tahoe cap pointing to root directory from which scanning should begin. |
---|
6100 | + **/ |
---|
6101 | +onmessage = function (event) { |
---|
6102 | + scan(new TahoeObject(event.data)); |
---|
6103 | +}; |
---|
6104 | adddir ./contrib/musicplayer/tests |
---|
6105 | adddir ./contrib/musicplayer/tests/data |
---|
6106 | addfile ./contrib/musicplayer/tests/data/songs.js |
---|
6107 | hunk ./contrib/musicplayer/tests/data/songs.js 1 |
---|
6108 | +SHARED.songs = { |
---|
6109 | + // ID3 v2.2 tag with UTF data |
---|
6110 | + v22: { |
---|
6111 | + data: "ID3%02%00%00%00%01I6TT2%00%00%11%01%EF%9F%BF%EF%9F%BEL%00j%00%EF%9F%B3%00s%00i%00%EF%9F%B0%00%00%00TP1%00%00!%01%EF%9F%BF%EF%9F%BE%EF%9F%93%00l%00a%00f%00u%00r%00%20%00A%00r%00n%00a%00l%00d%00s%00%00%00TP2%00%00!%01%EF%9F%BF%EF%9F%BE%EF%9F%93%00l%00a%00f%00u%00r%00%20%00A%00r%00n%00a%00l%00d%00s%00%00%00TCM%00%00!%01%EF%9F%BF%EF%9F%BE%EF%9F%93%00l%00a%00f%00u%00r%00%20%00A%00r%00n%00a%00l%00d%00s%00%00%00TAL%00%00%0D%00Found%20Songs%00TRK%00%00%05%007%2F7%00TYE%00%00%06%002009%00COM%00%00%10%00engiTunPGAP%000%00%00TEN%00%00%0E%00iTunes%208.0.2%00COM%00%00h%00engiTunNORM%00%20000007AA%2000000B2E%2000006443%200000967A%200000BF53%2000016300%200000821A%200000816B%2000010C29%20000166FA%00COM%00%00%EF%9E%82%00engiTunSMPB%00%2000000000%2000000210%200000079B%2000000000008BDDD5%2000000000%20004C0FD7%2000000000%2000000000%2000000000%2000000000%2000000000%2000000000%00TPA%00%00%05%001%2F1%00TCO%00%00%0F%00Neo-Classical%00COM%00%00%22%00eng%00available%20on%20ErasedTapes.com>>>PADDING<<<%EF%9F%BF", |
---|
6112 | + simplified: { |
---|
6113 | + title: "Lj\u00f3si\u00f0", |
---|
6114 | + artist: "\u00d3lafur Arnalds", |
---|
6115 | + album: "Found Songs", |
---|
6116 | + track: 7, |
---|
6117 | + year: 2009, |
---|
6118 | + genre: 0, |
---|
6119 | + lyrics: "", |
---|
6120 | + links: { |
---|
6121 | + official: "" |
---|
6122 | + } |
---|
6123 | + }, |
---|
6124 | + frames: { |
---|
6125 | + TT2: "Lj\u00f3si\u00f0", |
---|
6126 | + TP1: "\u00d3lafur Arnalds", |
---|
6127 | + TP2: "\u00d3lafur Arnalds", |
---|
6128 | + TAL: "Found Songs", |
---|
6129 | + TRK: 7, |
---|
6130 | + TYE: 2009 |
---|
6131 | + } |
---|
6132 | + }, |
---|
6133 | + |
---|
6134 | + // ID3 v2.3 tag |
---|
6135 | + v23: { |
---|
6136 | + data: "ID3%03%00%00%00%00Q%01TPOS%00%00%00%04%00%00%001%2F1TENC%00%00%00%0E%40%00%00iTunes%20v7.6.2TIT2%00%00%005%00%00%01%EF%9F%BF%EF%9F%BED%00e%00a%00t%00h%00%20%00W%00i%00l%00l%00%20%00N%00e%00v%00e%00r%00%20%00C%00o%00n%00q%00u%00e%00r%00%00%00TPE1%00%00%00%15%00%00%01%EF%9F%BF%EF%9F%BEC%00o%00l%00d%00p%00l%00a%00y%00%00%00TCON%00%00%00%0D%00%00%01%EF%9F%BF%EF%9F%BER%00o%00c%00k%00%00%00COMM%00%00%00h%00%00%00engiTunNORM%00%20000002F6%200000036E%2000001471%200000163D%2000000017%2000000017%20000069F3%2000006AA9%2000000017%2000000017%00RVAD%00%00%00%0A%00%00%03%105555>>>PADDING<<<%EF%9F%BF", |
---|
6137 | + simplified: { |
---|
6138 | + title: "Death Will Never Conquer", |
---|
6139 | + artist: "Coldplay", |
---|
6140 | + album: "Unknown", |
---|
6141 | + track: 0, |
---|
6142 | + year: 0, |
---|
6143 | + genre: 0, |
---|
6144 | + lyrics: "", |
---|
6145 | + links: { |
---|
6146 | + official: "" |
---|
6147 | + } |
---|
6148 | + }, |
---|
6149 | + frames: { |
---|
6150 | + TIT2: "Death Will Never Conquer", |
---|
6151 | + TPE1: "Coldplay", |
---|
6152 | + TCON: 0 |
---|
6153 | + } |
---|
6154 | + }, |
---|
6155 | + |
---|
6156 | + // ID3 v2.4 tag |
---|
6157 | + v24: { |
---|
6158 | + data: "ID3%04%00%00%00%00%02%00TRCK%00%00%00%05%00%00%006%2F10TIT2%00%00%00%08%00%00%00HalcyonTPE1%00%00%00%08%00%00%00DelphicTALB%00%00%00%08%00%00%00AcolyteTYER%00%00%00%05%00%00%002010TCON%00%00%00%0F%00%00%00(52)ElectronicWXXX%00%00%00%13%00%00%00%00http%3A%2F%2Fdelphic.ccTPUB%00%00%00%13%00%00%00Chimeric%20%2F%20PolydorTPOS%00%00%00%04%00%00%001%2F1%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%EF%9F%BF", |
---|
6159 | + simplified: { |
---|
6160 | + title: "Halcyon", |
---|
6161 | + artist: "Delphic", |
---|
6162 | + album: "Acolyte", |
---|
6163 | + track: 6, |
---|
6164 | + year: 2010, |
---|
6165 | + genre: 52, // Electornic, |
---|
6166 | + lyrics: "", |
---|
6167 | + links: { |
---|
6168 | + official: "http://delphic.cc" |
---|
6169 | + } |
---|
6170 | + }, |
---|
6171 | + frames: { |
---|
6172 | + TIT2: "Halcyon", |
---|
6173 | + TPE1: "Delphic", |
---|
6174 | + TALB: "Acolyte", |
---|
6175 | + TYER: 2010, |
---|
6176 | + TCON: 52, |
---|
6177 | + TRCK: 6, |
---|
6178 | + WXXX: "http://delphic.cc" |
---|
6179 | + } |
---|
6180 | + }, |
---|
6181 | + |
---|
6182 | + // ID3 v1 tag |
---|
6183 | + v1: { |
---|
6184 | + data: "TAGYeah%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00Queen%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00Made%20In%20Heaven%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%001995%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%0C%0C", |
---|
6185 | + simplified: { |
---|
6186 | + title: "Yeah", |
---|
6187 | + artist: "Queen", |
---|
6188 | + album: "Made In Heaven", |
---|
6189 | + track: 12, |
---|
6190 | + year: 1995, |
---|
6191 | + genre: 12 |
---|
6192 | + } |
---|
6193 | + }, |
---|
6194 | + |
---|
6195 | + // 1x1 transparent PNG file |
---|
6196 | + image: { |
---|
6197 | + data: "%EF%9E%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%01%00%00%00%01%01%03%00%00%00%25%EF%9F%9BV%EF%9F%8A%00%00%00%03PLTE%00%00%00%EF%9E%A7z%3D%EF%9F%9A%00%00%00%01tRNS%00%40%EF%9F%A6%EF%9F%98f%00%00%00%0AIDAT%08%EF%9F%97c%60%00%00%00%02%00%01%EF%9F%A2!%EF%9E%BC3%00%00%00%00IEND%EF%9E%AEB%60%EF%9E%82" |
---|
6198 | + } |
---|
6199 | +}; |
---|
6200 | + |
---|
6201 | +(function (args) { |
---|
6202 | + // SONGS.v22 and SONGS.v23 have vast amount of padding bits, |
---|
6203 | + // so we're adding them programatically |
---|
6204 | + |
---|
6205 | + function addPaddingTo(key, n) { |
---|
6206 | + var p = []; |
---|
6207 | + while(n--) |
---|
6208 | + p.push("%00"); |
---|
6209 | + |
---|
6210 | + SHARED.songs[key].data = SHARED.songs[key].data.replace(">>>PADDING<<<", p.join("")); |
---|
6211 | + } |
---|
6212 | + |
---|
6213 | + addPaddingTo("v22", 25241); |
---|
6214 | + addPaddingTo("v23", 10084); |
---|
6215 | +})(); |
---|
6216 | + |
---|
6217 | addfile ./contrib/musicplayer/tests/initialize.js |
---|
6218 | hunk ./contrib/musicplayer/tests/initialize.js 1 |
---|
6219 | +windmill.jsTest.require("shared.js"); |
---|
6220 | + |
---|
6221 | +windmill.jsTest.register([ |
---|
6222 | +// 'test_utils', |
---|
6223 | + 'test_Goal', |
---|
6224 | + 'test_BinaryFile', |
---|
6225 | + 'test_ID3', |
---|
6226 | + 'test_ID3v1', |
---|
6227 | + 'test_ID3v2', |
---|
6228 | + 'test_BrowserCouch', |
---|
6229 | + 'test_DocumentTemplate', |
---|
6230 | + |
---|
6231 | + 'test_NavigationController' |
---|
6232 | +]); |
---|
6233 | addfile ./contrib/musicplayer/tests/shared.js |
---|
6234 | hunk ./contrib/musicplayer/tests/shared.js 1 |
---|
6235 | +var SHARED = {}; |
---|
6236 | +var util = { |
---|
6237 | + wait_for_data: function (key) { |
---|
6238 | + return { |
---|
6239 | + method: 'waits.forJS', |
---|
6240 | + params: { |
---|
6241 | + js: function () { return !!SHARED[key]; } |
---|
6242 | + } |
---|
6243 | + } |
---|
6244 | + }, |
---|
6245 | + |
---|
6246 | + create_id3v2_test: function (version, size) { |
---|
6247 | + var ID3v2Parser = da.util.ID3v2Parser, |
---|
6248 | + vkey = "v" + (version * 10); |
---|
6249 | + |
---|
6250 | + return new function () { |
---|
6251 | + var self = this; |
---|
6252 | + |
---|
6253 | + this.setup = function () { |
---|
6254 | + self.simplified = null; |
---|
6255 | + self.frames = null; |
---|
6256 | hunk ./contrib/musicplayer/tests/shared.js 23 |
---|
6257 | + var data = da.util.BinaryFile.fromEncodedString(SHARED.songs[vkey].data); |
---|
6258 | + self.parser = new ID3v2Parser(data, { |
---|
6259 | + url: "/fake/" + Math.uuid(), |
---|
6260 | + onSuccess: function (simplified, frames) { |
---|
6261 | + self.simplified = simplified; |
---|
6262 | + self.frames = frames; |
---|
6263 | + } |
---|
6264 | + }, {}); |
---|
6265 | + }; |
---|
6266 | + |
---|
6267 | + this.test_waitForData = { |
---|
6268 | + method: 'waits.forJS', |
---|
6269 | + params: { |
---|
6270 | + js: function () { return !!self.simplified && !!self.frames; } |
---|
6271 | + } |
---|
6272 | + }; |
---|
6273 | + |
---|
6274 | + this.test_header = function () { |
---|
6275 | + jum.assertEquals("version should be " + version, self.parser.version, version); |
---|
6276 | + jum.assertEquals("no flags should be set", self.parser.header.flags, 0); |
---|
6277 | + jum.assertEquals("tag size shoudl be " + size, self.parser.header.size, size); |
---|
6278 | + }; |
---|
6279 | + |
---|
6280 | + this.test_verifySimplifiedResult = function () { |
---|
6281 | + jum.assertSameObjects(SHARED.songs[vkey].simplified, self.simplified); |
---|
6282 | + }; |
---|
6283 | + |
---|
6284 | + this.test_verifyDetectedFrames = function () { |
---|
6285 | + jum.assertSameObjects(SHARED.songs[vkey].frames,self.frames); |
---|
6286 | + }; |
---|
6287 | + |
---|
6288 | + return this; |
---|
6289 | + }; |
---|
6290 | + } |
---|
6291 | +}; |
---|
6292 | + |
---|
6293 | +jum.assertSameObjects = function (a, b) { |
---|
6294 | + if(a === b) |
---|
6295 | + return true; |
---|
6296 | + // catches cases when one of args is null |
---|
6297 | + if(!a || !b) |
---|
6298 | + jum.assertEquals(a, b); |
---|
6299 | + |
---|
6300 | + for(var prop in a) |
---|
6301 | + if(a.hasOwnProperty(prop)) |
---|
6302 | + if(prop in a && prop in b) |
---|
6303 | + if(typeof a[prop] === "object") |
---|
6304 | + jum.assertSameObjects(a[prop], b[prop]); |
---|
6305 | + else |
---|
6306 | + jum.assertEquals(a[prop], b[prop]); |
---|
6307 | + else |
---|
6308 | + jum.assertTrue("missing '" + prop +"' property", false); |
---|
6309 | + |
---|
6310 | + return true; |
---|
6311 | +}; |
---|
6312 | addfile ./contrib/musicplayer/tests/test_BinaryFile.js |
---|
6313 | hunk ./contrib/musicplayer/tests/test_BinaryFile.js 1 |
---|
6314 | +windmill.jsTest.require("data/"); |
---|
6315 | + |
---|
6316 | +var test_BinaryFile = new function () { |
---|
6317 | + var BinaryFile = da.util.BinaryFile, |
---|
6318 | + self = this; |
---|
6319 | + |
---|
6320 | + this.setup = function () { |
---|
6321 | + this.file_le = new BinaryFile("\0\0\1\0"); |
---|
6322 | + this.file_be = new BinaryFile("\0\1\0\0", {bigEndian: true}); |
---|
6323 | + this.bond = new BinaryFile("A\0\0\7James Bond\0"); |
---|
6324 | + }; |
---|
6325 | + |
---|
6326 | + this.test_options = function () { |
---|
6327 | + jum.assertEquals(4, this.file_le.length); |
---|
6328 | + jum.assertFalse(this.file_le.bigEndian); |
---|
6329 | + |
---|
6330 | + jum.assertEquals(4, this.file_be.length); |
---|
6331 | + jum.assertTrue(this.file_be.bigEndian); |
---|
6332 | + }; |
---|
6333 | + |
---|
6334 | + this.test_getByte = function () { |
---|
6335 | + jum.assertEquals(0, this.file_le.getByteAt(0)); |
---|
6336 | + jum.assertEquals(1, this.file_le.getByteAt(2)); |
---|
6337 | + |
---|
6338 | + jum.assertEquals(0, this.file_be.getByteAt(0)); |
---|
6339 | + jum.assertEquals(1, this.file_be.getByteAt(1)); |
---|
6340 | + }; |
---|
6341 | + |
---|
6342 | + this.test_getShort = function () { |
---|
6343 | + jum.assertEquals(0, this.file_le.getShortAt(0)); // 00 |
---|
6344 | + jum.assertEquals(256, this.file_le.getShortAt(1)); // 01 |
---|
6345 | + jum.assertEquals(1, this.file_le.getShortAt(2)); // 10 |
---|
6346 | + |
---|
6347 | + jum.assertEquals(1, this.file_be.getShortAt(0)); // 01 |
---|
6348 | + jum.assertEquals(256, this.file_be.getShortAt(1)); // 10 |
---|
6349 | + jum.assertEquals(0, this.file_be.getShortAt(2)); // 00 |
---|
6350 | + }; |
---|
6351 | + |
---|
6352 | + this.test_getLong = function () { |
---|
6353 | + jum.assertEquals(65536, this.file_le.getLongAt(0)); |
---|
6354 | + jum.assertEquals(65536, this.file_be.getLongAt(0)); |
---|
6355 | + }; |
---|
6356 | + |
---|
6357 | + this.test_getBits = function () { |
---|
6358 | + jum.assertSameObjects([0, 1], this.file_le.getBitsAt(2, 2)); |
---|
6359 | + jum.assertSameObjects([0, 0, 0, 1], this.file_be.getBitsAt(1, 4)); |
---|
6360 | + }; |
---|
6361 | + |
---|
6362 | + this.test_unpack = function () { |
---|
6363 | + jum.assertSameObjects(["A", 0, 0, 7], this.bond.unpack("c3i")); |
---|
6364 | + jum.assertSameObjects(["James Bond"], this.bond.unpack("4x10S")); |
---|
6365 | + }; |
---|
6366 | + |
---|
6367 | + this.test_toEncodedString = function () { |
---|
6368 | + jum.assertEquals("%00%00%01%00", this.file_le.toEncodedString()); |
---|
6369 | + jum.assertEquals("%00%01%00%00", this.file_be.toEncodedString()); |
---|
6370 | + }; |
---|
6371 | + |
---|
6372 | + return this; |
---|
6373 | +}; |
---|
6374 | addfile ./contrib/musicplayer/tests/test_BrowserCouch.js |
---|
6375 | hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 1 |
---|
6376 | +windmill.jsTest.require("shared.js"); |
---|
6377 | + |
---|
6378 | +var test_BrowserCouchDict = new function () { |
---|
6379 | + var BrowserCouch = da.db.BrowserCouch, |
---|
6380 | + self = this; |
---|
6381 | + |
---|
6382 | + this.setup = function () { |
---|
6383 | + self.dict = new BrowserCouch.Dictionary(); |
---|
6384 | + |
---|
6385 | + this.dict.set("a", 1); |
---|
6386 | + this.dict.set("b", 2); |
---|
6387 | + |
---|
6388 | + this.dict.setDocs([ |
---|
6389 | + {id: "c", value: 3}, |
---|
6390 | + {id: "d", value: 4}, |
---|
6391 | + {id: "a", value: 5} |
---|
6392 | + ]); |
---|
6393 | + }; |
---|
6394 | + |
---|
6395 | + this.test_set = function () { |
---|
6396 | + jum.assertTrue(this.dict.has("a")); |
---|
6397 | + jum.assertTrue(this.dict.has("b")); |
---|
6398 | + jum.assertFalse(this.dict.has("x")); |
---|
6399 | + |
---|
6400 | + jum.assertSameObjects({id:"a", value: 5}, this.dict.dict.a); |
---|
6401 | + jum.assertEquals(2, this.dict.dict.b); |
---|
6402 | + }; |
---|
6403 | + |
---|
6404 | + this.test_setDocs = function () { |
---|
6405 | + jum.assertTrue(this.dict.has("c")); |
---|
6406 | + jum.assertTrue(this.dict.has("d")); |
---|
6407 | + |
---|
6408 | + jum.assertEquals(3, this.dict.dict.c.value); |
---|
6409 | + jum.assertEquals(4, this.dict.dict.d.value); |
---|
6410 | + }; |
---|
6411 | + |
---|
6412 | + this.test_remove = function () { |
---|
6413 | + this.dict.remove("a"); |
---|
6414 | + jum.assertEquals(3, this.dict.keys.length); |
---|
6415 | + jum.assertFalse(this.dict.has("a")); |
---|
6416 | + }; |
---|
6417 | + |
---|
6418 | + this.test_unpickle = function () { |
---|
6419 | + this.dict.unpickle({ |
---|
6420 | + x: 2.2, |
---|
6421 | + y: 2.3 |
---|
6422 | + }); |
---|
6423 | + |
---|
6424 | + jum.assertEquals(2, this.dict.keys.length); |
---|
6425 | + jum.assertTrue(this.dict.has("x")); |
---|
6426 | + jum.assertTrue(this.dict.has("y")); |
---|
6427 | + jum.assertFalse(this.dict.has("a")); |
---|
6428 | + }; |
---|
6429 | + |
---|
6430 | + this.test_clear = function () { |
---|
6431 | + this.dict.clear(); |
---|
6432 | + |
---|
6433 | + jum.assertEquals(0, this.dict.keys.length); |
---|
6434 | + jum.assertFalse(this.dict.has("x")); |
---|
6435 | + jum.assertFalse(this.dict.has("b")); |
---|
6436 | + }; |
---|
6437 | +}; |
---|
6438 | + |
---|
6439 | +var test_BrowserCouch = new function () { |
---|
6440 | + var BrowserCouch = da.db.BrowserCouch, |
---|
6441 | + self = this; |
---|
6442 | + |
---|
6443 | + this.setup = function () { |
---|
6444 | + this.db = false; |
---|
6445 | + this.stored = {}; |
---|
6446 | + |
---|
6447 | + BrowserCouch.get("test1", function (db) { |
---|
6448 | + self.db = db; |
---|
6449 | + db.addEvent("store", function (doc) { |
---|
6450 | + self.stored[doc.id] = new Date(); |
---|
6451 | + }); |
---|
6452 | + }); |
---|
6453 | + }; |
---|
6454 | + |
---|
6455 | + this.test_waitForDb = { |
---|
6456 | + method: 'waits.forJS', |
---|
6457 | + params: { |
---|
6458 | + js: function () { return !!self.db; } |
---|
6459 | + } |
---|
6460 | + }; |
---|
6461 | + |
---|
6462 | + this.test_verifyDb = function () { |
---|
6463 | + jum.assertEquals(0, this.db.getLength()); |
---|
6464 | + }; |
---|
6465 | + |
---|
6466 | + this.test_put = function () { |
---|
6467 | + var cb = {doc1: 0, doc2: 0, doc3: 0}; |
---|
6468 | + this.db.put({id: "doc1", test: 1}, function () { cb.doc1++ }); |
---|
6469 | + this.db.put({id: "doc2", test: 2}, function () { cb.doc2++ }); |
---|
6470 | + this.db.put({id: "doc3", test: 3}, function () { cb.doc3++ }); |
---|
6471 | + this.db.put({id: "doc1", test: 4}, function () { cb.doc1++ }); |
---|
6472 | + |
---|
6473 | + jum.assertEquals(2, cb.doc1); |
---|
6474 | + jum.assertEquals(1, cb.doc2); |
---|
6475 | + jum.assertEquals(1, cb.doc3); |
---|
6476 | + }; |
---|
6477 | + |
---|
6478 | + this.test_storeEvent = function () { |
---|
6479 | + jum.assertTrue(self.stored.doc1 >= self.stored.doc3); |
---|
6480 | + jum.assertTrue(self.stored.doc3 >= self.stored.doc2); |
---|
6481 | + }; |
---|
6482 | + |
---|
6483 | + this.test_wipe = function () { |
---|
6484 | + jum.assertEquals(3, this.db.getLength()); |
---|
6485 | + this.db.wipe(); |
---|
6486 | + |
---|
6487 | + BrowserCouch.get("test1", function (db) { |
---|
6488 | + jum.assertEquals(0, db.getLength()); |
---|
6489 | + }); |
---|
6490 | + }; |
---|
6491 | + |
---|
6492 | + this.teardown = function () { |
---|
6493 | + self.db.wipe(); |
---|
6494 | + }; |
---|
6495 | + |
---|
6496 | + return this; |
---|
6497 | +}; |
---|
6498 | + |
---|
6499 | +var test_BrowserCouch_tempView = new function () { |
---|
6500 | + var BrowserCouch = da.db.BrowserCouch, |
---|
6501 | + self = this; |
---|
6502 | + |
---|
6503 | + this.setup = function () { |
---|
6504 | + BrowserCouch.get("test2", function (db) { |
---|
6505 | + self.db = db; |
---|
6506 | + self.map_called = 0; |
---|
6507 | + self.map_updated_called = false; |
---|
6508 | + self.reduce_updated_called = false; |
---|
6509 | + |
---|
6510 | + db.put([ |
---|
6511 | + {id: "doc1", nr: 1}, |
---|
6512 | + {id: "doc2", nr: 2}, |
---|
6513 | + {id: "doc3", nr: 3} |
---|
6514 | + ], function () { |
---|
6515 | + self.docs_saved = true; |
---|
6516 | + }); |
---|
6517 | + }); |
---|
6518 | + }; |
---|
6519 | + |
---|
6520 | + this.test_waitForDb = { |
---|
6521 | + method: 'waits.forJS', |
---|
6522 | + params: { |
---|
6523 | + js: function () { return !!self.db && self.docs_saved; } |
---|
6524 | + } |
---|
6525 | + }; |
---|
6526 | + |
---|
6527 | + this.test_map = function () { |
---|
6528 | + this.db.view({ |
---|
6529 | + temporary: true, |
---|
6530 | + |
---|
6531 | + map: function (doc, emit) { |
---|
6532 | + self.map_called++; |
---|
6533 | + if(doc.nr !== 2) |
---|
6534 | + emit(doc.id, doc.nr); |
---|
6535 | + }, |
---|
6536 | + |
---|
6537 | + finished: function (result) { |
---|
6538 | + self.map_result = result; |
---|
6539 | + |
---|
6540 | + self.db.put({id: "doc4", nr: 4}); |
---|
6541 | + }, |
---|
6542 | + |
---|
6543 | + updated: function () { |
---|
6544 | + self.map_updated_called = true; |
---|
6545 | + } |
---|
6546 | + }) |
---|
6547 | + }; |
---|
6548 | + |
---|
6549 | + this.test_waitForMapResult = { |
---|
6550 | + method: 'waits.forJS', |
---|
6551 | + params: { |
---|
6552 | + js: function () { return !!self.map_result } |
---|
6553 | + } |
---|
6554 | + }; |
---|
6555 | + |
---|
6556 | + this.test_verifyMapResult = function () { |
---|
6557 | + var mr = self.map_result; |
---|
6558 | + |
---|
6559 | + jum.assertEquals(3, self.map_called); |
---|
6560 | + jum.assertTrue("rows" in mr); |
---|
6561 | + jum.assertEquals(2, mr.rows.length); |
---|
6562 | + jum.assertEquals("function", typeof mr.findRow); |
---|
6563 | + jum.assertEquals("function", typeof mr.getRow); |
---|
6564 | + jum.assertFalse(self.map_updated_called); |
---|
6565 | + }; |
---|
6566 | + |
---|
6567 | + this.test_mapFindRow = function () { |
---|
6568 | + var mr = self.map_result; |
---|
6569 | + jum.assertEquals(-1, mr.findRow("doc2")); |
---|
6570 | + jum.assertEquals(-1, mr.findRow("doc4")); |
---|
6571 | + jum.assertEquals(-1, mr.findRow("doc7")); |
---|
6572 | + jum.assertEquals(0, mr.findRow("doc1")); |
---|
6573 | + }; |
---|
6574 | + |
---|
6575 | + this.test_reduce = function () { |
---|
6576 | + self.reduce_called = 0; |
---|
6577 | + self.db.view({ |
---|
6578 | + temporary: true, |
---|
6579 | + |
---|
6580 | + map: function (doc, emit) { |
---|
6581 | + emit(doc.nr%2 ? "odd" : "even", doc.nr); |
---|
6582 | + }, |
---|
6583 | + |
---|
6584 | + reduce: function (keys, values) { |
---|
6585 | + var sum = 0, n = values.length; |
---|
6586 | + self.reduce_called++; |
---|
6587 | + |
---|
6588 | + while(n--) |
---|
6589 | + sum += values[n]; |
---|
6590 | + |
---|
6591 | + return sum; |
---|
6592 | + }, |
---|
6593 | + |
---|
6594 | + finished: function (result) { |
---|
6595 | + self.reduce_result = result; |
---|
6596 | + self.db.put({id: "doc5", nr: 5}); |
---|
6597 | + }, |
---|
6598 | + |
---|
6599 | + updated: function () { |
---|
6600 | + self.reduce_updated_called = true; |
---|
6601 | + } |
---|
6602 | + }); |
---|
6603 | + }; |
---|
6604 | + |
---|
6605 | + this.test_waitForReduceResult = { |
---|
6606 | + method: 'waits.forJS', |
---|
6607 | + params: { |
---|
6608 | + js: function () { return !!self.reduce_result } |
---|
6609 | + } |
---|
6610 | + }; |
---|
6611 | + |
---|
6612 | + this.test_verifyReduceResult = function () { |
---|
6613 | + var rr = this.reduce_result; |
---|
6614 | + jum.assertFalse(this.reduce_updated_called); |
---|
6615 | + |
---|
6616 | + jum.assertEquals("function", typeof rr.findRow); |
---|
6617 | + jum.assertEquals("function", typeof rr.getRow); |
---|
6618 | + |
---|
6619 | + jum.assertEquals(2, self.reduce_called); |
---|
6620 | + jum.assertEquals(2, rr.rows.length); |
---|
6621 | + }; |
---|
6622 | + |
---|
6623 | + this.test_verifyReduceFindRow = function () { |
---|
6624 | + var rr = this.reduce_result; |
---|
6625 | + |
---|
6626 | + jum.assertTrue(rr.findRow("even") !== -1); |
---|
6627 | + jum.assertTrue(rr.findRow("odd") !== -1); |
---|
6628 | + jum.assertEquals(-1, rr.findRow("even/odd")); |
---|
6629 | + |
---|
6630 | + jum.assertEquals(6, rr.getRow("even")); // 2 + 4 |
---|
6631 | + jum.assertEquals(4, rr.getRow("odd")); // 1 + 3 |
---|
6632 | + }; |
---|
6633 | + |
---|
6634 | + this.teardown = function () { |
---|
6635 | + this.db.wipe(); |
---|
6636 | + }; |
---|
6637 | + |
---|
6638 | + return this; |
---|
6639 | +}; |
---|
6640 | + |
---|
6641 | +var test_BrowserCouch_liveView = new function () { |
---|
6642 | + var BrowserCouch = da.db.BrowserCouch, |
---|
6643 | + self = this; |
---|
6644 | + |
---|
6645 | + this.setup = function () { |
---|
6646 | + this.docs_saved = false; |
---|
6647 | + |
---|
6648 | + this.map_result = null; |
---|
6649 | + this.map_updated = null; |
---|
6650 | + this.map_finished_called = 0; |
---|
6651 | + this.map_updated_called = 0; |
---|
6652 | + |
---|
6653 | + this.reduce_result = null; |
---|
6654 | + this.reduce_updated = null; |
---|
6655 | + this.reduce_finished_called = 0; |
---|
6656 | + this.reduce_updated_called = 0; |
---|
6657 | + |
---|
6658 | + BrowserCouch.get("test3", function (db) { |
---|
6659 | + self.db = db; |
---|
6660 | + |
---|
6661 | + db.put([ |
---|
6662 | + {id: "Keane", albums: 3, formed: 1997}, |
---|
6663 | + {id: "Delphic", albums: 1, formed: 2010}, |
---|
6664 | + {id: "The Blue Nile", albums: 4, formed: 1981} |
---|
6665 | + ], function () { |
---|
6666 | + self.docs_saved = true; |
---|
6667 | + |
---|
6668 | + db.view({ |
---|
6669 | + id: "test1", |
---|
6670 | + |
---|
6671 | + map: function (doc, emit) { |
---|
6672 | + if(doc.id.toLowerCase().indexOf("the") === -1) |
---|
6673 | + emit(doc.id, doc.formed); |
---|
6674 | + }, |
---|
6675 | + |
---|
6676 | + finished: function (view) { |
---|
6677 | + self.map_finished_called++; |
---|
6678 | + self.map_result = view; |
---|
6679 | + }, |
---|
6680 | + |
---|
6681 | + updated: function (view) { |
---|
6682 | + self.map_updated_called++; |
---|
6683 | + self.map_updates = view; |
---|
6684 | + } |
---|
6685 | + }); |
---|
6686 | + }); |
---|
6687 | + }); |
---|
6688 | + }; |
---|
6689 | + |
---|
6690 | + this.test_waitForDb = { |
---|
6691 | + method: 'waits.forJS', |
---|
6692 | + params: { |
---|
6693 | + js: function () { return !!self.db && self.docs_saved && !!self.map_result } |
---|
6694 | + } |
---|
6695 | + }; |
---|
6696 | + |
---|
6697 | + this.test_verifyMap = function () { |
---|
6698 | + var mr = self.map_result; |
---|
6699 | + |
---|
6700 | + jum.assertEquals(1, self.map_finished_called); |
---|
6701 | + jum.assertEquals(2, mr.rows.length); |
---|
6702 | + jum.assertEquals("function", typeof mr.findRow); |
---|
6703 | + |
---|
6704 | + jum.assertEquals(-1, mr.findRow("The Drums")); |
---|
6705 | + jum.assertEquals(-1, mr.findRow("The Blue Nile")); |
---|
6706 | + jum.assertEquals(0, mr.findRow("Delphic")); |
---|
6707 | + |
---|
6708 | + self.db.put([ |
---|
6709 | + {id: "Marina and The Diamonds", albums: 1, formed: 2007}, |
---|
6710 | + {id: "Coldplay", albums: 4, formed: 1997}, |
---|
6711 | + {id: "Delphic", albums: 1, formed: 2009} |
---|
6712 | + ], function () { |
---|
6713 | + self.map_updates_saved = true; |
---|
6714 | + }); |
---|
6715 | + }; |
---|
6716 | + |
---|
6717 | + this.test_waitForUpdate = { |
---|
6718 | + method: 'waits.forJS', |
---|
6719 | + params: { |
---|
6720 | + js: function () { return self.map_updates_saved && !!self.map_updates } |
---|
6721 | + } |
---|
6722 | + }; |
---|
6723 | + |
---|
6724 | + this.test_verifyMapUpdates = function () { |
---|
6725 | + var mr = self.map_result, |
---|
6726 | + mu = self.map_updates; |
---|
6727 | + |
---|
6728 | + jum.assertEquals(1, self.map_updated_called); |
---|
6729 | + jum.assertEquals(1, self.map_finished_called); |
---|
6730 | + jum.assertEquals(2, mu.rows.length); |
---|
6731 | + jum.assertEquals(3, mr.rows.length); |
---|
6732 | + |
---|
6733 | + jum.assertEquals(-1, mu.findRow("Marina and The Diamonds")); |
---|
6734 | + jum.assertEquals(-1, mu.findRow("Keane")); |
---|
6735 | + jum.assertEquals(0, mu.findRow("Coldplay")); |
---|
6736 | + |
---|
6737 | + jum.assertEquals(-1, mr.findRow("Marina and The Diamonds")); |
---|
6738 | + jum.assertEquals(0, mr.findRow("Coldplay")); |
---|
6739 | + |
---|
6740 | + jum.assertEquals(2009, mr.getRow("Delphic")); |
---|
6741 | + }; |
---|
6742 | + |
---|
6743 | + this.test_killView = function () { |
---|
6744 | + self.db.killView("test1"); |
---|
6745 | + self.db.put({id: "Noisettes", formed: 2003, albums: 2}, $empty); |
---|
6746 | + }; |
---|
6747 | + |
---|
6748 | + this.test_waitForViewToDie = { |
---|
6749 | + method: 'waits.forJS', |
---|
6750 | + params: { |
---|
6751 | + js: function () { return !!!self.db.views.test1 } |
---|
6752 | + } |
---|
6753 | + }; |
---|
6754 | + |
---|
6755 | + this.test_viewIsDead = function () { |
---|
6756 | + jum.assertEquals(1, self.map_updated_called); |
---|
6757 | + }; |
---|
6758 | + |
---|
6759 | + this.test_reduce = function () { |
---|
6760 | + self.rereduce_args = null; |
---|
6761 | + self.rereduce_called = 0; |
---|
6762 | + self.reduce_called = 0; |
---|
6763 | + |
---|
6764 | + self.db.view({ |
---|
6765 | + id: "test2", |
---|
6766 | + |
---|
6767 | + map: function (doc, emit) { |
---|
6768 | + if(doc.albums) |
---|
6769 | + emit("albums", doc.albums); |
---|
6770 | + }, |
---|
6771 | + |
---|
6772 | + reduce: function (keys, values, rereduce) { |
---|
6773 | + if(rereduce) { |
---|
6774 | + self.rereduce_args = arguments; |
---|
6775 | + self.rereduce_called++; |
---|
6776 | + } else { |
---|
6777 | + self.reduce_called++; |
---|
6778 | + } |
---|
6779 | + |
---|
6780 | + var n = values.length, sum = 0; |
---|
6781 | + while(n--) sum += values[n]; |
---|
6782 | + return sum; |
---|
6783 | + }, |
---|
6784 | + |
---|
6785 | + finished: function (view) { |
---|
6786 | + self.reduce_finished_called++; |
---|
6787 | + self.reduce_result = view; |
---|
6788 | + }, |
---|
6789 | + |
---|
6790 | + updated: function (view) { |
---|
6791 | + self.reduce_updated_called++; |
---|
6792 | + self.reduce_updates = view; |
---|
6793 | + } |
---|
6794 | + }) |
---|
6795 | + }; |
---|
6796 | + |
---|
6797 | + this.test_waitForReduce = { |
---|
6798 | + method: 'waits.forJS', |
---|
6799 | + params: { |
---|
6800 | + js: function () { return !!self.reduce_result } |
---|
6801 | + } |
---|
6802 | + }; |
---|
6803 | + |
---|
6804 | + this.test_verifyReduceResult = function () { |
---|
6805 | + var rr = self.reduce_result; |
---|
6806 | + |
---|
6807 | + jum.assertEquals(1, self.reduce_finished_called); |
---|
6808 | + jum.assertEquals(0, self.reduce_updated_called); |
---|
6809 | + |
---|
6810 | + jum.assertEquals("function", typeof rr.findRow); |
---|
6811 | + |
---|
6812 | + jum.assertEquals(1, rr.rows.length); |
---|
6813 | + jum.assertEquals(0, rr.findRow("albums")); |
---|
6814 | + jum.assertEquals(15, rr.getRow("albums")); |
---|
6815 | + |
---|
6816 | + self.db.put([ |
---|
6817 | + {id: "Imaginary", albums: 0, formed: 2020}, |
---|
6818 | + {id: "Grizzly Bear", albums: 2, formed: 2000} |
---|
6819 | + ], function() { |
---|
6820 | + self.reduce_updates_saved = true; |
---|
6821 | + }); |
---|
6822 | + }; |
---|
6823 | + |
---|
6824 | + this.test_waitForUpdates = { |
---|
6825 | + method: 'waits.forJS', |
---|
6826 | + params: { |
---|
6827 | + js: function () { return self.reduce_updates_saved } |
---|
6828 | + } |
---|
6829 | + }; |
---|
6830 | + |
---|
6831 | + this.test_reduceUpdates = function () { |
---|
6832 | + var rr = self.reduce_result, |
---|
6833 | + ru = self.reduce_updates; |
---|
6834 | + |
---|
6835 | + jum.assertEquals(1, self.reduce_updated_called); |
---|
6836 | + jum.assertEquals(1, self.reduce_finished_called); |
---|
6837 | + |
---|
6838 | + jum.assertEquals(1, ru.rows.length); |
---|
6839 | + jum.assertEquals(-1, ru.findRow("Grizzly Bear")); |
---|
6840 | + jum.assertEquals(0, ru.findRow("albums")); |
---|
6841 | + |
---|
6842 | + jum.assertEquals(2, ru.getRow("albums")); |
---|
6843 | + jum.assertEquals(17, rr.getRow("albums")); |
---|
6844 | + }; |
---|
6845 | + |
---|
6846 | + this.test_rereduce = function () { |
---|
6847 | + jum.assertEquals(1, self.rereduce_called); |
---|
6848 | + jum.assertSameObjects([null, [2, 15], true], self.rereduce_args); |
---|
6849 | + } |
---|
6850 | + |
---|
6851 | + this.teardown = function () { |
---|
6852 | + self.db.killView("test2"); |
---|
6853 | + }; |
---|
6854 | + |
---|
6855 | + return this; |
---|
6856 | +}; |
---|
6857 | addfile ./contrib/musicplayer/tests/test_DocumentTemplate.js |
---|
6858 | hunk ./contrib/musicplayer/tests/test_DocumentTemplate.js 1 |
---|
6859 | +windmill.jsTest.require("shared.js"); |
---|
6860 | + |
---|
6861 | +var test_DocumentTemplate = new function () { |
---|
6862 | + var BrowserCouch = da.db.BrowserCouch, |
---|
6863 | + DocumentTemplate = da.db.DocumentTemplate, |
---|
6864 | + self = this; |
---|
6865 | + |
---|
6866 | + this.setup = function () { |
---|
6867 | + self.db = null; |
---|
6868 | + BrowserCouch.get("dt_test1", function (db) { |
---|
6869 | + self.db = db; |
---|
6870 | + }); |
---|
6871 | + }; |
---|
6872 | + |
---|
6873 | + this.waitForDb = { |
---|
6874 | + method: "waits.forJS", |
---|
6875 | + params: { |
---|
6876 | + js: function () { return !!self.db } |
---|
6877 | + } |
---|
6878 | + }; |
---|
6879 | + |
---|
6880 | + this.test_registerType = function () { |
---|
6881 | + DocumentTemplate.registerType("test_Person", self.db, new Class({ |
---|
6882 | + Extends: DocumentTemplate, |
---|
6883 | + |
---|
6884 | + hasMany: { |
---|
6885 | + cars: ["test_Car", "owner_id"] |
---|
6886 | + }, |
---|
6887 | + |
---|
6888 | + sayHi: function () { |
---|
6889 | + return "Hello! My name is %0 %1.".interpolate([ |
---|
6890 | + this.get("name"), |
---|
6891 | + this.get("surname") |
---|
6892 | + ]) |
---|
6893 | + } |
---|
6894 | + })); |
---|
6895 | + self.Person = DocumentTemplate.test_Person; |
---|
6896 | + |
---|
6897 | + DocumentTemplate.registerType("test_Car", self.db, new Class({ |
---|
6898 | + Extends: DocumentTemplate, |
---|
6899 | + |
---|
6900 | + belongsTo: { |
---|
6901 | + owner: "test_Person" |
---|
6902 | + }, |
---|
6903 | + |
---|
6904 | + start: function () { |
---|
6905 | + this.update({state: "inMotion"}) |
---|
6906 | + }, |
---|
6907 | + |
---|
6908 | + stop: function () { |
---|
6909 | + this.update({state: "stopped"}); |
---|
6910 | + }, |
---|
6911 | + |
---|
6912 | + isRunning: function () { |
---|
6913 | + return this.get("state") === "inMotion" |
---|
6914 | + } |
---|
6915 | + })); |
---|
6916 | + this.Car = DocumentTemplate.test_Car; |
---|
6917 | + |
---|
6918 | + jum.assertTrue("test_Person" in DocumentTemplate); |
---|
6919 | + jum.assertTrue("test_Person" in self.db.views); |
---|
6920 | + jum.assertEquals(self.db.name, self.Person.db().name); |
---|
6921 | + }; |
---|
6922 | + |
---|
6923 | + this.test_instanceFindNoResult = function () { |
---|
6924 | + this.instanceFind_success_called = 0; |
---|
6925 | + this.instanceFind_failure_called = 0; |
---|
6926 | + |
---|
6927 | + this.Car.find({ |
---|
6928 | + properties: {manufacturer: "Volkswagen"}, |
---|
6929 | + onSuccess: function () { |
---|
6930 | + self.instanceFind_success_called++; |
---|
6931 | + }, |
---|
6932 | + onFailure: function () { |
---|
6933 | + self.instanceFind_failure_called++; |
---|
6934 | + } |
---|
6935 | + }) |
---|
6936 | + }; |
---|
6937 | + |
---|
6938 | + this.test_waitForInstanceFind = { |
---|
6939 | + method: "waits.forJS", |
---|
6940 | + params: { |
---|
6941 | + js: function () { return self.instanceFind_failure_called } |
---|
6942 | + } |
---|
6943 | + }; |
---|
6944 | + |
---|
6945 | + this.test_verifyInstaceFind = function () { |
---|
6946 | + jum.assertEquals(1, self.instanceFind_failure_called); |
---|
6947 | + jum.assertEquals(0, self.instanceFind_success_called); |
---|
6948 | + }; |
---|
6949 | + |
---|
6950 | + this.test_createDoc = function () { |
---|
6951 | + self.herbie_saved = 0; |
---|
6952 | + self.Person.create({ |
---|
6953 | + id: "jim", |
---|
6954 | + first: "Jim", |
---|
6955 | + last: "Douglas" |
---|
6956 | + }, function (jim) { |
---|
6957 | + self.jim = jim; |
---|
6958 | + |
---|
6959 | + self.herbie = new self.Car({ |
---|
6960 | + id: "herbie", |
---|
6961 | + owner_id: "jim", |
---|
6962 | + state: "sleeping", |
---|
6963 | + diamods: 0 |
---|
6964 | + }); |
---|
6965 | + |
---|
6966 | + self.herbie.save(function () { |
---|
6967 | + self.herbie_saved++; |
---|
6968 | + }); |
---|
6969 | + }); |
---|
6970 | + }; |
---|
6971 | + |
---|
6972 | + this.test_waitForDocs = { |
---|
6973 | + method: "waits.forJS", |
---|
6974 | + params: { |
---|
6975 | + js: function () { return !!self.jim && !!self.herbie && self.herbie_saved } |
---|
6976 | + } |
---|
6977 | + }; |
---|
6978 | + |
---|
6979 | + this.test_verifyCreate = function () { |
---|
6980 | + jum.assertEquals("jim", self.jim.id); |
---|
6981 | + jum.assertEquals("herbie", self.herbie.id); |
---|
6982 | + jum.assertEquals(1, self.db.views.test_Person.view.rows.length); |
---|
6983 | + jum.assertEquals(1, self.db.views.test_Car.view.rows.length); |
---|
6984 | + }; |
---|
6985 | + |
---|
6986 | + this.test_get = function () { |
---|
6987 | + jum.assertEquals("Jim", self.jim.get("first")); |
---|
6988 | + jum.assertEquals("jim", self.herbie.get("owner_id")); |
---|
6989 | + }; |
---|
6990 | + |
---|
6991 | + this.test_belongsTo = function () { |
---|
6992 | + self.herbie.get("owner", function (owners) { |
---|
6993 | + jum.assertEquals(1, owners.length); |
---|
6994 | + jum.assertEquals(self.jim.id, owners[0].id); |
---|
6995 | + self.got_jim = true; |
---|
6996 | + }); |
---|
6997 | + }; |
---|
6998 | + |
---|
6999 | + this.test_waitForJim = { |
---|
7000 | + method: "waits.forJS", |
---|
7001 | + params: { |
---|
7002 | + js: function () { return self.got_jim } |
---|
7003 | + } |
---|
7004 | + }; |
---|
7005 | + |
---|
7006 | + this.test_hasMany = function () { |
---|
7007 | + self.jim.get("cars", function (cars) { |
---|
7008 | + jum.assertEquals(1, cars.length); |
---|
7009 | + jum.assertEquals(self.herbie.id, cars[0].id); |
---|
7010 | + self.got_herbie = true; |
---|
7011 | + }); |
---|
7012 | + }; |
---|
7013 | + |
---|
7014 | + this.test_waitForHerbie = { |
---|
7015 | + method: "waits.forJS", |
---|
7016 | + params: { |
---|
7017 | + js: function () { return self.got_herbie } |
---|
7018 | + } |
---|
7019 | + }; |
---|
7020 | + |
---|
7021 | + this.test_propertyChangeEvent = function () { |
---|
7022 | + self.herbie.addEvent("propertyChange", function (changes, herbie) { |
---|
7023 | + jum.assertEquals(self.herbie, herbie); |
---|
7024 | + jum.assertTrue("state" in changes); |
---|
7025 | + jum.assertFalse("id" in changes); |
---|
7026 | + jum.assertEquals("inMotion", herbie.get("state")); |
---|
7027 | + }); |
---|
7028 | + |
---|
7029 | + self.herbie.start(); |
---|
7030 | + }; |
---|
7031 | + |
---|
7032 | + this.test_findOrCreate = function () { |
---|
7033 | + self.foc_finished = 0; |
---|
7034 | + self.Person.findOrCreate({ |
---|
7035 | + properties: {id: "jim"}, |
---|
7036 | + onSuccess: function (jim, created) { |
---|
7037 | + self.foc_finished++; |
---|
7038 | + self.foc_jim = {jim: jim, created: created}; |
---|
7039 | + } |
---|
7040 | + }); |
---|
7041 | + |
---|
7042 | + self.john_props = {id: "john", first: "John", last: "Doe"}; |
---|
7043 | + self.Person.findOrCreate({ |
---|
7044 | + properties: self.john_props, |
---|
7045 | + onSuccess: function (john, created) { |
---|
7046 | + self.foc_finished++; |
---|
7047 | + self.foc_john = {john: john, created: created}; |
---|
7048 | + } |
---|
7049 | + }); |
---|
7050 | + }; |
---|
7051 | + |
---|
7052 | + this.test_waitForFindOrCreate = { |
---|
7053 | + method: "waits.forJS", |
---|
7054 | + params: { |
---|
7055 | + js: function () { return self.foc_finished === 2 } |
---|
7056 | + } |
---|
7057 | + }; |
---|
7058 | + |
---|
7059 | + this.test_verifyFindOrCreate = function () { |
---|
7060 | + jum.assertEquals("jim", self.foc_jim.jim.id); |
---|
7061 | + jum.assertTrue(self.foc_jim.created !== true); |
---|
7062 | + |
---|
7063 | + jum.assertEquals("john", self.foc_john.john.id); |
---|
7064 | + jum.assertSameObjects(self.john_props, self.foc_john.john.doc); |
---|
7065 | + jum.assertTrue(self.foc_john.created); |
---|
7066 | + }; |
---|
7067 | + |
---|
7068 | + this.test_destroy = function () { |
---|
7069 | + self.success_on_destroy = self.failure_on_destroy = false; |
---|
7070 | + self.jim.destroy(function () { |
---|
7071 | + self.Person.findFirst({ |
---|
7072 | + properties: {id: "jim"}, |
---|
7073 | + onSuccess: function() { |
---|
7074 | + self.success_on_destory = true; |
---|
7075 | + }, |
---|
7076 | + onFailure: function () { |
---|
7077 | + self.failure_on_destory = true; |
---|
7078 | + } |
---|
7079 | + }); |
---|
7080 | + }); |
---|
7081 | + }; |
---|
7082 | + |
---|
7083 | + this.wait_forDestroy = { |
---|
7084 | + method: "waits.forJS", |
---|
7085 | + params: { |
---|
7086 | + js: function () { return self.failure_on_destroy || self.success_on_destroy } |
---|
7087 | + } |
---|
7088 | + }; |
---|
7089 | + |
---|
7090 | + this.test_verifyDestroy = function () { |
---|
7091 | + jum.assertTrue(self.failure_on_destroy); |
---|
7092 | + jum.assertFalse(self.success_on_destroy); |
---|
7093 | + }; |
---|
7094 | + |
---|
7095 | + this.teardown = function () { |
---|
7096 | + self.db.wipe(); |
---|
7097 | + }; |
---|
7098 | + |
---|
7099 | + return this; |
---|
7100 | +}; |
---|
7101 | addfile ./contrib/musicplayer/tests/test_Goal.js |
---|
7102 | hunk ./contrib/musicplayer/tests/test_Goal.js 1 |
---|
7103 | +var test_Goal = new function () { |
---|
7104 | + var Goal = da.util.Goal, |
---|
7105 | + self = this; |
---|
7106 | + this.test_setup = function () { |
---|
7107 | + this._timestamps = {}; |
---|
7108 | + this._calls = {a: 0, b: 0, c: 0, afterC: 0, success: 0, setup: 0}; |
---|
7109 | + this._calls.setup++; |
---|
7110 | + this._goal = new Goal({ |
---|
7111 | + checkpoints: ["a", "b", "c"], |
---|
7112 | + |
---|
7113 | + onCheckpoint: function (name) { |
---|
7114 | + self._timestamps[name] = new Date(); |
---|
7115 | + self._calls[name]++; |
---|
7116 | + }, |
---|
7117 | + |
---|
7118 | + onFinish: function (name) { |
---|
7119 | + self._timestamps.success = new Date(); |
---|
7120 | + self._calls.success++; |
---|
7121 | + }, |
---|
7122 | + |
---|
7123 | + afterCheckpoint: { |
---|
7124 | + c: function () { |
---|
7125 | + self._timestamps.afterC = new Date(); |
---|
7126 | + self._calls.afterC++; |
---|
7127 | + } |
---|
7128 | + } |
---|
7129 | + }); |
---|
7130 | + |
---|
7131 | + this._goal.checkpoint("b"); |
---|
7132 | + this._goal.checkpoint("c"); |
---|
7133 | + this._goal.checkpoint("a"); |
---|
7134 | + |
---|
7135 | + this._goal.checkpoint("c"); |
---|
7136 | + this._goal.checkpoint("b"); |
---|
7137 | + }; |
---|
7138 | + |
---|
7139 | + this.test_allEventsCalledOnce = function () { |
---|
7140 | + jum.assertTrue(this._calls.a === 1); |
---|
7141 | + jum.assertTrue(this._calls.b === 1); |
---|
7142 | + jum.assertTrue(this._calls.c === 1); |
---|
7143 | + jum.assertTrue(this._calls.afterC === 1); |
---|
7144 | + jum.assertTrue(this._calls.success === 1); |
---|
7145 | + jum.assertTrue(this._goal.finished); |
---|
7146 | + }; |
---|
7147 | + |
---|
7148 | + this.test_timestamps = function () { |
---|
7149 | + jum.assertTrue(this._timestamps.b <= this._timestamps.c); |
---|
7150 | + jum.assertTrue(this._timestamps.c <= this._timestamps.a); |
---|
7151 | + jum.assertTrue(this._timestamps.c <= this._timestamps.afterC); |
---|
7152 | + }; |
---|
7153 | + |
---|
7154 | + this.test_successCalls = function () { |
---|
7155 | + jum.assertTrue(this._timestamps.success >= this._timestamps.a); |
---|
7156 | + jum.assertTrue(this._timestamps.success >= this._timestamps.b); |
---|
7157 | + jum.assertTrue(this._timestamps.success >= this._timestamps.c); |
---|
7158 | + jum.assertTrue(this._timestamps.success >= this._timestamps.afterC); |
---|
7159 | + }; |
---|
7160 | +}; |
---|
7161 | addfile ./contrib/musicplayer/tests/test_ID3.js |
---|
7162 | hunk ./contrib/musicplayer/tests/test_ID3.js 1 |
---|
7163 | +windmill.jsTest.require("shared.js"); |
---|
7164 | +windmill.jsTest.require("data/songs.js"); |
---|
7165 | + |
---|
7166 | +var test_ID3 = new function () { |
---|
7167 | + var BinaryFile = da.util.BinaryFile, |
---|
7168 | + ID3 = da.util.ID3; |
---|
7169 | + |
---|
7170 | + var ID3_patched = new Class({ |
---|
7171 | + Extends: ID3, |
---|
7172 | + |
---|
7173 | + _data: BinaryFile.fromEncodedString(SHARED.songs.image.data), |
---|
7174 | + _getFile: function (parser) { |
---|
7175 | + if(!parser) |
---|
7176 | + this.options.onFailure(); |
---|
7177 | + else |
---|
7178 | + this._onFileFetched(this._data); |
---|
7179 | + } |
---|
7180 | + }); |
---|
7181 | + ID3_patched.parsers = $A(ID3.parsers); |
---|
7182 | + |
---|
7183 | + var self = this; |
---|
7184 | + this.setup = function () { |
---|
7185 | + this.called_onSuccess = false; |
---|
7186 | + this.called_onFailure = false; |
---|
7187 | + |
---|
7188 | + new ID3_patched({ |
---|
7189 | + url: "/fake/" + Math.uuid(), |
---|
7190 | + onSuccess: function () { |
---|
7191 | + self.called_onSuccess = true; |
---|
7192 | + }, |
---|
7193 | + onFailure: function () { |
---|
7194 | + self.called_onFailure = true; |
---|
7195 | + } |
---|
7196 | + }); |
---|
7197 | + }; |
---|
7198 | + |
---|
7199 | + this.test_callbacks = function () { |
---|
7200 | + jum.assertTrue(self.called_onFailure); |
---|
7201 | + jum.assertFalse(self.called_onSuccess); |
---|
7202 | + }; |
---|
7203 | + |
---|
7204 | + this.teardown = function () { |
---|
7205 | + delete self.called_onSuccess; |
---|
7206 | + delete self.called_onFailure; |
---|
7207 | + }; |
---|
7208 | +}; |
---|
7209 | addfile ./contrib/musicplayer/tests/test_ID3v1.js |
---|
7210 | hunk ./contrib/musicplayer/tests/test_ID3v1.js 1 |
---|
7211 | +windmill.jsTest.require("shared.js"); |
---|
7212 | +windmill.jsTest.require("data/songs.js"); |
---|
7213 | + |
---|
7214 | +var test_ID3v1 = new function () { |
---|
7215 | + var BinaryFile = da.util.BinaryFile, |
---|
7216 | + ID3v1Parser = da.util.ID3v1Parser, |
---|
7217 | + self = this; |
---|
7218 | + |
---|
7219 | + this.setup = function () { |
---|
7220 | + this.tags = {}; |
---|
7221 | + |
---|
7222 | + SHARED.parser = new ID3v1Parser(BinaryFile.fromEncodedString(SHARED.songs.v1.data), { |
---|
7223 | + url: "/fake/" + Math.uuid(), |
---|
7224 | + onSuccess: function (tags) { |
---|
7225 | + self.tags = tags; |
---|
7226 | + } |
---|
7227 | + }, {}); |
---|
7228 | + }; |
---|
7229 | + |
---|
7230 | + this.test_waitForData = { |
---|
7231 | + method: 'waits.forJS', |
---|
7232 | + params: { |
---|
7233 | + js: function () { return !!self.tags; } |
---|
7234 | + } |
---|
7235 | + }; |
---|
7236 | + |
---|
7237 | + this.test_verifyResult = function () { |
---|
7238 | + jum.assertSameObjects(SHARED.songs.v1.simplified, self.tags); |
---|
7239 | + }; |
---|
7240 | + |
---|
7241 | + this.test_withID3v2 = function () { |
---|
7242 | + jum.assertFalse("ID3v1 parser should not parse ID3v2 tags", |
---|
7243 | + ID3v1Parser.test(BinaryFile.fromEncodedString(SHARED.songs.v24.data)) |
---|
7244 | + ); |
---|
7245 | + }; |
---|
7246 | + |
---|
7247 | + this.test_withPNGFile = function () { |
---|
7248 | + jum.assertFalse("ID3v1 parser should not parse PNG file", |
---|
7249 | + ID3v1Parser.test(BinaryFile.fromEncodedString(SHARED.songs.image.data)) |
---|
7250 | + ); |
---|
7251 | + }; |
---|
7252 | +}; |
---|
7253 | addfile ./contrib/musicplayer/tests/test_ID3v2.js |
---|
7254 | hunk ./contrib/musicplayer/tests/test_ID3v2.js 1 |
---|
7255 | +windmill.jsTest.require("shared.js"); |
---|
7256 | +windmill.jsTest.require("data/songs.js"); |
---|
7257 | + |
---|
7258 | +var test_ID3v2 = new function () { |
---|
7259 | + var BinaryFile = da.util.BinaryFile, |
---|
7260 | + ID3v2Parser = da.util.ID3v2Parser; |
---|
7261 | + |
---|
7262 | + // Sometimes the code gets exectued before data/songs.js |
---|
7263 | + this.test_waitForData = { |
---|
7264 | + method: "waits.forJS", |
---|
7265 | + params: { |
---|
7266 | + js: function () { return !!SHARED && !!SHARED.songs } |
---|
7267 | + } |
---|
7268 | + }; |
---|
7269 | + |
---|
7270 | + this.test_withPNGFile = function () { |
---|
7271 | + jum.assertFalse("should not parse PNG file", |
---|
7272 | + ID3v2Parser.test(BinaryFile.fromEncodedString(SHARED.songs.image.data)) |
---|
7273 | + ); |
---|
7274 | + }; |
---|
7275 | + |
---|
7276 | + this.test_withID3v1File = function () { |
---|
7277 | + jum.assertFalse("should not parse ID3v1 file", |
---|
7278 | + ID3v2Parser.test(BinaryFile.fromEncodedString(SHARED.songs.v1.data)) |
---|
7279 | + ); |
---|
7280 | + }; |
---|
7281 | + |
---|
7282 | + this.test_withID3v2Files = function () { |
---|
7283 | + jum.assertTrue("should detect v2.2", |
---|
7284 | + ID3v2Parser.test(BinaryFile.fromEncodedString(SHARED.songs.v22.data)) |
---|
7285 | + ); |
---|
7286 | + jum.assertTrue("should detect v2.3", |
---|
7287 | + ID3v2Parser.test(BinaryFile.fromEncodedString(SHARED.songs.v23.data)) |
---|
7288 | + ); |
---|
7289 | + jum.assertTrue(ID3v2Parser.test(BinaryFile.fromEncodedString(SHARED.songs.v24.data))); |
---|
7290 | + }; |
---|
7291 | + |
---|
7292 | + return this; |
---|
7293 | +}; |
---|
7294 | + |
---|
7295 | +var test_ID3v22 = util.create_id3v2_test(2.2, 25792); |
---|
7296 | +var test_ID3v23 = util.create_id3v2_test(2.3, 10379); |
---|
7297 | +var test_ID3v24 = util.create_id3v2_test(2.4, 266); |
---|
7298 | addfile ./contrib/musicplayer/tests/test_Menu.js |
---|
7299 | hunk ./contrib/musicplayer/tests/test_Menu.js 1 |
---|
7300 | - |
---|
7301 | +var test_Menu = new function () { |
---|
7302 | + var Menu = da.ui.Menu, |
---|
7303 | + self = this; |
---|
7304 | + |
---|
7305 | + this.setup = function () { |
---|
7306 | + self.menu = new Menu({ |
---|
7307 | + items: { |
---|
7308 | + a: {html: "a", id: "_test_first_menu_item"}, |
---|
7309 | + b: {html: "b", id: "_test_second_menu_item"}, |
---|
7310 | + _sep: Menu.separator, |
---|
7311 | + c: {html: "c", id: "_test_third_menu_item"} |
---|
7312 | + } |
---|
7313 | + }); |
---|
7314 | + }; |
---|
7315 | + |
---|
7316 | + this.test_domNode = function () { |
---|
7317 | + var el = self.menu.toElement(); |
---|
7318 | + |
---|
7319 | + jum.assertEquals("menu's element should be inserted into body of the page", |
---|
7320 | + el.getParent(), document.body |
---|
7321 | + ); |
---|
7322 | + //jum.assertEquals("should have four list items", ) |
---|
7323 | + }; |
---|
7324 | + |
---|
7325 | + this.test_events = function () { |
---|
7326 | + var el = self.menu.toElement(); |
---|
7327 | + |
---|
7328 | + self.menu.addEvent("click", function (key, element) { |
---|
7329 | + jum.assertEquals("clicked items' key should be 'b'", "b", key); |
---|
7330 | + }); |
---|
7331 | + // events are synchronous |
---|
7332 | + self.menu.click(null, el.getElement("li:nth-child(2)")); |
---|
7333 | + |
---|
7334 | + var showed = 0, |
---|
7335 | + hidden = 0; |
---|
7336 | + self.menu.addEvent("show", function () { |
---|
7337 | + showed++; |
---|
7338 | + }); |
---|
7339 | + self.menu.addEvent("hide", function () { |
---|
7340 | + hidden++; |
---|
7341 | + }); |
---|
7342 | + |
---|
7343 | + self.menu.show(); |
---|
7344 | + jum.assertEquals("shown menu should be visible to the user", |
---|
7345 | + "block", el.style.display |
---|
7346 | + ); |
---|
7347 | + |
---|
7348 | + self.menu.show(); |
---|
7349 | + jum.assertEquals("showing visible menu should not fire 'show' event ", |
---|
7350 | + 1, showed |
---|
7351 | + ); |
---|
7352 | + jum.assertEquals("calling `show` on visible menu should hide it", |
---|
7353 | + "none", el.style.display |
---|
7354 | + ); |
---|
7355 | + |
---|
7356 | + self.menu.hide(); |
---|
7357 | + jum.assertEquals("hiding hidden menu should not fire 'hide' event", |
---|
7358 | + 1, hidden |
---|
7359 | + ); |
---|
7360 | + }; |
---|
7361 | + |
---|
7362 | + this.teardown = function () { |
---|
7363 | + self.menu.destroy(); |
---|
7364 | + }; |
---|
7365 | + |
---|
7366 | + return this; |
---|
7367 | +}; |
---|
7368 | addfile ./contrib/musicplayer/tests/test_NavigationController.js |
---|
7369 | hunk ./contrib/musicplayer/tests/test_NavigationController.js 1 |
---|
7370 | +var test_NavigationController = new function () { |
---|
7371 | + var Navigation = da.controller.Navigation, |
---|
7372 | + self = this; |
---|
7373 | + |
---|
7374 | + // We can't use da.controller.CollectionScanner.isFinished() |
---|
7375 | + // here because scanner worker has one minute timeout |
---|
7376 | + this.test_waitForCollectionScanner = { |
---|
7377 | + method: "waits.forJS", |
---|
7378 | + params: { |
---|
7379 | + js: function () { |
---|
7380 | + return da.db.DEFAULT.views.Song.view.rows.length === 3 |
---|
7381 | + } |
---|
7382 | + } |
---|
7383 | + }; |
---|
7384 | + |
---|
7385 | + // Generated by Windmill |
---|
7386 | + // It clicks on a item in Artists column and than on a item in Albums column |
---|
7387 | + this.test_navigationBehaviour = [ |
---|
7388 | + {"params": {"xpath": "//div[@id='Artists_column_container']/div/div[2]/a[2]/span"}, |
---|
7389 | + "method": "click"}, |
---|
7390 | + {"params": {"xpath": "//div[@id='Albums_column_container']/div/div[2]/a/span"}, |
---|
7391 | + "method": "click"}, |
---|
7392 | + {"params": {"xpath": "//div[@id='Albums_column_container']/a/span", "validator": "Albums"}, |
---|
7393 | + "method": "asserts.assertText"} |
---|
7394 | + ]; |
---|
7395 | + |
---|
7396 | + this.test_activeColumns = function () { |
---|
7397 | + var ac = Navigation.activeColumns; |
---|
7398 | + jum.assertEquals("first column should be Root", |
---|
7399 | + "Root", ac[0].column_name |
---|
7400 | + ); |
---|
7401 | + jum.assertEquals("second column should be Artists", |
---|
7402 | + "Artists", ac[1].column_name |
---|
7403 | + ); |
---|
7404 | + jum.assertEquals("third colum should be Albums", |
---|
7405 | + "Albums", ac[2].column_name |
---|
7406 | + ); |
---|
7407 | + jum.assertEquals("fourth column should be Songs", |
---|
7408 | + "Songs", ac[3].column_name |
---|
7409 | + ); |
---|
7410 | + }; |
---|
7411 | + |
---|
7412 | + this.test_items = function () { |
---|
7413 | + var ac = Navigation.activeColumns, |
---|
7414 | + artists = ac[1].column, |
---|
7415 | + albums = ac[2].column, |
---|
7416 | + songs = ac[3].column; |
---|
7417 | + |
---|
7418 | + jum.assertEquals("there should be two artists", |
---|
7419 | + 2, artists.options.totalCount |
---|
7420 | + ); |
---|
7421 | + jum.assertEquals("first artist should be Keane", |
---|
7422 | + "Keane", artists.getItem(0).value.title |
---|
7423 | + ); |
---|
7424 | + jum.assertEquals("second artist should be Superhumanoids", |
---|
7425 | + "Superhumanoids", artists.getItem(1).value.title |
---|
7426 | + ); |
---|
7427 | + |
---|
7428 | + jum.assertEquals("there should be only one album by Superhumanoids", |
---|
7429 | + 1, albums.options.totalCount |
---|
7430 | + ); |
---|
7431 | + jum.assertEquals("first album should be Urgency", |
---|
7432 | + "Urgency", albums.getItem(0).value.title |
---|
7433 | + ); |
---|
7434 | + |
---|
7435 | + jum.assertEquals("there should be two songs on Urgency album", |
---|
7436 | + 2, songs.options.totalCount |
---|
7437 | + ); |
---|
7438 | + // indirectly tests sorting, since 'Hey Big Bang' is third track |
---|
7439 | + // while 'Persona' is first on the album |
---|
7440 | + jum.assertEquals("first song should be 'Persona'", |
---|
7441 | + "Persona", songs.getItem(0).value.title |
---|
7442 | + ); |
---|
7443 | + jum.assertEquals("second song should be 'Hey Big Bang'", |
---|
7444 | + "Hey Big Bang", songs.getItem(1).value.title |
---|
7445 | + ); |
---|
7446 | + }; |
---|
7447 | + |
---|
7448 | + return this; |
---|
7449 | +}; |
---|
7450 | addfile ./contrib/musicplayer/tests/test_utils.js |
---|
7451 | hunk ./contrib/musicplayer/tests/test_utils.js 1 |
---|
7452 | +var test_StringStrip = function () { |
---|
7453 | + jum.assertEquals("123ab", "123\0\0a\0\0b".strip()); |
---|
7454 | + jum.assertEquals("abc", "\0\0\0ab\0c\0\0\0".strip()); |
---|
7455 | + jum.assertEquals("d", "\0d".strip()); |
---|
7456 | + jum.assertEquals("e ", "e\0 ".strip()); |
---|
7457 | +}; |
---|
7458 | + |
---|
7459 | +var test_StringInterpolate = new function () { |
---|
7460 | + this.test_withNoArgs = function () { |
---|
7461 | + jum.assertEquals("test", "test".interpolate()); |
---|
7462 | + }; |
---|
7463 | + |
---|
7464 | + this.test_withArray = function () { |
---|
7465 | + jum.assertEquals("10/100%", "{0}/{1}%".interpolate([10, 100])); |
---|
7466 | + jum.assertEquals("100/100%", "{1}/{1}%".interpolate([10, 100])); |
---|
7467 | + jum.assertEquals("001011", "{0}{0}{1}{0}{1}{1}".interpolate([0, 1])); |
---|
7468 | + }; |
---|
7469 | + |
---|
7470 | + this.test_withObject = function () { |
---|
7471 | + jum.assertEquals("Hi John! How are you?", "Hi {name}! How are {who}?".interpolate({ |
---|
7472 | + name: "John", |
---|
7473 | + who: "you" |
---|
7474 | + })); |
---|
7475 | + }; |
---|
7476 | + |
---|
7477 | + this.test_missingProperties = function () { |
---|
7478 | + jum.assertEquals("Hi mum! {feeling} to see you!", "Hi {who}! {feeling} to see you!".interpolate({ |
---|
7479 | + who: "mum" |
---|
7480 | + })); |
---|
7481 | + }; |
---|
7482 | +}; |
---|
7483 | + |
---|
7484 | +var test_ArrayZip = new function () { |
---|
7485 | + this.test_oneArg = function () { |
---|
7486 | + jum.assertSameObjects([[1]], Array.zip([1])); |
---|
7487 | + }; |
---|
7488 | + |
---|
7489 | + this.test_twoArgs = function () { |
---|
7490 | + jum.assertSameObjects([[1, 1], [2, 2], [3, 3]], Array.zip([1, 2, 3], [1, 2, 3])); |
---|
7491 | + }; |
---|
7492 | + |
---|
7493 | + this.test_moreSimpleArgs = function () { |
---|
7494 | + jum.assertSameObjects([[1, 2, 3, 4, 5]], Array.zip([1], [2], [3], [4], [5])); |
---|
7495 | + }; |
---|
7496 | + |
---|
7497 | + this.test_notSameLength = function () { |
---|
7498 | + jum.assertSameObjects([[1, 2, 4]], Array.zip([1], [2, 3], [4, 5, 6])); |
---|
7499 | + jum.assertSameObjects([ |
---|
7500 | + [1, 4, 6], |
---|
7501 | + [2, 5, undefined], |
---|
7502 | + [3, undefined, undefined] |
---|
7503 | + ], Array.zip([1, 2, 3], [4, 5], [6])); |
---|
7504 | + }; |
---|
7505 | +}; |
---|
7506 | + |
---|
7507 | +var test_ArrayContainsAll = function () { |
---|
7508 | + jum.assertTrue( [] .containsAll([] )); |
---|
7509 | + jum.assertTrue( [1, 2, 3] .containsAll([1, 2, 3])); |
---|
7510 | + jum.assertTrue( [1, 2, 1] .containsAll([2, 1] )); |
---|
7511 | + jum.assertTrue( [1, 2] .containsAll([1, 2, 1])); |
---|
7512 | + jum.assertFalse([1, 2] .containsAll([3, 1, 2])); |
---|
7513 | +}; |
---|
7514 | + |
---|
7515 | +var test_HashContainsAll = function () { |
---|
7516 | + jum.assertTrue( $H({}) .containsAll({} )); |
---|
7517 | + jum.assertTrue( $H({a: 1, b: 2, c: 3}).containsAll({a: 1, b: 2})); |
---|
7518 | + jum.assertFalse($H({a: 1}) .containsAll({a: 1, b: 2})); |
---|
7519 | + jum.assertFalse($H({a: 1, b: 2}) .containsAll({a: 2, b: 3})); |
---|
7520 | +}; |
---|
7521 | addfile ./src/allmydata/test/test_musicplayer.py |
---|
7522 | hunk ./src/allmydata/test/test_musicplayer.py 1 |
---|
7523 | +import os, shutil |
---|
7524 | +from allmydata.test import tilting |
---|
7525 | +from allmydata.immutable import upload |
---|
7526 | +from base64 import b64decode |
---|
7527 | + |
---|
7528 | +timeout = 1200 |
---|
7529 | + |
---|
7530 | +DATA = {} |
---|
7531 | +DATA['persona'] = """ |
---|
7532 | +SUQzAwAAAAAGEFRJVDIAAAAJAAAAUGVyc29uYQBUUEUxAAAAEAAAAFN1cGVyaHVtYW5 |
---|
7533 | +vaWRzAFRBTEIAAAAJAAAAVXJnZW5jeQBUUkNLAAAABQAAADEvNgBUWUVSAAAABgAAAD |
---|
7534 | +IwMTAAVENPTgAAAAYAAAAoMTMpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7535 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7536 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7537 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7538 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7539 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7540 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7541 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7542 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7543 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7544 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7545 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7546 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7547 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=""".replace('\n', '') |
---|
7548 | + |
---|
7549 | +DATA['bigbang'] = """ |
---|
7550 | +SUQzAwAAAAACblRJVDIAAAAOAAAASGV5IEJpZyBCYW5nAFRQRTEAAAAQAAAAU3VwZXJo |
---|
7551 | +dW1hbm9pZHMAVEFMQgAAAAkAAABVcmdlbmN5AFRSQ0sAAAAFAAAAMy82AFRZRVIAAAAG |
---|
7552 | +AAAAMjAxMABUQ09OAAAABgAAACgxMykAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7553 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7554 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7555 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7556 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7557 | +AAAAAAAAAAAAAAAAAAAAAAAAAA==""".replace('\n', '') |
---|
7558 | + |
---|
7559 | +DATA['maps'] = """ |
---|
7560 | +SUQzBAAAAAAKClRJVDIAAAAFAAADTWFwc1RQRTEAAAAGAAADS2VhbmVURFJDAAAABQAA |
---|
7561 | +AzIwMTBUQUxCAAAAHwAAA1N1bnNoaW5lIFJldHJvc3BlY3RpdmUgQ29sbGVjdFRSQ0sA |
---|
7562 | +AAACAAADMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7563 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7564 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7565 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7566 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7567 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7568 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7569 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7570 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7571 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7572 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7573 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7574 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7575 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7576 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7577 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7578 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7579 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7580 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7581 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7582 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7583 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7584 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
---|
7585 | +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==""".replace('\n', '') |
---|
7586 | + |
---|
7587 | +class MusicPlayerJSTest: |
---|
7588 | + def _set_up_tree(self): |
---|
7589 | + self.settings['JAVASCRIPT_TEST_DIR'] = '../contrib/musicplayer/tests' |
---|
7590 | + self.settings['SCRIPT_APPEND_ONLY'] = True |
---|
7591 | + |
---|
7592 | + self.test_url = 'static/musicplayer/index_devel.html' |
---|
7593 | + shutil.copytree('../contrib/musicplayer/src', self.public_html_path + '/musicplayer') |
---|
7594 | + #os.makedirs(self.public_html_path + '/musicplayer/js/workers') |
---|
7595 | + shutil.copytree('../contrib/musicplayer/build/js/workers', self.public_html_path + '/musicplayer/js/workers') |
---|
7596 | + |
---|
7597 | + d = self.client.create_dirnode() |
---|
7598 | + def _created_music_dirnode(node): |
---|
7599 | + self.music_node = node |
---|
7600 | + self.music_cap = node.get_uri() |
---|
7601 | + |
---|
7602 | + return self.client.create_dirnode() |
---|
7603 | + d.addCallback(_created_music_dirnode) |
---|
7604 | + |
---|
7605 | + def _created_settings_dirnode(node): |
---|
7606 | + self.settings_cap = node.get_uri() |
---|
7607 | + d.addCallback(_created_settings_dirnode) |
---|
7608 | + |
---|
7609 | + def _write_config_file(ign): |
---|
7610 | + config = open(os.path.join(self.public_html_path, 'musicplayer', 'config.json'), 'w+') |
---|
7611 | + config.write("""{ |
---|
7612 | + "music_cap": "%s", |
---|
7613 | + "settings_cap": "%s" |
---|
7614 | + }\n""" % (self.music_cap, self.settings_cap)) |
---|
7615 | + config.close() |
---|
7616 | + d.addCallback(_write_config_file) |
---|
7617 | + |
---|
7618 | + persona = upload.Data(b64decode(DATA['persona']), None) |
---|
7619 | + d.addCallback(lambda ign: self.music_node.add_file(u'persona', persona)) |
---|
7620 | + |
---|
7621 | + bigbang = upload.Data(b64decode(DATA['bigbang']), None) |
---|
7622 | + d.addCallback(lambda ign: self.music_node.add_file(u'bigbang', bigbang)) |
---|
7623 | + |
---|
7624 | + maps = upload.Data(b64decode(DATA['maps']), None) |
---|
7625 | + d.addCallback(lambda ign: self.music_node.add_file(u'maps', maps)) |
---|
7626 | + |
---|
7627 | + return d |
---|
7628 | + |
---|
7629 | +class ChromeTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Chrome): |
---|
7630 | + pass |
---|
7631 | + |
---|
7632 | +# . |
---|
7633 | +#class FirefoxTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Firefox): |
---|
7634 | + #pass |
---|
7635 | addfile ./src/allmydata/test/tilting.py |
---|
7636 | hunk ./src/allmydata/test/tilting.py 1 |
---|
7637 | +# Note: may be Apache 2.0 license-contaminated. |
---|
7638 | +# (I haven't checked whether there is a significant license compatibility issue here.) |
---|
7639 | + |
---|
7640 | +import os, logging, tempfile, windmill |
---|
7641 | +from windmill.bin import admin_lib |
---|
7642 | +from twisted.internet import defer |
---|
7643 | +from twisted.trial import unittest |
---|
7644 | +from foolscap.api import eventually |
---|
7645 | + |
---|
7646 | +from allmydata.util import log, fileutil |
---|
7647 | +from allmydata.scripts.create_node import create_node, create_introducer |
---|
7648 | +from allmydata.scripts.startstop_node import do_start, do_stop |
---|
7649 | +from allmydata.immutable import upload |
---|
7650 | +from allmydata.test.no_network import GridTestMixin |
---|
7651 | + |
---|
7652 | +from time import sleep |
---|
7653 | + |
---|
7654 | +class TiltingMixin(GridTestMixin): |
---|
7655 | + # adapted from |
---|
7656 | + # http://github.com/windmill/windmill/blob/master/windmill/authoring/unit.py |
---|
7657 | + # http://github.com/windmill/windmill/blob/master/windmill/bin/shell_objects.py |
---|
7658 | + |
---|
7659 | + def _set_up(self, basedir, num_clients=1, num_servers=10): |
---|
7660 | + self.basedir = 'tilting/' + basedir |
---|
7661 | + self.set_up_grid(num_clients=num_clients, num_servers=num_servers) |
---|
7662 | + self.client = self.g.clients[0] |
---|
7663 | + |
---|
7664 | + self._set_up_windmill() |
---|
7665 | + d = defer.maybeDeferred(self._set_up_tree) |
---|
7666 | + d.addCallback(lambda ign: self._start_windmill()) |
---|
7667 | + return d |
---|
7668 | + |
---|
7669 | + def _set_up_windmill(self): |
---|
7670 | + self.browser_debugging = True |
---|
7671 | + self.browser_name = 'firefox' |
---|
7672 | + self.test_url = '/' |
---|
7673 | + self._js_test_details = [] |
---|
7674 | + self.settings = { |
---|
7675 | + 'EXIT_ON_DONE': False, |
---|
7676 | + 'CONSOLE_LOG_LEVEL': logging.CRITICAL, |
---|
7677 | + 'controllers': []} |
---|
7678 | + |
---|
7679 | + self.public_html_path = "public_html" |
---|
7680 | + fileutil.make_dirs(self.public_html_path) |
---|
7681 | + |
---|
7682 | + log.msg("setting up Windmill for browser '%s'" % (self.browser_name)) |
---|
7683 | + windmill.block_exit = True |
---|
7684 | + # Windmill loves to output all sorts of stuff |
---|
7685 | + windmill.stdout = tempfile.TemporaryFile() |
---|
7686 | + admin_lib.configure_global_settings(logging_on=False) |
---|
7687 | + self.configure() |
---|
7688 | + |
---|
7689 | + def _start_windmill(self): |
---|
7690 | + if self.browser_name == 'firefox': |
---|
7691 | + self.settings['INSTALL_FIREBUG'] = True |
---|
7692 | + for (setting, value) in self.settings.iteritems(): |
---|
7693 | + windmill.settings[setting] = value |
---|
7694 | + windmill.settings['TEST_URL'] = self.client_baseurls[0] + self.test_url |
---|
7695 | + |
---|
7696 | + self.shell_objects = admin_lib.setup() |
---|
7697 | + self.jsonrpc = self.shell_objects['httpd'].jsonrpc_methods_instance |
---|
7698 | + self.jsonrpc_app = self.shell_objects['httpd'].namespaces['windmill-jsonrpc'] |
---|
7699 | + |
---|
7700 | + d = defer.Deferred() |
---|
7701 | + # Windmill prints success/failure statistics on its own |
---|
7702 | + # and this unfortunately seems to be the only way to stop it from doing that. |
---|
7703 | + # This is just a stripped down version of teardown method from windmill.server.convergence.JSONRPCMethods |
---|
7704 | + def _windmill_teardown(**kwargs): |
---|
7705 | + if windmill.settings['EXIT_ON_DONE']: |
---|
7706 | + admin_lib.teardown(admin_lib.shell_objects_dict) |
---|
7707 | + windmill.runserver_running = False |
---|
7708 | + sleep(.25) |
---|
7709 | + |
---|
7710 | + eventually(d.callback, None) |
---|
7711 | + |
---|
7712 | + self.jsonrpc_app.__dict__[u'teardown'] = _windmill_teardown |
---|
7713 | + |
---|
7714 | + log.msg("starting browser") |
---|
7715 | + self.shell_objects['start_' + self.browser_name]() |
---|
7716 | + |
---|
7717 | + if self.browser_debugging: |
---|
7718 | + ready_d = defer.Deferred() |
---|
7719 | + admin_lib.on_ide_awake.append(lambda: eventually(ready_d.callback, None)) |
---|
7720 | + |
---|
7721 | + self.xmlrpc = windmill.tools.make_xmlrpc_client() |
---|
7722 | + ready_d.addCallback(lambda ign: |
---|
7723 | + self.xmlrpc.add_command({'method':'commands.setOptions', |
---|
7724 | + 'params':{'runTests':False, 'priority':'normal'}})) |
---|
7725 | + |
---|
7726 | + if self.settings['JAVASCRIPT_TEST_DIR']: |
---|
7727 | + self._log_js_test_results() |
---|
7728 | + |
---|
7729 | + return d |
---|
7730 | + |
---|
7731 | + def tearDown(self): |
---|
7732 | + if self.browser_debugging: |
---|
7733 | + self.xmlrpc.add_command({'method':'commands.setOptions', |
---|
7734 | + 'params':{'runTests':True, 'priority':'normal'}}) |
---|
7735 | + else: |
---|
7736 | + log.msg("shutting down browser '%s'" % (self.browser_name)) |
---|
7737 | + admin_lib.teardown(self.shell_objects) |
---|
7738 | + log.msg("browser shutdown done") |
---|
7739 | + |
---|
7740 | + return GridTestMixin.tearDown(self) |
---|
7741 | + |
---|
7742 | + def _log_js_test_results(self): |
---|
7743 | + # When running JS tests in windmill, only a "X tests of Y failed" string is printed |
---|
7744 | + # when all tests finish. This replaces Windmill's reporting method so that |
---|
7745 | + # all test results (success/failure) are collected in self._js_test_details and later reported |
---|
7746 | + # to Trial via self.failUnless(). This way Trial can easily pickup failing tests |
---|
7747 | + # and display the error messages (if any). |
---|
7748 | + |
---|
7749 | + def _report_without_resolve(**kwargs): |
---|
7750 | + self.jsonrpc._test_resolution_suite.report_without_resolve(*kwargs) |
---|
7751 | + self._js_test_details.append(kwargs) |
---|
7752 | + |
---|
7753 | + return 200 |
---|
7754 | + |
---|
7755 | + del self.jsonrpc_app.__dict__[u'report_without_resolve'] |
---|
7756 | + self.jsonrpc_app.register_method(_report_without_resolve, u'report_without_resolve') |
---|
7757 | + |
---|
7758 | +class JSTestsMixin: |
---|
7759 | + """ |
---|
7760 | + Mixin for running tests written in JavaScript. |
---|
7761 | + Remember to set self.settings['JS_TESTS_DIR'] (path is relative to _trial_tmp) as well as self.test_url. |
---|
7762 | + """ |
---|
7763 | + |
---|
7764 | + def test_js(self): |
---|
7765 | + d = self._set_up('test_js') |
---|
7766 | + d.addCallback(lambda ign: self._report_results()) |
---|
7767 | + return d |
---|
7768 | + |
---|
7769 | + def _report_results(self): |
---|
7770 | + for test in self._js_test_details: |
---|
7771 | + self.failUnless(test['result'], test['debug']) |
---|
7772 | + |
---|
7773 | +class Chrome(TiltingMixin, unittest.TestCase): |
---|
7774 | + """Starts tests in Chrome.""" |
---|
7775 | + def configure(self): |
---|
7776 | + self.browser_name = "chrome" |
---|
7777 | + |
---|
7778 | +class Firefox(TiltingMixin, unittest.TestCase): |
---|
7779 | + """Starts tests in Firefox.""" |
---|
7780 | + def configure(self): |
---|
7781 | + self.browser_name = "firefox" |
---|
7782 | + |
---|
7783 | +class InternetExplorer(TiltingMixin, unittest.TestCase): |
---|
7784 | + """Starts tests in Internet Explorer.""" |
---|
7785 | + def configure(self): |
---|
7786 | + self.browser_name = "ie" |
---|
7787 | + |
---|
7788 | +class Safari(TiltingMixin, unittest.TestCase): |
---|
7789 | + """Starts tests in Safari.""" |
---|
7790 | + def configure(self): |
---|
7791 | + self.browser_name = "safari" |
---|
7792 | } |
---|
7793 | |
---|
7794 | Context: |
---|
7795 | |
---|
7796 | [quickstart.html: python 2.5 -> 2.6 as recommended version |
---|
7797 | david-sarah@jacaranda.org**20100705175858 |
---|
7798 | Ignore-this: bc3a14645ea1d5435002966ae903199f |
---|
7799 | ] |
---|
7800 | [SFTP: don't call .stopProducing on the producer registered with OverwriteableFileConsumer (which breaks with warner's new downloader). |
---|
7801 | david-sarah@jacaranda.org**20100628231926 |
---|
7802 | Ignore-this: 131b7a5787bc85a9a356b5740d9d996f |
---|
7803 | ] |
---|
7804 | [docs/how_to_make_a_tahoe-lafs_release.txt: trivial correction, install.html should now be quickstart.html. |
---|
7805 | david-sarah@jacaranda.org**20100625223929 |
---|
7806 | Ignore-this: 99a5459cac51bd867cc11ad06927ff30 |
---|
7807 | ] |
---|
7808 | [setup: in the Makefile, refuse to upload tarballs unless someone has passed the environment variable "BB_BRANCH" with value "trunk" |
---|
7809 | zooko@zooko.com**20100619034928 |
---|
7810 | Ignore-this: 276ddf9b6ad7ec79e27474862e0f7d6 |
---|
7811 | ] |
---|
7812 | [trivial: tiny update to in-line comment |
---|
7813 | zooko@zooko.com**20100614045715 |
---|
7814 | Ignore-this: 10851b0ed2abfed542c97749e5d280bc |
---|
7815 | (I'm actually committing this patch as a test of the new eager-annotation-computation of trac-darcs.) |
---|
7816 | ] |
---|
7817 | [docs: about.html link to home page early on, and be decentralized storage instead of cloud storage this time around |
---|
7818 | zooko@zooko.com**20100619065318 |
---|
7819 | Ignore-this: dc6db03f696e5b6d2848699e754d8053 |
---|
7820 | ] |
---|
7821 | [docs: update about.html, especially to have a non-broken link to quickstart.html, and also to comment out the broken links to "for Paranoids" and "for Corporates" |
---|
7822 | zooko@zooko.com**20100619065124 |
---|
7823 | Ignore-this: e292c7f51c337a84ebfeb366fbd24d6c |
---|
7824 | ] |
---|
7825 | [TAG allmydata-tahoe-1.7.0 |
---|
7826 | zooko@zooko.com**20100619052631 |
---|
7827 | Ignore-this: d21e27afe6d85e2e3ba6a3292ba2be1 |
---|
7828 | ] |
---|
7829 | Patch bundle hash: |
---|
7830 | bdbbdc1c68b20ad0e265eb9b0afd16e4ac7c678d |
---|