Ticket #1023: updates01082010.dpatch

File updates01082010.dpatch, 401.7 KB (added by josipl, at 2010-08-01T00:25:13Z)

Adds player's play/pause/next/prev interface, information about currently playing song, access to additional info about the artist, recommendations and music videos.

Line 
17 patches for repository /home/josip/bin/tahoe-tmp:
2
3Mon Jul 12 16:26:21 CEST 2010  josip.lisec@gmail.com
4  * add-navigation-and-settings-tests
5  Added tests for da.controller.Navigation, da.controller.Settings and
6  da.controller.CollectionScanner.
7
8Tue Jul 13 20:11:06 CEST 2010  josip.lisec@gmail.com
9  * add-progress-bars
10  Added da.ui.ProgressBar and da.ui.SegmentedProgressBar classes,
11  the latter one will be used mainly for visualizing progress of
12  currently playing song and load percentage.
13
14Mon Jul 19 14:13:57 CEST 2010  josip.lisec@gmail.com
15  * player-interface
16   * Fixed bugs in (Segmented)ProgressBar
17   * Added basic player controls
18   * Added album cover fetcing via Last.fm
19   * Reorganised the way external libraries are being imported
20   * Fixed tests
21
22Sun Jul 25 11:46:52 CEST 2010  josip.lisec@gmail.com
23  * add-controller-tests
24  Added tests for:
25    * da.ui.Dialog
26    * da.controller.CollectionScanner
27    * da.controller.Settings
28
29Wed Jul 28 15:19:19 CEST 2010  josiplisec@gmail.com
30  * add-player-controls
31  * Added player controls (previos/play/next) with tests.
32  * Made other, smaller, improvements to da.ui.NavigationColumn (smarter re-rendering)
33  * Limited both scanner and indexer workers to allow only one request to Tahoe-LAFS,
34    thus making network I/O almoast synchronous, mainly due to the fact that
35    Tahoe never completes requests under high load.
36 
37
38Sun Aug  1 01:12:11 CEST 2010  josip.lisec@gmail.com
39  * updated-docs
40  * Added NOTES file with instructions for running the tests.
41  * Fixed typos in INSTALL
42
43Sun Aug  1 01:13:16 CEST 2010  josip.lisec@gmail.com
44  * added-songcontext-and-services
45  * Added da.controller.SongContext with default contexts:
46    * 'Artist' - shows basic information about the artist
47      the currently playing song,
48    * 'Recommendations' - shows similar artists and songs,
49    * 'Music Videos' - presents search results from YouTube
50      of currently playing song.
51  * Added da.service.* APIs which are used by contexts above
52    * da.service.lastFm - interface to the Last.fm services
53    * da.service.artistInfo
54    * da.service.recommendations
55    * da.service.musicVideo
56  * UTF-8 related fixes to ID3v2 parser
57
58New patches:
59
60[add-navigation-and-settings-tests
61josip.lisec@gmail.com**20100712142621
62 Ignore-this: 559424eabbd88c496d69d076f2fcd5de
63 Added tests for da.controller.Navigation, da.controller.Settings and
64 da.controller.CollectionScanner.
65] {
66hunk ./contrib/musicplayer/manage.py 274
67     
68     subprocess.call(args)
69 
70+class VerifyTests(Command):
71+  description = 'runs syntax checks on tests'
72+  user_options = []
73
74+  def initialize_options(self):
75+    pass
76+  def finalize_options(self):
77+    pass
78+   
79+  def run(self):
80+    args = [
81+      'java',
82+      '-jar',             CLOSURE_COMPILER_PATH,
83+      '--js_output_file', '_tmp.out',
84+      '--dev_mode',       'START_AND_END']
85+   
86+    for filename in os.listdir('tests'):
87+      if filename.endswith('.js'):
88+        args.append('--js')
89+        args.append('tests/' + filename)
90+   
91+    r = subprocess.call(args)
92+    if r == 0:
93+      print 'Everything seems to be okay! Yay!'
94+   
95+    os.remove('_tmp.out')
96+
97 setup(
98   name = 'tahoe-music-player',
99   cmdclass = {
100hunk ./contrib/musicplayer/manage.py 307
101     'build':    Build,
102     'install':  Install,
103     'watch':    Watch,
104+    'tests':    VerifyTests,
105     'docs':     Docs
106   }
107 )
108hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 43
109       checkpoints: ["scanner", "indexer"],
110       onFinish: function () {
111         this.finished = true;
112-        this.terminate()
113+        this.destroy()
114       }.bind(this)
115     })
116   },
117hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 52
118    *  CollectionScanner#finished -> true | false
119    **/
120   finished: false,
121
122-  /**
123-   *  CollectionScanner#terminate() -> undefined
124-   * 
125-   *  Instantly kills both workers.
126-   **/
127-  terminate: function () {
128-    this.indexer.terminate();
129-    this.scanner.terminate();
130-  },
131
132+   
133   onScannerMessage: function (event) {
134     var cap = event.data;
135     if(cap === "**FINISHED**") {
136hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 94
137                   artist_id:  artist_id,
138                   album_id:   album_id
139                 });
140+               
141+                delete links;
142               }
143             });
144           },
145hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 126
146           links.checkpoint("artist");
147       }
148     });
149+  },
150
151+  /**
152+   *  CollectionScanner#destroy() -> undefined
153+   * 
154+   *  Instantly kills both workers.
155+   **/
156+  destroy: function () {
157+    this.indexer.terminate();
158+    this.scanner.terminate();
159+   
160+    delete this.indexer;
161+    delete this.scanner;
162+    delete this.goal;
163   }
164 });
165 
166hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 146
167 var SCANNER;
168 /**
169  * da.controller.CollectionScanner
170+ * Public interface of [[CollectionScanner]].
171  **/
172 da.controller.CollectionScanner = {
173   /**
174hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 152
175    *  da.controller.CollectionScanner.scan() -> undefined
176    *  Starts scanning music directory
177-   *
178-   *  Part of public API.
179    **/
180   scan: function (cap) {
181hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 154
182-    if(!SCANNER || (SCANNER && SCANNER.finished))
183+    if(!SCANNER || (SCANNER && SCANNER.finished)) {
184+      delete SCANNER;
185       SCANNER = new CollectionScanner(cap);
186hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 157
187+    }
188   },
189   
190   /**
191hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 162
192    *  da.controller.CollectionScanner.isScanning() -> true | false
193-   *
194-   *  Part of public API.
195    **/
196   isScanning: function () {
197     return SCANNER && !SCANNER.finished;
198hunk ./contrib/musicplayer/src/controllers/Navigation.js 76
199 
200     if(!(this.menu = options.menu))
201       this.createMenu();
202+    else
203+      this.header.store("menu", this.menu);
204     
205hunk ./contrib/musicplayer/src/controllers/Navigation.js 79
206-    if(this.column.constructor.filters && this.column.constructor.filters.length)
207-      this.column.addEvent("click", this.listItemClick.bind(this));
208+    if(this.column.constructor.filters.length)
209+      this.column.addEvent("click", this.createFilteredColumn.bind(this));
210     
211     var first_item = this.column._el.getElement(".column_item");
212     if(first_item)
213hunk ./contrib/musicplayer/src/controllers/Navigation.js 112
214       href: "#"
215     });
216     
217-    this.header.addEvent("click", function (e) {
218+    this.header.addEvent("click", function (event) {
219       var menu = this.retrieve("menu");
220       if(menu)
221hunk ./contrib/musicplayer/src/controllers/Navigation.js 115
222-        menu.show(e);
223+        menu.show(event);
224     });
225     
226     this._el.grab(this.header.grab(new Element("span", {
227hunk ./contrib/musicplayer/src/controllers/Navigation.js 129
228   /**
229    *  NavigationColumnContainer#createMenu() -> this | false
230    * 
231-   *  Creates menu for current column if it has filters.
232+   *  Creates menu for current column (if it has filters).
233    *  [[da.ui.Menu]] instance is stored to `header` element with `menu` key.
234    **/
235   createMenu: function () {
236hunk ./contrib/musicplayer/src/controllers/Navigation.js 146
237     this.menu = new Menu({
238       items: items
239     });
240-   
241     this.menu._el.addClass("navigation_menu");
242     this.header.store("menu", this.menu);
243     
244hunk ./contrib/musicplayer/src/controllers/Navigation.js 149
245-    this.menu.addEvent("show", function () {
246+    this.menu.addEvent("show", function () {     
247       var header = this.header;
248       header.addClass("active");
249hunk ./contrib/musicplayer/src/controllers/Navigation.js 152
250-      header.retrieve("menu")._el.style.width = header.getWidth() + "px";
251+      // adjusting menu's width to the width of the header
252+      header.retrieve("menu").toElement().style.width = header.getWidth() + "px";
253     }.bind(this));
254     
255     this.menu.addEvent("hide", function () {
256hunk ./contrib/musicplayer/src/controllers/Navigation.js 161
257     }.bind(this));
258     
259     if(filters && filters.length)
260-      this.menu.addEvent("click", this.menuItemClick.bind(this.parent_column || this));
261+      this.menu.addEvent("click", this.replace.bind(this.parent_column || this));
262     
263     return this;
264   },
265hunk ./contrib/musicplayer/src/controllers/Navigation.js 167
266   
267   /**
268-   *  NavigationColumnContainer#menuItemClick(filterName, event, element) -> undefined
269+   *  NavigationColumnContainer#replace(filterName[, event][, element]) -> undefined
270    *  - filterName (String): id of the menu item.
271    *  - event (Event): DOM event.
272hunk ./contrib/musicplayer/src/controllers/Navigation.js 170
273-   *  - element (Element): clicked `Element`.
274+   *  - element (Element): clicked menu item.
275    * 
276    *  Function called on menu click. If `filterName` is name of an actual filter then
277    *  list in current column is replaced with a new one (provided by that filter).
278hunk ./contrib/musicplayer/src/controllers/Navigation.js 175
279    **/
280-  menuItemClick: function (filter_name, event, element) {
281+  replace: function (filter_name, event, element) {
282     if(!Navigation.columns[filter_name])
283       return;
284     
285hunk ./contrib/musicplayer/src/controllers/Navigation.js 179
286-    var parent = this.filter_column._el,
287-        header = this.filter_column.header,
288-        menu   = this.filter_column.menu;
289+    var parent  = this.filter_column._el,
290+        header  = this.filter_column.header,
291+        menu    = this.filter_column.menu,
292+        filter  = this.filter_column.column.options.filter;
293     
294     // we need to keep the menu and header, since
295hunk ./contrib/musicplayer/src/controllers/Navigation.js 185
296-    // all we need to do is to replace the list
297+    // all we need to do is to replace the list.
298+    // null-ifying those properties will make sure that
299+    // filter_column's destroy won't destroy them
300     this.filter_column.menu = null;
301     this.filter_column._el = null;
302     this.filter_column.destroy();
303hunk ./contrib/musicplayer/src/controllers/Navigation.js 194
304     
305     this.filter_column = new NavigationColumnContainer({
306       columnName: filter_name,
307-      filter: this.filter_column.column.options.filter,
308-      container: parent,
309-      header: header,
310-      menu: menu
311+      filter:     filter,
312+      container:  parent,
313+      header:     header,
314+      menu:       menu
315     });
316hunk ./contrib/musicplayer/src/controllers/Navigation.js 199
317-       
318+   
319     if(menu.last_clicked)
320       menu.last_clicked.removeClass("checked");
321hunk ./contrib/musicplayer/src/controllers/Navigation.js 202
322-    element.addClass("checked");
323+    if(element)
324+      element.addClass("checked");
325     
326hunk ./contrib/musicplayer/src/controllers/Navigation.js 205
327-    header.getElement(".column_title").empty().appendText(filter_name);
328+    header.getElement(".column_title").set("text", filter_name);
329   },
330   
331   /**
332hunk ./contrib/musicplayer/src/controllers/Navigation.js 209
333-   *  NavigationColumnContainer#listItemClick(item) -> undefined
334+   *  NavigationColumnContainer#createFilteredColumn(item) -> undefined
335    *  - item (Object): clicked item.
336    * 
337hunk ./contrib/musicplayer/src/controllers/Navigation.js 212
338-   *  Creates a new column after this one with applied filter.
339+   *  Creates a new column after (on the right) this one with applied filter which
340+   *  is generated using the data of the clicked item.
341    **/
342hunk ./contrib/musicplayer/src/controllers/Navigation.js 215
343-  listItemClick: function (item) {
344+  createFilteredColumn: function (item) {
345     if(this.filter_column)
346       this.filter_column.destroy();
347     
348hunk ./contrib/musicplayer/src/controllers/Navigation.js 228
349   /**
350    *  NavigationColumnContainer#destroy() -> this
351    * 
352-   *  Destroys this column (including menu).
353+   *  Destroys this column (including menu and header).
354    *  Removes itself from [[da.controller.Navigation.activeColumns]].
355    **/
356   destroy: function () {
357hunk ./contrib/musicplayer/src/controllers/Navigation.js 240
358       this.menu.destroy();
359       delete this.menu;
360     }
361-    this.column.destroy();
362-    delete this.column;
363+    if(this.column) {
364+      this.column.destroy();
365+      delete this.column;
366+    }
367     if(this._el) {
368       this._el.destroy();
369       delete this._el;
370hunk ./contrib/musicplayer/src/controllers/Navigation.js 249
371     }
372     
373-    var index = Navigation.activeColumns.indexOf(this);
374-    delete Navigation.activeColumns[index];
375-    Navigation.activeColumns = Navigation.activeColumns.clean();
376+    Navigation.activeColumns.erase(this);
377     
378     return this;
379   },
380hunk ./contrib/musicplayer/src/controllers/Navigation.js 271
381    *  da.controller.Navigation.columns
382    * 
383    *  Contains all known columns.
384-   *
385+   * 
386    *  #### Notes
387    *  Use [[da.controller.Navigation.registerColumn]] to add new ones,
388    *  *do not* add them manually.
389hunk ./contrib/musicplayer/src/controllers/Navigation.js 281
390   /**
391    *  da.controller.Navigation.activeColumns -> [NavigationColumnContainer, ...]
392    * 
393-   *  Array of currently active columns.
394+   *  Array of currently active (visible) columns.
395    *  The first column is always [[da.controller.Navigation.columns.Root]].
396    **/
397   activeColumns: [],
398hunk ./contrib/musicplayer/src/controllers/Navigation.js 304
399     root_column.filter_column = artists_column;
400     root_column.header = artists_column.header;
401     
402-    this._header_height = root_column.header.getHeight();
403+    this._header_height = artists_column.header.getHeight();
404     window.addEvent("resize", function () {
405       var columns = Navigation.activeColumns,
406           n = columns.length,
407hunk ./contrib/musicplayer/src/controllers/Navigation.js 344
408     this.columns[name] = col;
409     if(name !== "Root")
410       this.columns.Root.filters.push(name);
411+   
412+    // TODO: If Navigation is initialized
413+    // then Root's menu has to be updated.
414   }
415 };
416 
417hunk ./contrib/musicplayer/src/controllers/Settings.js 266
418   }
419 });
420 
421+// Settings dialog (DOM nodes) will be destroyed
422+// a minute after last da.controller.Settings.hide() call.
423+// Freeing up memory.
424+var destroy_timeout;
425+function destroySettingsDialog () {
426+  Settings.initialized = false;
427
428+  Settings.column.destroy();
429+  Settings.dialog.destroy();
430
431+  delete Settings.column;
432+  delete Settings.dialog;
433+  delete Settings._el;
434+  delete Settings._controls;
435+}
436+
437 /**
438  * da.controller.Settings
439  *
440hunk ./contrib/musicplayer/src/controllers/Settings.js 296
441    *    The description will be displayed at the top of settings dialog.
442    **/
443   registerGroup: function (config) {
444-    GROUPS[name] = config;
445+    GROUPS.push(config);
446+   
447     return this;
448   },
449 
450hunk ./contrib/musicplayer/src/controllers/Settings.js 303
451   /**
452    *  da.controller.Settings.addRenderer(name, renderer) -> this
453-   *  - name (String): name of the renderer. [[da.db.DocumentTemplate.Setting]] uses this in `representAs` property.
454+   *  - name (String): name of the renderer.
455+   *    [[da.db.DocumentTemplate.Setting]] uses this in `representAs` property.
456    *  - renderer (Function): function which renderes specific setting.
457    *
458    *  As first argument `renderer` function takes [[Setting]] object,
459hunk ./contrib/musicplayer/src/controllers/Settings.js 311
460    *  while the second one is the result of [[da.db.DocumentTemplate.Setting.getDetails]].
461    *
462    *  The function *must* return an [[Element]] with `setting_box` CSS class name.
463-   *  The very same element *must* contain another element with `setting_<setting id>` id property.
464+   *  The very same element *must* contain another element whose `id` attribute
465+   *  must mach following pattern: `setting_<setting id>`. ie. it should
466+   *  return something like:
467+   *
468+   *      <div class="setting_box">
469+   *        <label for="setting_first_name">Your name:</label>
470+   *        <input type="text" id="setting_first_name"/>
471+   *      </div>
472+   *
473    *  That element will be passed to the serializer function.
474    *
475    *  #### Default renderers
476hunk ./contrib/musicplayer/src/controllers/Settings.js 324
477    *  * `text`
478-   *  * `numeric` (same as `text`, the only difference is in serializer)
479+   *  * `numeric` (same as `text`, the only difference is in the serializer
480+   *    which will convert value into [[Number]])
481    *  * `password`
482    *  * `checkbox`
483hunk ./contrib/musicplayer/src/controllers/Settings.js 328
484+   *
485    **/
486   addRenderer: function (name, fn) {
487     if(!(name in RENDERERS))
488hunk ./contrib/musicplayer/src/controllers/Settings.js 347
489     if(!(name in SERIALIZERS))
490       SERIALIZERS[name] = serializer;
491     
492-    return this; 
493+    return this;
494   },
495   
496   /**
497hunk ./contrib/musicplayer/src/controllers/Settings.js 356
498    *  Shows the settings dialog.
499    **/
500   show: function () {
501+    clearTimeout(destroy_timeout);
502     if(!Settings.initialized)
503       Settings.initialize();
504     
505hunk ./contrib/musicplayer/src/controllers/Settings.js 372
506    **/
507   hide: function () {
508     Settings.hide();
509+    destroy_timeout = setTimeout(destroySettingsDialog, 60*60*1000);
510   },
511   
512   /**
513hunk ./contrib/musicplayer/src/controllers/Settings.js 381
514    **/
515   showGroup: function (group) {
516     this.show();
517+   
518     var n = GROUPS.length;
519     while(n--)
520       if(GROUPS[n].id === group)
521hunk ./contrib/musicplayer/src/controllers/Settings.js 387
522         break;
523     
524-    Settings.renderGroup({id: group, value: GROUPS[n+1]});
525+    Settings.renderGroup({id: group, value: GROUPS[n]});
526   }
527 };
528 
529hunk ./contrib/musicplayer/src/doctemplates/Artist.js 5
530 /**
531  *  class da.db.DocumentTemplate.Artist < da.db.DocumentTemplate
532  *  hasMany: [[da.db.DocumentTemplate.Song]]
533- *  belongsTo: [[da.db.DocumentTemplate.Artist]]
534  *
535  *  #### Standard properties
536hunk ./contrib/musicplayer/src/doctemplates/Artist.js 7
537- *  * `title` - name of the artist
538+ *  - title (String): name of the artist
539  *
540  **/
541 (function () {
542hunk ./contrib/musicplayer/src/doctemplates/Artist.js 18
543   
544   hasMany: {
545     songs: "Song"
546-  },
547
548-  belongsTo: {
549-    artist: "Artist"
550   }
551 }));
552 
553hunk ./contrib/musicplayer/src/doctemplates/Setting.js 56
554       properties: {id: template.id},
555       onSuccess: function (doc, was_created) {
556         if(was_created)
557-          doc.update({   
558+          doc.update({
559             group_id: template.group_id,
560             value:    template.value
561           });
562hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 433
563    *  - id (String): name of the view.
564    **/
565   killView: function (id) {
566+    delete this.views[id].view;
567     delete this.views[id];
568     return this;
569   }
570hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 274
571         if(!result.rows.length)
572           return options.onFailure();
573         
574-        var n = result.rows.length;
575+        var n = result.rows.length, row, type;
576         while(n--) {
577hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 276
578-          var row = result.rows[n].value,
579-              type = DocumentTemplate[row.type];
580+          row = result.rows[n].value;
581+          type = DocumentTemplate[row.type];
582           
583           result.rows[n] = type ? new type(row) : row;
584         }
585hunk ./contrib/musicplayer/src/libs/ui/Column.js 3
586 //#require "libs/ui/ui.js"
587 
588+(function () {
589 /** section: UserInterface
590  *  class da.ui.Column
591  *  implements Events, Options
592hunk ./contrib/musicplayer/src/libs/ui/Column.js 10
593  * 
594  *  Widget which can efficiently display large amounts of items in a list.
595  **/
596+var IDS = 0;
597 da.ui.Column = new Class({
598   Implements: [Events, Options],
599   
600hunk ./contrib/musicplayer/src/libs/ui/Column.js 15
601   options: {
602-    id:             undefined,
603+    id:            null,
604     rowHeight:     30,
605     totalCount:     0,
606hunk ./contrib/musicplayer/src/libs/ui/Column.js 18
607-    renderTimeout: 120,
608-    itemClassNames: "column_item"
609+    renderTimeout: 120
610   },
611   /**
612    *  new da.ui.Column(options)
613hunk ./contrib/musicplayer/src/libs/ui/Column.js 26
614    *    if ommited, random one will be generated.
615    *  - options.rowHeight (Number): height of an row. Defaults to 30.
616    *  - options.totalCount (Number): number of items this column has to show in total.
617-   *  - options.itemClassNames (String): CSS class names added to each item. Defaults to `column_item`.
618    *  - options.renderTimeout (Number): milliseconds to wait during the scroll before rendering
619    *    items. Defaults to 120.
620    * 
621hunk ./contrib/musicplayer/src/libs/ui/Column.js 41
622   initialize: function (options) {
623     this.setOptions(options);
624     if(!this.options.id)
625-      this.options.id = "column_" + Math.uuid(5);
626-
627+      this.options.id = "column_" + (IDS++);
628+   
629     this._populated = false;
630     // #_rendered will contain keys of items which have been rendered.
631     // What is a key is up to particular implementation.
632hunk ./contrib/musicplayer/src/libs/ui/Column.js 49
633     this._rendered = [];
634     
635     this._el = new Element("div", {
636-      id: options.id,
637-      'class': 'column',
638+      id: options.id + "_column",
639+      "class": "column",
640       styles: {
641         overflowX: "hidden",
642         overflowY: "auto",
643hunk ./contrib/musicplayer/src/libs/ui/Column.js 72
644     this._weight.injectBottom(this._el);
645 
646     // scroll event is fired for even smallest changes
647-    // of scrollbars positions, since rendering items can be
648+    // of scrollbar's position, since rendering items can be
649     // expensive a small timeout will be set in order to save
650hunk ./contrib/musicplayer/src/libs/ui/Column.js 74
651-    // some bandwidth - the negative side is that flicker is seen
652+    // some bandwidth - the downside is that flickering will be seen
653     // while scrolling.
654     var scroll_timer = null,
655         timeout = this.options.renderTimeout,
656hunk ./contrib/musicplayer/src/libs/ui/Column.js 86
657     });
658     
659     // We're caching lists' height so we won't have to
660-    // ask for it in every #render() - which can be quite expensiv.
661+    // ask for it in every #render() - which can be quite expensive.
662     this._el.addEvent("resize", function () {
663       this._el_height = this._el.getHeight();
664     }.bind(this));
665hunk ./contrib/musicplayer/src/libs/ui/Column.js 117
666         ids = this.getVisibleIndexes(),
667         n = Math.max(0, ids[0] - 6),
668         m = Math.max(Math.min(ids[1] + 10, total_count), total_count),
669-        box = new Element("div", {"class": "column_items_box"}),
670-        item_class = this.options.itemClassNames,
671-        first_rendered = null;
672+        first_rendered = -1,
673+        box;
674 
675     for( ; n < m; n++) {
676       if(!this._rendered.contains(n)) {
677hunk ./contrib/musicplayer/src/libs/ui/Column.js 122
678-        // First item in viewport could be already rendered
679-        // this helps minimizing amount of DOM nodes that will be inserted
680-        if(first_rendered === null)
681+        // First item in viewport could be already rendered,
682+        // by detecting the first item we're 'gonna render
683+        // helps minimizing amount of DOM nodes that will be inserted
684+        // (and avoids duplicaton).
685+        if(first_rendered === -1) {
686           first_rendered = n;
687hunk ./contrib/musicplayer/src/libs/ui/Column.js 128
688+          box = new Element("div", {"class": "column_items_box"});
689+        }
690 
691hunk ./contrib/musicplayer/src/libs/ui/Column.js 131
692-        this.renderItem(n).addClass(item_class).store("column_index", n).injectBottom(box);
693+        this.renderItem(n)
694+            .addClass("column_item")
695+            .store("column_index", n)
696+            .injectBottom(box);
697         this._rendered.push(n);
698       }
699     }
700hunk ./contrib/musicplayer/src/libs/ui/Column.js 139
701     
702-    if(first_rendered !== null) {
703+    if(first_rendered !== -1) {
704       var coords = this.getBoxCoords(first_rendered);
705       box.setStyles({
706         position: "absolute",       
707hunk ./contrib/musicplayer/src/libs/ui/Column.js 143
708-        top: coords[0],
709-        left: coords[1]
710+        top:      coords[0],
711+        left:     coords[1]
712       }).injectBottom(this._el);
713     }
714     
715hunk ./contrib/musicplayer/src/libs/ui/Column.js 245
716   destroy: function (dispose) {
717     this._el.destroy();
718     delete this._el;
719+   
720+    this._weight.destroy();
721+    delete this._weight;
722+   
723     return this;
724   },
725   
726hunk ./contrib/musicplayer/src/libs/ui/Column.js 259
727     return this._el;
728   }
729 });
730+
731+})();
732+
733hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 19
734 
735   /**
736    *  new da.ui.Dialog(options)
737-   *  - options.title (String): title of the dialog. optional.
738-   *  - options.hideOnOutsideClick (Boolean): if `true`, the dialog will be hidden when
739-   *    click outside the dialog element (ie. on the dimmed portion of screen) occurs.
740+   *  - options.title (String | Element): title of the dialog. optional.
741+   *  - options.hideOnOutsideClick (Boolean): if `true`, the dialog will be
742+   *    hidden when the click outside the dialog element occurs (ie. on the dimmed
743+   *    portion of screen)
744    *  - options.show (Boolean): if `true` the dialog will be shown immediately as it's created.
745    *    Defaults to `false`.
746    *  - options.html (Element): contents of the.
747hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 79
748    *  fires show
749    **/
750   show: function () {
751+    if(this._el.style.display !== "none")
752+      return this;
753+   
754     this._el.show();
755     this.fireEvent("show", [this]);
756     return this;
757hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 88
758   },
759 
760   /**
761-   *  da.ui.Dialog#hide(event) -> this
762+   *  da.ui.Dialog#hide([event]) -> this
763    *  fires hide
764    **/
765   hide: function (event) {
766hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 92
767-    if(event && event.target !== this._el)
768+    if((event && event.target !== this._el)
769+      || this._el.style.display === "none")
770       return this;
771hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 95
772-
773+   
774     this._el.hide();
775     this.fireEvent("hide", [this]);
776     return this;
777hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 101
778   },
779 
780+  /**
781+   *  da.ui.Dialog#toElement() -> Element
782+   **/
783+  toElement: function () {
784+    return this._el;
785+  },
786+
787   /**
788    *  da.ui.Dialog#destroy() -> this
789hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 110
790+   *  fires hide
791    **/
792hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 112
793-  destory: function () {
794-    this.options.html.destroy();
795+  destroy: function () {
796+    this.hide();
797+   
798     this._el.destroy();
799hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 116
800+    delete this._el;
801+    delete this.options;
802+   
803     return this;
804   }
805 });
806hunk ./contrib/musicplayer/src/libs/ui/Menu.js 4
807 //#require "libs/ui/ui.js"
808 
809 (function () {
810-var VISIBLE_MENU;
811-
812 /** section: UserInterface
813  *  class da.ui.Menu
814  *  implements Events, Options
815hunk ./contrib/musicplayer/src/libs/ui/Menu.js 61
816  *  * [MooTools Element class](http://mootools.net/docs/core/Element/Element#Element:constructor)
817  **/
818 
819+var VISIBLE_MENU, ID = 0;
820 da.ui.Menu = new Class({
821   Implements: [Events, Options],
822   
823hunk ./contrib/musicplayer/src/libs/ui/Menu.js 90
824     this._el = (new Element("ul")).addClass("menu").addClass("no_selection");
825     this._el.style.display = "none";
826     this._el.addEvent("click:relay(.menu_item a)", this.click.bind(this));
827-    //this._el.injectBottom(document.body);
828+    this._id = "_menu_" + (ID++) + "_";
829     
830     this.render();
831   },
832hunk ./contrib/musicplayer/src/libs/ui/Menu.js 129
833     var options = this.options.items[id], el;
834     
835     if(typeof options === "function")
836-      el = options(this).addClass("menu_item");
837+      el = options(id).addClass("menu_item");
838     else
839       el = new Element("li").grab(new Element("a", options));
840hunk ./contrib/musicplayer/src/libs/ui/Menu.js 132
841-     
842+   
843+    el.id = this._id + id;
844+   
845     return el.addClass("menu_item").store("menu_key", id);
846   },
847   
848hunk ./contrib/musicplayer/src/libs/ui/Menu.js 157
849    *  If `value` is an [[Object]] then it will be passed as second argument to MooTools's [[Element]] class.
850    *  If `value` is an [[Function]] then it has return an [[Element]],
851    *  first argument of the function is id of the item that needs to be rendered.
852+   * 
853+   *  #### Notes
854+   *  `id` attribute of the element will be overwritten in both cases.
855+   *   *Do not* depend on them in your code.
856    **/
857   addItem: function (id, value) {
858     this.options.items[id] = value;
859hunk ./contrib/musicplayer/src/libs/ui/Menu.js 180
860   },
861   
862   /**
863+   *  da.ui.Menu#getItem(id) -> Element
864+   *  - id (String): id of the item.
865+   * 
866+   *  Returns DOM representing the menu item.
867+   * 
868+   *  #### Notes
869+   *  Never overwrite `id` attribute of the element,
870+   *  as this very method relies on it.
871+   **/
872+  getItem: function (id) {
873+    return $(this._id + id);
874+  },
875
876+  /**
877    *  da.ui.Menu#addSeparator() -> this
878    * 
879    *  Adds separator to the menu.
880hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 199
881   
882   destroy: function () {
883     this.parent();
884+    delete this._rows;
885+   
886     if(this.view)
887       if(this.options.killView)
888         (this.options.db || da.db.DEFAULT).killView(this.view.id);
889hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 204
890-      else
891+      else
892         (this.options.db || da.db.DEFAULT).removeEvent("update." + this.view.id, this.view.updated);
893   },
894   
895hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 329
896     }
897     
898     return result;
899+  },
900
901+  destroy: function () {
902+    delete this.data;
903   }
904 });
905 
906hunk ./contrib/musicplayer/src/libs/util/ID3.js 63
907   },
908   
909   _onFileFetched: function (data) {
910-    if(this.parsers[0] && this.parsers[0].test(data))
911-      this.parser = (new this.parsers[0](data, this.options, this.request));
912-    else
913+    var parser = this.parsers[0];
914+    if(parser && parser.test(data)) {
915+      this.parser = (new parser(data, this.options, this.request));
916+      delete this.parsers;
917+    } else
918       this._getFile(this.parsers.shift());
919hunk ./contrib/musicplayer/src/libs/util/ID3.js 69
920+  },
921
922+  /**
923+   *  ID3#destory() -> undefined
924+   **/
925+  destroy: function () {
926+    if(this.parser)
927+      this.parser.destroy();
928+    delete this.parser;
929+    delete this.request;
930+    delete this.options;
931   }
932 });
933 
934hunk ./contrib/musicplayer/src/libs/util/ID3v1.js 48
935       this.tags.year = 0;
936     
937     this.options.onSuccess(CACHE[this.options.url] = this.tags);
938+  },
939
940+  destroy: function () {
941+    delete this.data;
942+    delete this.options;
943   }
944 });
945 
946hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 375
947         official: f.WOAR || f.WXXX || f.WAR || f.WXX || ""
948       }
949     };
950+  },
951
952+  /**
953+   *  ID3v2Parser#destroy() -> undefined
954+   **/
955+  destroy: function () {
956+    this.data.destroy();
957+    delete this.data;
958+    delete this.options;
959+    delete this._request;
960   }
961 });
962 
963hunk ./contrib/musicplayer/src/workers/indexer.js 39
964       uri = "/uri/" + encodeURIComponent(cap);
965   
966   queue++;
967-  new da.util.ID3({
968+  var parser = new da.util.ID3({
969     url: uri,
970     onSuccess: function (tags) {
971hunk ./contrib/musicplayer/src/workers/indexer.js 42
972-      // To avoid duplication, we're using id property (which is mandatary) to store
973-      // read-cap, which is probably already "more unique" than Math.uuid()
974       if(tags && typeof tags.title !== "undefined" && typeof tags.artist !== "undefined") {
975         tags.id = cap;
976         postMessage(tags);
977hunk ./contrib/musicplayer/src/workers/indexer.js 46
978       }
979-
980+     
981+      parser.destroy();
982+      delete parser;
983+     
984       // Not all files are reporeted instantly so it might
985       // take some time for scanner.js/CollectionScanner.js to
986       // report the files, maximum delay we're allowing here
987hunk ./contrib/musicplayer/src/workers/indexer.js 58
988         setTimeout(checkQueue, 1000*60*1);
989     },
990     onFailure: function () {
991+      parser.destroy();
992+      delete parser;
993+     
994       if(!--queue)
995         setTimeout(checkQueue, 1000*60*1);
996     }
997hunk ./contrib/musicplayer/src/workers/scanner.js 27
998   obj.get(function () {
999     queue--;
1000     
1001-    if(obj.type === "filenode")
1002+    if(obj.type === "filenode") {
1003+      delete obj;
1004       return postMessage(obj.uri);
1005hunk ./contrib/musicplayer/src/workers/scanner.js 30
1006+    }
1007     
1008     var n = obj.children.length;
1009     while(n--) {
1010hunk ./contrib/musicplayer/src/workers/scanner.js 42
1011         scan(child);
1012     }
1013     
1014+    delete obj;
1015     if(!queue)
1016       postMessage("**FINISHED**");
1017   });
1018hunk ./contrib/musicplayer/tests/initialize.js 4
1019 windmill.jsTest.require("shared.js");
1020 
1021 windmill.jsTest.register([
1022-//  'test_utils',
1023   'test_Goal',
1024   'test_BinaryFile',
1025   'test_ID3',
1026hunk ./contrib/musicplayer/tests/initialize.js 9
1027   'test_ID3v1',
1028   'test_ID3v2',
1029+  'test_BrowserCouchDict',
1030   'test_BrowserCouch',
1031hunk ./contrib/musicplayer/tests/initialize.js 11
1032+  'test_BrowserCouch_tempView',
1033+  'test_BrowserCouch_liveView',
1034   'test_DocumentTemplate',
1035   
1036hunk ./contrib/musicplayer/tests/initialize.js 15
1037-  'test_NavigationController'
1038+  'test_CollectionScannerController',
1039+  'test_Menu',
1040+  'test_NavigationController',
1041+  'test_Dialog',
1042+  'test_SettingsController'
1043 ]);
1044hunk ./contrib/musicplayer/tests/shared.js 31
1045             self.frames = frames;
1046           }
1047         }, {});
1048+        data = null;
1049       };
1050 
1051       this.test_waitForData = {
1052hunk ./contrib/musicplayer/tests/shared.js 55
1053         jum.assertSameObjects(SHARED.songs[vkey].frames,self.frames);
1054       };
1055       
1056+      this.teardown = function () {
1057+        self.parser.destroy();
1058+        delete self.parser;
1059+      };
1060+     
1061       return this;
1062     };
1063   }
1064hunk ./contrib/musicplayer/tests/shared.js 71
1065   // catches cases when one of args is null
1066   if(!a || !b)
1067     jum.assertEquals(a, b);
1068
1069-  for(var prop in a)
1070-    if(a.hasOwnProperty(prop))
1071-      if(prop in a && prop in b)
1072-        if(typeof a[prop] === "object")
1073-          jum.assertSameObjects(a[prop], b[prop]);
1074-        else
1075-          jum.assertEquals(a[prop], b[prop]);
1076+   
1077+  if($type(a) === "array") {
1078+    jum.assertTrue("arrays should be of the same length",
1079+      a.length === b.length
1080+    );
1081+   
1082+    // we're doing this in case of of the
1083+    // objects is an 'arguments' object,
1084+    // which lack .indexOf()
1085+    a = $A(a);
1086+    b = $A(b);
1087+   
1088+    for(var n = 0, m = a.length; n < m; n++)
1089+      if(typeof a[n] === "object")
1090+        jum.assertSameObjects(a[n], b[n]);
1091       else
1092hunk ./contrib/musicplayer/tests/shared.js 87
1093-        jum.assertTrue("missing '" + prop +"' property", false);
1094+        jum.assertTrue("should contain '" + a[n] + "'",
1095+          b.indexOf(a[n]) !== -1
1096+        );
1097+  } else {
1098+    for(var prop in a)
1099+      if(a.hasOwnProperty(prop))
1100+        if(prop in a && prop in b)
1101+          if(typeof a[prop] === "object")
1102+            jum.assertSameObjects(a[prop], b[prop]);
1103+          else
1104+            jum.assertEquals(a[prop], b[prop]);
1105+        else
1106+          jum.assertTrue("missing '" + prop +"' property", false);
1107+  }
1108   
1109hunk ./contrib/musicplayer/tests/shared.js 102
1110+  delete a;
1111+  delete b;
1112   return true;
1113 };
1114hunk ./contrib/musicplayer/tests/shared.js 106
1115+
1116hunk ./contrib/musicplayer/tests/test_BinaryFile.js 8
1117       self = this;
1118   
1119   this.setup = function () {
1120-    this.file_le = new BinaryFile("\0\0\1\0");
1121-    this.file_be = new BinaryFile("\0\1\0\0", {bigEndian: true});   
1122-    this.bond = new BinaryFile("A\0\0\7James Bond\0");
1123+    self.file_le = new BinaryFile("\0\0\1\0");
1124+    self.file_be = new BinaryFile("\0\1\0\0", {bigEndian: true});   
1125+    self.bond    = new BinaryFile("A\0\0\7James Bond\0");
1126   };
1127   
1128   this.test_options = function () {
1129hunk ./contrib/musicplayer/tests/test_BinaryFile.js 14
1130-    jum.assertEquals(4, this.file_le.length);
1131-    jum.assertFalse(this.file_le.bigEndian);
1132+    jum.assertEquals(4, self.file_le.length);
1133+    jum.assertFalse(self.file_le.bigEndian);
1134     
1135hunk ./contrib/musicplayer/tests/test_BinaryFile.js 17
1136-    jum.assertEquals(4, this.file_be.length);
1137-    jum.assertTrue(this.file_be.bigEndian);
1138+    jum.assertEquals(4, self.file_be.length);
1139+    jum.assertTrue(self.file_be.bigEndian);
1140   };
1141   
1142   this.test_getByte = function () {
1143hunk ./contrib/musicplayer/tests/test_BinaryFile.js 22
1144-    jum.assertEquals(0, this.file_le.getByteAt(0));
1145-    jum.assertEquals(1, this.file_le.getByteAt(2));
1146+    jum.assertEquals(0, self.file_le.getByteAt(0));
1147+    jum.assertEquals(1, self.file_le.getByteAt(2));
1148     
1149hunk ./contrib/musicplayer/tests/test_BinaryFile.js 25
1150-    jum.assertEquals(0, this.file_be.getByteAt(0));
1151-    jum.assertEquals(1, this.file_be.getByteAt(1));
1152+    jum.assertEquals(0, self.file_be.getByteAt(0));
1153+    jum.assertEquals(1, self.file_be.getByteAt(1));
1154   };
1155   
1156   this.test_getShort = function () {
1157hunk ./contrib/musicplayer/tests/test_BinaryFile.js 30
1158-    jum.assertEquals(0,   this.file_le.getShortAt(0)); // 00
1159-    jum.assertEquals(256, this.file_le.getShortAt(1)); // 01
1160-    jum.assertEquals(1,   this.file_le.getShortAt(2)); // 10
1161+    jum.assertEquals(0,   self.file_le.getShortAt(0)); // 00
1162+    jum.assertEquals(256, self.file_le.getShortAt(1)); // 01
1163+    jum.assertEquals(1,   self.file_le.getShortAt(2)); // 10
1164     
1165hunk ./contrib/musicplayer/tests/test_BinaryFile.js 34
1166-    jum.assertEquals(1,   this.file_be.getShortAt(0)); // 01
1167-    jum.assertEquals(256, this.file_be.getShortAt(1)); // 10
1168-    jum.assertEquals(0,   this.file_be.getShortAt(2)); // 00
1169+    jum.assertEquals(1,   self.file_be.getShortAt(0)); // 01
1170+    jum.assertEquals(256, self.file_be.getShortAt(1)); // 10
1171+    jum.assertEquals(0,   self.file_be.getShortAt(2)); // 00
1172   };
1173   
1174   this.test_getLong = function () {
1175hunk ./contrib/musicplayer/tests/test_BinaryFile.js 40
1176-    jum.assertEquals(65536, this.file_le.getLongAt(0));
1177-    jum.assertEquals(65536, this.file_be.getLongAt(0));
1178+    jum.assertEquals(65536, self.file_le.getLongAt(0));
1179+    jum.assertEquals(65536, self.file_be.getLongAt(0));
1180   };
1181   
1182   this.test_getBits = function () {
1183hunk ./contrib/musicplayer/tests/test_BinaryFile.js 45
1184-    jum.assertSameObjects([0, 1], this.file_le.getBitsAt(2, 2));
1185-    jum.assertSameObjects([0, 0, 0, 1], this.file_be.getBitsAt(1, 4));
1186+    jum.assertSameObjects([0, 1], self.file_le.getBitsAt(2, 2));
1187+    jum.assertSameObjects([0, 0, 0, 1], self.file_be.getBitsAt(1, 4));
1188   };
1189   
1190   this.test_unpack = function () {
1191hunk ./contrib/musicplayer/tests/test_BinaryFile.js 50
1192-    jum.assertSameObjects(["A", 0, 0, 7], this.bond.unpack("c3i"));
1193-    jum.assertSameObjects(["James Bond"], this.bond.unpack("4x10S"));
1194+    jum.assertSameObjects(["A", 0, 0, 7], self.bond.unpack("c3i"));
1195+    jum.assertSameObjects(["James Bond"], self.bond.unpack("4x10S"));
1196   };
1197   
1198   this.test_toEncodedString = function () {
1199hunk ./contrib/musicplayer/tests/test_BinaryFile.js 55
1200-    jum.assertEquals("%00%00%01%00", this.file_le.toEncodedString());
1201-    jum.assertEquals("%00%01%00%00", this.file_be.toEncodedString());
1202+    jum.assertEquals("%00%00%01%00", self.file_le.toEncodedString());
1203+    jum.assertEquals("%00%01%00%00", self.file_be.toEncodedString());
1204+  };
1205
1206+  this.teardown = function () {
1207+    self.file_le.destroy();
1208+    self.file_be.destroy();
1209+    self.bond.destroy();
1210+   
1211+    delete self.file_le;
1212+    delete self.file_be;
1213+    delete self.bond;
1214   };
1215   
1216   return this;
1217hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 4
1218 windmill.jsTest.require("shared.js");
1219 
1220 var test_BrowserCouchDict = new function () {
1221-  var BrowserCouch = da.db.BrowserCouch,
1222-      self = this;
1223+  var self = this;
1224   
1225   this.setup = function () {
1226hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 7
1227-    self.dict = new BrowserCouch.Dictionary();
1228+    self.dict = new da.db.BrowserCouch.Dictionary();
1229     
1230hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 9
1231-    this.dict.set("a", 1);
1232-    this.dict.set("b", 2);
1233+    self.dict.set("a", 1);
1234+    self.dict.set("b", 2);
1235     
1236hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 12
1237-    this.dict.setDocs([
1238+    self.dict.setDocs([
1239       {id: "c", value: 3},
1240       {id: "d", value: 4},
1241       {id: "a", value: 5}
1242hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 20
1243   };
1244   
1245   this.test_set = function () {
1246-    jum.assertTrue(this.dict.has("a"));
1247-    jum.assertTrue(this.dict.has("b"));
1248-    jum.assertFalse(this.dict.has("x"));
1249+    jum.assertTrue(self.dict.has("a"));
1250+    jum.assertTrue(self.dict.has("b"));
1251+    jum.assertFalse(self.dict.has("x"));
1252     
1253     jum.assertSameObjects({id:"a", value: 5}, this.dict.dict.a);
1254hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 25
1255-    jum.assertEquals(2, this.dict.dict.b);
1256+    jum.assertEquals(2, self.dict.dict.b);
1257   };
1258   
1259   this.test_setDocs = function () {
1260hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 29
1261-    jum.assertTrue(this.dict.has("c"));
1262-    jum.assertTrue(this.dict.has("d"));
1263+    jum.assertTrue(self.dict.has("c"));
1264+    jum.assertTrue(self.dict.has("d"));
1265     
1266hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 32
1267-    jum.assertEquals(3, this.dict.dict.c.value);
1268-    jum.assertEquals(4, this.dict.dict.d.value);
1269+    jum.assertEquals(3, self.dict.dict.c.value);
1270+    jum.assertEquals(4, self.dict.dict.d.value);
1271   };
1272   
1273   this.test_remove = function () {
1274hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 37
1275-    this.dict.remove("a");
1276-    jum.assertEquals(3, this.dict.keys.length);
1277-    jum.assertFalse(this.dict.has("a"));   
1278+    self.dict.remove("a");
1279+    jum.assertEquals(3, self.dict.keys.length);
1280+    jum.assertFalse(self.dict.has("a"));   
1281   };
1282   
1283   this.test_unpickle = function () {
1284hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 43
1285-    this.dict.unpickle({
1286+    self.dict.unpickle({
1287       x: 2.2,
1288       y: 2.3
1289     });
1290hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 48
1291     
1292-    jum.assertEquals(2, this.dict.keys.length);
1293-    jum.assertTrue(this.dict.has("x"));
1294-    jum.assertTrue(this.dict.has("y"));
1295-    jum.assertFalse(this.dict.has("a"));
1296+    jum.assertEquals(2, self.dict.keys.length);
1297+    jum.assertTrue(self.dict.has("x"));
1298+    jum.assertTrue(self.dict.has("y"));
1299+    jum.assertFalse(self.dict.has("a"));
1300   };
1301   
1302   this.test_clear = function () {
1303hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 55
1304-    this.dict.clear();
1305+    self.dict.clear();
1306     
1307hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 57
1308-    jum.assertEquals(0, this.dict.keys.length);
1309-    jum.assertFalse(this.dict.has("x"));
1310-    jum.assertFalse(this.dict.has("b"));
1311+    jum.assertEquals(0, self.dict.keys.length);
1312+    jum.assertFalse(self.dict.has("x"));
1313+    jum.assertFalse(self.dict.has("b"));
1314   };
1315 };
1316 
1317hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 68
1318       self = this;
1319   
1320   this.setup = function () {
1321-    this.db = false;
1322-    this.stored = {};
1323+    self.db = false;
1324+    self.stored = {};
1325     
1326     BrowserCouch.get("test1", function (db) {
1327       self.db = db;
1328hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 92
1329   
1330   this.test_put = function () {
1331     var cb = {doc1: 0, doc2: 0, doc3: 0};
1332-    this.db.put({id: "doc1", test: 1}, function () { cb.doc1++ });
1333-    this.db.put({id: "doc2", test: 2}, function () { cb.doc2++ });
1334-    this.db.put({id: "doc3", test: 3}, function () { cb.doc3++ });
1335-    this.db.put({id: "doc1", test: 4}, function () { cb.doc1++ });
1336+    self.db.put({id: "doc1", test: 1}, function () { cb.doc1++ });
1337+    self.db.put({id: "doc2", test: 2}, function () { cb.doc2++ });
1338+    self.db.put({id: "doc3", test: 3}, function () { cb.doc3++ });
1339+    self.db.put({id: "doc1", test: 4}, function () { cb.doc1++ });
1340     
1341     jum.assertEquals(2, cb.doc1);
1342     jum.assertEquals(1, cb.doc2);
1343hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 108
1344   };
1345   
1346   this.test_wipe = function () {
1347-    jum.assertEquals(3, this.db.getLength());
1348-    this.db.wipe();
1349+    jum.assertEquals(3, self.db.getLength());
1350+    self.db.wipe();
1351     
1352     BrowserCouch.get("test1", function (db) {
1353       jum.assertEquals(0, db.getLength());
1354hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 152
1355   };
1356   
1357   this.test_map = function () {
1358+    self.mapped_docs = [];
1359     this.db.view({
1360       temporary: true,
1361       
1362hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 157
1363       map: function (doc, emit) {
1364-        self.map_called++;
1365+        self.mapped_docs.push(doc.id);
1366         if(doc.nr !== 2)
1367           emit(doc.id, doc.nr);
1368       },
1369hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 181
1370     }
1371   };
1372   
1373+  this.test_verifyMapArguments = function () {
1374+    jum.assertEquals("map() should have been called three times",
1375+      3, self.mapped_docs.length
1376+    );
1377+    jum.assertSameObjects(["doc1", "doc2", "doc3"], self.mapped_docs);
1378+  };
1379
1380   this.test_verifyMapResult = function () {
1381     var mr = self.map_result;
1382     
1383hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 191
1384-    jum.assertEquals(3, self.map_called);
1385     jum.assertTrue("rows" in mr);
1386     jum.assertEquals(2, mr.rows.length);
1387     jum.assertEquals("function", typeof mr.findRow);
1388hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 244
1389   };
1390   
1391   this.test_verifyReduceResult = function () {
1392-    var rr = this.reduce_result;
1393-    jum.assertFalse(this.reduce_updated_called);
1394+    var rr = self.reduce_result;
1395+    jum.assertFalse(self.reduce_updated_called);
1396     
1397     jum.assertEquals("function", typeof rr.findRow);
1398     jum.assertEquals("function", typeof rr.getRow);
1399hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 255
1400   };
1401   
1402   this.test_verifyReduceFindRow = function () {
1403-    var rr = this.reduce_result;
1404+    var rr = self.reduce_result;
1405     
1406     jum.assertTrue(rr.findRow("even") !== -1);
1407     jum.assertTrue(rr.findRow("odd")  !== -1);
1408hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 266
1409   };
1410   
1411   this.teardown = function () {
1412-    this.db.wipe();
1413+    self.db.wipe();
1414   };
1415   
1416   return this;
1417hunk ./contrib/musicplayer/tests/test_Goal.js 5
1418   var Goal = da.util.Goal,
1419       self = this;
1420   this.test_setup = function () {
1421-    this._timestamps = {};
1422-    this._calls = {a: 0, b: 0, c: 0, afterC: 0, success: 0, setup: 0};
1423-    this._calls.setup++;
1424-    this._goal = new Goal({
1425+    self.timestamps = {};
1426+    self.calls = {a: 0, b: 0, c: 0, afterC: 0, success: 0, setup: 0};
1427+    self.calls.setup++;
1428+    self.goal = new Goal({
1429       checkpoints: ["a", "b", "c"],
1430       
1431       onCheckpoint: function (name) {
1432hunk ./contrib/musicplayer/tests/test_Goal.js 12
1433-        self._timestamps[name] = new Date();
1434-        self._calls[name]++;
1435+        self.timestamps[name] = new Date();
1436+        self.calls[name]++;
1437       },
1438       
1439       onFinish: function (name) {
1440hunk ./contrib/musicplayer/tests/test_Goal.js 17
1441-        self._timestamps.success = new Date();
1442-        self._calls.success++;
1443+        self.timestamps.success = new Date();
1444+        self.calls.success++;
1445       },
1446       
1447       afterCheckpoint: {
1448hunk ./contrib/musicplayer/tests/test_Goal.js 23
1449         c: function () {
1450-          self._timestamps.afterC = new Date();
1451-          self._calls.afterC++;
1452+          self.timestamps.afterC = new Date();
1453+          self.calls.afterC++;
1454         }
1455       }
1456     });
1457hunk ./contrib/musicplayer/tests/test_Goal.js 29
1458     
1459-    this._goal.checkpoint("b");
1460-    this._goal.checkpoint("c");
1461-    this._goal.checkpoint("a");
1462+    self.goal.checkpoint("b");
1463+    self.goal.checkpoint("c");
1464+    self.goal.checkpoint("a");
1465   
1466hunk ./contrib/musicplayer/tests/test_Goal.js 33
1467-    this._goal.checkpoint("c");
1468-    this._goal.checkpoint("b");
1469+    self.goal.checkpoint("c");
1470+    self.goal.checkpoint("b");
1471   };
1472   
1473   this.test_allEventsCalledOnce = function () {
1474hunk ./contrib/musicplayer/tests/test_Goal.js 38
1475-    jum.assertTrue(this._calls.a        === 1);
1476-    jum.assertTrue(this._calls.b        === 1);
1477-    jum.assertTrue(this._calls.c        === 1);
1478-    jum.assertTrue(this._calls.afterC   === 1);
1479-    jum.assertTrue(this._calls.success  === 1);
1480-    jum.assertTrue(this._goal.finished);
1481+    jum.assertTrue(self.calls.a        === 1);
1482+    jum.assertTrue(self.calls.b        === 1);
1483+    jum.assertTrue(self.calls.c        === 1);
1484+    jum.assertTrue(self.calls.afterC   === 1);
1485+    jum.assertTrue(self.calls.success  === 1);
1486+    jum.assertTrue(self.goal.finished);
1487   };
1488   
1489   this.test_timestamps = function () {
1490hunk ./contrib/musicplayer/tests/test_Goal.js 47
1491-    jum.assertTrue(this._timestamps.b <= this._timestamps.c);
1492-    jum.assertTrue(this._timestamps.c <= this._timestamps.a);
1493-    jum.assertTrue(this._timestamps.c <= this._timestamps.afterC);
1494+    jum.assertTrue(self.timestamps.b <= self.timestamps.c);
1495+    jum.assertTrue(self.timestamps.c <= self.timestamps.a);
1496+    jum.assertTrue(self.timestamps.c <= self.timestamps.afterC);
1497   };
1498   
1499   this.test_successCalls = function () {
1500hunk ./contrib/musicplayer/tests/test_Goal.js 53
1501-    jum.assertTrue(this._timestamps.success >= this._timestamps.a);
1502-    jum.assertTrue(this._timestamps.success >= this._timestamps.b);
1503-    jum.assertTrue(this._timestamps.success >= this._timestamps.c);
1504-    jum.assertTrue(this._timestamps.success >= this._timestamps.afterC);
1505+    jum.assertTrue(self.timestamps.success >= self.timestamps.a);
1506+    jum.assertTrue(self.timestamps.success >= self.timestamps.b);
1507+    jum.assertTrue(self.timestamps.success >= self.timestamps.c);
1508+    jum.assertTrue(self.timestamps.success >= self.timestamps.afterC);
1509   };
1510 };
1511hunk ./contrib/musicplayer/tests/test_ID3.js 23
1512   
1513   var self = this;
1514   this.setup = function () {
1515-    this.called_onSuccess = false;
1516-    this.called_onFailure = false;
1517+    self.called_onSuccess = false;
1518+    self.called_onFailure = false;
1519     
1520     new ID3_patched({
1521       url: "/fake/" + Math.uuid(),
1522hunk ./contrib/musicplayer/tests/test_ID3v1.js 10
1523       self        = this;
1524   
1525   this.setup = function () {
1526-    this.tags = {};
1527+    self.tags = {};
1528     
1529     SHARED.parser = new ID3v1Parser(BinaryFile.fromEncodedString(SHARED.songs.v1.data), {
1530       url: "/fake/" + Math.uuid(),
1531hunk ./contrib/musicplayer/tests/test_Menu.js 8
1532   this.setup = function () {
1533     self.menu = new Menu({
1534       items: {
1535-        a: {html: "a", id: "_test_first_menu_item"},
1536-        b: {html: "b", id: "_test_second_menu_item"},
1537+        a: {html: "a"},
1538+        b: {html: "b"},
1539         _sep: Menu.separator,
1540hunk ./contrib/musicplayer/tests/test_Menu.js 11
1541-        c: {html: "c", id: "_test_third_menu_item"}
1542+        c: function () {
1543+          return (new Element("li")).grab(new Element("span", {html: "c"}))
1544+        }
1545       }
1546     });
1547   };
1548hunk ./contrib/musicplayer/tests/test_Menu.js 18
1549   
1550-  this.test_domNode = function () {
1551+  this.test_domNodes = function () {
1552     var el = self.menu.toElement();
1553     
1554     jum.assertEquals("menu's element should be inserted into body of the page",
1555hunk ./contrib/musicplayer/tests/test_Menu.js 24
1556       el.getParent(), document.body
1557     );
1558-    //jum.assertEquals("should have four list items", )
1559+   
1560+    jum.assertEquals("should have four child nodes",
1561+      4, el.getChildren().length
1562+    );
1563+   
1564+    jum.assertEquals("item added as Object should be element",
1565+     "element",
1566+      $type(self.menu.getItem("b"))
1567+    );
1568+    jum.assertEquals("item added as Function should be element",
1569+      "element",
1570+      $type(self.menu.getItem("c"))
1571+    );
1572+    jum.assertEquals("separator should be also an element",
1573+      "element",
1574+      $type(self.menu.getItem("_sep"))
1575+    );
1576   };
1577   
1578   this.test_events = function () {
1579hunk ./contrib/musicplayer/tests/test_Menu.js 50
1580       jum.assertEquals("clicked items' key should be 'b'", "b", key);
1581     });
1582     // events are synchronous
1583-    self.menu.click(null, el.getElement("li:nth-child(2)"));
1584+    self.menu.click(null, self.menu.getItem("b"));
1585     
1586     var showed = 0,
1587         hidden = 0;
1588hunk ./contrib/musicplayer/tests/test_Menu.js 80
1589     );
1590   };
1591   
1592
1593   this.teardown = function () {
1594     self.menu.destroy();
1595   };
1596hunk ./contrib/musicplayer/tests/test_NavigationController.js 5
1597   var Navigation = da.controller.Navigation,
1598       self = this;
1599   
1600+  this.setup = function () {
1601+    self.maps_updated = false;
1602+    // We're doing this for sorting test
1603+    da.db.DocumentTemplate.Song.findFirst({
1604+      properties: {title: "Maps"},
1605+      onSuccess: function (maps) {
1606+        maps.update({track: 6}, function () {
1607+          self.maps_updated = true;
1608+        })
1609+      }
1610+    });
1611+  };
1612+     
1613   // We can't use da.controller.CollectionScanner.isFinished()
1614   // here because scanner worker has one minute timeout
1615   this.test_waitForCollectionScanner = {
1616hunk ./contrib/musicplayer/tests/test_NavigationController.js 24
1617     method: "waits.forJS",
1618     params: {
1619       js: function () {
1620-        return da.db.DEFAULT.views.Song.view.rows.length === 3
1621+        return da.db.DEFAULT.views.Song.view.rows.length === 3 && self.maps_updated;
1622       }
1623     }
1624   };
1625hunk ./contrib/musicplayer/tests/test_NavigationController.js 56
1626     );
1627   };
1628   
1629+  // Indirectly tests column re-rendering as all these
1630+  // items were dynamically added when CollectionScanner
1631+  // discovered them, as well as filtering.
1632   this.test_items = function () {
1633     var ac      = Navigation.activeColumns,
1634         artists = ac[1].column,
1635hunk ./contrib/musicplayer/tests/test_NavigationController.js 95
1636     );
1637   };
1638   
1639+  this.test_replaceFilter = function () {
1640+    var Root = Navigation.activeColumns[0];
1641+    Root.menu.click(null, Root.menu.getItem("Songs"));
1642+   
1643+    var container = Navigation.activeColumns[1],
1644+        column = container.column;
1645+   
1646+    jum.assertEquals("there should be only two active columns",
1647+      2, Navigation.activeColumns.length
1648+    );
1649+    jum.assertEquals("header should have new title",
1650+      "Songs", container.header.get('text')
1651+    );
1652+   
1653+    jum.assertEquals("there should be three songs",
1654+      3, column.options.totalCount
1655+    );
1656+
1657+    // again, indirect sorting test as 'Maps' is 6th track on the album
1658+    // but first alphabetically
1659+    jum.assertEquals("last song should be 'Maps'",
1660+      "Maps", column.getItem(2).value.title
1661+    );
1662+    jum.assertEquals("first song should be 'Persona'",
1663+      "Persona", column.getItem(0).value.title
1664+    );
1665+  };
1666
1667   return this;
1668 };
1669hunk ./src/allmydata/test/test_musicplayer.py 95
1670       }\n""" % (self.music_cap, self.settings_cap))
1671       config.close()
1672     d.addCallback(_write_config_file)
1673-
1674+   
1675     persona = upload.Data(b64decode(DATA['persona']), None)
1676     d.addCallback(lambda ign: self.music_node.add_file(u'persona', persona))
1677hunk ./src/allmydata/test/test_musicplayer.py 98
1678-
1679+   
1680     bigbang = upload.Data(b64decode(DATA['bigbang']), None)
1681     d.addCallback(lambda ign: self.music_node.add_file(u'bigbang', bigbang))
1682hunk ./src/allmydata/test/test_musicplayer.py 101
1683-
1684+   
1685     maps = upload.Data(b64decode(DATA['maps']), None)
1686     d.addCallback(lambda ign: self.music_node.add_file(u'maps',    maps))
1687 
1688hunk ./src/allmydata/test/test_musicplayer.py 110
1689 class ChromeTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Chrome):
1690   pass
1691 
1692-#  .
1693 #class FirefoxTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Firefox):
1694hunk ./src/allmydata/test/test_musicplayer.py 111
1695-  #pass
1696+#  pass
1697}
1698[add-progress-bars
1699josip.lisec@gmail.com**20100713181106
1700 Ignore-this: b96a74ab38924967a55383f75f888439
1701 Added da.ui.ProgressBar and da.ui.SegmentedProgressBar classes,
1702 the latter one will be used mainly for visualizing progress of
1703 currently playing song and load percentage.
1704] {
1705hunk ./contrib/musicplayer/src/index_devel.html 43
1706     <script src="libs/ui/NavigationColumn.js" type="text/javascript" charset="utf-8"></script>
1707     <script src="libs/ui/Menu.js" type="text/javascript" charset="utf-8"></script>
1708     <script src="libs/ui/Dialog.js" type="text/javascript" charset="utf-8"></script>
1709+    <script src="libs/ui/ProgressBar.js" type="text/javascript" charset="utf-8"></script>
1710+    <script src="libs/ui/SegmentedProgressBar.js" type="text/javascript" charset="utf-8"></script>
1711 
1712     <script src="controllers/controllers.js" type="text/javascript" charset="utf-8"></script>
1713     <script src="controllers/Navigation.js" type="text/javascript" charset="utf-8"></script>
1714addfile ./contrib/musicplayer/src/libs/ui/ProgressBar.js
1715hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 1
1716+//#require <libs/ui/ui.js>
1717+
1718+(function () {
1719+/** section: UserInterface
1720+ *  class da.ui.ProgressBar
1721+ *
1722+ *  Canvas-based progress bar.
1723+ **/
1724+var ProgressBar = new Class({
1725+  Implements: Options,
1726
1727+  options: {
1728+    width: 200,
1729+    height: 20,
1730+    foreground: "#33519d"
1731+  },
1732
1733+  /**
1734+   *  new da.ui.ProgressBar([canvas], options)
1735+   *  - canvas (Element): already existing DOM node.
1736+   *    Note that in this case, this istance of [[da.ui.ProgressBar]] won't
1737+   *    monitor width/height changes.
1738+   *  - options.width (Number): width of the progress bar.
1739+   *  - options.height (Number): height of the progress bar.
1740+   *  - options.foreground (String): colour of the progress bar.
1741+   * 
1742+   *  #### Notes
1743+   *  To resize the progress bar after initialization use MooTools'
1744+   *  `setStyle` method, since it properly fires `resize` event.
1745+   *
1746+   *      progress_bar.toElement().setStyle("width", 100);
1747+   *
1748+   **/
1749+  initialize: function (canvas, options) {
1750+    this.setOptions(options);
1751+   
1752+    if(canvas) {
1753+      this._el = canvas;
1754+    } else {
1755+      this._el = new Element("canvas");
1756+      this._el.width = this.options.width;
1757+      this._el.height = this.options.height;
1758+      this._el.addClass("progressbar");
1759+     
1760+      this._el.addEvent("resize", function () {
1761+        this.options.width = this._el.getWidth();
1762+        this.options.height = this._el.getHeight();
1763+     
1764+        this.progress -= 0.0001;
1765+        this.setProgress(this.progress + 0.0001);
1766+      }.bind(this));
1767+    }
1768+   
1769+    this.ctx = this._el.getContext("2d");
1770+   
1771+    this.progress = 0;
1772+  },
1773
1774+  /**
1775+   *  da.ui.ProgressBar#setProgress(progress) -> this
1776+   *  - progress (Number): in 0-1 range.
1777+   *
1778+   *  #### Notes
1779+   *  Use animation with care, it should not be necessary for small changes
1780+   *  especially if the progress bar is narrow.
1781+   *
1782+   **/
1783+  setProgress: function (p) {
1784+    var current_progress  = this.progress,
1785+        el_width          = this.options.width,
1786+        diff              = p - current_progress,
1787+        increment         = diff > 0,
1788+        x, width;
1789+   
1790+    if(!diff)
1791+      return this;
1792+   
1793+    if(increment) {
1794+      x = current_progress * el_width;
1795+      width = diff * el_width;
1796+    } else {
1797+      x = (current_progress - (-diff)) * el_width;
1798+      width = (current_progress - p) * el_width;
1799+    }
1800+   
1801+    this.ctx.fillStyle = this.options.foreground;
1802+    this.ctx[increment ? "fillRect" : "clearRect"](x, 0, width, this.options.height);
1803+    this.progress = p;
1804+    return this;
1805+  },
1806
1807+  /**
1808+   *  da.ui.ProgressBar#rerender() -> this
1809+   **/
1810+  rerender: function () {
1811+    this.ctx.fillStyle = this.options.foreground;
1812+    this.ctx.fillRect(0, 0, this.progress * this.options.width, this.options.height);
1813+   
1814+    return this;
1815+  },
1816
1817+  /**
1818+   *  da.ui.ProgressBar#toElement() -> Element
1819+   **/
1820+  toElement: function () {
1821+    return this._el;
1822+  },
1823
1824+  /**
1825+   *  da.ui.ProgressBar#destroy() -> Element
1826+   **/
1827+  destroy: function () {
1828+    this._el.destroy();
1829+    delete this._el;
1830+    delete this.ctx;
1831+    delete this._fx;
1832+  }
1833+});
1834+da.ui.ProgressBar = ProgressBar;
1835+
1836+})();
1837addfile ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js
1838hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 1
1839+//#require <libs/ui/ui.js>
1840+//#require "libs/ui/ProgressBar.js"
1841+
1842+(function () {
1843+var ProgressBar = da.ui.ProgressBar;
1844+/** section: UserInterface
1845+ *  class da.ui.SegmentedProgressBar
1846+ *
1847+ *  Display multiple progress bars inside one canvas tag.
1848+ **/
1849+var SegmentedProgressBar = new Class({
1850
1851+  /**
1852+   *  new da.ui.SegmentedProgressBar(width, height, segments)
1853+   *
1854+   *  #### Example
1855+   *      var mb = new da.ui.SegmentedProgressBar(100, 15, {
1856+   *        track: "#f00",
1857+   *        load:  "#f3f3f3"
1858+   *      });
1859+   *     
1860+   *      mb.setProgress("track", 0.2);
1861+   *      mb.setProgress("load", 0.52);
1862+   *
1863+   *  The first define progress bar will be in foreground, while
1864+   *  the last defined will be in background;
1865+   **/
1866+  initialize: function (width, height, bars) {
1867+    this.index = [];
1868+   
1869+    for(var bar in bars)
1870+      this.index.push(bar);
1871+   
1872+    var last_bar_name = this.index.getLast(),
1873+        last_bar = new ProgressBar(null, {
1874+          width: width,
1875+          height: height,
1876+          foreground: bars[last_bar_name]
1877+        });
1878+    this._el = last_bar.toElement();
1879+    this.ctx = last_bar.ctx;
1880+   
1881+    this.bars = {};
1882+    var bar;
1883+    for(var n = 0, m = this.index.length - 1; n < m; n++) {
1884+      bar = this.index[n];
1885+      this.bars[bar] = new ProgressBar(this._el, {
1886+        width:      width,
1887+        height:     height,
1888+        foreground: bars[bar]
1889+      });
1890+    }
1891+    this.bars[last_bar_name] = last_bar;
1892+    last_bar = null;
1893+  },
1894
1895+  /**
1896+   *  da.ui.SegmentedProgressBar#setProgress(segment, progress) -> this | false
1897+   *  - segment (String): name of the bar whose progress needs to be updated
1898+   *  - progress (Number): number in 0-1 range.
1899+   **/
1900+  setProgress: function (bar, p) {
1901+    var current_p = this.bars[bar].progress,
1902+        increment = current_p < p,
1903+        n = this.index.indexOf(bar);
1904+   
1905+    bar = this.bars[bar];
1906+    if(!bar)
1907+      return false;
1908+   
1909+    var idx = this.index;
1910+    if(increment) {
1911+      if(this.index.indexOf(bar) == 0)
1912+        return this.setProgress(p);
1913+     
1914+      m = n + 1;
1915+      bar.setProgress(p);
1916+      while(m--) {
1917+        var b = this.bars[idx[m]];
1918+        if(b.progress <= p)
1919+          b.rerender();
1920+      }
1921+    } else {
1922+      m = this.index.length;
1923+      bar.setProgress(p);
1924+     
1925+      while(m--)
1926+        this.bars[idx[m]].rerender();
1927+    }
1928+   
1929+    return this;
1930+  },
1931
1932+  /**
1933+   *  da.ui.SegmentedProgressBar#destroy() -> undefined
1934+   **/
1935+  destroy: function () {
1936+    this._el.destroy();
1937+    delete this._el;
1938+    delete this.index;
1939+    delete this.bars;
1940+  }
1941+});
1942+da.ui.SegmentedProgressBar = SegmentedProgressBar;
1943+})();
1944hunk ./contrib/musicplayer/src/resources/css/app.css 353
1945   border-bottom: 1px transparent;
1946 }
1947 
1948+#player_pane {
1949+  width: 500px;
1950+}
1951+
1952hunk ./contrib/musicplayer/tests/initialize.js 19
1953   'test_Menu',
1954   'test_NavigationController',
1955   'test_Dialog',
1956-  'test_SettingsController'
1957+  'test_SettingsController',
1958+  'test_ProgressBar',
1959+  'test_SegmentedProgressBar'
1960 ]);
1961addfile ./contrib/musicplayer/tests/test_ProgressBar.js
1962hunk ./contrib/musicplayer/tests/test_ProgressBar.js 1
1963-
1964+var test_ProgressBar = new function () {
1965+  var ProgressBar = da.ui.ProgressBar,
1966+      BLACK       = [0,   0, 0, 255],
1967+      RED         = [255, 0, 0, 255],
1968+      TRANSPARENT = [0,   0, 0, 0],
1969+      self        = this;
1970+     
1971+  this.setup = function () {
1972+    self.pb = new ProgressBar(null, {
1973+      width: 100,
1974+      height: 1,
1975+      foreground: "rgba(0, 0, 0, 255)"
1976+    });
1977+  };
1978
1979+  function getPixel(x) {
1980+    return self.pb.ctx.getImageData(x, 0, 1, 1).data
1981+  }
1982
1983+  this.test_incrementation = function () {
1984+    self.pb.setProgress(0.5);
1985+   
1986+    jum.assertSameObjects(BLACK,        getPixel(0));
1987+    jum.assertSameObjects(BLACK,        getPixel(49));
1988+    jum.assertSameObjects(TRANSPARENT,  getPixel(100));
1989+   
1990+    self.pb.options.foreground = "rgba(255, 0, 0, 255)";
1991+    self.pb.setProgress(0.7);
1992+   
1993+    jum.assertSameObjects(BLACK,        getPixel(0));
1994+    jum.assertSameObjects(RED,          getPixel(61));
1995+    jum.assertSameObjects(TRANSPARENT,  getPixel(100));
1996+  };
1997
1998+  this.test_decrementation = function () {
1999+    self.pb.options.foreground = "rgba(0, 255, 0, 255)";
2000+    self.pb.setProgress(0.6);
2001+   
2002+    jum.assertSameObjects(RED,          getPixel(0));
2003+    jum.assertSameObjects(TRANSPARENT,  getPixel(61));
2004+    jum.assertSameObjects(TRANSPARENT,  getPixel(70));
2005+  };
2006
2007+  this.teardown = function () {
2008+    this.pb.destroy();
2009+  };
2010
2011+  return this;
2012+};
2013addfile ./contrib/musicplayer/tests/test_SegmentedProgressBar.js
2014hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 1
2015-
2016+var test_SegmentedProgressBar = new function () {
2017+  var SegmentedProgressBar = da.ui.SegmentedProgressBar,
2018+      RED   = [255,   0,   0, 255],
2019+      GREEN = [  0, 255,   0, 255],
2020+      BLUE  = [  0,   0, 255, 255],
2021+      TRANS = [  0,   0,   0,   0],
2022+      self  = this;
2023
2024+  this.setup = function () {
2025+    self.pb = new SegmentedProgressBar(100, 1, {
2026+      r: "#f00",
2027+      g: "#0f0",
2028+      b: "#00f"
2029+    });
2030+  };
2031
2032+  function getPixel(x) {
2033+    return self.pb.ctx.getImageData(x, 0, 1, 1).data;
2034+  }
2035
2036+  this.test_incrementation = function () {
2037+    var ctx = self.pb.ctx;
2038+   
2039+    self.pb.setProgress("g", 0.666);
2040+    self.pb.setProgress("r", 0.333);
2041+    self.pb.setProgress("b", 0.999);
2042+   
2043+    jum.assertSameObjects(RED,    getPixel(0));
2044+    jum.assertSameObjects(RED,    getPixel(20));
2045+    jum.assertSameObjects(RED,    getPixel(34));
2046+   
2047+    jum.assertSameObjects(GREEN,  getPixel(35));
2048+    jum.assertSameObjects(GREEN,  getPixel(50));
2049+    jum.assertSameObjects(GREEN,  getPixel(65));
2050+   
2051+    jum.assertSameObjects(BLUE,   getPixel(68));
2052+    jum.assertSameObjects(BLUE,   getPixel(80));
2053+    jum.assertSameObjects(BLUE,   getPixel(98));
2054+  };
2055
2056+  this.test_incrementingMiddleSegment = function () {
2057+    self.pb.setProgress("g", 0.8);
2058+   
2059+    jum.assertSameObjects(RED,    getPixel(0));
2060+    jum.assertSameObjects(RED,    getPixel(30));
2061+   
2062+    jum.assertSameObjects(GREEN,  getPixel(34));
2063+    jum.assertSameObjects(GREEN,  getPixel(66));
2064+    jum.assertSameObjects(GREEN,  getPixel(80));
2065+   
2066+    jum.assertSameObjects(BLUE,   getPixel(81));
2067+  };
2068
2069+  this.test_incrementingFirstSegment = function () {
2070+    self.pb.setProgress("r", 0.9);
2071+   
2072+    jum.assertSameObjects(RED,    getPixel(0));
2073+    jum.assertSameObjects(RED,    getPixel(66));
2074+    jum.assertSameObjects(RED,    getPixel(79));
2075+    jum.assertSameObjects(RED,    getPixel(79));
2076+    jum.assertSameObjects(BLUE,   getPixel(91));
2077+  };
2078
2079+  this.test_decrementingFirstSegment = function () {
2080+    self.pb.setProgress("r", 0.7);
2081+   
2082+    jum.assertSameObjects(RED,    getPixel(69));
2083+    jum.assertSameObjects(GREEN,  getPixel(71));
2084+    jum.assertSameObjects(GREEN,  getPixel(79));
2085+    jum.assertSameObjects(BLUE,   getPixel(81));
2086+    jum.assertSameObjects(BLUE,   getPixel(91));
2087+  };
2088
2089+  this.test_decrementingLastSegment = function () {
2090+    self.pb.setProgress("b", 0.2);
2091+   
2092+    jum.assertSameObjects(GREEN,   getPixel(79));
2093+    jum.assertSameObjects(TRANS,   getPixel(81));
2094+  };
2095
2096+  this.teardown = function () {
2097+    this.pb.destroy();
2098+  };
2099
2100+  return this;
2101+};
2102}
2103[player-interface
2104josip.lisec@gmail.com**20100719121357
2105 Ignore-this: b7287f386bcbfeb6e59b8686da0ec65c
2106  * Fixed bugs in (Segmented)ProgressBar
2107  * Added basic player controls
2108  * Added album cover fetcing via Last.fm
2109  * Reorganised the way external libraries are being imported
2110  * Fixed tests
2111] {
2112hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 39
2113     
2114     this.finished = false;
2115     
2116+    this._songs_found = 0;
2117     this._goal = new Goal({
2118       checkpoints: ["scanner", "indexer"],
2119       onFinish: function () {
2120hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 44
2121         this.finished = true;
2122-        this.destroy()
2123+        this.destroy();
2124+       
2125+        da.ui.ROAR.alert(
2126+          "Collection scanner finished",
2127+          "{0} songs have been found. {1}".interpolate([
2128+            this._found_songs,
2129+            this._found_songs ? "Your patience has paid off." : "Make sure your files have proper ID3 tags."
2130+          ])
2131+        );
2132       }.bind(this)
2133hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 54
2134-    })
2135+    });
2136+   
2137+    da.ui.ROAR.alert(
2138+      "Collection scanner started",
2139+      "We're scanning your musical collection. Soon new artists and songs\
2140+      should start popping up. Patience."
2141+    );
2142   },
2143   
2144   /**
2145hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 72
2146     var cap = event.data;
2147     if(cap === "**FINISHED**") {
2148       this._goal.checkpoint("scanner");
2149-      return; // this.scanner.terminate();
2150+      return;
2151     }
2152     
2153hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 75
2154+    this._found_files++;
2155     if(da.db.DEFAULT.views.Song.view.findRow(cap) === -1)
2156       this.indexer.postMessage(cap);
2157   },
2158hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 83
2159   onIndexerMessage: function (event) {
2160     if(event.data === "**FINISHED**") {
2161       this._goal.checkpoint("indexer");
2162-      return; //this.indexer.terminate();
2163+      return;
2164     }
2165 
2166     // Lots of async stuff is going on, a short summary would look something like:
2167hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 112
2168                 });
2169                 
2170                 delete links;
2171+                delete artist_id;
2172+                delete album_id;
2173               }
2174             });
2175           },
2176hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 172
2177    *  Starts scanning music directory
2178    **/
2179   scan: function (cap) {
2180-    if(!SCANNER || (SCANNER && SCANNER.finished)) {
2181-      delete SCANNER;
2182+    if(!SCANNER || (SCANNER && SCANNER.finished))
2183       SCANNER = new CollectionScanner(cap);
2184hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 174
2185-    }
2186   },
2187   
2188   /**
2189hunk ./contrib/musicplayer/src/controllers/Navigation.js 69
2190       this.createHeader();
2191     
2192     this.column = new Navigation.columns[this.column_name]({
2193+      id: this.column_name,
2194       filter: options.filter,
2195       parentElement: this._el
2196     });
2197hunk ./contrib/musicplayer/src/controllers/Navigation.js 83
2198     if(this.column.constructor.filters.length)
2199       this.column.addEvent("click", this.createFilteredColumn.bind(this));
2200     
2201-    var first_item = this.column._el.getElement(".column_item");
2202-    if(first_item)
2203-      first_item.focus();
2204+    this._el.focus();
2205   },
2206   
2207   /**
2208hunk ./contrib/musicplayer/src/controllers/Navigation.js 307
2209     window.addEvent("resize", function () {
2210       var columns = Navigation.activeColumns,
2211           n = columns.length,
2212-          windowHeight = window.getHeight();
2213+          height = window.getHeight() - this._header_height,
2214+          width = (window.getWidth() - $("player_pane").getWidth())/3 - 1;
2215       
2216       while(n--)
2217hunk ./contrib/musicplayer/src/controllers/Navigation.js 311
2218-        columns[n].column._el.setStyle("height", windowHeight - this._header_height);
2219+        columns[n].column._el.setStyles({
2220+          height: height,
2221+          width: width
2222+        });
2223+     
2224+      //$("navigation_pane").style.width = width*3 + "px";
2225+      $("navigation_pane").style.height = window.getHeight() + "px";
2226     }.bind(this));
2227     
2228     window.fireEvent("resize");
2229hunk ./contrib/musicplayer/src/controllers/Navigation.js 330
2230    *  Adjusts column's height to window.
2231    **/
2232   adjustColumnSize: function (column) {
2233-    column._el.setStyle("height", window.getHeight() - this._header_height);
2234+    column._el.setStyles({
2235+      height: window.getHeight() - this._header_height,
2236+      // -1 for te right border
2237+      width: (window.getWidth() - $("player_pane").getWidth())/3 - 1
2238+    });
2239   },
2240   
2241   /**
2242hunk ./contrib/musicplayer/src/controllers/Player.js 2
2243 //#require "libs/vendor/soundmanager/script/soundmanager2.js"
2244+//#require "libs/ui/SegmentedProgressBar.js"
2245+//#require "services/albumCover.js"
2246 
2247 (function () {
2248hunk ./contrib/musicplayer/src/controllers/Player.js 6
2249+var DocumentTemplate      = da.db.DocumentTemplate,
2250+    Song                  = DocumentTemplate.Song,
2251+    Setting               = DocumentTemplate.Setting,
2252+    SegmentedProgressBar  = da.ui.SegmentedProgressBar;
2253+
2254 /** section: Controllers
2255  *  class Player
2256  * 
2257hunk ./contrib/musicplayer/src/controllers/Player.js 19
2258  *  #### Notes
2259  *  This class is private.
2260  *  Public interface is provided through [[da.controller.Player]].
2261+ *
2262+ *  #### Links
2263+ *  * [soundManager2 API](http://www.schillmania.com/projects/soundmanager2/doc/)
2264+ *
2265  **/
2266 var Player = {
2267   /**
2268hunk ./contrib/musicplayer/src/controllers/Player.js 31
2269    **/
2270   initialize: function () {
2271     var path = location.protocol + "//" + location.host + location.pathname;
2272+    //path = path.slice(-path.lastIndexOf("/"));
2273     $extend(soundManager, {
2274       useHTML5Audio:  false,
2275       url:            path + 'resources/flash/',
2276hunk ./contrib/musicplayer/src/controllers/Player.js 42
2277     soundManager.onready(function () {
2278       da.app.startup.checkpoint("soundmanager");
2279     });
2280+   
2281+    da.app.addEvent("ready", this.initializeUI.bind(this));
2282+   
2283+    // We're using term sound for soundManager objects, while
2284+    // song for DocumentTemplate.Song instances.
2285+    /**
2286+     *  Player#sounds -> Object
2287+     *
2288+     *  Cache of SoundManager's object. Keys are id's of [[da.db.DocumentTemplate.Song]].
2289+     **/
2290+    this.sounds = {};
2291+    /**
2292+     *  Player#playlist -> [Song, ...]
2293+     **/
2294+    this.playlist = [];
2295+    /**
2296+     *  Player#nowPlaying -> {song: <da.db.DocumentTemplate.Song>, sound: <SMSound>}
2297+     **/
2298+    this.nowPlaying = {song: null, sound: null};
2299+   
2300+    this._loading = [];
2301+  },
2302
2303+  initializeUI: function () {
2304+    this._el = $("player_pane");
2305+    window.addEvent("resize", function () {
2306+       this._el.style.height = window.getHeight() + "px";
2307+    }.bind(this));
2308+    window.fireEvent("resize");
2309+   
2310+    this.progress_bar = new SegmentedProgressBar(150, 5, {
2311+      track: "#33519d",
2312+      load:  "#C1C6D4"
2313+    });
2314+   
2315+    this.progress_bar.toElement().id = "track_progress";
2316+    this.progress_bar.toElement().addEvents({
2317+      // Has some issues in firefox - the object's width also gets scaled
2318+      /*
2319+      mouseenter: function () {
2320+        this.tween("height", 15);
2321+      },
2322+      mouseleave: function () {
2323+        this.tween("height", 5);
2324+      },
2325+      */
2326+      mouseup: function (e) {
2327+        var sound = Player.nowPlaying.sound;
2328+        if(!sound)
2329+          return;
2330+       
2331+        var p = e.event.offsetX / this.getWidth();
2332+        sound.setPosition(sound.durationEstimate * p);
2333+      }
2334+    });
2335+   
2336+    var info_block    = new Element("div",  {id: "song_info_block" }),
2337+        wrapper       = new Element("div",  {id: "song_details"}),
2338+        cover_wrapper = new Element("div",  {id: "song_album_cover_wrapper"}),
2339+        album_cover   = new Element("img",  {id: "song_album_cover"}),
2340+        song_title    = new Element("h2",   {id: "song_title"}),
2341+        album_title   = new Element("span", {id: "song_album_title"}),
2342+        artist_name   = new Element("span", {id: "song_artist_name"}),
2343+        position      = new Element("span", {id: "song_position"}),
2344+        controls      = new Element("div",  {id: "player_controls"}),
2345+        play          = new Element("a",    {id: "play_button", href: "#"});
2346+   
2347+    controls.adopt(play, this.progress_bar.toElement());
2348+   
2349+    wrapper.grab(song_title);
2350+    wrapper.appendText("from ");
2351+    wrapper.grab(album_title);
2352+    wrapper.appendText(" by ");
2353+    wrapper.adopt(artist_name, position, controls);
2354+   
2355+    cover_wrapper.grab(album_cover);
2356+   
2357+    info_block.adopt(cover_wrapper, wrapper, new Element("div", {"class": "clear"}));
2358+    info_block.style.visibility = "hidden";
2359+    this._info_block_visible = false;
2360+   
2361+    this._el.adopt(info_block);
2362+   
2363+    play.addEvent("click", function () {
2364+      Player.playPause();
2365+    });
2366+  },
2367
2368+  /**
2369+   *  Player#play(song) -> undefined
2370+   *  - song (da.db.DocumentTemplate.Song): song to play.
2371+   *
2372+   *  If there is currently another song playing, it will be stopped.
2373+   **/
2374+  play: function (song) {
2375+    var np = this.nowPlaying;
2376+    if(!song && np.song)
2377+      np.sound.resume();
2378+   
2379+    $("play_button").addClass("active");
2380+   
2381+    if(np.song && song.id === np.song.id)
2382+      return;
2383+   
2384+    if(this.sounds[song.id]) {
2385+      np.sound.stop();
2386+      this.sounds[song.id].play();
2387+      return;
2388+    }
2389+   
2390+    if(np.sound)
2391+      np.sound.stop();
2392+   
2393+    this._loading.push(song.id);
2394+    this.sounds[song.id] = soundManager.createSound({
2395+      id: song.id,
2396+      url: "/uri/" + encodeURIComponent(song.id),
2397+      autoLoad: false,
2398+      autoPlay: false,
2399+      stream: true,
2400+     
2401+      onload: function () {
2402+        this._loading.remove(song.id);
2403+        if(!song.get("duration"))
2404+          song.update({duration: this.duration});
2405+      },
2406+     
2407+      onplay: function () {
2408+        Player.setNowPlaying(song);
2409+      },
2410+      whileloading: function () {
2411+        Player.progress_bar.setProgress("load", this.bytesLoaded/this.bytesTotal)
2412+      },
2413+      whileplaying: function () {
2414+        Player.progress_bar.setProgress("track", this.position/this.durationEstimate);
2415+      },
2416+      onfinish: function () {
2417+        Player.playbackFinished();
2418+      }
2419+    });
2420+   
2421+    this.sounds[song.id].play();
2422+    np = null;
2423+   
2424+    if(!this._info_block_visible) {
2425+      this._info_block_visible = true;
2426+      $("song_info_block").style.visibility = "visible";
2427+    }
2428+  },
2429
2430+  /**
2431+   *  Player#setNowPlaying(song) -> undefined
2432+   *  fires nowPlaying
2433+   **/
2434+  setNowPlaying: function (song) {
2435+    if(this.nowPlaying.sound)
2436+      this.nowPlaying.sound._last_played = +(new Date());
2437+   
2438+    this.nowPlaying = {
2439+      song:   song,
2440+      sound:  this.sounds[song.id]
2441+    };
2442+   
2443+    var song_title = song.get("title"),
2444+        song_title_el = $("song_title");
2445+    song_title_el.set("text", song_title);
2446+    song_title_el.title = song_title;
2447+    $("song_position").title = "total " + song.get("duration");
2448+   
2449+    song.get("album", function (album) {
2450+      var title = album.get("title"),
2451+          album_covers = album.get("album_cover_urls"),
2452+          el = $("song_album_title");
2453+     
2454+      el.set("text", title);
2455+      el.title = title;
2456+     
2457+      title = null;
2458+      delete el;
2459+     
2460+      if(album_covers)
2461+        $("song_album_cover").src = album_covers[2];
2462+      else
2463+        da.service.albumCover(album, function (urls) {
2464+          $("song_album_cover").src = urls[2];
2465+        });
2466+     
2467+      album = null;
2468+      album_covers = null;
2469+    });
2470+    song.get("artist", function (artist) {
2471+      var aname = artist.get("title"),
2472+           el    = $("song_artist_name");
2473+      el.set("text", aname);
2474+      el.title = aname;
2475+     
2476+      aname = null;
2477+      delete el;
2478+    });
2479+   
2480+    da.controller.Player.fireEvent("nowPlaying", [song]);
2481+  },
2482
2483+  /**
2484+   *  Player#playbackFinished() -> undefined
2485+   *
2486+   *  Called when song finishes playing.
2487+   *  Deterimnes what to do next - load next song in playlist,
2488+   *  repeat this song, etc.
2489+   **/
2490+  playbackFinished: function () {
2491+    $("play_button").removeClass("active");
2492+  },
2493
2494+  /**
2495+   *  Player#pause() -> undefined
2496+   *  fires pause
2497+   *
2498+   **/
2499+  pause: function () {
2500+    if(!this.nowPlaying.song)
2501+      return;
2502+   
2503+    this.nowPlaying.sound.pause();
2504+    $("play_button").removeClass("active");
2505+    da.controller.Player.fireEvent("pause", [this.nowPlaying.song]);
2506+  },
2507
2508+  /**
2509+   *  Player#playPause([song]) -> undefined
2510+   *
2511+   *  Checks if there is a paused song, if so, it'll be resumed.
2512+   *  If there aren't any paused songs, the fallback `song`,
2513+   *  if provided, will be played instead.
2514+   *
2515+   **/
2516+  playPause: function (song) {
2517+    if(!this.nowPlaying.song && song)
2518+      this.play(song);
2519+   
2520+    if(this.nowPlaying.sound.paused)
2521+      this.play();
2522+    else
2523+      this.pause();
2524+  },
2525
2526+  /**
2527+   *  Player#free() -> undefined
2528+   *
2529+   *  Frees memory taken by loaded songs. This method is ran about every
2530+   *  eight minutes and it destroys all SMSound objects which were played
2531+   *  over eight minutes ago, ie. we're caching only about two songs in memory.
2532+   *
2533+   *  #### Links
2534+   *  * (The Universality of Song Length?)[http://a-candle-in-the-dark.blogspot.com/2010/02/song-length.html]
2535+   *
2536+   **/
2537+  free: function () {
2538+    var eight_mins_ago = (+ new Date()) - 8*60*1000;
2539+     
2540+    var sound;
2541+    for(var id in this.sounds) {
2542+      sound = this.sounds[id];
2543+      if(this.sounds.hasOwnProperty(id)
2544+        && this.nowPlaying.song.id !== id
2545+        && (
2546+          sound._last_played < eight_mins_ago || !sound.loaded
2547+        ))
2548+      {
2549+        sound.destruct();
2550+        delete this.sounds[id];
2551+      }
2552+    }
2553+   
2554+    delete sound;
2555   }
2556 };
2557hunk ./contrib/musicplayer/src/controllers/Player.js 319
2558-
2559 Player.initialize();
2560 
2561hunk ./contrib/musicplayer/src/controllers/Player.js 321
2562+// Check is performed every four minutes
2563+Player.free.periodical(8*60*1000, Player);
2564+
2565+/**
2566+TODO: Should be moved to another controller, Statistics or something alike.
2567+function findAverageDuration(callback) {
2568+  da.db.DEFAULT.view({
2569+    temporary: true,
2570+    map: function (doc, emit) {
2571+      if(doc._deleted || doc.type !== "Song" || !doc.duration)
2572+        return;
2573+   
2574+      emit("duration", doc.duration);
2575+    },
2576
2577+    reduce: function (key, values, rereduce) {
2578+      var sum = 0, n = count = values.length;
2579+      while(n--) sum += values[n];
2580+      return {average: sum/count, sample: count};
2581+    },
2582
2583+    finished: function (row) {
2584+      var d = row.getRow("duration");
2585+      if(d.average)
2586+        Setting.create({
2587+          id: "average_duration",
2588+          value: d.average,
2589+          sample: d.sample
2590+        });
2591+     
2592+      console.log("average", d.average || 4*60*1000);
2593+    }
2594+  });
2595+}
2596+**/
2597+
2598 /**
2599  * da.controller.Player
2600  **/
2601hunk ./contrib/musicplayer/src/controllers/Player.js 362
2602 da.controller.Player = {
2603   /**
2604-   *  da.controller.Player.play([uri]) -> Boolean
2605-   *  - uri (String): location of the audio.
2606+   *  da.controller.Player.play([song]) -> Boolean
2607+   *  - cap (da.db.DocumentTemplate.Song): the track to be played.
2608    *
2609    *  If `uri` is omitted and there is paused playback, then the paused
2610    *  file will resume playing.
2611hunk ./contrib/musicplayer/src/controllers/Player.js 368
2612    **/
2613-  play: function (uri) { return false },
2614+  play: function (song) {
2615+    Player.play(song);
2616+  },
2617   
2618   /**
2619    *  da.controller.Player.pause() -> Boolean
2620hunk ./contrib/musicplayer/src/controllers/Player.js 377
2621    * 
2622    *  Pauses the playback (if any).
2623    **/
2624-  pause: function () { return false },
2625+  pause: function () {
2626+    Player.pause()
2627+  },
2628   
2629   /**
2630hunk ./contrib/musicplayer/src/controllers/Player.js 382
2631-   *  da.controller.Player.queue(uri) -> Boolean
2632+   *  da.controller.Player.queue(song) -> Boolean
2633    *  - uri (String): location of the audio file.
2634    * 
2635    *  Adds file to the play queue and plays it as soon as currently playing
2636hunk ./contrib/musicplayer/src/controllers/Player.js 388
2637    *  file finishes playing (if any).
2638    **/
2639-  queue: function (uri) { return false }
2640+  queue: function (song) { return false },
2641
2642+  /**
2643+   *  da.controller.Player.nowPlaying() -> da.db.DocumentTemplate.Song
2644+   **/
2645+  nowPlaying: function () {
2646+    return Player.nowPlaying.song;
2647+  }
2648 };
2649hunk ./contrib/musicplayer/src/controllers/Player.js 397
2650+$extend(da.controller.Player, new Events());
2651 
2652 da.app.fireEvent("ready.controller.Player", [], 1);
2653 
2654hunk ./contrib/musicplayer/src/controllers/Settings.js 4
2655 //#require "doctemplates/Setting.js"
2656 //#require "libs/ui/NavigationColumn.js"
2657 //#require "libs/ui/Dialog.js"
2658+//#require "libs/vendor/Roar.js"
2659 
2660 (function () {
2661 /** section: Controllers
2662hunk ./contrib/musicplayer/src/controllers/Settings.js 29
2663   }
2664 ];
2665 
2666-// Renderers are used render interface elements for each setting (input boxes, checkboxes etc.)
2667+// Renderers are used to render the interface elements for each setting (ie. the input boxes, checkboxes etc.)
2668 // Settings and renderers are bound together via "representAs" property which
2669 // defaults to "text" for each setting.
2670 // All renderer has to do is to renturn a DIV element with "setting_box" CSS class
2671hunk ./contrib/musicplayer/src/controllers/Settings.js 79
2672 RENDERERS.numeric = RENDERERS.text;
2673 
2674 // Serializers do the opposite job of the one that renderers do,
2675-// they take an element and return its value.
2676+// they take an element and return its value which will be then stored to the DB.
2677 var SERIALIZERS = {
2678   text: function (input) {
2679     return input.value;
2680hunk ./contrib/musicplayer/src/controllers/Settings.js 210
2681   },
2682   
2683   save: function () {
2684-    var settings = this.serialize();
2685+    var settings = this.serialize(), setting;
2686+
2687     for(var id in settings)
2688hunk ./contrib/musicplayer/src/controllers/Settings.js 213
2689-      Setting.findFirst({
2690-        properties: {id: id},
2691-        onSuccess: function (setting) {
2692-          setting.update({value: settings[id]});
2693-        }
2694-      });
2695+      Setting.findById(id).update({value: settings[id]});
2696+   
2697+    da.ui.ROAR.alert("Saved", "Your settings have been saved");
2698   },
2699   
2700   serialize: function () {
2701hunk ./contrib/musicplayer/src/controllers/Settings.js 221
2702     var values = this._controls.getElement("form").getElements("input[id^=setting_]"),
2703         serialized = {},
2704-        // in combo with el.id.slice is approx. x10 faster
2705+        // fun fact: in combo with el.id.slice is approx. x10 faster
2706         // than el.id.split("setting_")[1]
2707         setting_l = "setting_".length,
2708         n = values.length;
2709hunk ./contrib/musicplayer/src/controllers/Settings.js 234
2710     }
2711     
2712     return serialized;
2713+  },
2714
2715+  /**
2716+   *  Settings#free() -> undefined
2717+   *
2718+   *  About a minute after last [[da.controller.Settings.hide]] call,
2719+   *  all DOM nodes created by settings dialog will be destroyes - thus
2720+   *  freeing memory.
2721+   * 
2722+   **/
2723+  free: function () {
2724+    Settings.initialized = false;
2725
2726+    Settings.column.destroy();
2727+    Settings.dialog.destroy();
2728
2729+    delete Settings.column;
2730+    delete Settings.dialog;
2731+    delete Settings._el;
2732+    delete Settings._controls;
2733   }
2734 };
2735 $extend(Settings, new Events());
2736hunk ./contrib/musicplayer/src/controllers/Settings.js 285
2737   }
2738 });
2739 
2740-// Settings dialog (DOM nodes) will be destroyed
2741-// a minute after last da.controller.Settings.hide() call.
2742-// Freeing up memory.
2743 var destroy_timeout;
2744hunk ./contrib/musicplayer/src/controllers/Settings.js 286
2745-function destroySettingsDialog () {
2746-  Settings.initialized = false;
2747
2748-  Settings.column.destroy();
2749-  Settings.dialog.destroy();
2750
2751-  delete Settings.column;
2752-  delete Settings.dialog;
2753-  delete Settings._el;
2754-  delete Settings._controls;
2755-}
2756-
2757 /**
2758  * da.controller.Settings
2759  *
2760hunk ./contrib/musicplayer/src/controllers/Settings.js 376
2761    **/
2762   hide: function () {
2763     Settings.hide();
2764-    destroy_timeout = setTimeout(destroySettingsDialog, 60*60*1000);
2765+    destroy_timeout = setTimeout(Settings.free, 60*60*1000);
2766   },
2767   
2768   /**
2769hunk ./contrib/musicplayer/src/controllers/default_columns.js 83
2770 Navigation.registerColumn("Albums", ["Songs"], new Class({
2771   Extends: NavigationColumn,
2772 
2773+  options: {
2774+    rowHeight: 72/3,
2775+    iconSize: 1,
2776+    renderImmediately: false
2777+  },
2778
2779   // We can't reuse "Album" view because of #_passesFilter().
2780   view: {
2781     id: "albums_column",
2782hunk ./contrib/musicplayer/src/controllers/default_columns.js 98
2783 
2784       if(doc.type === "Album")
2785         emit(doc.id, {
2786-          title: doc.title
2787+          title: doc.title,
2788+          icons: doc.album_cover_urls || []
2789         });
2790     }
2791   },
2792hunk ./contrib/musicplayer/src/controllers/default_columns.js 104
2793   
2794+  initialize: function (options) {
2795+    this.parent(options);
2796+   
2797+    this.options.iconSize = this.options.totalCount <= (this.getVisibleIndexes()[1] + 1) ? 2 : 1;
2798+    this._row_dim = this.options.iconSize === 1 ? 64 : 174;
2799+   
2800+    this._el.addEvent("resize", function () {
2801+      this.options.rowHeight = this._el.getWidth() / (4 + this._row_dim + 4);
2802+    }.bind(this));
2803+    this._el.fireEvent("resize");
2804+   
2805+    this.render();
2806+  },
2807+
2808   createFilter: function (item) {
2809     return {album_id: item.id};
2810hunk ./contrib/musicplayer/src/controllers/default_columns.js 120
2811+  },
2812
2813+  renderItem: function (index) {
2814+    var item = this.getItem(index).value,
2815+        el = new Element("a", {
2816+          href: "#",
2817+          title: item.title
2818+        });
2819+   
2820+    el.style.width = this._row_dim + "px";
2821+    el.style.height = this._row_dim + "px";
2822+   
2823+    el.grab(new Element("img",  {src: item.icons[this.options.iconSize]}));   
2824+    return el;
2825+  },
2826
2827+  getBoxCoords: function(index) {
2828+    return [0, (this.options.rowHeight * index)/3];
2829   }
2830 }));
2831 
2832hunk ./contrib/musicplayer/src/controllers/default_columns.js 156
2833     this._el.style.width = "300px";
2834     
2835     this.addEvent("click", function (item) {
2836-      if(this.sound)
2837-        soundManager.stop(item.id);
2838-
2839-      this.sound = soundManager.createSound({
2840-        id: item.id,
2841-        url: "/uri/" + encodeURIComponent(item.id),
2842-        autoLoad: true,
2843-        onload: function () {
2844-          this.play();
2845-        }
2846-      });
2847-    }.bind(this));
2848+      da.controller.Player.play(new da.db.DocumentTemplate.Song(
2849+        da.db.DEFAULT.views.Song.view.getRow(item.id))
2850+      );
2851+    });
2852   },
2853   
2854   view: {
2855hunk ./contrib/musicplayer/src/controllers/default_columns.js 170
2856       if(doc.type === "Song" && doc.title)
2857         emit(doc.title, {
2858           title: doc.title,
2859-          subtitle: doc.track,
2860           track: doc.track
2861         });
2862     }
2863hunk ./contrib/musicplayer/src/controllers/default_columns.js 175
2864   },
2865   
2866+  /*
2867   renderItem: function (index) {
2868     var item = this.getItem(index).value,
2869         el = new Element("a", {href: "#", title: item.title});
2870hunk ./contrib/musicplayer/src/controllers/default_columns.js 183
2871     el.grab(new Element("span", {html: item.title, "class": "title"}));
2872     
2873     return el;
2874-  },
2875+  }, */
2876   
2877   compareFunction: function (a, b) {
2878     a = a && a.value ? a.value.track : a;
2879hunk ./contrib/musicplayer/src/doctemplates/doctemplates.js 12
2880 //#require "doctemplates/Artist.js"
2881 //#require "doctemplates/Album.js"
2882 //#require "doctemplates/Song.js"
2883+//#require "doctemplates/Playlist.js"
2884+
2885hunk ./contrib/musicplayer/src/index.html 4
2886 <!DOCTYPE html>
2887 <html>
2888   <head>
2889+    <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
2890     <title>Music Player for Tahoe-LAFS</title>
2891hunk ./contrib/musicplayer/src/index.html 6
2892-   
2893     <link rel="stylesheet" href="resources/css/reset.css" type="text/css" media="screen" charset="utf-8"/>
2894     <link rel="stylesheet" href="resources/css/text.css" type="text/css" media="screen" charset="utf-8"/>
2895     <link rel="stylesheet" href="resources/css/app.css" type="text/css" media="screen" charset="utf-8"/>
2896hunk ./contrib/musicplayer/src/index_devel.html 4
2897 <!DOCTYPE html>
2898 <html>
2899   <head>
2900+    <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
2901     <title>Music Player for Tahoe-LAFS</title>
2902     
2903     <link rel="stylesheet" href="resources/css/reset.css" type="text/css" media="screen" charset="utf-8"/>
2904hunk ./contrib/musicplayer/src/index_devel.html 15
2905     <script src="libs/vendor/mootools-1.2.4.4-more.js" type="text/javascript" charset="utf-8"></script>
2906     <script src="libs/vendor/persist-js/src/persist.js" type="text/javascript" charset="utf-8"></script>
2907     <script src="libs/vendor/soundmanager/script/soundmanager2.js" type="text/javascript" charset="utf-8"></script>
2908+    <script src="libs/vendor/roar/Roar.js" type="text/javascript" charset="utf-8"></script>
2909+    <script src="libs/vendor/javascript-last.fm-api/lastfm.api.md5.js" type="text/javascript" charset="utf-8"></script>
2910+    <script src="libs/vendor/javascript-last.fm-api/lastfm.api.js" type="text/javascript" charset="utf-8"></script>
2911 
2912     <script type="text/javascript" charset="utf-8">
2913       this.da = {};
2914hunk ./contrib/musicplayer/src/index_devel.html 22
2915     </script>
2916-
2917+   
2918+    <script src="libs/vendor/vendor.js" type="text/javascript" charset="utf-8"></script>
2919+    <!--<script src="libs/vendor/Persist.js" type="text/javascript" charset="utf-8"></script>-->
2920+    <script type="text/javascript" charset="utf-8">
2921+      this.da.vendor.Persist = Persist;
2922+    </script>
2923+   
2924     <script src="libs/db/db.js" type="text/javascript" charset="utf-8"></script>
2925     <script src="libs/db/PersistStorage.js" type="text/javascript" charset="utf-8"></script>
2926     <script src="libs/db/BrowserCouch.js" type="text/javascript" charset="utf-8"></script>
2927hunk ./contrib/musicplayer/src/index_devel.html 47
2928     <script src="doctemplates/Artist.js" type="text/javascript" charset="utf-8"></script>
2929     <script src="doctemplates/Album.js" type="text/javascript" charset="utf-8"></script>
2930     <script src="doctemplates/Song.js" type="text/javascript" charset="utf-8"></script>
2931+    <script src="doctemplates/Playlist.js" type="text/javascript" charset="utf-8"></script>
2932 
2933     <script src="libs/ui/ui.js" type="text/javascript" charset="utf-8"></script>
2934     <script src="libs/ui/Column.js" type="text/javascript" charset="utf-8"></script>
2935hunk ./contrib/musicplayer/src/index_devel.html 56
2936     <script src="libs/ui/Dialog.js" type="text/javascript" charset="utf-8"></script>
2937     <script src="libs/ui/ProgressBar.js" type="text/javascript" charset="utf-8"></script>
2938     <script src="libs/ui/SegmentedProgressBar.js" type="text/javascript" charset="utf-8"></script>
2939+    <script src="libs/vendor/Roar.js" type="text/javascript" charset="utf-8"></script>
2940+   
2941+    <script src="services/services.js" type="text/javascript" charset="utf-8"></script>
2942+    <script src="libs/vendor/LastFM.js" type="text/javascript" charset="utf-8"></script>
2943+    <script src="services/lastfm.js" type="text/javascript" charset="utf-8"></script>
2944+    <script src="services/albumCover.js" type="text/javascript" charset="utf-8"></script>
2945 
2946     <script src="controllers/controllers.js" type="text/javascript" charset="utf-8"></script>
2947     <script src="controllers/Navigation.js" type="text/javascript" charset="utf-8"></script>
2948hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 341
2949    *    Defaults to current database.
2950    **/
2951   view: function DB_view(options, dict) {
2952-    if(!options.id && !options.temporary)
2953+    if(!options.temporary && !options.id)
2954       return false;
2955     if(!options.map)
2956       return false;
2957hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 114
2958    *  - callback (Function): needed only if `key` points to an property defined by an relationship.
2959    **/
2960   get: function (key, callback) {
2961-    if(key in this.doc)
2962+    if(!this.belongsTo[key] && !this.hasMany[key])
2963       return this.doc[key];
2964     
2965     if(!callback)
2966hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 130
2967       if(!this.doc[key + "_id"])
2968         return callback(null);
2969       
2970+      var type = this.belongsTo[key],
2971+          owner = DocumentTemplate[type].findById(this.doc[key + "_id"]);
2972+     
2973+      if(owner)
2974+        this[cache_key] = owner;
2975+     
2976+      callback(owner);
2977+     
2978+      /*
2979       DocumentTemplate.find({
2980         properties: {
2981           id:   this.doc[key + "_id"],
2982hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 152
2983         
2984         onFailure: callback
2985       }, this.constructor.db());
2986+      */
2987     } else if(key in this.hasMany) {
2988       var relation = this.hasMany[key],
2989           props = {type: relation[0]};
2990hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 161
2991       
2992       DocumentTemplate.find({
2993         properties: props,
2994-        onSuccess: callback,
2995-        onFailure: callback
2996+        onSuccess:  callback,
2997+        onFailure:  callback
2998       }, DocumentTemplate[relation[0]].db());
2999     }
3000     
3001hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 236
3002    *  Removes all document's properties except for `id` and adds `_deleted` property.
3003    **/
3004   destroy: function (callback) {
3005-    this.doc = {id: this.id, _deleted: true};
3006+    for(var property in this.doc)
3007+      delete this.doc[property];
3008+   
3009+    this.doc.id = this.id;
3010+    this.doc._deleted = true;
3011+   
3012     this.constructor.db().put(this.doc, function () {
3013       this.fireEvent("destroy", [this]);
3014       if(callback)
3015hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 297
3016           result.rows[n] = type ? new type(row) : row;
3017         }
3018         
3019+        delete n;
3020+        delete row;
3021+        delete type;
3022         options.onSuccess(options.onlyFirst ? result.rows[0] : result.rows);
3023hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 301
3024+        options = null;
3025+        result = null;
3026       }
3027     });
3028   },
3029hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 355
3030     
3031     template.find = function (options) {
3032       options.properties.type = type;
3033-      DocumentTemplate.find(options, db);
3034+      if(options.id)
3035+        template.findById(options.id, function (doc) {
3036+          if(doc)
3037+            options.onSuccess([doc]);
3038+          else
3039+            options.onFailure();
3040+        });
3041+      else
3042+        DocumentTemplate.find(options, db);
3043     };
3044     
3045     template.findFirst = function (options) {
3046hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 380
3047       DocumentTemplate.findOrCreate(options, db);
3048     };
3049     
3050+    template.findById = function (id) {
3051+      var doc = template.db().views[type].view.getRow(id);
3052+      return doc ? new template(doc) : null;
3053+    };
3054+   
3055     template.db().view({
3056       id: type,
3057       map: function (doc, emit) {
3058hunk ./contrib/musicplayer/src/libs/db/PersistStorage.js 1
3059-//#require "libs/vendor/persist-js/src/persist.js"
3060+//#require "libs/vendor/Persist.js"
3061 //#require "libs/db/db.js"
3062 
3063 (function () {
3064hunk ./contrib/musicplayer/src/libs/db/PersistStorage.js 5
3065+var Persist = da.vendor.Persist;
3066+
3067 /** section: Database
3068  *  class da.db.PersistStorage
3069  * 
3070hunk ./contrib/musicplayer/src/libs/ui/Column.js 76
3071     // expensive a small timeout will be set in order to save
3072     // some bandwidth - the downside is that flickering will be seen
3073     // while scrolling.
3074-    var scroll_timer = null,
3075-        timeout = this.options.renderTimeout,
3076-        timeout_fn = this.render.bind(this);
3077+    var timeout     = this.options.renderTimeout,
3078+        timeout_fn  = this.render.bind(this),
3079+        scroll_timer;
3080 
3081     this._el.addEvent("scroll", function () {
3082       clearTimeout(scroll_timer);
3083hunk ./contrib/musicplayer/src/libs/ui/Column.js 82
3084-      scroll_timer = setTimeout(scroll_timer, timeout);
3085+      scroll_timer = setTimeout(timeout_fn, timeout);
3086     });
3087     
3088     // We're caching lists' height so we won't have to
3089hunk ./contrib/musicplayer/src/libs/ui/Column.js 143
3090       var coords = this.getBoxCoords(first_rendered);
3091       box.setStyles({
3092         position: "absolute",       
3093-        top:      coords[0],
3094-        left:     coords[1]
3095+        top:      coords[1],
3096+        left:     coords[0]
3097       }).injectBottom(this._el);
3098     }
3099     
3100hunk ./contrib/musicplayer/src/libs/ui/Column.js 170
3101    *  da.ui.Column#rerender() -> this
3102    **/
3103   rerender: function () {
3104+    if(!this._el)
3105+      return false;
3106+   
3107+    console.log("rerender", this.options.id, this._deleted);
3108     var weight = this._weight;
3109     this._el.empty();
3110     this._el.grab(weight);
3111hunk ./contrib/musicplayer/src/libs/ui/Column.js 211
3112    *  Returns X and Y coordinates at which item with given `index` should be rendered at.
3113    **/
3114   getBoxCoords: function(index) {
3115-    return [this.options.rowHeight * index, 0];
3116+    return [0, this.options.rowHeight * index];
3117   },
3118 
3119   /**
3120hunk ./contrib/musicplayer/src/libs/ui/Column.js 246
3121    * 
3122    *  Removes column from DOM.
3123    **/
3124-  destroy: function (dispose) {
3125+  destroy: function () {
3126     this._el.destroy();
3127     delete this._el;
3128     
3129hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 61
3130     
3131     // Small speed-hack
3132     if(!this.options.filter)
3133-      this._passesFilter = $lambda(true);
3134+      this._passesFilter = function () { return true };
3135     
3136     this._el.addEvent("click:relay(.column_item)", this.click.bind(this));
3137     
3138hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 82
3139       (options.db || da.db.DEFAULT).view(this.view);
3140     } else if(this.options.totalCount) {
3141       this.injectBottom(this.options.parentElement || document.body);
3142-      this.render();
3143+      if(!this.options.renderImmediately)
3144+        this.render();
3145     }
3146   },
3147   
3148hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 101
3149     
3150     this.updateTotalCount(values.rows.length);
3151     this.injectBottom(this.options.parentElement || document.body);
3152-    return this.render();
3153+    if(this.options.renderImmediately !== false)
3154+      this.render();
3155+    return this;
3156   },
3157   
3158   /**
3159hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 137
3160    **/
3161   renderItem: function (index) {
3162     var item = this.getItem(index).value,
3163-        el = new Element("a", {href: "#", title: item.title});
3164+        el = new Element("a", {
3165+          href: "#",
3166+          title: item.title,
3167+          "class": index%2 ? "even" : "odd"
3168+        });
3169     
3170     if(item.icon)
3171       el.grab(new Element("img",  {src: item.icon}));
3172hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 87
3173     }
3174     
3175     this.ctx.fillStyle = this.options.foreground;
3176-    this.ctx[increment ? "fillRect" : "clearRect"](x, 0, width, this.options.height);
3177+    // We're adding +-1 here because some browsers (even different implementations
3178+    // of WebKit are unable to render this precisely when small changes are
3179+    // in place.
3180+    this.ctx[increment ? "fillRect" : "clearRect"](x - 1, 0, width + 1, this.options.height);
3181     this.progress = p;
3182hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 92
3183+   
3184     return this;
3185   },
3186   
3187hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 100
3188    *  da.ui.ProgressBar#rerender() -> this
3189    **/
3190   rerender: function () {
3191-    this.ctx.fillStyle = this.options.foreground;
3192-    this.ctx.fillRect(0, 0, this.progress * this.options.width, this.options.height);
3193+    var opts = this.options;
3194+    this.ctx.fillStyle = opts.foreground;
3195+    this.ctx.fillRect(0, 0, this.progress * opts.width, opts.height);
3196     
3197     return this;
3198   },
3199hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 28
3200    *  The first define progress bar will be in foreground, while
3201    *  the last defined will be in background;
3202    **/
3203-  initialize: function (width, height, bars) {
3204-    this.index = [];
3205+  initialize: function (width, height, segments) {
3206+    this._index = [];
3207+    this.segments = {};
3208     
3209hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 32
3210-    for(var bar in bars)
3211-      this.index.push(bar);
3212+    this._el = new Element("canvas");
3213+    this._el.width = width;
3214+    this._el.height = height;
3215+    this._el.className = "progressbar";
3216+    this.ctx = this._el.getContext("2d");
3217     
3218hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 38
3219-    var last_bar_name = this.index.getLast(),
3220-        last_bar = new ProgressBar(null, {
3221-          width: width,
3222-          height: height,
3223-          foreground: bars[last_bar_name]
3224-        });
3225-    this._el = last_bar.toElement();
3226-    this.ctx = last_bar.ctx;
3227+    this._el.addEvent("resize", function () {
3228+      var idx = this._index,
3229+           n = idx.length;
3230+     
3231+      while(n--)
3232+        this.segments[idx[n]].rerender();
3233+    }.bind(this));
3234     
3235hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 46
3236-    this.bars = {};
3237-    var bar;
3238-    for(var n = 0, m = this.index.length - 1; n < m; n++) {
3239-      bar = this.index[n];
3240-      this.bars[bar] = new ProgressBar(this._el, {
3241-        width:      width,
3242-        height:     height,
3243-        foreground: bars[bar]
3244-      });
3245-    }
3246-    this.bars[last_bar_name] = last_bar;
3247-    last_bar = null;
3248+    for(var segment in segments)
3249+      if(segments.hasOwnProperty(segment)) {
3250+        this._index.push(segment);
3251+        this.segments[segment] = new ProgressBar(this._el, {
3252+          width:      width,
3253+          height:     height,
3254+          foreground: segments[segment]
3255+        });
3256+      }
3257   },
3258   
3259   /**
3260hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 62
3261    *  - segment (String): name of the bar whose progress needs to be updated
3262    *  - progress (Number): number in 0-1 range.
3263    **/
3264-  setProgress: function (bar, p) {
3265-    var current_p = this.bars[bar].progress,
3266-        increment = current_p < p,
3267-        n = this.index.indexOf(bar);
3268-   
3269-    bar = this.bars[bar];
3270-    if(!bar)
3271+  setProgress: function (segment, p) {
3272+    segment = this.segments[segment];
3273+    if(!segment)
3274       return false;
3275     
3276hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 67
3277-    var idx = this.index;
3278-    if(increment) {
3279-      if(this.index.indexOf(bar) == 0)
3280-        return this.setProgress(p);
3281-     
3282-      m = n + 1;
3283-      bar.setProgress(p);
3284-      while(m--) {
3285-        var b = this.bars[idx[m]];
3286-        if(b.progress <= p)
3287-          b.rerender();
3288-      }
3289-    } else {
3290-      m = this.index.length;
3291-      bar.setProgress(p);
3292-     
3293-      while(m--)
3294-        this.bars[idx[m]].rerender();
3295-    }
3296+    var idx = this._index,
3297+        n = idx.length;
3298+   
3299+    // Indeed, this is quite naive implementation
3300+    segment.setProgress(p);
3301+    while(n--)
3302+      this.segments[idx[n]].rerender();
3303     
3304     return this;
3305   },
3306hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 79
3307   
3308   /**
3309+   *  da.ui.SegmentedProgressBar#toElement() -> Element
3310+   **/
3311+  toElement: function () {
3312+    return this._el;
3313+  },
3314
3315+  /**
3316    *  da.ui.SegmentedProgressBar#destroy() -> undefined
3317    **/
3318   destroy: function () {
3319hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 91
3320     this._el.destroy();
3321     delete this._el;
3322-    delete this.index;
3323-    delete this.bars;
3324+    delete this._index;
3325+    delete this.segments;
3326+    delete this.ctx;
3327   }
3328 });
3329 da.ui.SegmentedProgressBar = SegmentedProgressBar;
3330hunk ./contrib/musicplayer/src/libs/util/ID3.js 51
3331   
3332   _getFile: function (parser) {
3333     if(!parser)
3334-      return this.options.onFailure();
3335+      return this.options.onFailure("fromID3");
3336     
3337     this.request = new Request.Binary({
3338hunk ./contrib/musicplayer/src/libs/util/ID3.js 54
3339-      url: this.options.url,
3340-      range: parser.range,
3341-      onSuccess: this._onFileFetched.bind(this)
3342+      url:        this.options.url,
3343+      range:      parser.range,
3344+      noCache:    true,
3345+      onSuccess:  this._onFileFetched.bind(this),
3346+      onFailure: function () {
3347+        this.options.onFailure("failedRequest");
3348+      }.bind(this)
3349     });
3350     
3351     this.request.send();
3352hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 143
3353   WAR: FRAMES.WOAR,
3354   WXX: FRAMES.WXXX
3355 });
3356-
3357 var ID3v2Parser = new Class({
3358   /**
3359    *  new da.util.ID3v2Parser(data, options, request)
3360addfile ./contrib/musicplayer/src/libs/vendor/JSON.js
3361hunk ./contrib/musicplayer/src/libs/vendor/JSON.js 1
3362+(function () {
3363+/**
3364+ * da.vendor.JSON
3365+ *
3366+ * JSON parser/serializer.
3367+ **/
3368+
3369+var has_JSON = typeof JSON === "undefined",
3370+    broken_JSON = false;
3371+
3372+if(has_JSON && JSON.stringify([1,2,3]) === "[1,2,3]")
3373+  broken_JSON = true;
3374
3375+if(!has_JSON || broken_JSON) {
3376+//#require "libs/vendor/json/json2.js"
3377+
3378+}
3379+})();
3380addfile ./contrib/musicplayer/src/libs/vendor/LastFM.js
3381hunk ./contrib/musicplayer/src/libs/vendor/LastFM.js 1
3382+(function () {
3383+
3384+//#require "libs/vendor/javascript-last.fm-api/lastfm.api.md5.js"
3385+//#require "libs/vendor/javascript-last.fm-api/lastfm.api.js"
3386+
3387+// TODO: cache is broken, fix it (something with agumenting localStorage)
3388+///require "libs/vendor/javascript-last.fm-api/lastfm.api.cache.js"
3389+
3390+/**
3391+ *  class da.vendor.LastFM
3392+ * 
3393+ *  Last.fm API wrapper.
3394+ *
3395+ *  #### Links
3396+ *  * [Last.fm API](http://last.fm/api/intro)
3397+ *  * [Last.fm API wrapper](http://github.com/fxb/javascript-last.fm-api)
3398+ *
3399+ **/
3400+da.vendor.LastFM = LastFM;
3401+
3402+})();
3403addfile ./contrib/musicplayer/src/libs/vendor/Persist.js
3404hunk ./contrib/musicplayer/src/libs/vendor/Persist.js 1
3405+//#require <libs/vendor/vendor.js>
3406+
3407+(function () {
3408+/**
3409+ *  class da.vendor.Persist
3410+ * 
3411+ *  Persist.JS
3412+ *
3413+ *  #### Links
3414+ *  * [PersistJS project page](http://google.com)
3415+ **/
3416+
3417+// tiny hack which allows us to use
3418+// index_devel.html
3419+if(typeof Persist === "undefined") {
3420+  var Persist;
3421+//#require "libs/vendor/persist-js/src/persist.js"
3422+  da.vendor.Persist = Persist;
3423+} else
3424+  da.vendor.Persist = window.Persist;
3425+
3426+})();
3427addfile ./contrib/musicplayer/src/libs/vendor/Roar.js
3428hunk ./contrib/musicplayer/src/libs/vendor/Roar.js 1
3429+//#require <libs/vendor/vendor.js>
3430+
3431+(function () {
3432+//#require "libs/vendor/roar/Roar.js"
3433+
3434+/**
3435+ *  class da.vendor.Roar
3436+ *
3437+ *  Roar notifications library.
3438+ *
3439+ *  #### Links
3440+ *  * [Roar project page](http://digitarald.de/project/roar/)
3441+ *
3442+ **/
3443+
3444+da.vendor.Roar = Roar;
3445+
3446+/**
3447+ *  da.ui.ROAR = da.vendor.Roar
3448+ *
3449+ *  The default instance of [[da.vendor.Roar]].
3450+ *
3451+ **/
3452+da.ui.ROAR = new Roar({
3453+  position: "lowerLeft"
3454+});
3455+
3456+})();
3457addfile ./contrib/musicplayer/src/libs/vendor/vendor.js
3458hunk ./contrib/musicplayer/src/libs/vendor/vendor.js 1
3459+/**
3460+ * da.vendor
3461+ *
3462+ * Interfce to 3rd party libraries.
3463+ *
3464+ **/
3465+
3466+if(!da.vendor)
3467+  da.vendor = {};
3468hunk ./contrib/musicplayer/src/resources/css/app.css 10
3469 }
3470 
3471 body {
3472-  font-family: 'Liberation Sans', 'Helvetica Neue', Helvetica, sans-serif;
3473+  font-family: 'Droid Sans', 'Lucida Grande', 'Lucida Sans', 'Bitstream Vera', sans-serif;
3474   overflow: hidden;
3475hunk ./contrib/musicplayer/src/resources/css/app.css 12
3476+  background: #c0c0c0;
3477 }
3478 
3479 a {
3480hunk ./contrib/musicplayer/src/resources/css/app.css 65
3481   cursor: default;
3482 }
3483 
3484+.clear {
3485+  display: block;
3486+  width: auto;
3487+  clear: both;
3488+  padding: 0;
3489+  margin: 0;
3490+}
3491+
3492+/*** Scroll bars ***/
3493+::-webkit-scrollbar {
3494+    width: 6px;
3495+    height: 6px;
3496+    background: #fff;
3497+    padding: 0 1px;
3498+}
3499+
3500+::-webkit-scrollbar:hover {
3501+  visibility: visible;
3502+}
3503+
3504+::-webkit-scrollbar-button:start:decrement,
3505+::-webkit-scrollbar-button:end:increment {
3506+    display: none;
3507+}
3508+
3509+::-webkit-scrollbar-button:vertical:increment {
3510+    background: transparent;
3511+}
3512+
3513+::-webkit-scrollbar-track-piece {
3514+    background-color: transparent;
3515+    -webkit-border-radius: 3px;
3516+}
3517+
3518+::-webkit-scrollbar-thumb:vertical {
3519+    height: 50px;
3520+}
3521+
3522+::-webkit-scrollbar-thumb:horizontal {
3523+    width: 50px;
3524+}
3525+
3526+::-webkit-scrollbar-thumb:vertical, ::-webkit-scrollbar-thumb:horizontal {
3527+  background-color: rgba(0, 0, 0, 0.1);
3528+  -webkit-border-radius: 3px;
3529+}
3530+
3531+::-webkit-scrollbar-thumb:vertical:hover, ::-webkit-scrollbar-thumb:horizontal:hover {
3532+  background-color: rgba(0, 0, 0, 0.4);
3533+}
3534+
3535+::-webkit-scrollbar-thumb:vertical:active, ::-webkit-scrollbar-thumb:horizontal:active {
3536+  background-color: rgba(0, 0, 0, 0.7);
3537+}
3538+
3539 /*** Dialogs ***/
3540 .dialog_wrapper {
3541   width: 100%;
3542hunk ./contrib/musicplayer/src/resources/css/app.css 162
3543   padding: 50px 0 0 0;
3544 }
3545 
3546+/*** Notifications (Roar) ***/
3547+/* Contents of Roar.css (distributed under MIT) */
3548+.roar-body {
3549+       position: absolute;
3550+       color: #fff;
3551+       text-align: left;
3552+       z-index: 999;
3553+       font-size: 0.8em;
3554+}
3555+
3556+.roar {
3557+       position: absolute;
3558+       width: 300px;
3559+       cursor: default;
3560+       padding: 5px;
3561+}
3562+
3563+.roar-bg {
3564+       position: absolute;
3565+       z-index: 1000;
3566+       width: 100%;
3567+       height: 100%;
3568+       left: 0;
3569+       top: 0;
3570+       background-color: #000;
3571+       border: 2px solid #000;
3572+
3573+       -webkit-border-radius: 5px;
3574+       -moz-border-radius: 5px;
3575+       -o-border-radius: 5px;
3576+       border-radius: 5px;
3577+       
3578+       -webkit-box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px;
3579+  -moz-box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px;
3580+       -o-box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px;
3581+       box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px;
3582+}
3583+
3584+
3585+.roar h3 {
3586+       position: relative;
3587+       margin: 0;
3588+       border: 0;
3589+       font-size: 13px;
3590+       color: #fff;
3591+       z-index: 1002;
3592+}
3593+
3594+.roar p {
3595+       position: relative;
3596+       margin: 0;
3597+       font-size: 12px;
3598+       color: #fff;
3599+       z-index: 1002;
3600+}
3601+
3602 /*** Navigation columns ***/
3603 .column_container {
3604   float: left;
3605hunk ./contrib/musicplayer/src/resources/css/app.css 221
3606+  /**
3607   min-width: 200px;
3608   margin-right: 1px;
3609hunk ./contrib/musicplayer/src/resources/css/app.css 224
3610+  **/
3611 }
3612 
3613 .column_container .column_header {
3614hunk ./contrib/musicplayer/src/resources/css/app.css 262
3615   border-right: 1px solid #ddd;
3616 }
3617 
3618-.column_container .navigation_column:last {
3619-  border-right: 5px solid #ddd;
3620-}
3621 
3622 .navigation_column {
3623   width: 100%;
3624hunk ./contrib/musicplayer/src/resources/css/app.css 271
3625 }
3626 
3627 .navigation_column .column_items_box {
3628-  width: inherit;
3629+  width: 100%;
3630 }
3631 
3632 .navigation_column .column_item {
3633hunk ./contrib/musicplayer/src/resources/css/app.css 280
3634   padding: 5px 0;
3635   width: inherit;
3636   overflow: hidden;
3637-  text-overflow: ellipsis;
3638   text-indent: 5px;
3639hunk ./contrib/musicplayer/src/resources/css/app.css 281
3640-  white-space: nowrap;
3641+}
3642+
3643+.column_item.even {
3644+  background-color: #f0f6fd;
3645+}
3646+
3647+.column_item.odd {
3648+  background-color: #fff;
3649 }
3650 
3651 .navigation_column a.column_item {
3652hunk ./contrib/musicplayer/src/resources/css/app.css 301
3653 }
3654 
3655 .navigation_column .column_item span {
3656-  /*display: block;*/
3657+  display: block;
3658   vertical-align: middle;
3659hunk ./contrib/musicplayer/src/resources/css/app.css 303
3660+  text-overflow: ellipsis;
3661+  white-space: nowrap;
3662+  overflow: hidden;
3663 }
3664 
3665 .navigation_column .column_item span.subtitle {
3666hunk ./contrib/musicplayer/src/resources/css/app.css 319
3667   margin-left: 20px;
3668 }
3669 
3670-.navigation_column .active_column_item, .menu_item:hover, .navigation_column .column_item:focus, .menu_item a:focus {
3671+.navigation_column .active_column_item, .menu_item:hover {
3672   background: #33519d !important;
3673   text-shadow: #000 0 1px 0;
3674   color: #fff !important;
3675hunk ./contrib/musicplayer/src/resources/css/app.css 326
3676   outline: 0 !important;
3677 }
3678 
3679+.navigation_column .column_item:focus, .menu_item a:focus {
3680+  -webkit-box-shadow: #758FCF 0 0 5px;
3681+  outline: 0 !important;
3682+}
3683+
3684+/** Albums column **/
3685+#Albums_column {
3686+  background: #595959;
3687+}
3688+
3689+#Albums_column .column_item {
3690+  width: 64px;
3691+  height: 64px;
3692+  padding: 4px;
3693+  text-indent: 0;
3694+  border-radius: 2px;
3695+  display: inline-block;
3696+  margin: 0;
3697+}
3698+
3699+#Albums_column .column_item.even, #Albums_column .column_item.odd {
3700+  background: transparent;
3701+}
3702+
3703+#Albums_column .column_item img {
3704+  display: block;
3705
3706+  -webkit-box-shadow: rgba(0, 0, 0, 0.5) 0 1px 3px;
3707+}
3708+
3709 /*** Menus ***/
3710 .menu {
3711   display: block;
3712hunk ./contrib/musicplayer/src/resources/css/app.css 434
3713 #settings .navigation_column {
3714   border-right: 1px solid #c0c0c0;
3715   width: 150px;
3716-  float: left;
3717+  display: inline-block;
3718 }
3719 
3720 #settings_controls {
3721hunk ./contrib/musicplayer/src/resources/css/app.css 503
3722   border-bottom: 1px transparent;
3723 }
3724 
3725+/** Navigation columns **/
3726+#navigation_pane {
3727+  position: fixed;
3728+  top: 0;
3729+  left: 0;
3730+  overflow: hidden;
3731+}
3732+
3733+#navigation_pane, #player_pane {
3734+  background: #fff;
3735+  -webkit-box-shadow: #585858 0 0 50px;
3736+  -moz-box-shadow: #585858 0 0 50px;
3737+  -o-box-shadow: #585858 0 0 50px;
3738+  -box-shadow: #585858 0 0 50px;
3739+}
3740+
3741+/** Player **/
3742 #player_pane {
3743   width: 500px;
3744hunk ./contrib/musicplayer/src/resources/css/app.css 522
3745+  position: fixed;
3746+  top: 0;
3747+  right: 0;
3748+}
3749+
3750+#song_info_block {
3751+  width: 100%;
3752+  background: #f3f3f3;
3753+  border-bottom: 1px solid #ddd;
3754+}
3755+
3756+#song_details {
3757+  font-size: 0.9em;
3758+  color: #585858;
3759+  float: left;
3760+  width: 300px;
3761+  padding: 10px;
3762+  margin-top: 20px;
3763+  cursor: default;
3764+}
3765+
3766+#song_details h2, #song_details span {
3767+  color: #000;
3768+  margin: 0;
3769+  font-style: normal;
3770+  white-space: nowrap;
3771+  text-overflow: ellipsis;
3772+  overflow: hidden;
3773+}
3774+
3775+#song_details span {
3776+  display: inline-block;
3777+  max-width: 125px;
3778+}
3779+
3780+#song_album_cover_wrapper {
3781+  text-align: center;
3782+  width: 160px;
3783+  max-width: 160px;
3784+  overflow: hidden;
3785+  padding: 10px;
3786+  float: left;
3787+  display: block;
3788+}
3789+
3790+#song_album_cover {
3791+  /*float: left;
3792+  display: block;
3793+  margin: 10px;*/
3794+  max-width: 150px;
3795+  border: 1px solid #fff;
3796+  outline: 1px solid #ddd;
3797
3798+  -webkit-box-shadow: #c0c0c0 0 3px 5px;
3799+  -moz-box-shadow: #c0c0c0 0 3px 5px;
3800+  -o-box-shadow: #c0c0c0 0 3px 5px;
3801+  box-shadow: #c0c0c0 0 3px 5px;
3802+}
3803+
3804+#song_title {
3805+  display: block;
3806+}
3807+
3808+#player_controls {
3809+  vertical-align: middle;
3810+  margin-top: 20px;
3811+}
3812+
3813+#track_progress {
3814+  display: inline-block;
3815+  vertical-align: middle;
3816+  margin: 0 0 0 10px;
3817+  -webkit-border-radius: 2px;
3818+  -moz-border-radius: 2px;
3819+  -o-border-radius: 2px;
3820+  border-radius: 2px;
3821+}
3822+
3823+
3824+#play_button {
3825+  display: inline-block;
3826+  width: 40px;
3827+  height: 40px;
3828+  background: url(../images/play.png) 0 0;
3829+  outline: 0;
3830+  vertical-align: middle;
3831+  cursor: default;
3832+}
3833+
3834+#play_button:active, #play_button:focus, #play_button.active {
3835+  background-position: 0 -40px;
3836+}
3837+
3838+#play_button.active:active, #play_button.active:focus {
3839+  background-position: 0 -80px;
3840 }
3841adddir ./contrib/musicplayer/src/services
3842addfile ./contrib/musicplayer/src/services/albumCover.js
3843hunk ./contrib/musicplayer/src/services/albumCover.js 1
3844+//#require <services/services.js>
3845+//#require "services/lastfm.js"
3846+//#require "libs/util/Goal.js"
3847+
3848+(function () {
3849+var lastfm = da.service.lastFm,
3850+    Goal = da.util.Goal,
3851+    Artists = da.db.DEFAULT.views.Artist.view;
3852+
3853+function fetchAlbumCover(search_params, album, callback) {
3854+  lastfm.album.getInfo(search_params, {
3855+    success: function (data) {
3856+      var urls = data.album.image ? data.album.image : null,
3857+          n = urls.length;
3858+     
3859+      while(n--)
3860+        urls[n] = urls[n]["#text"];
3861+     
3862+      album.set({
3863+        album_cover_urls: urls,
3864+        lastfm_id:        data.album.id,
3865+        mbid:             data.album.mbid.length ? data.album.mbid : null
3866+      });
3867+      album.save();
3868+     
3869+      // fun fact: typeof /.?/ === "function"
3870+      if(urls && typeof callback === "function")
3871+        callback(urls);
3872+    }
3873+  });
3874+}
3875+
3876+/**
3877+ *  da.service.albumCover(song[, callback]) -> undefined
3878+ *  - song (da.db.DocumentTemplate): song whose album art needs to be fetched
3879+ *  - callback (Function): called once album cover is fetched, with first
3880+ *    argument being the URL of the album cover.
3881+ *
3882+ *  #### Notes
3883+ *  The URL will be saved to the `song` under 'cover_art' propety.
3884+ **/
3885+da.service.albumCover = function (album, callback) {
3886+  var search_params = {};
3887+  if(album.get("lastfm_id"))
3888+    search_params.id = album.get("lastfm_id");
3889+  else if(album.get("mbid"))
3890+    search_params.mbid = album.get("mbid");
3891
3892+  if(!search_params.id && !search_params.mbid)
3893+    search_params = {
3894+      album: album.get("title"),
3895+      artist: Artists.getRow(album.get("artist_id")).title
3896+    };
3897
3898+  fetchAlbumCover(search_params, album, callback);
3899+  search_params = null;
3900+};
3901+
3902+})();
3903addfile ./contrib/musicplayer/src/services/lastfm.js
3904hunk ./contrib/musicplayer/src/services/lastfm.js 1
3905+//#require "libs/vendor/LastFM.js"
3906+
3907+(function () {
3908+//var cache = new LastFMCache();
3909+/**
3910+ *  da.service.lastFm -> da.vendor.LastFM
3911+ * 
3912+ *  Default instance of `LastFM` API wrapper.
3913+ * 
3914+ **/
3915+da.service.lastFm = new da.vendor.LastFM({
3916+  apiKey:     "d5219a762390c878548b338d67a28f67",
3917+  // not so secret anymore :)
3918+  apiSecret:  "9ff1e4083ec6e86ef4467262db5b1509",
3919+  cache:      null
3920+});
3921+
3922+})();
3923addfile ./contrib/musicplayer/src/services/services.js
3924hunk ./contrib/musicplayer/src/services/services.js 1
3925+/**
3926+ * da.service
3927+ * Access to 3rd party services - last.fm, MusicBrainz, etc.
3928+ **/
3929+
3930+if(!da.service)
3931+  da.service = {};
3932hunk ./contrib/musicplayer/src/workers/indexer.js 17
3933  *  an MP3 file stored in Tahoe (without /uri/ prefix).
3934  *
3935  *  Messages sent from this worker are objects returned by ID3 parser.
3936+ *
3937+ *  #### Notes
3938+ *  It has been detected that Tahoe (to be correct, it's web server) can't
3939+ *  handle well large number of simoultanious requests, therefore we're limiting
3940+ *  the number of files that can be fetched at the same time to one.
3941  * 
3942hunk ./contrib/musicplayer/src/workers/indexer.js 23
3943+ *  Since it's also possible that it will take more time for the [[Scanner]]
3944+ *  to find all the files than it will take the [[Indexer]] we're allowing
3945+ *  about 30-second delay before finally sending the "I'm done" message to the
3946+ *  [[da.controller.CollectionScanner]].
3947+ *
3948  **/
3949 
3950 var window = this,
3951hunk ./contrib/musicplayer/src/workers/indexer.js 32
3952     document = {},
3953-    queue = 0;
3954+    queue = [];
3955 
3956 this.da = {};
3957 importScripts("env.js");
3958hunk ./contrib/musicplayer/src/workers/indexer.js 44
3959  * 
3960  *  When tags are parsed, `postMessage` is called.
3961  **/
3962-onmessage = function (event) {
3963-  var cap = event.data,
3964-      uri = "/uri/" + encodeURIComponent(cap);
3965+onmessage = function (event) { 
3966+  queue.push(event.data);
3967   
3968hunk ./contrib/musicplayer/src/workers/indexer.js 47
3969-  queue++;
3970+  if(queue.length < 3)
3971+    getTags(event.data);
3972+};
3973+
3974+function getTags(cap) {
3975+  if(!cap) return false;
3976   var parser = new da.util.ID3({
3977hunk ./contrib/musicplayer/src/workers/indexer.js 54
3978-    url: uri,
3979+    url: "/uri/" + encodeURIComponent(cap),
3980+   
3981     onSuccess: function (tags) {
3982       if(tags && typeof tags.title !== "undefined" && typeof tags.artist !== "undefined") {
3983         tags.id = cap;
3984hunk ./contrib/musicplayer/src/workers/indexer.js 65
3985       parser.destroy();
3986       delete parser;
3987       
3988-      // Not all files are reporeted instantly so it might
3989-      // take some time for scanner.js/CollectionScanner.js to
3990-      // report the files, maximum delay we're allowing here
3991-      // for new files to come in is one minute.
3992-      if(!--queue)
3993-        setTimeout(checkQueue, 1000*60*1);
3994+      queue.erase(cap);
3995+      checkQueue();
3996     },
3997hunk ./contrib/musicplayer/src/workers/indexer.js 68
3998-    onFailure: function () {
3999+   
4000+    onFailure: function (calledBy) {
4001       parser.destroy();
4002       delete parser;
4003       
4004hunk ./contrib/musicplayer/src/workers/indexer.js 73
4005-      if(!--queue)
4006-        setTimeout(checkQueue, 1000*60*1);
4007+      queue.erase(cap);
4008+      checkQueue();
4009     }
4010   });
4011hunk ./contrib/musicplayer/src/workers/indexer.js 77
4012-};
4013+}
4014 
4015hunk ./contrib/musicplayer/src/workers/indexer.js 79
4016+var finish_timeout;
4017 function checkQueue() {
4018hunk ./contrib/musicplayer/src/workers/indexer.js 81
4019-  if(!queue)
4020-    postMessage("**FINISHED**");
4021+  if(!queue.length)
4022+    finish_timeout = setTimeout(finish, 30*60*1000);
4023+  else {
4024+    clearTimeout(finish_timeout);
4025+    setTimeout(function () {
4026+      getTags(queue.shift());
4027+    }, 444);
4028+  }
4029 }
4030 
4031hunk ./contrib/musicplayer/src/workers/indexer.js 91
4032+function finish () {
4033+  postMessage("**FINISHED**");
4034+}
4035hunk ./contrib/musicplayer/src/workers/scanner.js 25
4036 function scan (obj) {
4037   queue++;
4038   obj.get(function () {
4039-    queue--;
4040-   
4041     if(obj.type === "filenode") {
4042hunk ./contrib/musicplayer/src/workers/scanner.js 26
4043-      delete obj;
4044       return postMessage(obj.uri);
4045     }
4046     
4047hunk ./contrib/musicplayer/src/workers/scanner.js 39
4048         scan(child);
4049     }
4050     
4051-    delete obj;
4052-    if(!queue)
4053+    if(!--queue)
4054       postMessage("**FINISHED**");
4055   });
4056 }
4057hunk ./contrib/musicplayer/tests/shared.js 65
4058   }
4059 };
4060 
4061-jum.assertSameObjects = function (a, b) {
4062+jum.assertEqualArrays = function (a, b) {
4063+  jum.assertEquals("arrays should have same length", a.length, b.length);
4064
4065+  var aa = ["array", "arguments"];
4066+  for(var n = 0, m = a.length; n < m; n++)
4067+    if(aa.contains($type(a[n])))
4068+      jum.assertEqualArrays(a[n], b[n]);
4069+    else if($type(a[n]) === "object")
4070+      jum.assertSameObjects(a[n], b[n]);
4071+    else
4072+      jum.assertEquals(
4073+        "was '" + JSON.stringify(b) + "', expected '" + JSON.stringify(a) + "'",
4074+        a[n], b[n]
4075+      );
4076
4077+  return true;
4078+};
4079+
4080+jum.assertSameArrays = function (a, b) {
4081+  jum.assertEquals("arrays should have same length", a.length, b.length);
4082
4083+  var aa = ["array", "arguments"];
4084+  for(var n = 0, m = a.length; n < m; n++)
4085+    if(aa.contains($type(a[n])))
4086+      jum.assertEqualArrays(a[n], b[n]);
4087+    else if($type(a[n]) === "object")
4088+      jum.assertSameObjects(a[n], b[n]);
4089+    else
4090+      jum.assertTrue("should contain '" + a[n] + "'", b.indexOf(a[n]) !== -1);
4091
4092+  return true;
4093+};
4094+
4095+jum.assertSameObjects = function (a, b, useSameArrays) {
4096   if(a === b)
4097     return true;
4098hunk ./contrib/musicplayer/tests/shared.js 101
4099-  // catches cases when one of args is null
4100   if(!a || !b)
4101     jum.assertEquals(a, b);
4102   
4103hunk ./contrib/musicplayer/tests/shared.js 104
4104-  if($type(a) === "array") {
4105-    jum.assertTrue("arrays should be of the same length",
4106-      a.length === b.length
4107-    );
4108-   
4109-    // we're doing this in case of of the
4110-    // objects is an 'arguments' object,
4111-    // which lack .indexOf()
4112-    a = $A(a);
4113-    b = $A(b);
4114-   
4115-    for(var n = 0, m = a.length; n < m; n++)
4116-      if(typeof a[n] === "object")
4117-        jum.assertSameObjects(a[n], b[n]);
4118-      else
4119-        jum.assertTrue("should contain '" + a[n] + "'",
4120-          b.indexOf(a[n]) !== -1
4121-        );
4122-  } else {
4123-    for(var prop in a)
4124-      if(a.hasOwnProperty(prop))
4125-        if(prop in a && prop in b)
4126-          if(typeof a[prop] === "object")
4127-            jum.assertSameObjects(a[prop], b[prop]);
4128-          else
4129-            jum.assertEquals(a[prop], b[prop]);
4130+  for(var prop in a)
4131+    if(a.hasOwnProperty(prop))
4132+      if(prop in a && prop in b)
4133+        if($type(a[prop]) === "object")
4134+          jum.assertSameObjects(a[prop], b[prop]);
4135+        else if($type(a[prop]) === "array")
4136+          jum[useSameArrays ? "assertSameArrays" : "assertEqualArrays"](a[prop], b[prop]);
4137         else
4138hunk ./contrib/musicplayer/tests/shared.js 112
4139-          jum.assertTrue("missing '" + prop +"' property", false);
4140-  }
4141+          jum.assertEquals("propety '" + prop + "' differs", a[prop], b[prop]);
4142+    else
4143+      jum.assertTrue("missing '" + prop +"' property", false);
4144   
4145hunk ./contrib/musicplayer/tests/shared.js 116
4146-  delete a;
4147-  delete b;
4148   return true;
4149 };
4150hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 185
4151     jum.assertEquals("map() should have been called three times",
4152       3, self.mapped_docs.length
4153     );
4154-    jum.assertSameObjects(["doc1", "doc2", "doc3"], self.mapped_docs);
4155+    jum.assertSameArrays(["doc1", "doc2", "doc3"], self.mapped_docs);
4156   };
4157   
4158   this.test_verifyMapResult = function () {
4159hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 403
4160           emit("albums", doc.albums);
4161       },
4162       
4163-      reduce: function (keys, values, rereduce) {
4164+      reduce: function (key, values, rereduce) {
4165         if(rereduce) {
4166           self.rereduce_args = arguments;
4167           self.rereduce_called++;
4168hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 479
4169   
4170   this.test_rereduce = function () {
4171     jum.assertEquals(1, self.rereduce_called);
4172-    jum.assertSameObjects([null, [2, 15], true], self.rereduce_args);
4173-  }
4174+    jum.assertEqualArrays([null, [2, 15], true], self.rereduce_args);
4175+  };
4176   
4177   this.teardown = function () {
4178     self.db.killView("test2");
4179hunk ./contrib/musicplayer/tests/test_DocumentTemplate.js 134
4180   };
4181   
4182   this.test_belongsTo = function () {
4183-    self.herbie.get("owner", function (owners) {
4184-      jum.assertEquals(1, owners.length);
4185-      jum.assertEquals(self.jim.id, owners[0].id);
4186+    self.herbie.get("owner", function (owner) {
4187+      jum.assertEquals(self.jim.id, owner.id);
4188       self.got_jim = true;
4189     });
4190   };
4191hunk ./contrib/musicplayer/tests/test_DocumentTemplate.js 209
4192     jum.assertTrue(self.foc_john.created);
4193   };
4194   
4195+  this.test_findById = function () {
4196+    var jim = self.Person.findById("jim");
4197+    jum.assertEquals("jim", jim.id);
4198+    jum.assertEquals("Jim", jim.get("first"));
4199+   
4200+    jum.assertTrue(self.Car.findById("KITT") === null);
4201+  };
4202
4203   this.test_destroy = function () {
4204     self.success_on_destroy = self.failure_on_destroy = false;
4205     self.jim.destroy(function () {
4206hunk ./contrib/musicplayer/tests/test_DocumentTemplate.js 220
4207-      self.Person.findFirst({
4208-        properties: {id: "jim"},
4209-        onSuccess: function() {
4210-          self.success_on_destory = true;
4211-        },
4212-        onFailure: function () {
4213-          self.failure_on_destory = true;
4214-        }
4215-      });
4216+      self.destroyed = true;
4217+      self.found_destroyed_doc = !!self.Person.findById("jim");
4218     });
4219   };
4220   
4221hunk ./contrib/musicplayer/tests/test_DocumentTemplate.js 228
4222   this.wait_forDestroy = {
4223     method: "waits.forJS",
4224     params: {
4225-      js: function () { return self.failure_on_destroy || self.success_on_destroy }
4226+      js: function () { return self.destroyed }
4227     }
4228   };
4229   
4230hunk ./contrib/musicplayer/tests/test_DocumentTemplate.js 233
4231   this.test_verifyDestroy = function () {
4232-    jum.assertTrue(self.failure_on_destroy);
4233-    jum.assertFalse(self.success_on_destroy);
4234+    jum.assertFalse(self.found_destroyed_doc);
4235   };
4236   
4237   this.teardown = function () {
4238hunk ./contrib/musicplayer/tests/test_ProgressBar.js 17
4239   };
4240   
4241   function getPixel(x) {
4242-    return self.pb.ctx.getImageData(x, 0, 1, 1).data
4243+    // px is an ImageData object which mimics Array.
4244+    var px = self.pb.ctx.getImageData(x, 0, 1, 1).data;
4245+    return [px[0], px[1], px[2], px[3]];
4246   }
4247   
4248hunk ./contrib/musicplayer/tests/test_ProgressBar.js 22
4249+  // Precise pixels are not being used due to the fact
4250+  // that each percent is widend for apporx. two pixels,
4251+  // in order to fix browser inconsistencies.
4252   this.test_incrementation = function () {
4253     self.pb.setProgress(0.5);
4254     
4255hunk ./contrib/musicplayer/tests/test_ProgressBar.js 28
4256-    jum.assertSameObjects(BLACK,        getPixel(0));
4257-    jum.assertSameObjects(BLACK,        getPixel(49));
4258-    jum.assertSameObjects(TRANSPARENT,  getPixel(100));
4259+    jum.assertEqualArrays(BLACK,        getPixel(1));
4260+    jum.assertEqualArrays(BLACK,        getPixel(49));
4261+    jum.assertEqualArrays(TRANSPARENT,  getPixel(100));
4262     
4263     self.pb.options.foreground = "rgba(255, 0, 0, 255)";
4264     self.pb.setProgress(0.7);
4265hunk ./contrib/musicplayer/tests/test_ProgressBar.js 35
4266     
4267-    jum.assertSameObjects(BLACK,        getPixel(0));
4268-    jum.assertSameObjects(RED,          getPixel(61));
4269-    jum.assertSameObjects(TRANSPARENT,  getPixel(100));
4270+    jum.assertEqualArrays(BLACK,        getPixel(1));
4271+    jum.assertEqualArrays(RED,          getPixel(52));
4272+    jum.assertEqualArrays(RED,          getPixel(69));
4273+    jum.assertEqualArrays(TRANSPARENT,  getPixel(100));
4274   };
4275   
4276   this.test_decrementation = function () {
4277hunk ./contrib/musicplayer/tests/test_ProgressBar.js 45
4278     self.pb.options.foreground = "rgba(0, 255, 0, 255)";
4279     self.pb.setProgress(0.6);
4280     
4281-    jum.assertSameObjects(RED,          getPixel(0));
4282-    jum.assertSameObjects(TRANSPARENT,  getPixel(61));
4283-    jum.assertSameObjects(TRANSPARENT,  getPixel(70));
4284+    jum.assertEqualArrays(RED,          getPixel(52));
4285+    jum.assertEqualArrays(RED,          getPixel(58));
4286+    jum.assertEqualArrays(TRANSPARENT,  getPixel(60));
4287+    jum.assertEqualArrays(TRANSPARENT,  getPixel(70));
4288   };
4289   
4290   this.teardown = function () {
4291hunk ./contrib/musicplayer/tests/test_ProgressBar.js 52
4292-    this.pb.destroy();
4293+    //this.pb.destroy();
4294   };
4295   
4296   return this;
4297hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 18
4298   };
4299   
4300   function getPixel(x) {
4301-    return self.pb.ctx.getImageData(x, 0, 1, 1).data;
4302+    // px is an ImageData object which mimics Array.
4303+    var px = self.pb.ctx.getImageData(x, 0, 1, 1).data;
4304+    return [px[0], px[1], px[2], px[3]];
4305   }
4306   
4307   this.test_incrementation = function () {
4308hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 26
4309     var ctx = self.pb.ctx;
4310     
4311-    self.pb.setProgress("g", 0.666);
4312-    self.pb.setProgress("r", 0.333);
4313-    self.pb.setProgress("b", 0.999);
4314+    self.pb.setProgress("g", 0.6);
4315+    self.pb.setProgress("r", 0.3);
4316+    self.pb.setProgress("b", 1);
4317     
4318hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 30
4319-    jum.assertSameObjects(RED,    getPixel(0));
4320-    jum.assertSameObjects(RED,    getPixel(20));
4321-    jum.assertSameObjects(RED,    getPixel(34));
4322+    jum.assertEqualArrays(RED,    getPixel(2));
4323+    jum.assertEqualArrays(RED,    getPixel(20));
4324+    jum.assertEqualArrays(RED,    getPixel(29));
4325     
4326hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 34
4327-    jum.assertSameObjects(GREEN,  getPixel(35));
4328-    jum.assertSameObjects(GREEN,  getPixel(50));
4329-    jum.assertSameObjects(GREEN,  getPixel(65));
4330+    jum.assertEqualArrays(GREEN,  getPixel(31));
4331+    jum.assertEqualArrays(GREEN,  getPixel(50));
4332+    jum.assertEqualArrays(GREEN,  getPixel(59));
4333     
4334hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 38
4335-    jum.assertSameObjects(BLUE,   getPixel(68));
4336-    jum.assertSameObjects(BLUE,   getPixel(80));
4337-    jum.assertSameObjects(BLUE,   getPixel(98));
4338+    jum.assertEqualArrays(BLUE,   getPixel(61));
4339+    jum.assertEqualArrays(BLUE,   getPixel(85));
4340+    jum.assertEqualArrays(BLUE,   getPixel(99));
4341   };
4342   
4343   this.test_incrementingMiddleSegment = function () {
4344hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 46
4345     self.pb.setProgress("g", 0.8);
4346     
4347-    jum.assertSameObjects(RED,    getPixel(0));
4348-    jum.assertSameObjects(RED,    getPixel(30));
4349+    jum.assertEqualArrays(RED,    getPixel(2));
4350+    jum.assertEqualArrays(RED,    getPixel(29));
4351     
4352hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 49
4353-    jum.assertSameObjects(GREEN,  getPixel(34));
4354-    jum.assertSameObjects(GREEN,  getPixel(66));
4355-    jum.assertSameObjects(GREEN,  getPixel(80));
4356+    jum.assertEqualArrays(GREEN,  getPixel(31));
4357+    jum.assertEqualArrays(GREEN,  getPixel(66));
4358+    jum.assertEqualArrays(GREEN,  getPixel(79));
4359     
4360hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 53
4361-    jum.assertSameObjects(BLUE,   getPixel(81));
4362+    jum.assertEqualArrays(BLUE,   getPixel(81));
4363   };
4364   
4365   this.test_incrementingFirstSegment = function () {
4366hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 59
4367     self.pb.setProgress("r", 0.9);
4368     
4369-    jum.assertSameObjects(RED,    getPixel(0));
4370-    jum.assertSameObjects(RED,    getPixel(66));
4371-    jum.assertSameObjects(RED,    getPixel(79));
4372-    jum.assertSameObjects(RED,    getPixel(79));
4373-    jum.assertSameObjects(BLUE,   getPixel(91));
4374+    jum.assertEqualArrays(RED,    getPixel(2));
4375+    jum.assertEqualArrays(RED,    getPixel(66));
4376+    jum.assertEqualArrays(RED,    getPixel(79));
4377+   
4378+    jum.assertEqualArrays(BLUE,   getPixel(91));
4379   };
4380   
4381   this.test_decrementingFirstSegment = function () {
4382hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 69
4383     self.pb.setProgress("r", 0.7);
4384     
4385-    jum.assertSameObjects(RED,    getPixel(69));
4386-    jum.assertSameObjects(GREEN,  getPixel(71));
4387-    jum.assertSameObjects(GREEN,  getPixel(79));
4388-    jum.assertSameObjects(BLUE,   getPixel(81));
4389-    jum.assertSameObjects(BLUE,   getPixel(91));
4390+    jum.assertEqualArrays(RED,    getPixel(69));
4391+    jum.assertEqualArrays(GREEN,  getPixel(71));
4392+    jum.assertEqualArrays(GREEN,  getPixel(79));
4393+    jum.assertEqualArrays(BLUE,   getPixel(81));
4394+    jum.assertEqualArrays(BLUE,   getPixel(91));
4395   };
4396   
4397   this.test_decrementingLastSegment = function () {
4398hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 79
4399     self.pb.setProgress("b", 0.2);
4400     
4401-    jum.assertSameObjects(GREEN,   getPixel(79));
4402-    jum.assertSameObjects(TRANS,   getPixel(81));
4403+    jum.assertEqualArrays(GREEN,   getPixel(79));
4404+    jum.assertEqualArrays(TRANS,   getPixel(81));
4405   };
4406   
4407   this.teardown = function () {
4408hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 84
4409-    this.pb.destroy();
4410+    //this.pb.destroy();
4411   };
4412   
4413   return this;
4414hunk ./contrib/musicplayer/tests/test_utils.js 35
4415 
4416 var test_ArrayZip = new function () {
4417   this.test_oneArg = function () {
4418-    jum.assertSameObjects([[1]], Array.zip([1]));
4419+    jum.assertEqualArrays([[1]], Array.zip([1]));
4420   };
4421   
4422   this.test_twoArgs = function () {
4423hunk ./contrib/musicplayer/tests/test_utils.js 39
4424-    jum.assertSameObjects([[1, 1], [2, 2], [3, 3]], Array.zip([1, 2, 3], [1, 2, 3]));
4425+    jum.assertEqualArrays([[1, 1], [2, 2], [3, 3]], Array.zip([1, 2, 3], [1, 2, 3]));
4426   };
4427   
4428   this.test_moreSimpleArgs = function () {
4429hunk ./contrib/musicplayer/tests/test_utils.js 43
4430-    jum.assertSameObjects([[1, 2, 3, 4, 5]], Array.zip([1], [2], [3], [4], [5]));
4431+    jum.assertEqualArrays([[1, 2, 3, 4, 5]], Array.zip([1], [2], [3], [4], [5]));
4432   };
4433   
4434   this.test_notSameLength = function () {
4435hunk ./contrib/musicplayer/tests/test_utils.js 47
4436-    jum.assertSameObjects([[1, 2, 4]], Array.zip([1], [2, 3], [4, 5, 6]));
4437-    jum.assertSameObjects([
4438+    jum.assertEqualArrays([[1, 2, 4]], Array.zip([1], [2, 3], [4, 5, 6]));
4439+    jum.assertEqualArrays([
4440       [1, 4, 6],
4441       [2, 5, undefined],
4442       [3, undefined, undefined]
4443}
4444[add-controller-tests
4445josip.lisec@gmail.com**20100725094652
4446 Ignore-this: 506444f1ed082b7fd7caca82c1a2129f
4447 Added tests for:
4448   * da.ui.Dialog
4449   * da.controller.CollectionScanner
4450   * da.controller.Settings
4451] {
4452addfile ./contrib/musicplayer/tests/test_CollectionScannerController.js
4453hunk ./contrib/musicplayer/tests/test_CollectionScannerController.js 1
4454+var test_CollectionScannerController = new function () {
4455+  var CS = da.controller.CollectionScanner,
4456+      self = this;
4457
4458+  this.test_waitForScannerToFinish = {
4459+    method: "waits.forJS",
4460+    params: {
4461+      js: function () {
4462+        return da.db.DEFAULT.views.Song.view.rows.length === 3
4463+      }
4464+    }
4465+  };
4466
4467+  this.test_foundSongs = function () {
4468+    var songs = $A(da.db.DEFAULT.views.Song.view.rows);
4469+    for(var n = 0, m = songs.length; n < m; n++)
4470+      songs[n] = songs[n].value.title;
4471+   
4472+    jum.assertEquals("there should be three songs",
4473+      3, songs.length
4474+    );
4475+   
4476+    jum.assertSameArrays(["Maps", "Persona", "Hey Big Bang"], songs);
4477+  };
4478
4479+  this.test_foundArtists = function () {
4480+    var artists = $A(da.db.DEFAULT.views.Artist.view.rows);
4481+    for(var n = 0, m = artists.length; n < m; n++)
4482+      artists[n] = artists[n].value.title;
4483+   
4484+    jum.assertEquals("there should be two artists",
4485+      2, artists.length
4486+    );
4487+    jum.assertSameArrays(["Keane", "Superhumanoids"], artists);
4488+  };
4489
4490+  this.test_foundAlbums = function () {
4491+    var albums = $A(da.db.DEFAULT.views.Album.view.rows);
4492+    for(var n = 0, m = albums.length; n < m; n++)
4493+      albums[n] = albums[n].value.title;
4494+   
4495+    jum.assertEquals("there should be two albums",
4496+      2, albums.length
4497+    );
4498+    jum.assertSameArrays(["Sunshine Retrospective Collect", "Urgency"], albums);
4499+  };
4500+};
4501addfile ./contrib/musicplayer/tests/test_Dialog.js
4502hunk ./contrib/musicplayer/tests/test_Dialog.js 1
4503+var test_Dialog = new function () {
4504+  var Dialog = da.ui.Dialog,
4505+      self = this;
4506
4507+  this.setup = function () {
4508+    self.dialog = new Dialog({
4509+      title: "dialog_title",
4510+      html: new Element("div", {id: "_t_dialog", html: "dialog_content"})
4511+    });
4512+  };
4513
4514+  this.test_domNodes = function () {
4515+    var el = self.dialog.toElement();
4516+    jum.assertTrue("dialog should have been inserted into document's body",
4517+      !!$("_t_dialog")
4518+    );
4519+    jum.assertTrue("dialog should be hidden",
4520+      el.style.display !== "block"
4521+    );
4522+    jum.assertEquals("dialog should have title bar",
4523+      "dialog_title",
4524+      el.getElement(".dialog_title").get("text")
4525+    );
4526+    jum.assertTrue("dialog should have its contents",
4527+      el.getElement(".dialog").get("text").indexOf("dialog_content") > 0
4528+    );
4529+  };
4530
4531+  this.test_events = function () {
4532+    var show = hide = 0;
4533+    self.dialog.addEvent("show", function () {
4534+      show++;
4535+    });
4536+    self.dialog.addEvent("hide", function () {
4537+      hide++;
4538+    });
4539+   
4540+    var el = self.dialog.toElement();
4541+    self.dialog.show();
4542+    jum.assertEquals("dialog should be visible",
4543+      "block", el.style.display
4544+    );
4545+    jum.assertEquals("show event should be fired",
4546+      1, show
4547+    );
4548+   
4549+    self.dialog.show();
4550+    jum.assertEquals("show event should be fired only first time",
4551+      1, show
4552+    );
4553+   
4554+    self.dialog.hide();
4555+    jum.assertEquals("dialog should be hidden",
4556+      "none", el.style.display
4557+    );
4558+    jum.assertEquals("hide event should be fired",
4559+      1, hide
4560+    );
4561+   
4562+    self.dialog.hide();
4563+    jum.assertEquals("hide event should be fired only first time",
4564+      1, hide
4565+    );
4566+  };
4567
4568+  this.test_destroy = function () {
4569+    var el = self.dialog.toElement();
4570+    self.dialog.destroy();
4571+   
4572+    jum.assertEquals("nodes should be removed from document",
4573+      null, el.getParent()
4574+    );
4575+  };
4576
4577+  return this;
4578+};
4579addfile ./contrib/musicplayer/tests/test_SettingsController.js
4580hunk ./contrib/musicplayer/tests/test_SettingsController.js 1
4581-
4582+var test_SettingsController = new function () {
4583+  var Settings = da.controller.Settings,
4584+      Setting = da.db.DocumentTemplate.Setting,
4585+      self = this;
4586
4587+  this.setup = function () {
4588+    Setting.register({
4589+      id:           "_is_this_working",
4590+      group_id:     "_test",
4591+      representAs:  "_test_question",
4592+      title:        "Is this working?",
4593+      help:         "Check this box if you think that it's going to work.",
4594+      value:        false
4595+    });
4596+   
4597+    Settings.registerGroup({
4598+      id:           "_test",
4599+      title:        "Test",
4600+      description:  "Help can be usually reached by calling 112."
4601+    });
4602+   
4603+    self.renderer_called = 0;
4604+    Settings.addRenderer("_test_question", function (setting, details) {
4605+      self.renderer_called++;
4606+     
4607+      jum.assertEquals("'_is_this_working' setting should be passed",
4608+        "_is_this_working",
4609+        setting.id
4610+      );
4611+     
4612+      return this.checkbox(setting, details);
4613+    });
4614+   
4615+    self.serializer_called = 0;
4616+    Settings.addSerializer("_test_question", function (input) {
4617+      self.serializer_called++;
4618+      jum.assertEquals("right input element should be passed to serializer",
4619+        "setting__is_this_working",
4620+        input.id
4621+      );
4622+      return input.checked;
4623+    });
4624+  };
4625
4626+  this.test_getDetails = function () {
4627+    jum.assertSameObjects({
4628+      representAs: "_test_question",
4629+      title: "Is this working?",
4630+      help: "Check this box if you think that it's going to work."
4631+    }, Setting.getDetails("_is_this_working"));
4632+  };
4633
4634+  this.test_waitForSetting = {
4635+    method: "waits.forJS",
4636+    params: {
4637+      js: function () {
4638+        return da.db.SETTINGS.views.Setting.view.findRow("_is_this_working") !== -1
4639+      }
4640+    }
4641+  };
4642
4643+  this.test_render = function () {
4644+    Settings.showGroup("_test");
4645+    jum.assertEquals("renderer should have been called once",
4646+      1, self.renderer_called
4647+    );
4648+    jum.assertEquals("serializer shouldn't have been called yet",
4649+      0, self.serializer_called
4650+    );
4651+  };
4652
4653+  this.test_revert = function () {
4654+    $("setting__is_this_working").checked = true;
4655+    $("revert_settings").click();
4656+   
4657+    jum.assertFalse("checkbox should be unchecked",
4658+      $("setting__is_this_working").checked
4659+    );
4660+   
4661+    jum.assertEquals("renderer should be called during revert",
4662+      2, self.renderer_called
4663+    );
4664+  };
4665
4666+  this.test_serialize = function () {
4667+    $("setting__is_this_working").checked = true;
4668+    $("save_settings").click();
4669+   
4670+    jum.assertEquals("serializer called once",
4671+      1, self.serializer_called
4672+    );
4673+   
4674+    self.saved_doc = null;
4675+    Setting.findFirst({
4676+      properties: {id: "_is_this_working"},
4677+      onSuccess: function (doc) {
4678+        self.saved_doc = doc;
4679+      }
4680+    });
4681+  };
4682
4683+  this.test_waitForDoc = {
4684+    method: "waits.forJS",
4685+    params: {
4686+      js: function () { return !!self.saved_doc }
4687+    }
4688+  };
4689
4690+  this.test_verifySave = function () {
4691+    jum.assertTrue("setting's value should be saved to the db",
4692+      self.saved_doc.get("value")
4693+    );
4694+  };
4695
4696+  this.teardown = function () {
4697+    Settings.hide();
4698+  };
4699
4700+  return this;
4701+};
4702}
4703[add-player-controls
4704josiplisec@gmail.com**20100728131919
4705 Ignore-this: 56f8d094357df285a679a04bf536c582
4706 * Added player controls (previos/play/next) with tests.
4707 * Made other, smaller, improvements to da.ui.NavigationColumn (smarter re-rendering)
4708 * Limited both scanner and indexer workers to allow only one request to Tahoe-LAFS,
4709   thus making network I/O almoast synchronous, mainly due to the fact that
4710   Tahoe never completes requests under high load.
4711 
4712] {
4713hunk ./contrib/musicplayer/manage.py 6
4714 
4715 import os, shutil, sys, subprocess, re
4716 from time import sleep
4717+from tempfile import mkstemp
4718 from setuptools import setup
4719 from setuptools import Command
4720 
4721hunk ./contrib/musicplayer/manage.py 125
4722     shutil.copy('src/libs/vendor/browser-couch/js/worker-map-reducer.js', 'build/js/workers/map-reducer.js')
4723     
4724     print 'Calculating dependencies...'
4725-    appjs = JSDepsBuilder('src/')
4726-    appjs.write_to_file('build/app.js')
4727-    self._compress('build/js/app.js', ['build/app.js'])
4728-    os.remove('build/app.js')
4729+    self.deps = JSDepsBuilder('src/')
4730     
4731hunk ./contrib/musicplayer/manage.py 127
4732-    # Libraries used by web workers
4733-    self._compile_js('src/libs', 'build/js/workers/env.js', files = [
4734-      'vendor/mootools-1.2.4-core-server.js',
4735-      'vendor/mootools-1.2.4-request.js',
4736-     
4737-      'util/util.js',
4738-      'util/BinaryFile.js',
4739-      'util/ID3.js',
4740-      'util/ID3v2.js',
4741-      'util/ID3v1.js',
4742-      'TahoeObject.js'
4743-    ])
4744+    self._make_js('Application.js', 'build/js/app.js')
4745+   
4746+    #deps.write_to_file('build/app.js')
4747+    #self._compress('build/js/app.js', ['build/app.js'])
4748+    #os.remove('build/app.js')
4749     
4750hunk ./contrib/musicplayer/manage.py 133
4751-    # Compressing the workers themselves
4752-    self._compile_js('src/workers', 'build/js/workers/', join = False)
4753+    for worker in os.listdir('src/workers'):
4754+      if worker.endswith('.js'):
4755+        self._make_js('workers/' + worker, 'build/js/workers/' + worker)
4756+        #deps.write_to_file('build/worker_' + worker, root_file = 'workers/' + worker)
4757     
4758     print 'Done!'
4759   
4760hunk ./contrib/musicplayer/manage.py 140
4761-  def _compile_js(self, source, output_file, files = None, join = True):
4762-    js_files = files
4763-    if not js_files:
4764-      js_files = []
4765-      for filename in os.listdir(source):
4766-        if filename.endswith('.js'):
4767-          js_files.append(os.path.join(source, filename))
4768-         
4769-      js_files.sort()
4770-    else:
4771-      js_files = [os.path.join(source, path) for path in files]
4772-   
4773-    if join:
4774-      self._compress(output_file, js_files)
4775-    else:
4776-      for js_file in js_files:
4777-        self._compress(output_file + os.path.basename(js_file), [js_file])
4778+  #def _compile_js(self, source, output_file, files = None, join = True):
4779+  #  js_files = files
4780+  #  if not js_files:
4781+  #    js_files = []
4782+  #    for filename in os.listdir(source):
4783+  #      if filename.endswith('.js'):
4784+  #        js_files.append(os.path.join(source, filename))
4785+  #       
4786+  #    js_files.sort()
4787+  #  else:
4788+  #    js_files = [os.path.join(source, path) for path in files]
4789+  # 
4790+  #  if join:
4791+  #    self._compress(output_file, js_files)
4792+  #  else:
4793+  #    for js_file in js_files:
4794+  #      self._compress(output_file + os.path.basename(js_file), [js_file])
4795
4796+  def _make_js(self, root, output):
4797+    tmp_file = mkstemp()[1]
4798+    self.deps.write_to_file(tmp_file, root_file = root)
4799+    self._compress(output, [tmp_file])
4800+    os.remove(tmp_file)
4801   
4802   def _compress(self, output_file, files):
4803     print 'Compressing %s...' % output_file
4804hunk ./contrib/musicplayer/manage.py 264
4805     
4806     root_dirs = [
4807       'src/', 'src/libs', 'src/libs/ui', 'src/libs/db',
4808-      'src/libs/util', 'src/controllers', 'src/doctemplates'
4809+      'src/libs/util', 'src/controllers', 'src/doctemplates',
4810+      'src/services'
4811     ]
4812     for root_dir in root_dirs:
4813       for filename in os.listdir(root_dir):
4814hunk ./contrib/musicplayer/src/Application.js 3
4815 //#require "libs/vendor/mootools-1.2.4-core-ui.js"
4816 //#require "libs/vendor/mootools-1.2.4.4-more.js"
4817+//#require "libs/util/console.js"
4818 
4819 /**
4820  * da
4821hunk ./contrib/musicplayer/src/Application.js 55
4822       da.db.DEFAULT = db;
4823       this.startup.checkpoint("data_db");
4824     }.bind(this), new PersistStorage("tahoemp_data"));
4825+   
4826+    this.addEvent("ready.controller.CollectionScanner", function () {
4827+      if(!da.db.DEFAULT.getLength())
4828+        da.controller.CollectionScanner.scan();
4829+    });
4830   },
4831   
4832   loadInitialSettings: function () {
4833hunk ./contrib/musicplayer/src/Application.js 73
4834           {id: "settings_cap",  type: "Setting", group_id: "caps", value: data.settings_cap}
4835         ], function () {
4836           this.startup.checkpoint("settings_db");
4837-
4838-          this.caps.music = data.music_cap,
4839+         
4840+          this.caps.music = data.music_cap;
4841           this.caps.settings = data.settings_cap;
4842hunk ./contrib/musicplayer/src/Application.js 76
4843-
4844+         
4845           this.startup.checkpoint("caps");
4846hunk ./contrib/musicplayer/src/Application.js 78
4847+         
4848+          if(!da.db.DEFAULT.getLength())
4849+            da.controller.CollectionScanner.scan();
4850         }.bind(this));
4851       }.bind(this),
4852       
4853hunk ./contrib/musicplayer/src/Application.js 110
4854       },
4855       
4856       finished: function (result) {
4857+        if(!result.rows.length)
4858+          return this.loadInitialSettings();
4859+       
4860         this.caps.music = result.getRow("music_cap");
4861         this.caps.settings = result.getRow("settings_cap");
4862         if(!this.caps.music.length || !this.caps.music.length)
4863hunk ./contrib/musicplayer/src/Application.js 122
4864       }.bind(this),
4865       
4866       updated: function (result) {
4867-        if(result.findRow("music_cap") !== -1) {
4868-          this.caps.music = result.getRow("music_cap");
4869-          da.controller.CollectionScanner.scan(this.caps.music);
4870-        }
4871-        if(result.findRow("settings_cap") !== -1)
4872-          this.caps.settings = result.getRow("settings_cap")
4873+        var music    = result.getRow("music_cap"),
4874+            settings = result.getRow("settings_cap");
4875+       
4876+        if(music)
4877+          da.controller.CollectionScanner.scan(this.caps.music = music);
4878+       
4879+        if(settings)
4880+          this.caps.settings = settings;
4881+       
4882+        this.startup.checkpoint("caps");
4883       }.bind(this)
4884     });
4885   },
4886hunk ./contrib/musicplayer/src/Application.js 146
4887     $("loader").destroy();
4888     $("panes").setStyle("display", "block");
4889     
4890-    if(da.db.DEFAULT.getLength() === 0)
4891-      da.controller.CollectionScanner.scan();
4892-
4893     this.fireEvent("ready");
4894   }
4895 };
4896hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 29
4897    *  Starts a new scan using [[Application.caps.music]] as root directory.
4898    **/
4899   initialize: function (root) {
4900+    console.log("collection scanner started");
4901     this.indexer = new Worker("js/workers/indexer.js");
4902     this.indexer.onmessage = this.onIndexerMessage.bind(this);
4903     
4904hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 40
4905     
4906     this.finished = false;
4907     
4908-    this._songs_found = 0;
4909+    this._found_files = 0;
4910     this._goal = new Goal({
4911       checkpoints: ["scanner", "indexer"],
4912       onFinish: function () {
4913hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 50
4914         da.ui.ROAR.alert(
4915           "Collection scanner finished",
4916           "{0} songs have been found. {1}".interpolate([
4917-            this._found_songs,
4918-            this._found_songs ? "Your patience has paid off." : "Make sure your files have proper ID3 tags."
4919+            this._found_files,
4920+            this._found_files ? "Your patience has paid off." : "Make sure your files have proper ID3 tags."
4921           ])
4922         );
4923       }.bind(this)
4924hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 76
4925       return;
4926     }
4927     
4928-    this._found_files++;
4929     if(da.db.DEFAULT.views.Song.view.findRow(cap) === -1)
4930       this.indexer.postMessage(cap);
4931   },
4932hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 85
4933       this._goal.checkpoint("indexer");
4934       return;
4935     }
4936-
4937+   
4938     // Lots of async stuff is going on, a short summary would look something like:
4939     // 1. find or create artist with given name and save its id
4940     //    to artist_id.
4941hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 92
4942     // 2. look for an album with given artist_id (afterCheckpoint.artist)
4943     // 3. save the album data.
4944     // 4. look for song with given id and save the new data.
4945+    this._found_files++;
4946     
4947     var tags = event.data,
4948         album_id, artist_id,
4949hunk ./contrib/musicplayer/src/controllers/Navigation.js 265
4950  * da.controller.Navigation
4951  **/
4952 var Navigation = {
4953-  // This is not really a class, but PDoc refuses to generate docs otherwise.
4954   /**
4955hunk ./contrib/musicplayer/src/controllers/Navigation.js 266
4956-   *  da.controller.Navigation.columns
4957+   * da.controller.Navigation.columns
4958    * 
4959hunk ./contrib/musicplayer/src/controllers/Navigation.js 268
4960-   *  Contains all known columns.
4961-   * 
4962-   *  #### Notes
4963-   *  Use [[da.controller.Navigation.registerColumn]] to add new ones,
4964-   *  *do not* add them manually.
4965+   * Contains all known columns.
4966+   *
4967+   * #### Notes
4968+   * Use [[da.controller.Navigation.registerColumn]] to add new ones,
4969+   * *do not* add them manually.
4970    **/
4971   columns: {},
4972   
4973hunk ./contrib/musicplayer/src/controllers/Player.js 1
4974-//#require "libs/vendor/soundmanager/script/soundmanager2.js"
4975+//#require "libs/vendor/SoundManager.js"
4976+//#require "doctemplates/Song.js"
4977 //#require "libs/ui/SegmentedProgressBar.js"
4978hunk ./contrib/musicplayer/src/controllers/Player.js 4
4979-//#require "services/albumCover.js"
4980 
4981 (function () {
4982hunk ./contrib/musicplayer/src/controllers/Player.js 6
4983-var DocumentTemplate      = da.db.DocumentTemplate,
4984-    Song                  = DocumentTemplate.Song,
4985-    Setting               = DocumentTemplate.Setting,
4986+var soundManager          = da.vendor.soundManager,
4987+    Song                  = da.db.DocumentTemplate.Song,
4988     SegmentedProgressBar  = da.ui.SegmentedProgressBar;
4989 
4990 /** section: Controllers
4991hunk ./contrib/musicplayer/src/controllers/Player.js 24
4992  *
4993  **/
4994 var Player = {
4995
4996+  // We're using term sound for soundManager objects, while
4997+  // song for DocumentTemplate.Song instances.
4998+  /**
4999+   *  Player#sounds -> Object
5000+   *
5001+   *  Cache of SoundManager's object. Keys are id's of [[da.db.DocumentTemplate.Song]].
5002+   **/
5003+  sounds: {},
5004
5005+  /**
5006+   *  Player#playlist -> [String, ...]
5007+   *  Contains id's of songs in the playlist.
5008+   **/
5009+  playlist: [],
5010+  _playlist_pos: 0,
5011
5012+  /**
5013+   *  Player#queue -> [da.db.DocumentTemplate.Song, ...]
5014+   **/
5015+  queue: [],
5016
5017+  /**
5018+   *  Player#nowPlaying -> {song: <da.db.DocumentTemplate.Song>, sound: <SMSound>}
5019+   **/
5020+  nowPlaying: {
5021+    song: null,
5022+    sound: null
5023+  },
5024+  _loading: [],
5025
5026   /**
5027    *  new Player()
5028    *  Sets up soundManager2 and initializes player's interface.
5029hunk ./contrib/musicplayer/src/controllers/Player.js 60
5030    **/
5031   initialize: function () {
5032-    var path = location.protocol + "//" + location.host + location.pathname;
5033-    //path = path.slice(-path.lastIndexOf("/"));
5034-    $extend(soundManager, {
5035-      useHTML5Audio:  false,
5036-      url:            path + 'resources/flash/',
5037-      debugMode:      false,
5038-      debugFlash:     false
5039-    });
5040-   
5041     soundManager.onready(function () {
5042       da.app.startup.checkpoint("soundmanager");
5043     });
5044hunk ./contrib/musicplayer/src/controllers/Player.js 65
5045     
5046     da.app.addEvent("ready", this.initializeUI.bind(this));
5047-   
5048-    // We're using term sound for soundManager objects, while
5049-    // song for DocumentTemplate.Song instances.
5050-    /**
5051-     *  Player#sounds -> Object
5052-     *
5053-     *  Cache of SoundManager's object. Keys are id's of [[da.db.DocumentTemplate.Song]].
5054-     **/
5055-    this.sounds = {};
5056-    /**
5057-     *  Player#playlist -> [Song, ...]
5058-     **/
5059-    this.playlist = [];
5060-    /**
5061-     *  Player#nowPlaying -> {song: <da.db.DocumentTemplate.Song>, sound: <SMSound>}
5062-     **/
5063-    this.nowPlaying = {song: null, sound: null};
5064-   
5065-    this._loading = [];
5066   },
5067   
5068   initializeUI: function () {
5069hunk ./contrib/musicplayer/src/controllers/Player.js 81
5070     
5071     this.progress_bar.toElement().id = "track_progress";
5072     this.progress_bar.toElement().addEvents({
5073-      // Has some issues in firefox - the object's width also gets scaled
5074+      // Has some issues in Firefox - the object's width also gets scaled
5075       /*
5076       mouseenter: function () {
5077         this.tween("height", 15);
5078hunk ./contrib/musicplayer/src/controllers/Player.js 100
5079       }
5080     });
5081     
5082-    var info_block    = new Element("div",  {id: "song_info_block" }),
5083-        wrapper       = new Element("div",  {id: "song_details"}),
5084-        cover_wrapper = new Element("div",  {id: "song_album_cover_wrapper"}),
5085-        album_cover   = new Element("img",  {id: "song_album_cover"}),
5086-        song_title    = new Element("h2",   {id: "song_title"}),
5087-        album_title   = new Element("span", {id: "song_album_title"}),
5088-        artist_name   = new Element("span", {id: "song_artist_name"}),
5089-        position      = new Element("span", {id: "song_position"}),
5090-        controls      = new Element("div",  {id: "player_controls"}),
5091-        play          = new Element("a",    {id: "play_button", href: "#"});
5092+    var els = {
5093+      info_block:     new Element("div",  {id: "song_info_block" }),
5094+      wrapper:        new Element("div",  {id: "song_details"}),
5095+      cover_wrapper:  new Element("div",  {id: "song_album_cover_wrapper"}),
5096+      album_cover:    new Element("img",  {id: "song_album_cover"}),
5097+      song_title:     new Element("h2",   {id: "song_title"}),
5098+      album_title:    new Element("span", {id: "song_album_title"}),
5099+      artist_name:    new Element("span", {id: "song_artist_name"}),
5100+      controls:       new Element("div",  {id: "player_controls", "class": "no_selection"}),
5101+      play:           new Element("a",    {id: "play_button", href: "#"}),
5102+      next:           new Element("a",    {id: "next_song", href: "#"}),
5103+      prev:           new Element("a",    {id: "prev_song", href: "#"})
5104+    };
5105+   
5106+    var play_wrapper = new Element("div", {id: "play_button_wrapper"});
5107+    play_wrapper.grab(els.play);
5108+    els.controls.adopt(els.prev, play_wrapper, els.next, this.progress_bar.toElement());
5109+   
5110+    els.wrapper.grab(els.song_title);
5111+    els.wrapper.appendText("from ");
5112+    els.wrapper.grab(els.album_title);
5113+    els.wrapper.appendText(" by ");
5114+    els.wrapper.adopt(els.artist_name, els.controls);
5115+   
5116+    els.cover_wrapper.grab(els.album_cover);
5117     
5118hunk ./contrib/musicplayer/src/controllers/Player.js 126
5119-    controls.adopt(play, this.progress_bar.toElement());
5120+    els.info_block.adopt(els.cover_wrapper, els.wrapper, new Element("div", {"class": "clear"}));
5121+   
5122+    this._el.adopt(els.info_block);
5123     
5124hunk ./contrib/musicplayer/src/controllers/Player.js 130
5125-    wrapper.grab(song_title);
5126-    wrapper.appendText("from ");
5127-    wrapper.grab(album_title);
5128-    wrapper.appendText(" by ");
5129-    wrapper.adopt(artist_name, position, controls);
5130+    this._el.style.visibility = "hidden";
5131+    this._visible = false;
5132     
5133hunk ./contrib/musicplayer/src/controllers/Player.js 133
5134-    cover_wrapper.grab(album_cover);
5135+    var next = els.next, prev = els.prev;
5136+    els.play.addEvents({
5137+      click: function () {
5138+        Player.playPause();
5139+      },
5140+     
5141+      mouseover: function () {
5142+        if(!next.hasClass("inactive"))
5143+          next.fade("show");
5144+        if(!prev.hasClass("inactive"))
5145+          prev.fade("show");
5146+      }
5147+    });
5148     
5149hunk ./contrib/musicplayer/src/controllers/Player.js 147
5150-    info_block.adopt(cover_wrapper, wrapper, new Element("div", {"class": "clear"}));
5151-    info_block.style.visibility = "hidden";
5152-    this._info_block_visible = false;
5153+    var hideNextPrev = function () {
5154+      next.fade("out");
5155+      prev.fade("out");
5156+    };
5157     
5158hunk ./contrib/musicplayer/src/controllers/Player.js 152
5159-    this._el.adopt(info_block);
5160+    els.next.addEvents({
5161+      click:   function () { Player.playNext() }
5162+    });
5163+    els.next.set("tween", {duration: "short", link: "cancel"});
5164     
5165hunk ./contrib/musicplayer/src/controllers/Player.js 157
5166-    play.addEvent("click", function () {
5167-      Player.playPause();
5168+    els.prev.addEvents({
5169+      click:    function () { Player.playPrev() }
5170     });
5171hunk ./contrib/musicplayer/src/controllers/Player.js 160
5172+    els.prev.set("tween", {duration: "short", link: "cancel"});
5173+   
5174+    els.controls.addEvent("mouseleave", hideNextPrev);
5175+   
5176+    this.elements = els;
5177+    delete els;
5178+    delete play_wrapper;
5179   },
5180   
5181   /**
5182hunk ./contrib/musicplayer/src/controllers/Player.js 177
5183    **/
5184   play: function (song) {
5185     var np = this.nowPlaying;
5186-    if(!song && np.song)
5187-      np.sound.resume();
5188-   
5189-    $("play_button").addClass("active");
5190-   
5191-    if(np.song && song.id === np.song.id)
5192+    if(!song || (song && np.song && np.song.id === song.id))
5193       return;
5194     
5195hunk ./contrib/musicplayer/src/controllers/Player.js 180
5196+    this.elements.play.addClass("active");
5197+   
5198     if(this.sounds[song.id]) {
5199       np.sound.stop();
5200       this.sounds[song.id].play();
5201hunk ./contrib/musicplayer/src/controllers/Player.js 185
5202+      this.progress_bar.setProgress("load", 1);
5203       return;
5204     }
5205     
5206hunk ./contrib/musicplayer/src/controllers/Player.js 209
5207       onplay: function () {
5208         Player.setNowPlaying(song);
5209       },
5210+     
5211       whileloading: function () {
5212hunk ./contrib/musicplayer/src/controllers/Player.js 211
5213-        Player.progress_bar.setProgress("load", this.bytesLoaded/this.bytesTotal)
5214+        if(Player.nowPlaying.sound === this)
5215+          Player.progress_bar.setProgress("load", this.bytesLoaded/this.bytesTotal);
5216       },
5217hunk ./contrib/musicplayer/src/controllers/Player.js 214
5218+     
5219       whileplaying: function () {
5220hunk ./contrib/musicplayer/src/controllers/Player.js 216
5221-        Player.progress_bar.setProgress("track", this.position/this.durationEstimate);
5222+        if(Player.nowPlaying.sound === this)
5223+          Player.progress_bar.setProgress("track", this.position/this.durationEstimate);
5224       },
5225hunk ./contrib/musicplayer/src/controllers/Player.js 219
5226+     
5227       onfinish: function () {
5228hunk ./contrib/musicplayer/src/controllers/Player.js 221
5229-        Player.playbackFinished();
5230+        if(Player.nowPlaying.sound === this)
5231+          Player.playbackFinished();
5232       }
5233     });
5234     
5235hunk ./contrib/musicplayer/src/controllers/Player.js 229
5236     this.sounds[song.id].play();
5237     np = null;
5238     
5239-    if(!this._info_block_visible) {
5240-      this._info_block_visible = true;
5241-      $("song_info_block").style.visibility = "visible";
5242+    if(!this._visible) {
5243+      this._visible = true;
5244+      this._el.style.visibility = "visible";
5245     }
5246   },
5247   
5248hunk ./contrib/musicplayer/src/controllers/Player.js 237
5249   /**
5250    *  Player#setNowPlaying(song) -> undefined
5251-   *  fires nowPlaying
5252+   *  fires play
5253    **/
5254   setNowPlaying: function (song) {
5255     if(this.nowPlaying.sound)
5256hunk ./contrib/musicplayer/src/controllers/Player.js 243
5257       this.nowPlaying.sound._last_played = +(new Date());
5258     
5259+    var pp = this.playlist.indexOf(song.id);
5260+    if(pp !== -1)
5261+      this._playlist_pos = pp;
5262+    delete pp;
5263+   
5264     this.nowPlaying = {
5265       song:   song,
5266       sound:  this.sounds[song.id]
5267hunk ./contrib/musicplayer/src/controllers/Player.js 254
5268     };
5269     
5270     var song_title = song.get("title"),
5271-        song_title_el = $("song_title");
5272-    song_title_el.set("text", song_title);
5273-    song_title_el.title = song_title;
5274-    $("song_position").title = "total " + song.get("duration");
5275+        els = this.elements;
5276+   
5277+    els.song_title.set("text", song_title);
5278+    els.song_title.title = song_title;
5279     
5280     song.get("album", function (album) {
5281       var title = album.get("title"),
5282hunk ./contrib/musicplayer/src/controllers/Player.js 262
5283           album_covers = album.get("album_cover_urls"),
5284-          el = $("song_album_title");
5285-     
5286-      el.set("text", title);
5287-      el.title = title;
5288+          has_album_cover = album_covers && album_covers[2] && album_covers[2].length;
5289       
5290hunk ./contrib/musicplayer/src/controllers/Player.js 264
5291-      title = null;
5292-      delete el;
5293+      els.album_title.set("text", title);
5294+      els.album_title.title = title;
5295+      els.album_cover.src = has_album_cover ? album_covers[2] : "resources/images/album_cover_2.png";
5296       
5297hunk ./contrib/musicplayer/src/controllers/Player.js 268
5298-      if(album_covers)
5299-        $("song_album_cover").src = album_covers[2];
5300-      else
5301-        da.service.albumCover(album, function (urls) {
5302-          $("song_album_cover").src = urls[2];
5303-        });
5304-     
5305-      album = null;
5306-      album_covers = null;
5307+      delete album;
5308+      delete title;
5309+      delete album_covers;
5310+      delete has_album_cover;
5311     });
5312hunk ./contrib/musicplayer/src/controllers/Player.js 273
5313+   
5314     song.get("artist", function (artist) {
5315hunk ./contrib/musicplayer/src/controllers/Player.js 275
5316-      var aname = artist.get("title"),
5317-           el    = $("song_artist_name");
5318-      el.set("text", aname);
5319-      el.title = aname;
5320+      var aname = artist.get("title");
5321+      els.artist_name.set("text", aname);
5322+      els.artist_name.title = aname;
5323       
5324       aname = null;
5325hunk ./contrib/musicplayer/src/controllers/Player.js 280
5326-      delete el;
5327+      delete els;
5328     });
5329     
5330hunk ./contrib/musicplayer/src/controllers/Player.js 283
5331-    da.controller.Player.fireEvent("nowPlaying", [song]);
5332+    da.controller.Player.fireEvent("play", [song]);
5333+    song = null;
5334+   
5335+    this.updateNextPrev();
5336   },
5337   
5338   /**
5339hunk ./contrib/musicplayer/src/controllers/Player.js 297
5340    *  repeat this song, etc.
5341    **/
5342   playbackFinished: function () {
5343-    $("play_button").removeClass("active");
5344+    this.elements.play.removeClass("active");
5345+   
5346+    this.playNext();
5347   },
5348   
5349   /**
5350hunk ./contrib/musicplayer/src/controllers/Player.js 305
5351    *  Player#pause() -> undefined
5352    *  fires pause
5353-   *
5354    **/
5355   pause: function () {
5356     if(!this.nowPlaying.song)
5357hunk ./contrib/musicplayer/src/controllers/Player.js 311
5358       return;
5359     
5360     this.nowPlaying.sound.pause();
5361-    $("play_button").removeClass("active");
5362+    this.elements.play.removeClass("active");
5363     da.controller.Player.fireEvent("pause", [this.nowPlaying.song]);
5364   },
5365   
5366hunk ./contrib/musicplayer/src/controllers/Player.js 327
5367     if(!this.nowPlaying.song && song)
5368       this.play(song);
5369     
5370-    if(this.nowPlaying.sound.paused)
5371-      this.play();
5372-    else
5373+    if(this.nowPlaying.sound.paused) {
5374+      this.nowPlaying.sound.resume();
5375+    } else
5376       this.pause();
5377   },
5378   
5379hunk ./contrib/musicplayer/src/controllers/Player.js 334
5380   /**
5381+   *  Player#playPrev() -> undefined
5382+   **/
5383+  playPrev: function () {
5384+    this.play(Song.findById(this.playlist[this._playlist_pos - 1]));
5385+  },
5386
5387+  /**
5388+   *  Player#getNext() -> String
5389+   * 
5390+   *  Returns the ID of the song that will be played next.
5391+   **/
5392+  getNext: function () {
5393+    if(this.queue.length)
5394+      return this.queue[0];
5395+   
5396+    if(this.playlist.length)
5397+      return this.playlist[this._playlist_pos + 1];
5398+  },
5399
5400+  /**
5401+   *  Player#playNext() -> undefined
5402+   **/
5403+  playNext: function () {
5404+    var next = this.getNext();
5405+    if(!next)
5406+      return;
5407+   
5408+    if(this.queue.length)
5409+      this.queue.shift();
5410+    if(this.playlist.length)
5411+      this._playlist_pos++;
5412+   
5413+    this.play(Song.findById(next));
5414+  },
5415
5416+  /**
5417+   *  Player#positionNextPrev() -> undefined
5418+   **/
5419+  updateNextPrev: function () {
5420+    var els  = this.elements,
5421+        prev = this.playlist[this._playlist_pos - 1],
5422+        next = this.getNext();
5423+   
5424+    prev = prev ? Song.findById(prev) : null;
5425+    next = next ? Song.findById(next) : null;
5426+   
5427+    if(prev)
5428+      els.prev
5429+        .set("text", prev.get("title"))
5430+        .show()
5431+        .position({
5432+          position: "centerLeft",
5433+          edge:     "centerRight",
5434+          relativeTo: els.play
5435+        })
5436+        .removeClass("inactive");
5437+    else
5438+      els.prev.hide().addClass("inactive");
5439+   
5440+    if(next)
5441+      els.next
5442+        .set("text", next.get("title"))
5443+        .show()
5444+        .position({
5445+          position: "centerRight",
5446+          edge:     "centerLeft",
5447+          relativeTo: els.play
5448+        })
5449+        .removeClass("inactive");
5450+    else
5451+      els.next.hide().addClass("inactive");
5452+   
5453+    delete els;
5454+    delete next;
5455+    delete prev;
5456+  },
5457
5458+  /**
5459    *  Player#free() -> undefined
5460    *
5461    *  Frees memory taken by loaded songs. This method is ran about every
5462hunk ./contrib/musicplayer/src/controllers/Player.js 424
5463    **/
5464   free: function () {
5465     var eight_mins_ago = (+ new Date()) - 8*60*1000;
5466-     
5467+   
5468     var sound;
5469     for(var id in this.sounds) {
5470       sound = this.sounds[id];
5471hunk ./contrib/musicplayer/src/controllers/Player.js 442
5472     delete sound;
5473   }
5474 };
5475+
5476 Player.initialize();
5477 
5478 // Check is performed every four minutes
5479hunk ./contrib/musicplayer/src/controllers/Player.js 446
5480-Player.free.periodical(8*60*1000, Player);
5481+setTimeout(function () {
5482+  Player.free();
5483+}, 8*60*1000);
5484 
5485hunk ./contrib/musicplayer/src/controllers/Player.js 450
5486-/**
5487+/*
5488 TODO: Should be moved to another controller, Statistics or something alike.
5489 function findAverageDuration(callback) {
5490   da.db.DEFAULT.view({
5491hunk ./contrib/musicplayer/src/controllers/Player.js 481
5492     }
5493   });
5494 }
5495-**/
5496+*/
5497 
5498 /**
5499  * da.controller.Player
5500hunk ./contrib/musicplayer/src/controllers/Player.js 488
5501  **/
5502 da.controller.Player = {
5503   /**
5504-   *  da.controller.Player.play([song]) -> Boolean
5505+   *  da.controller.Player.play([song]) -> undefined
5506    *  - cap (da.db.DocumentTemplate.Song): the track to be played.
5507hunk ./contrib/musicplayer/src/controllers/Player.js 490
5508-   *
5509-   *  If `uri` is omitted and there is paused playback, then the paused
5510-   *  file will resume playing.
5511+   *  fires play
5512    **/
5513   play: function (song) {
5514     Player.play(song);
5515hunk ./contrib/musicplayer/src/controllers/Player.js 497
5516   },
5517   
5518   /**
5519-   *  da.controller.Player.pause() -> Boolean
5520+   *  da.controller.Player.pause() -> undefined
5521+   *  fires pause
5522    * 
5523    *  Pauses the playback (if any).
5524    **/
5525hunk ./contrib/musicplayer/src/controllers/Player.js 503
5526   pause: function () {
5527-    Player.pause()
5528+    Player.pause();
5529   },
5530   
5531   /**
5532hunk ./contrib/musicplayer/src/controllers/Player.js 507
5533-   *  da.controller.Player.queue(song) -> Boolean
5534-   *  - uri (String): location of the audio file.
5535+   *  da.controller.Player.queue(id) -> [String, ...]
5536+   *  - id (String): location of the audio file.
5537    * 
5538    *  Adds file to the play queue and plays it as soon as currently playing
5539    *  file finishes playing (if any).
5540hunk ./contrib/musicplayer/src/controllers/Player.js 512
5541+   * 
5542+   *  Returned array contains queued songs.
5543    **/
5544hunk ./contrib/musicplayer/src/controllers/Player.js 515
5545-  queue: function (song) { return false },
5546+  queue: function (id) {
5547+    Player.queue.include(id);
5548+    return Player.queue;
5549+  },
5550
5551+  /**
5552+   *  da.controller.Player.getNext() -> String
5553+   *  Returns ID of the [[da.db.DocumentTemplate.Song]] which will be played next.
5554+   **/
5555+  getNext: function () {
5556+    return Player.getNext();
5557+  },
5558
5559+  /**
5560+   *  da.controller.Player.getPrev() -> String
5561+   *  Returns ID of the [[da.db.DocumentTemplate.Song]] which which was played before this one.
5562+   **/
5563+  getPrev: function () {
5564+    return Player.playlist[Player._playlist_pos - 1];
5565+  },
5566
5567+  /**
5568+   *  da.controller.Player.setPlaylist(playlist) -> undefined
5569+   *  - playlist ([String, ...]): array containing id's of [[da.db.DocumentTemplate.Song]] documents.
5570+   **/
5571+  setPlaylist: function (playlist) {
5572+    if(!playlist || $type(playlist) !== "array")
5573+      return false;
5574+   
5575+    Player.playlist = playlist;
5576+    Player._playlist_pos = 0;
5577+  },
5578   
5579   /**
5580    *  da.controller.Player.nowPlaying() -> da.db.DocumentTemplate.Song
5581hunk ./contrib/musicplayer/src/controllers/default_columns.js 2
5582 //#require "libs/ui/NavigationColumn.js"
5583+//#require "services/albumCover.js"
5584 
5585 (function () {
5586hunk ./contrib/musicplayer/src/controllers/default_columns.js 5
5587-var Navigation = da.controller.Navigation,
5588-    NavigationColumn = da.ui.NavigationColumn;
5589+var Navigation        = da.controller.Navigation,
5590+    NavigationColumn  = da.ui.NavigationColumn,
5591+    Album             = da.db.DocumentTemplate.Album,
5592+    Song              = da.db.DocumentTemplate.Song,
5593+    fetchAlbumCover   = da.service.albumCover;
5594 
5595 /** section: Controller
5596  *  class da.controller.Navigation.columns.Root < da.ui.NavigationColumn
5597hunk ./contrib/musicplayer/src/controllers/default_columns.js 111
5598   initialize: function (options) {
5599     this.parent(options);
5600     
5601+    // TODO: select icon size depending on the column's width
5602+    //       also, adjust margins between the elements accordingly
5603     this.options.iconSize = this.options.totalCount <= (this.getVisibleIndexes()[1] + 1) ? 2 : 1;
5604     this._row_dim = this.options.iconSize === 1 ? 64 : 174;
5605     
5606hunk ./contrib/musicplayer/src/controllers/default_columns.js 117
5607     this._el.addEvent("resize", function () {
5608-      this.options.rowHeight = this._el.getWidth() / (4 + this._row_dim + 4);
5609+      var width = this._el.getWidth();
5610+      // 4 + 4 being padding on the element
5611+      this.options.rowHeight = width / (4 + this._row_dim + 4);
5612     }.bind(this));
5613     this._el.fireEvent("resize");
5614     
5615hunk ./contrib/musicplayer/src/controllers/default_columns.js 125
5616     this.render();
5617   },
5618-
5619
5620   createFilter: function (item) {
5621     return {album_id: item.id};
5622   },
5623hunk ./contrib/musicplayer/src/controllers/default_columns.js 131
5624   
5625   renderItem: function (index) {
5626-    var item = this.getItem(index).value,
5627+    var item = this.getItem(index),
5628+        data = item.value,
5629         el = new Element("a", {
5630hunk ./contrib/musicplayer/src/controllers/default_columns.js 134
5631-          href: "#",
5632-          title: item.title
5633-        });
5634+          id:     this.options.id + "_column_item_" + item.id,
5635+          href:   "#",
5636+          title:  data.title
5637+        }),
5638+        cover = data.icons[this.options.iconSize];
5639     
5640     el.style.width = this._row_dim + "px";
5641     el.style.height = this._row_dim + "px";
5642hunk ./contrib/musicplayer/src/controllers/default_columns.js 142
5643+    if(!cover || !cover.length) {
5644+      cover = "resources/images/album_cover_" + this.options.iconSize + ".png";
5645+      fetchAlbumCover(Album.findById(item.id));
5646+    }
5647     
5648hunk ./contrib/musicplayer/src/controllers/default_columns.js 147
5649-    el.grab(new Element("img",  {src: item.icons[this.options.iconSize]}));   
5650+    el.grab(new Element("img",  {src: cover}));   
5651     return el;
5652   },
5653   
5654hunk ./contrib/musicplayer/src/controllers/default_columns.js 169
5655     this.parent(options);
5656     
5657     this._el.style.width = "300px";
5658+    this._playlist = [];
5659     
5660hunk ./contrib/musicplayer/src/controllers/default_columns.js 171
5661-    this.addEvent("click", function (item) {
5662-      da.controller.Player.play(new da.db.DocumentTemplate.Song(
5663-        da.db.DEFAULT.views.Song.view.getRow(item.id))
5664-      );
5665+    this.addEvent("click", function (item, event, el) {
5666+      da.controller.Player.setPlaylist(this._playlist);
5667+      da.controller.Player.play(Song.findById(item.id));
5668     });
5669   },
5670   
5671hunk ./contrib/musicplayer/src/controllers/default_columns.js 190
5672     }
5673   },
5674   
5675-  /*
5676-  renderItem: function (index) {
5677-    var item = this.getItem(index).value,
5678-        el = new Element("a", {href: "#", title: item.title});
5679+  mapReduceUpdated: function (result) {
5680+    this.parent(result);
5681     
5682hunk ./contrib/musicplayer/src/controllers/default_columns.js 193
5683-    el.grab(new Element("span", {html: item.title, "class": "title"}));
5684+    var n = this.options.totalCount,
5685+        playlist = new Array(n);
5686     
5687hunk ./contrib/musicplayer/src/controllers/default_columns.js 196
5688-    return el;
5689-  }, */
5690+    while(n--)
5691+      playlist = this._rows[n].id;
5692+   
5693+    this._playlist = playlist;
5694+    delete playlist;
5695+  },
5696   
5697   compareFunction: function (a, b) {
5698     a = a && a.value ? a.value.track : a;
5699hunk ./contrib/musicplayer/src/index_devel.html 11
5700     <link rel="stylesheet" href="resources/css/text.css" type="text/css" media="screen" charset="utf-8"/>
5701     <link rel="stylesheet" href="resources/css/app.css" type="text/css" media="screen" charset="utf-8"/>
5702     
5703-    <script src="libs/vendor/mootools-1.2.4-core-ui.js" type="text/javascript" charset="utf-8"></script>
5704-    <script src="libs/vendor/mootools-1.2.4.4-more.js" type="text/javascript" charset="utf-8"></script>
5705-    <script src="libs/vendor/persist-js/src/persist.js" type="text/javascript" charset="utf-8"></script>
5706-    <script src="libs/vendor/soundmanager/script/soundmanager2.js" type="text/javascript" charset="utf-8"></script>
5707-    <script src="libs/vendor/roar/Roar.js" type="text/javascript" charset="utf-8"></script>
5708-    <script src="libs/vendor/javascript-last.fm-api/lastfm.api.md5.js" type="text/javascript" charset="utf-8"></script>
5709-    <script src="libs/vendor/javascript-last.fm-api/lastfm.api.js" type="text/javascript" charset="utf-8"></script>
5710-
5711     <script type="text/javascript" charset="utf-8">
5712       this.da = {};
5713     </script>
5714hunk ./contrib/musicplayer/src/index_devel.html 16
5715     
5716     <script src="libs/vendor/vendor.js" type="text/javascript" charset="utf-8"></script>
5717-    <!--<script src="libs/vendor/Persist.js" type="text/javascript" charset="utf-8"></script>-->
5718+    <script src="libs/util/console.js" type="text/javascript" charset="utf-8"></script>
5719+   
5720+    <script src="libs/vendor/mootools-1.2.4-core-ui.js" type="text/javascript" charset="utf-8"></script>
5721+    <script src="libs/vendor/mootools-1.2.4.4-more.js" type="text/javascript" charset="utf-8"></script>
5722+    <script src="libs/vendor/persist-js/src/persist.js" type="text/javascript" charset="utf-8"></script>
5723+    <script src="libs/vendor/soundmanager/script/soundmanager2.js" type="text/javascript" charset="utf-8"></script>
5724     <script type="text/javascript" charset="utf-8">
5725       this.da.vendor.Persist = Persist;
5726hunk ./contrib/musicplayer/src/index_devel.html 24
5727+      this.da.vendor.SoundManger = SoundManager;
5728+      this.da.vendor.soundManager = soundManager;
5729     </script>
5730hunk ./contrib/musicplayer/src/index_devel.html 27
5731-   
5732+    <script src="libs/vendor/SoundManager.js" type="text/javascript" charset="utf-8"></script>
5733+    <script src="libs/vendor/roar/Roar.js" type="text/javascript" charset="utf-8"></script>
5734+    <script src="libs/vendor/javascript-last.fm-api/lastfm.api.md5.js" type="text/javascript" charset="utf-8"></script>
5735+    <script src="libs/vendor/javascript-last.fm-api/lastfm.api.js" type="text/javascript" charset="utf-8"></script>
5736+
5737     <script src="libs/db/db.js" type="text/javascript" charset="utf-8"></script>
5738     <script src="libs/db/PersistStorage.js" type="text/javascript" charset="utf-8"></script>
5739     <script src="libs/db/BrowserCouch.js" type="text/javascript" charset="utf-8"></script>
5740hunk ./contrib/musicplayer/src/index_devel.html 65
5741     <script src="libs/vendor/LastFM.js" type="text/javascript" charset="utf-8"></script>
5742     <script src="services/lastfm.js" type="text/javascript" charset="utf-8"></script>
5743     <script src="services/albumCover.js" type="text/javascript" charset="utf-8"></script>
5744-
5745+   
5746     <script src="controllers/controllers.js" type="text/javascript" charset="utf-8"></script>
5747     <script src="controllers/Navigation.js" type="text/javascript" charset="utf-8"></script>
5748     <script src="controllers/default_columns.js" type="text/javascript" charset="utf-8"></script>
5749hunk ./contrib/musicplayer/src/libs/TahoeObject.js 1
5750+//#require <libs/util/util.js>
5751+
5752 (function () {
5753 /**
5754  *  == Tahoe ==
5755hunk ./contrib/musicplayer/src/libs/TahoeObject.js 121
5756     return result;
5757   }
5758 });
5759-window.TahoeObject = TahoeObject;
5760+
5761+da.util.TahoeObject = TahoeObject;
5762 
5763 })();
5764hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 310
5765   /**
5766    *  da.db.DocumentTemplate.findFirst(options[, db]) -> undefined
5767    *  - options (Object): same options as in [[da.db.DocumentTemplate.find]] apply here.
5768+   * 
5769+   *  #### Notes
5770+   *  This method is also available on all classes which inherit from [[da.db.DocumentTemplate]].
5771    **/
5772   findFirst: function (options, db) {
5773     options.onlyFirst = true;
5774hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 323
5775    *  da.db.DocumentTemplate.findOrCreate(options[, db]) -> undefined
5776    *  - options (Object): same options as in [[da.db.DocumentTemplate.find]] apply here.
5777    *  - options.properties.type (String): must be set to the desired [[da.db.DocumentTemplate]] type.
5778+   * 
5779+   *  #### Notes
5780+   *  This method is also available on all classes which inherit from [[da.db.DocumentTemplate]].
5781    **/
5782   findOrCreate: function (options, db) {
5783     options.onSuccess = options.onSuccess || $empty;
5784hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 336
5785   },
5786   
5787   /**
5788-   *  da.db.DocumentTemplate.registerType(typeName[, db = Application.db], template) -> da.db.DocumentTemplate
5789-   *  - typeName (String): name of the type. ex.: `Car`, `Chocolate` etc.
5790+   *  da.db.DocumentTemplate.registerType(typeName[, db = da.db.DEFAULT], template) -> da.db.DocumentTemplate
5791+   *  - typeName (String): name of the type. ex.: `Car`, `Chocolate`, `Song`, etc.
5792    *  - db (BrowserCouch): database to be used.
5793hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 339
5794-   *  - template (da.db.DocumentTemplate): the actual [[da.db.DocumentTemplate]] [[Class]].
5795+   *  - template (da.db.DocumentTemplate): class which extends [[da.db.DocumentTemplate]].
5796    *
5797    *  New classes are accessible from `da.db.DocumentTemplate.<typeName>`.
5798hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 342
5799+   * 
5800+   *  #### Notes
5801+   *  You _must_ use this method in order to
5802    **/
5803   registerType: function (type, db, template) {
5804     if(arguments.length === 2) {
5805hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 389
5806       DocumentTemplate.findOrCreate(options, db);
5807     };
5808     
5809+    /**
5810+     *  da.db.DocumentTemplate.findById(id) -> da.db.DocumentTemplate
5811+     *  - id (String): id of the document
5812+     * 
5813+     *  #### Notes
5814+     *  This method is available *only* on classes which extend [[da.db.DocumentTemplate]]
5815+     *  and are registered using [[da.db.DocumentTemplate.registerType]].
5816+     **/
5817     template.findById = function (id) {
5818hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 398
5819-      var doc = template.db().views[type].view.getRow(id);
5820-      return doc ? new template(doc) : null;
5821+      var doc = template.view().getRow(id);
5822+      return doc && !doc._deleted ? new template(doc) : null;
5823+    };
5824+   
5825+    template.view = function () {
5826+      return template.db().views[type].view;
5827     };
5828     
5829     template.db().view({
5830hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 409
5831       id: type,
5832       map: function (doc, emit) {
5833-        if(doc && doc.type === type)
5834+        if(doc && !doc._deleted && doc.type === type)
5835           emit(doc.id, doc);
5836       },
5837       finished: $empty
5838hunk ./contrib/musicplayer/src/libs/ui/Column.js 10
5839  * 
5840  *  Widget which can efficiently display large amounts of items in a list.
5841  **/
5842+
5843 var IDS = 0;
5844 da.ui.Column = new Class({
5845   Implements: [Events, Options],
5846hunk ./contrib/musicplayer/src/libs/ui/Column.js 21
5847     totalCount:     0,
5848     renderTimeout: 120
5849   },
5850
5851   /**
5852    *  new da.ui.Column(options)
5853    *  - options.id (String): desired ID of the column's DIV element, `_column` will be appended.
5854hunk ./contrib/musicplayer/src/libs/ui/Column.js 43
5855   initialize: function (options) {
5856     this.setOptions(options);
5857     if(!this.options.id)
5858-      this.options.id = "column_" + (IDS++);
5859+      this.options.id = "duC_" + (IDS++);
5860     
5861     this._populated = false;
5862     // #_rendered will contain keys of items which have been rendered.
5863hunk ./contrib/musicplayer/src/libs/ui/Column.js 184
5864     this._populated = false;
5865     return this.render();
5866   },
5867
5868   /**
5869    *  da.ui.Column#updateTotalCount(totalCount) -> this | false
5870    *  - totalCount (Number): total number of items this column is going to display
5871hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 115
5872    *  rendered ones (due to sorting).
5873    **/
5874   mapReduceUpdated: function (values) {
5875-    this._rows = $A(da.db.DEFAULT.views[this.view.id].view.rows);
5876-    this._rows.sort(this.compareFunction);
5877-    this.options.totalCount = this._rows.length;
5878-    return this.rerender();
5879+    var new_rows = $A(da.db.DEFAULT.views[this.view.id].view.rows);
5880+    new_rows.sort(this.compareFunction);
5881+   
5882+    // Noting new was added, so we can simply re-render those elements
5883+    if(this.options.totalCount === new_rows.length) {
5884+      values = values.rows;
5885+      var n = values.length,
5886+          id_prefix = this.options.id + "_column_item_",
5887+          item, el, index;
5888+     
5889+      while(n--) {
5890+        item = values[n];
5891+        el = $(id_prefix + item.id);
5892+        if(el) {
5893+          index = el.retrieve("column_index");
5894+         
5895+          this.renderItem(index)
5896+            .addClass("column_item")
5897+            .store("column_index", index)
5898+            .replaces(el);
5899+        }
5900+      }
5901+     
5902+      this._rows = new_rows;
5903+    } else {
5904+      this.options.totalCount = new_rows.length;
5905+      this._rows = new_rows;
5906+      return this.rerender();
5907+    }
5908   },
5909   
5910   /**
5911hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 158
5912    *  da.ui.NavigationColumn#renderItem(index) -> Element
5913    *  - index (Number): position of the item that needs to be rendered.
5914    * 
5915-   *  This function relies on `title`, `subtitle` and `icon` properties from emitted documents.
5916+   *  This function relies on `title`, `subtitle` and `icon` properties from emitted documents.
5917+   * 
5918+   *  #### Note
5919+   *  If you are overwriting this method, make sure that the returned element has the `id` attribute
5920+   *  that follows this convention:
5921+   * 
5922+   *      this.options.id + "_column_item_" + item.id
5923+   * 
5924+   *  Where `item.id` represents unique identifier of the item that is being rendered (not to be mistaken
5925+   *  with `index` argument).
5926+   * 
5927+   *  This is necessary for updating views which are bound to [[da.db.BrowserCouch]] views.
5928+   * 
5929    **/
5930   renderItem: function (index) {
5931hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 173
5932-    var item = this.getItem(index).value,
5933+    var item = this.getItem(index),
5934+        data = this.getItem(index).value,
5935         el = new Element("a", {
5936hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 176
5937-          href: "#",
5938-          title: item.title,
5939-          "class": index%2 ? "even" : "odd"
5940+          id:       this.options.id + "_column_item_" + item.id,
5941+          href:     "#",
5942+          title:    data.title,
5943+          "class":  index%2 ? "even" : "odd"
5944         });
5945     
5946hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 182
5947-    if(item.icon)
5948-      el.grab(new Element("img",  {src: item.icon}));
5949-    if(item.title)
5950-      el.grab(new Element("span", {html: item.title,    "class": "title"}));
5951-    if(item.subtitle)
5952-      el.grab(new Element("span", {html: item.subtitle, "class": "subtitle"}));
5953+    if(data.icon)
5954+      el.grab(new Element("img",  {src:  data.icon}));
5955+    if(data.title)
5956+      el.grab(new Element("span", {html: data.title,    "class": "title"}));
5957+    if(data.subtitle)
5958+      el.grab(new Element("span", {html: data.subtitle, "class": "subtitle"}));
5959     
5960hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 189
5961+    delete item;
5962+    delete data;
5963     return el;
5964   },
5965   
5966hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 73
5967         el_width          = this.options.width,
5968         diff              = p - current_progress,
5969         increment         = diff > 0,
5970-        x, width;
5971+        // Since most of the time we'll be incrementing
5972+        // progress we can save one if/else condition
5973+        // by "caching" the results immediately
5974+        x                 = current_progress,
5975+        width             = diff;
5976     
5977     if(!diff)
5978       return this;
5979hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 82
5980     
5981-    if(increment) {
5982-      x = current_progress * el_width;
5983-      width = diff * el_width;
5984-    } else {
5985-      x = (current_progress - (-diff)) * el_width;
5986-      width = (current_progress - p) * el_width;
5987+    if(!increment) {
5988+      x = current_progress - (-diff);
5989+      width = current_progress - p;
5990     }
5991     
5992hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 87
5993+    // This allows SegmentedProgressBar to acutally
5994+    // draw bars with different colours.
5995     this.ctx.fillStyle = this.options.foreground;
5996hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 90
5997-    // We're adding +-1 here because some browsers (even different implementations
5998-    // of WebKit are unable to render this precisely when small changes are
5999-    // in place.
6000-    this.ctx[increment ? "fillRect" : "clearRect"](x - 1, 0, width + 1, this.options.height);
6001+    // We're adding +-1px here because some browsers are unable
6002+    // to render small changes precisely. (even different implementations of WebKit)
6003+    this.ctx[increment ? "fillRect" : "clearRect"](
6004+      (x * el_width) - 1,     0,
6005+      (width * el_width) + 1, this.options.height
6006+    );
6007+   
6008     this.progress = p;
6009     
6010     return this;
6011hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 104
6012   
6013   /**
6014    *  da.ui.ProgressBar#rerender() -> this
6015+   * 
6016+   *  #### Notes
6017+   *  Unlike [[da.ui.ProgressBar.setProgress]] this method will render the whole bar,
6018+   *  and thus is not really efficient (but indeed, needed in some situations).
6019    **/
6020   rerender: function () {
6021     var opts = this.options;
6022hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 114
6023     this.ctx.fillStyle = opts.foreground;
6024     this.ctx.fillRect(0, 0, this.progress * opts.width, opts.height);
6025     
6026+    delete opts;
6027     return this;
6028   },
6029   
6030hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 130
6031    **/
6032   destroy: function () {
6033     this._el.destroy();
6034+   
6035     delete this._el;
6036     delete this.ctx;
6037hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 133
6038-    delete this._fx;
6039+    delete this.progress;
6040+    delete this.options;
6041   }
6042 });
6043 da.ui.ProgressBar = ProgressBar;
6044hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 1
6045+//#require <libs/util/util.js>
6046+
6047 /*
6048  *  Binary Ajax 0.2
6049  * 
6050hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 20
6051  * 
6052  *  Class containing methods for working with files as binary data.
6053  **/
6054+
6055+var UNPACK_FORMAT = /(\d+\w|\w)/g,
6056+    WHITESPACE    = /\s./g;
6057+
6058 var BinaryFile = new Class({
6059   /**
6060    *  new da.util.BinaryFile(data[, options])
6061hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 45
6062       // In this case we're probably dealing with IE,
6063       // and in order for this to work, VisualBasic-script magic is needed,
6064       // for which we don't have enough of mana.
6065-      throw Exception("This browser is not supported");
6066+      throw Exception("We're lacking some mana. Please use different browser.");
6067     }
6068   },
6069   
6070hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 268
6071    *  #### External resources
6072    *  * [Python implementation of `unpack`](http://docs.python.org/library/struct.html#format-strings)
6073    **/
6074-  _unpack_format: /(\d+\w|\w)/g,
6075-  _whitespace: /\s./g,
6076   unpack: function (format) {
6077hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 269
6078-    format = format.replace(this._whitespace, "");
6079-    var pairs = format.match(this._unpack_format),
6080+    format = format.replace(WHITESPACE, "");
6081+    var pairs = format.match(UNPACK_FORMAT),
6082         n = pairs.length,
6083         result = [];
6084     
6085hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 333
6086     }
6087     
6088     return result;
6089-  },
6090
6091-  destroy: function () {
6092-    delete this.data;
6093+  },
6094
6095+  destroy: function () {
6096+    delete this.data;
6097   }
6098 });
6099 
6100hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 414
6101       range[0] += +this.headRequest.getHeader("Content-Length");
6102     range[1] = range[0] + range[1] - 1;
6103     this.options.range = range;
6104-
6105+   
6106     if(this.headRequest.isSuccess())
6107       this.send(this._send_options || {});
6108   },
6109hunk ./contrib/musicplayer/src/libs/util/ID3.js 1
6110+//#require <libs/util/util.js>
6111+//#require "libs/util/BinaryFile.js"
6112+
6113 /**
6114  *  == ID3 ==
6115  * 
6116hunk ./contrib/musicplayer/src/libs/util/ID3.js 79
6117   },
6118   
6119   /**
6120-   *  ID3#destory() -> undefined
6121+   *  da.util.ID3#destory() -> undefined
6122    **/
6123   destroy: function () {
6124     if(this.parser)
6125hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 377
6126   },
6127   
6128   /**
6129-   *  ID3v2Parser#destroy() -> undefined
6130+   *  da.util.ID3v2Parser#destroy() -> undefined
6131    **/
6132   destroy: function () {
6133     this.data.destroy();
6134addfile ./contrib/musicplayer/src/libs/util/console.js
6135hunk ./contrib/musicplayer/src/libs/util/console.js 1
6136+if(!this.console)
6137+  this.console = {
6138+    log: function () {}
6139+  };
6140addfile ./contrib/musicplayer/src/libs/vendor/SoundManager.js
6141hunk ./contrib/musicplayer/src/libs/vendor/SoundManager.js 1
6142+//#require <libs/vendor/vendor.js>
6143+
6144+(function () {
6145+/**
6146+ *  class da.vendor.SoundManager
6147+ *  SoundManager2 class
6148+ * 
6149+ *  #### Links
6150+ *  * http://www.schillmania.com/projects/soundmanager2/
6151+ * 
6152+ **/
6153+/**
6154+ * da.vendor.soundManager
6155+ * 
6156+ * Default instance of [[da.vendor.SoundManager]].
6157+ *
6158+ **/
6159+
6160+(function (window) {
6161+//#require "libs/vendor/soundmanager/script/soundmanager2.js"
6162+})(da.vendor);
6163+
6164+var url = location.protocol + "//" + location.host + location.pathname.split("/").slice(0, -1).join("/"),
6165+    path = location.pathname.contains("devel.html") ? "/libs/vendor/soundmanager/swf/" : "/resources/flash/";
6166+
6167+$extend(da.vendor.soundManager, {
6168+  useHTML5Audio:  false,
6169+  url:            url + path,
6170+  debugMode:      false,
6171+  debugFlash:     false
6172+});
6173+
6174+})();
6175hunk ./contrib/musicplayer/src/resources/css/app.css 160
6176   height: 100%;
6177   text-align: center;
6178   padding: 50px 0 0 0;
6179+  text-shadow: #fff 0 1px 0;
6180 }
6181 
6182 /*** Notifications (Roar) ***/
6183hunk ./contrib/musicplayer/src/resources/css/app.css 166
6184 /* Contents of Roar.css (distributed under MIT) */
6185 .roar-body {
6186-       position: absolute;
6187-       color: #fff;
6188-       text-align: left;
6189-       z-index: 999;
6190-       font-size: 0.8em;
6191+  position: absolute;
6192+  color: #fff;
6193+  text-align: left;
6194+  z-index: 999;
6195+  font-size: 0.8em;
6196 }
6197 
6198 .roar {
6199hunk ./contrib/musicplayer/src/resources/css/app.css 174
6200-       position: absolute;
6201-       width: 300px;
6202-       cursor: default;
6203-       padding: 5px;
6204+  position: absolute;
6205+  width: 300px;
6206+  cursor: default;
6207+  padding: 5px;
6208 }
6209 
6210 .roar-bg {
6211hunk ./contrib/musicplayer/src/resources/css/app.css 181
6212-       position: absolute;
6213-       z-index: 1000;
6214-       width: 100%;
6215-       height: 100%;
6216-       left: 0;
6217-       top: 0;
6218-       background-color: #000;
6219-       border: 2px solid #000;
6220+  position: absolute;
6221+  z-index: 1000;
6222+  width: 100%;
6223+  height: 100%;
6224+  left: 0;
6225+  top: 0;
6226+  background-color: #000;
6227+  border: 2px solid #000;
6228+
6229+  -webkit-border-radius: 5px;
6230+  -moz-border-radius: 5px;
6231+  -o-border-radius: 5px;
6232+  border-radius: 5px;
6233 
6234hunk ./contrib/musicplayer/src/resources/css/app.css 195
6235-       -webkit-border-radius: 5px;
6236-       -moz-border-radius: 5px;
6237-       -o-border-radius: 5px;
6238-       border-radius: 5px;
6239-       
6240-       -webkit-box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px;
6241+  -webkit-box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px;
6242   -moz-box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px;
6243hunk ./contrib/musicplayer/src/resources/css/app.css 197
6244-       -o-box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px;
6245-       box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px;
6246+  -o-box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px;
6247+  box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px;
6248 }
6249 
6250 
6251hunk ./contrib/musicplayer/src/resources/css/app.css 203
6252 .roar h3 {
6253-       position: relative;
6254-       margin: 0;
6255-       border: 0;
6256-       font-size: 13px;
6257-       color: #fff;
6258-       z-index: 1002;
6259+  position: relative;
6260+  margin: 0;
6261+  border: 0;
6262+  font-size: 13px;
6263+  color: #fff;
6264+  z-index: 1002;
6265 }
6266 
6267 .roar p {
6268hunk ./contrib/musicplayer/src/resources/css/app.css 212
6269-       position: relative;
6270-       margin: 0;
6271-       font-size: 12px;
6272-       color: #fff;
6273-       z-index: 1002;
6274+  position: relative;
6275+  margin: 0;
6276+  font-size: 12px;
6277+  color: #fff;
6278+  z-index: 1002;
6279 }
6280 
6281 /*** Navigation columns ***/
6282hunk ./contrib/musicplayer/src/resources/css/app.css 320
6283   margin-left: 20px;
6284 }
6285 
6286-.navigation_column .active_column_item, .menu_item:hover {
6287-  background: #33519d !important;
6288+.navigation_column .active_column_item, .menu_item:hover, #next_song:hover, #prev_song:hover {
6289+  background-color: #33519d !important;
6290   text-shadow: #000 0 1px 0;
6291   color: #fff !important;
6292   outline: 0 !important;
6293hunk ./contrib/musicplayer/src/resources/css/app.css 334
6294 
6295 /** Albums column **/
6296 #Albums_column {
6297-  background: #595959;
6298+  background: #fff;
6299 }
6300 
6301 #Albums_column .column_item {
6302hunk ./contrib/musicplayer/src/resources/css/app.css 556
6303 #song_details span {
6304   display: inline-block;
6305   max-width: 125px;
6306+  vertical-align: bottom;
6307 }
6308 
6309 #song_album_cover_wrapper {
6310hunk ./contrib/musicplayer/src/resources/css/app.css 602
6311   border-radius: 2px;
6312 }
6313 
6314-
6315-#play_button {
6316+#play_button_wrapper {
6317   display: inline-block;
6318   width: 40px;
6319   height: 40px;
6320hunk ./contrib/musicplayer/src/resources/css/app.css 606
6321+  vertical-align: middle;
6322+}
6323+
6324+#play_button {
6325+  display: block;
6326+  width: inherit;
6327+  height: inherit;
6328   background: url(../images/play.png) 0 0;
6329   outline: 0;
6330hunk ./contrib/musicplayer/src/resources/css/app.css 615
6331-  vertical-align: middle;
6332   cursor: default;
6333hunk ./contrib/musicplayer/src/resources/css/app.css 616
6334+  position: absolute;
6335+  z-index: 2;
6336 }
6337 
6338 #play_button:active, #play_button:focus, #play_button.active {
6339hunk ./contrib/musicplayer/src/resources/css/app.css 628
6340   background-position: 0 -80px;
6341 }
6342 
6343+#next_song, #prev_song {
6344+  min-height: 12px;
6345+  min-width: 12px;
6346+  max-width: 200px;
6347+  position: absolute;
6348+  z-index: 1;
6349+  display: block;
6350+  cursor: default;
6351+  color: #000;
6352+  background: rgba(255, 255, 255, 0.8) no-repeat;
6353+  text-shadow: #fff 0 1px 0;
6354
6355+  overflow: hidden;
6356+  text-overflow: ellipsis;
6357+  white-space: nowrap;
6358
6359+  -webkit-box-shadow: rgba(0, 0, 0, 0.4) 0 1px 3px;
6360+}
6361+
6362+#next_song:hover, #prev_song:hover {
6363+  color: #fff;
6364+}
6365+
6366+#next_song:active, #prev_song:active {
6367+  text-shadow: #000 0 -1px 0;
6368+}
6369+
6370+#next_song {
6371+  text-align: left;
6372+  background-image: url(../images/next.png);
6373+  background-position: 0% 50%;
6374+  padding: 2px 4px 2px 15px;
6375+  margin-left: -5px;
6376+}
6377+
6378+#next_song:hover {
6379+  background-image: url(../images/next_active.png);
6380+}
6381+
6382+#prev_song {
6383+  text-align: right;
6384+  background-image: url(../images/previous.png);
6385+  background-position: 100% 50%;
6386+  padding: 2px 15px 2px 4px;
6387+  margin-right: -5px;
6388+}
6389+
6390+#prev_song:hover {
6391+  background-image: url(../images/previous_active.png);
6392+}
6393addfile ./contrib/musicplayer/src/resources/images/album_cover_1.png
6394binary ./contrib/musicplayer/src/resources/images/album_cover_1.png
6395oldhex
6396*
6397newhex
6398*89504e470d0a1a0a0000000d4948445200000040000000400806000000aa6971de000000047342
6399*4954080808087c086488000000097048597300000ddb00000ddb01995f140f0000001974455874
6400*536f667477617265007777772e696e6b73636170652e6f72679bee3c1a00000af449444154789c
6401*d55b6d8c5547197eced9b3df0b5d7661615b2a4b972db1b61aaad590968fa669ac1f241a6b53ad
6402*a49416132b89d9c6567f6862fd20fd61b14634a9fcf68728ea4f112220b471ed87b6b1600aa5cb
6403*22fb01b78dcbb2ecbd7bcecceb8f33336766ceccb977776f280cb97bef9df79d99f799f76bde39
6404*97e0c1ad8f110080d23700c83ee98d0c429e87729db91929708d421064e3f52902c54319b34736
6405*ca3eba65977f2d7a449c3b04d67aaa8142e0e47183a23c2800c4e168049e7d74d2d5a72af43c4f
6406*f6252222ac5c791316b5772061896fbc060ad535e56d8526848581aac66df6742e5e8ca1575e43
6407*4400a28610a7de7907b371ac189ca03c42d5627e7e81ab8132d72f02e5e6719bf0da5b07c089a7
6408*2e401c20226df51494cb67ec49af2550e4a2399724101188840b1008200e12cee8063e075035d0
6409*6b05250576b61c4fe0a4b9b626d5372122019c88c0b9bdd0f505ca8258340c00078823e29c403c
6410*054f4436bb655ef925af255041d1f65b669dba0009172002692e70dd827276fbe44f959eba800c
6411*0a5c9fcc02e5587dfea0aaf1b869540d94e2a94ee742e911a5aea032c1f50caa882117dd940b80
6412*83c0c53fd3e1af6950808859850cd60ca69b122744693e103ddc0c82570bd447efb803a74f9fc6
6413*9599998581320575aca41dfb45fa0fd3e8cf413c4b87fa8b730257343f9d7bc6a73cc5f41ddbb7
6414*61d3867b040f419ec9d44b642ae2046ed3889b2f489ba6ec9f477622429811807a824a4f98bc26
6415*508d8d8d58b76e5ddd40291ae700e7c6c2363dca095dc5fc4cd39a9ff9d93c04a077452f08dc9f
6416*b5b4f86417d58654e4a299e353516510e45c1c843838b952dffc40193d55401101611461f9b21e
6417*4c4c4ccc1b94fa486470bac24a6ac50c61dea774f3f2981ffce607399e4b13f49b1f781a784908
6418*b9edd14753c1d478e1269a5b32cec058822449c012862466489244bd581283b144f03024090363
6419*fa2ba531418b6c50be469a2603a3dfafa99cb67c964029bdaf6f357a962dc7d8f8a8ca06f6f1dc
6420*9d24b2f5dd4b982642429e34088a4056a429b726dd9ab203112711081df4349032657d1cc0e053
6421*8308c3109c3341e342fb59bd92b3524977d1885b59c4e409eb0d4af148817996613867ca84e358
6422*9a2d533a220216dfd089c1c1a7325005a07da0b2a0aeb990c4a65e69265269707ea060f0caac93
6423*651e019ac54892182c61e0090363dc9a5fba41fade3fb0160f7fe5118736ab8122636394b21c74
6424*883922b5c30294ed337e9fb3d24a20fd8bd4620a557ed6fc6c1a2b01b8f7befb71766418c78ffd
6425*cd62a7bc9f3b3ac84b97518b8b2b312bfae740f956d1f01311883115b86c76fbf490a313a13120
6426*742c5a8ca92b6554e2040460ebb627303e3a8653a7deae0994bfdb91c085d24323c94953963d3e
6427*f36204c61892244612cf822571e63e35985fe6668207c04f77efc6f6c71fc31ff6fd064d418c20
6428*080004187cfabb686d6fcbfc5c9bdff6796eace13ebe7361e98b3a3ad2209832430b26467d9ce5
6429*6a4ee02c4192c488935930963840ebc0c901da8cce9227b502a0542a61ff1fff84ed8f3f81d2e8
6430*59440d21c2a8113b777eab2a285e03bdb5a505f7dcbd1e777de2e3f8d71b6f72227a2b82166840
6431*e2286c99677a4ad4ce08054e98b73ae7d9d05a0300b8da8c4aa5826f3ffd0cbef9e493b8ebee4d
6432*b8e5d6dbb06ad587303c3c6c8d71cf6ff2a4f4d57d7d58bab41b070f1ec2f9736731333dfdeb24
6433*49be11a6c0f269837161e2490cc65981f9c92ca2650883a79aa6b80a8259be4ffbf7fc720fde9f
6434*180507b073f0196d0eb7a63319cd42adb3b313cdcdcd3870e0004e9d7c0b972f5d0263ac0700c2
6435*cce2d39492e6e859b02401e37cfea02c7aa1d042532efa8f7ef803b43645e8b8a10babfa561782
6436*266eb99f780df4afc13f5f7f1de747ce82678f027bd30da0f4349606b4188cb1fa809a8ba6903f
6437*53487aa954c2f1c37f0101d8b869b313b48c273670e21c3dcb96e1ccbb6770fedc08f4e7a0006e
6438*0480308e6753d08cd715149f8ba6002008bca0f6eedd8b300830b0f6c3063d3b0c650158d1c53d
6439*4477d7128c8e8de1caf4653b44ac0080c806e58b58de50a3051a7f9de23b02893e6102727dfb98
6440*5429971182a36b698f0a94949bc0dde224c1ccf4b48bd41c04417764044139511d40d942e5bed9
6441*8384267d3c33d353686c6e1316e3af2ce55825a29d72ac16d9e6e9163813d282e115d8cde7e621
6442*7984e6fe72bc5cae2049123d883957b18d2108033435b7b8065488e83d752344220fd70b544aa9
6443*4d5324df73a69c816a696bc799b74f583c6ed03a7d627c1c37af5c89f323c398b97245278e0340
6444*68a4b45c30b1d395ccf77ab0b1798aca658d474fb102855efd49ba2cd6c2a6161c3f7a240bc0ea
6445*2a2f3fb74ebf70e122bababab074f90a71bc566d14102ec091a5bd224d55d56671a4f46a2bed0b
6446*d4fa36cb6db7df0e20c4cb2f1f877c7ce77546471176eebf23b873dd3a946766509a1897563406
6447*001117b73d4610ac0328a35c74807205d15c1014ed6bdb7600952954ca1577e875169a59e7c50b
6448*257046d8b86123868686f0dec50ba854ca1300ac6b712d0d2e14949bc7a32911ad8d342ce86118
6449*a2f7e6d57861d7f7abd42356b2b6e8a5d245542a656cdabc114110e2c4c9935fefeb1ff85464fb
6450*4cdd4055a1675fe55faecc5b6f0f3db215212be3d5d75e758076ac52c03379e9128e1d7b0900b0
6451*79d3c68623478fde1999d74b949fa51a28278f1f74da656da1dc58690adad0fb3efd79bcb8e767
6452*d6e6589298a722c77ac8edd0e4e4248808917da1385f5039589e2f411862fdfa0de8eceac2c8f0
6453*bbf8f79b6fa814689c0409e8bdf12634508223870fd704caa057db14d27f20a10e428e205803a8
6454*5accefcb5fdd8a7befff2c3a96748328fbdd07c51575e3ce898cc92e4f4de2d9ef7d273bfe5601
6455*25bb5d79c21e266faa22fb36672ea06a31bf8628c2aee77f81debe0180c41378ad058dcd606203
6456*ec343c393989c9c9c99a4115542c4a581d1791fc8d90fea8cb89c511f60acc0f4801ade8edc573
6457*3f7f11514b7b51bd2206643742f306e514d9bd75b25c8ee4bdbcbcd181935d0e72ae6074e9cb3d
6458*fbdc6e34b4b41741506d627c2cdd8005807208e93160520a8fec1b1fd7a2f331bfd5fd6bd0b6a4
6459*c73dc66a5118e03f274fc0fb78def2351f2887585a473ef30817302f38f4d10b31bf2d5ff8927f
6460*acd51aa9823849cc4e07a85c8fc7afa880960d4d6b1af5535999090a616b4255d3d4d2eeee4201
6461*646b6b6ac00b3ffe89b6f90b005595273bdacb1ba32c0bd857dfe6acf9ae023a001cfdeb410c7c
6462*ec93b9a8afb7eef626fceaf95d3874e86081c8a819d45ce852e9a17eb7e6bb70344a61c593a7eb
6463*e5f2c1037f467372192d8da1b17010a45aef6a4ab063eb43f8fdfefd05e57276b7e72a795549ed
6464*a153215d640113b8bb1c9e8ff9254982871ffc223ef3c003f8dc962d58b3f623f8dffb25bcf2f7
6465*973034f40f1c3e7204e572d99ea17af5491ef7d3e8da9b9f2e9e84454a8bdccac30e50ce650be8
6466*535397b0ef77fbf0db7dfb8a66a81ba83c8f7b2001ca1244390c65054031a8ea9a123c1f00a82a
6467*038df02e5d3db27dcadfae0228efc7da40791637b954b2219905ccbb359fd05713942572cda05c
6468*f2b9931401b218e2fa3940a6c1eb1594fdad808713cc721894bf91216bd0b50e2a1f979c025b41
6469*d02887a530750495e3991b28c006e6075544b7a552c550aa7d7988c9bb803dcbb50ccacfe31aaf
6470*b240fa1f25e4ef6bbc023bd7b9c64019c38a4b6ae502b21ceebfa51fb3b3b3f9856bf02b8f2896
6471*c005dc7502e5dd3a87d9767777831321686d6ba720009a9b9b8d018502d7c2f30180ca7d2ba82c
6472*1963886763042dadad068eab092a2fb07bed3c4f81157ae82629a3ff1f4bca40d105b38e8e0000
6473*000049454e44ae426082
6474addfile ./contrib/musicplayer/src/resources/images/album_cover_2.png
6475binary ./contrib/musicplayer/src/resources/images/album_cover_2.png
6476oldhex
6477*
6478newhex
6479*89504e470d0a1a0a0000000d49484452000000ae000000ae0806000000af0c36ee000000047342
6480*4954080808087c086488000000097048597300000ddb00000ddb01995f140f0000001974455874
6481*536f667477617265007777772e696e6b73636170652e6f72679bee3c1a0000200049444154789c
6482*ed9d7974dcc67dc7bfd805efe52152a44899b244d122255bb6654bb6d334579336796eea28499d
6483*c4ce693b8edcb8b5d3b47e2fe97bbd92a6699a1e69d3367d4efb9a363de3d7f63549dba43eeaba
6484*b55b5b8ea548b2adc3966c51e6215ea22851229700a67f60710c30339801b0d4ee12a3077131f3
6485*fb7d6776f0d91f7ec06201edd68fdc4910ab10ea0fa73575dd787d10e16abc3e822d9ab276d05c
6486*d338da9253a271ea89c833f6862282b524ba7c67e27ba1339bca05a38448f5c0c8c38476898291
6487*aa62d4699cd72c1849e8856ab9b430aa149d94664bd33474b477c41b4ccdc028d7474d45c64425
6488*5d18a39ce6e6cec2e1d58db875ba8e9eeeb5989c9a92eab47a604c2732fa6b2b3e32262a2b0ba3
6489*acd9dab56b71fefc028ac5650004babb1508c1858b17317b664ebd875516196ddfb4f329091853
6490*d20ed5961146d6873c8e5e4b4b73c986d839ae634e9c4a857c34daac7a22a3edcb81278391b652
6491*d2568388674d88b700f0475c50dc6630f28a60d75f066daab6c6608ca75bca714969c5ffaf1260
6492*f4f754d330c6d2ae3518d9a6c139f2222e816eb76a00f12d1cbd9584d1d5cb60a4ad6a0946254d
6493*3a25d03d9288b7f8742a0bc6b073b961a45a6a1a4675ed68af181b83e3e2e5b84eaa507a414a30
6494*3b9398c1a8ae4959653046ba90288390b567a76b7060243e830c46aea5b4690663ecc27375822b
6495*88177103298494627a30f29d3318c566550fa3db1c34d002ed7e3c35e874a42561810c46caecd2
6496*c2a83e824a8631d426d273c975735c5f7d64c405d7a09a61541c452cb30c46f94e592efe93b580
6497*7bad02092c01810c46a6d9aa85516a38d130aa178f4feaac822f12470e44289f368c3ed30c4639
6498*d7ea8231bad589b6be884be7b8dc54a08a61147b66302a0c41b224fcf8070c34385f311068a11c
6499*9780ba8841b1ab90696dc1184353d26d35c3a8e00d67a6d4226eb9606498961b46ba69a56004f8
6500*40d60e8c80ff5dca8d503633254e1eab1671e507c2ec54d6505197dd94c118bf956d502e1893bc
6501*639d76e6e7b8198c198c720df1df3109bdf02a08f11600ced56170994d9ce34698971b46db4436
6502*6fac62181946d5066392a23b1b995004478d82575d093096da3218251ae2bdeb72c1482905a5a2
6503*226ef840ac7a61941c86b2a6b27e06a3ad94668eeb8fb8f4976aac9e250697c1987c04296de06a
6504*8391af5d22938ab84e5f81884b0f256d189d4e796b714ab482ea672483b19cda49735ce72b5f70
6505*b94506a36c630663eac5bd242194e3068eac23de6006a35c116fcb940e05ab1dc60412ba930848
6506*e5b8fc9124b74808a3d0328331a05d99300ab57d0b81ef86200ceb683d45838a863145cd904a06
6507*2357db5de51a6a003410df0230ae55888cb8550023e5b962309643bbb66194d323de6bea5a8552
6508*0f6e1dc9602caff62a8791a1196ae6d4f93086ee5cdfe80fc69183cc600c686730c68131429053
6509*e78fb80845e2585d853c5724b74b57bb5c30eebee5a7b0beaf0f737373f8bb6f3d94aa36802a86
6510*51ce231471bdc1059b385d663032c5a39487b60c6170f3004cd3c443fff4cf300c435adb5de51a
6511*56278cd29604d139ae7353b1b44af9614c4f33289ea6eaa68d970300f2f93c2eebebc3c9919188
6512*3e56018c094a20e2ca2956e5e99d8ac819ed32343c8457474e2969869a258695d46ba561a41503
6513*62a16fcebc7a37f2a63d8ada8751323296ca95c35bf1f0c38f86bb545289e7555930d2aba2fb1c
6514*3b754ebd770ba6881c3734880c469f265f89d5d2df7f5944dfab0746b62b4b9313719df6b4735c
6515*5b983ba454b5a3f5cb07a3946ce96fa1b5154d4d8db878f1620a7d10e6cb2465e5608c5f7c3704
6516*518bb825a734c6c0d5755785c69712c6187811408386ed576fc7debdcf467bac108c00ffd6b261
6517*2d8e48192379f8eab090c7cac028eea33a6054b6269efdeb6fbc097b9fd9abd457582e5d18995b
6518*7e05d30a9542455c4214af0dcb60f4ac239de8b91d1a1a422e97836559022bf6806a15469e0829
6519*f5238eb880c2ae3a1d18c57da4e3511e1865d5484832afebd87ed5553870f02080d509a39276c0
6520*2e1c7109a07ce58e5c5feaa32b8775b9724686b61fc6d06c11e0965b6ec1c18307ab0a4642fde7
6521*cb7689af0f496d420068721775599605cb32619a264a3776f64d2f71d42206ae54aa07465b4aee
6522*883a6964240036f46fc0f6abb7e3d0c14331469a0e8c3683be91123a75b148d2398e70248025a1
6523*4d0801b1887b124177ae2877f25bb9f19513df4b0763f075582761642461d3ddb7eca6c04d7337
6524*ed9ddaf47243200d18a39d23e25f0c5d02ce2f2048e06ffc2eb896d50423a72a4e219c1e0880fe
6525*0dfdb8e6da6b70e087079455dd57bee0636fdfb8035f6918218d46e9adb9767ad0258351aea846
6526*c6523ae733f5e66ff7bb7697c0e56c0b275253e7dc6501ad5c1895347d1117a01e42ed6c0cc6fe
6527*2c41491f4686c825825156957e456f550260fdfacbb063c70eecdfbfdf05d4fd4b680d9981c587
6528*51a05d1618d9262c0f3ae212ff43a849900e413fe9c048bbae108ca968f32363a41f0780f7bcf7
6529*a771f0e001148b45b6670dc2a8ace9445d0039afca89b9f4ed6edc93bebec5fff4548d7857ed38
6530*0b7197c03f22d205bd0887cff8c7d0262c5dae366d24d6a5a643a0ebd3f3edcc58f1b3a7a7077b
6531*f6dc13d2f6ed1d85e37517e7004630dee0940bf518daa155aeb6ff960751ba115d06cc727e1891
6532*c1a804236b2184c022049665c1342d188609c3305d4f9692b372cdb5d7e13def7d2f77ebd53a8c
6533*2c067802a51cd73322209190a994f4f346c25e4ba42b768eda4d07bfc4616a93508dbbe25fbff9
6534*e677e2d4c8293cfb2cef3a068937aa3ce50a93c731e52b486a4799f9830aa81c57be9f15833191
6535*763218edc2cad00908b12858835df2a445c07a5d6ab8e3cebb30717a1ca74a3fef6189d6048c94
6536*49c4f6a2a26e20c795d94d3377d5dc21d10b5f9bde75f0b5c5bb27a95d29d140483011e21d425a
6537*2096056299304d0386b10cc330609a162c8bc0b202fd8486ec7fef74152b2e3b75f50d0db8efbe
6538*9f474ba1106f370dc2af662d01eda8dd7464d7ae59f43fea9d4bbc2d473cb75230f2f3c69586d1
6539*73646b3bf9a99d9b9a9609d3f222acfc5623d4aadb35630a48608510a0634d27eebfffd3d0345f
6540*9f350ea3dc3fdb3cb73a60646d549f37b16059064c7319a669c0b24c106271c8e08d970d8cbf37
6541*16b024b0e2b719d83c883beeba3b1e8c20dcea4a8551a8e76d2c0024ddc745f17d9d22008f5547
6542*f8add145e445dc0b36fcf7078eafa766cafa9c7366003ff2fa37607c6c14dffbf77f53d4569c35
6543*2973d9b9521d42d89055e3400e846e7a1776a91e18c3cdf407d50195befa497a241c33be3711ac
6544*79953c60fd65f77b6ec5f8f804f6effb8168842a830b19a50f63d838d2551c6ba8c89a13eda6c3
6545*bb6a8e6282ddb4fcfe8c04766f8c45731602020b84986e0a605906083119da115d5266f177d3d4
6546*7bf669b266226897cbe5f1f14fdc830d1b2e979ab2d477d3fe255021a31d4a2b1963150d8235db
6547*ee5985d014fa042b0646ffd7750c4d6259b00c13e6b201d3306199168845c25da60423ff6db134
6548*6d1b4dd390d7f3ccd90a4dbfafaabea111f77ffa01145a5b6b16c6a02b5bc2aee444dcc0fb4a02
6549*236b7bcbc2486933aa2dc0322d9886593a4d65c2221673facb0123bf0ff6941100f5ba8e6d03fd
6550*b872733f36adefc1baae0e149a1ab8c0fa15db3bd6e0171ff82cf4bc5e23304652caa590795621
6551*551879402aefa64b53439cd3554e0a10380320d02e078cbcad14d476cab261e06b0f7e1d8f3ff1
6552*04cecc4e636d472b36f7f76268631f3ada9a91cb692160ddae09b0beff72dc7bdfa76a12469184
6553*b3bd9c151dc4330625075f5a1b9c424ee19889bde5b48949e0ffc68aaf223956a52110356586a1
6554*53659a26bef1577fedd6373535e0865dbb70dbfbdf871b76ee84699a989899c3f4dc79d727f896
6555*b75f731d3e70fb87f10f7ff737f2039069556f90ef23b1366da713ad544145c668dd34608c32b3
6556*4a796bf84c40726d6518958c89600d5404be7071114ffcf79378e2bf9fc4e0e6cdb8fd03efc3cd
6557*ef783bda0b2d3839318de2b2c9f4fdb19f78074e9d1ac153fff3847868150d23c393c79bef580b
6558*0072c1382ddc4df3771c0181886637c487774b16b1dcaf572dd3a0a115ed757cfb189914c0fd88
6559*466afab4b9b321b19ba6b6023d5ea7e2f889e3f8c26f7d09b7def641bc74ec28b66d5a8f8e4293
6560*cfcd13d2340db77de8a3686f5fb3e2bb69596db70f8166e872029f4e2ea7614d473b06366d44ef
6561*ba7514d539ff66e402299d33469310ba5d3f018845609a16cc65039661ba67022a1e46c18727d8
6562*e036513db181191b9fc09e7b7f0e7ffaf53fc386759db8acbb8382d699b7fa8646dcf3b3f75515
6563*8c224da7343536617868086f7ee39b70c5e0200cd3c00ff6edc3e2d292ab493f84daffd7f75213
6564*7462b707df95d01c80ddb9594a05a44aa426db50ca4d62bcb15a790d44d46817cb32f1177ff957
6565*78fa9967f0c5dff83c36f474e2d4e46c68573ab865186f7ddbdbf1d8a30f4b8d555478bbe9249a
6566*7cedb06663630386b60c41d7f3383d3989ff79ea492c2f2fb3e44ce159052a3aca44af88c86881
6567*c0b44c18a6bd10d32a4f64f486131d193946dcc44818a444d12b1813e522e30b2f1ec15d7b7e06
6568*0b67cfa0bfa7b374d10dbd49def3fedbd1d1d1c184a19c91514693afed154dd33030b0093bafbf
6569*1e274746b06fdf0f31fada188ac56514978a989d99192784bc9b10b29310d24708a9a7236e4833
6570*38e1128561ec9cc2a2cf0854416494eb546c4dc2ab5e4a2557666666f0897b7f167ff6b53f417f
6571*7727462667a9f6ba8606ecb9f73efcf66f7e4e69acac221319e309b35e12e4f379ecb8f61a1886
6572*81a79fd9eb3e1b636e761653a7c771f1c20500d0474ebcfc6dbf5c4e267ac94545e75d972216b1
6573*a3ebb2b10cc3344a5f0c5443646447ca28eda89c31ea631a1519a7a7a771f727efc585f93358d3
6574*daec8dabd4c5e0d0366c1ad81c0686a91d2f32b2c5ed25529bb1951a1a1ab06be7f59898388d03
6575*070fc1300c9c3f378f970ebf8891578e3bd00240b7a6696bfcdd86ce2a88600c831206c724a4f4
6576*3b2b031637158809236323941f46156d3e30a0fec6db4d3b91b7a3b90e757a9e6a2600de77db87
6577*571646c2da4ad1ba20f6834dafdcba15070f3d8fd746474108c1c4d8284e1c3b8a8b171658de5b
6578*fd2b6e8eeb75af9e33da5fbd9a3096976119ce29ac32c018b181cb0323bd31a222231f186fdb71
6579*b7ab84e6d4d4147eed739fc7e0fa6e689a46a90f0e6f436f6f2f471c6586d17b03d15117181c1c
6580*c4c953a770e1c2055896859113c731393e26ea69d8bfe25eabc0de66e20d6b5a160cd380612edb
6581*bf12e0b8d62e8cd11bd71d0a00276148ba9b7ef2a9ffc577bffb1df4ac29d00d9a860f7ce863c9
6582*61847f18b23012b1b24fafabb313200433d3d300211839711c67e7ce448d880237f4106a6f7a81
6583*d0892e62b712cb0adc90586222a2ac85ef58bd9094f5c4dad19a24f4426c1815a3bff2875fc50d
6584*bb7642cfd7c130eda001026cdbbe03854201e7cf9f8f1c44e299907853c196fafa7a6cd8d08f1f
6585*1eb06faf3a31fa1acecd9f95e92d1871bd6b6fdd6b7089ef96a3ce27ce32ed9fb618f6852d9191
6586*9176174746c948b612915166572aabe98907fb4abe9b5e2a16f12bbffe39aceb2878d30740cbe5
6587*f08637bd2572572d1319790b4f2f726608b06d782b0e1f3e026259983f7306d393a7a3e7cf2e34
6588*b8a22e09084ce2a403a67dedc08ac118dec01507a3a4b69722482987e0e1e9bef0e261bc74f879
6589*e8f91ce576e3eb7e54a897368c51daced2d9d58999d9195c5c5c844508c6c75e93990da750a74c
6590*72549fee6b62ffcad539d8b2ac4b00237b1357128cca39a3ef4dc8680b954b8d5f7bf0eb58b7a6
6591*40d9f5f55fbe6230ca683b4b5f6f2f4647470142303b3d8565ce7dd238a551d3b47a67857e246a
6592*e90217d6d558843b83ce3b8d5fc2dac9f45852245891a2b6f342a4ec07446e2a391fdcc0fae123
6593*47f0f2e1e7d1d4b3b10429a0e5eb303078054e1c7f595a57d447b443b447634303968b4bee1751
6594*d3a727547b018056003340e9ac826559f6d7b08659125ee591d1176d124746d08d843139224d99
6595*c8f8b5071f444b83eeed1001bce14d6f891d19c118a370094e1d63e9ea5e8bf1d313202058387f
6596*5eeee9f1e1d2eabcd00dc30ed7ba9ee74d8f373066534a11ac4222635c4d6eb3625b9cc8f8e2e1
6597*23c81183aabb62689801bafa3b971e4584615ba115a3af8d0204b2671158c503d7792ffe1c3783
6598*511d46195d4250baf5a52038286bdb1e87f6efc3866dd7b9d56b3abbb8fda405a384825bb49c56
6599*7af60449055cce5985881155db6edaa799da6e5a429bde4d33dc787dc4d84dffc7c3df4783ee9d
6600*5da8ab6f086897fe49e9b2df307fdc9eb69bfe0516fbeb7f02588477a9a24cf1226e7044a9465c
6601*4aaa0a22a38476923edc3d9ae42e5b25323ef7dc3ee8790d8b86bd9ecbeb52fd44c6fe1436582e
6602*9f876199202030cc65e9f7cf28fe54c1014a3132fa5eac5a18636e00a951284a1b8681a5c54500
6603*75b6b3966348a4072309be1268e6733998cb064000a318eba0cc298188eb74ec8b06ab16464019
6604*48e5b896ea419207ced2e245688dce26d512bc8ff40007005dcfc13096e12415a968fa23ae54bc
6605*cd60a42d2465ddd431f00b8634b49db2706e1e85463b28e57df76748a2c92f2165aef6f2b2095d
6606*af032180aed725e9f49cf32290e312e606ce604cb30f80c4cff1027dd0e33dbfb08042b7fd3ae7
6607*1c88c55497dc6c52a5582ca2bede4e61f47c0ed0d4f706a5e281ab1c71810cc698da22b3342263
6608*2e9fb7dd086059fe5c327d187942bccde7c5440d757575aa5ff73a2510719d31f8725c5ea92818
6609*e58c15fb28e3018cb39630e2fa35fd3036b7b4ba15cba59f72c7d5a56a120fd786cb0eb4042d85
6610*02e6666723bd18c5072e71226df87ca3782c69c3e8b3aa261815a3ad4c58881b199b5a0aaedb85
6611*85e0f5b8c10f8f9a76b8708e7138bab367e6d0d9d985e9e969b414da92834bf5c3c9717965b5c3
6612*184b9da41571696d00d0ebeae1dcb0e9b5932762f42398edd843b61d2726c671e5b66d989e9e42
6613*537313f2791da6a97c6a8c8eb8fe9155048c09b4c3eee5835139321250671544daee9acab873f6
6614*0f28f3390dfbdce7a4950fc6a86affaa695ab088fd7374c330b166edda3857888523ae9b28a47e
6615*52bdc6618ca19d3ce286fdbb7b7a6012fbc7932d0d3a9edebb57b11f751855b5c74647b1f1f2cb
6616*71fcc409b47774e0ecec8ccad7bf8b8410f7882efc803ee941a6034fb5c3e8ae45682b1d70065b
6617*24c6fd8e77eef65291e2459c3d7b96afa9bcad254522aa67cfcca1bf7f031a1a1ab1b8b888ceee
6618*1e9c1e1b95edf0847fc51771fd5157568b3f48ea558dc2184bb3f47fdabbe95d37fda8db7470df
6619*b3700ebad3d056ac165a1d3d76145b878771e0e041145a5bb1b4a6137367a40ed48efa57e81c97
6620*c80c328351f588da69235c3b9163b4652e97434b47172c02d4eb1a1e7ef8e1f0f68adfa5ba85a0
6621*7969710967cfcea3afaf0f6363e3e8ecee41b158649c0509151a5c370238f0a47cc45bcb302aeb
6622*1130afc74d1a19dff0e6b7c02a557534d563fffefd31fa4896eb70668f59fbcaabafe09aabafc6
6623*d2d212666666b0aeaf0f93131358387f8e695f2abc881b3db80cc664919104e74f465ba2cb1ffb
6624*f19b01d89f89a9d15751740f781227de0c13c9c98b303b74e8795c77dd0e148bcb989f9f474f5f
6625*1fceccd4636e7686e71200d7cbbae05e689c5aa97d1855b5ed0328c18198a2662e97c3e5570cc3
6626*02d0daa4e3cfbffef79193512e18c366a2034e821f1e3880eb775c8793232731393985359d5d68
6627*6a6ac2ecf434969616832e47fc2bee4f77e4226e78309509235fb31c30ca74e9df4b29832368de
6628*fddef7c32a3daeaead3e8747fff371615c57ed9a6d963c920380699878eeb9e7b06ddb5674ae59
6629*8323c78ea1a1a9097d1b3660e1dc399c9d3b83e2d212004c1142a87b34e95e4fde92c1a8dca594
6630*05158952da4dbfede6770100eaf31abefb2fff647f1ba5300132d151a699e710e5661282e75f78
6631*11bdebd6e1c65dbbf0f2f1e3989999454ba1152d8556188681c58b178d812b867603e414803100
6632*93ded561c45b5447bf9a615412281d9cd1f3153f326e1ad88cc6d60e0040777b33bef5d04381cf
6633*c4a581514ddb369c383d81d933b3b8627010570c0ee2e51327303d3d8dbc9e474b6ba18f80fc8b
6634*cfc564def40ea85418e5b5e5fb8815fa229aa3725822dd7558d17bf5c13bef0600e434e0e5170e
6635*60726a2a368c52c3518451c99d004b4b45bcf0e261d4d7d561f3e6cdd83a3484f3e71730393585
6636*e9e969148b4547254fff3c5d18713318a58a649e4b9baa47c65c2e872d575e0b0b40576b23fef4
6637*9bdf0cd84946c732c31847bbb85cc491a34780a340737333d6ae5d8b1b6fbc017b9f7d164b4b4b
6638*00e1dc663483319e196d2a8aba72392e4f9d00b8f5f60fc1420e390dc82f9dc3d37b9f610d424a
6639*4fae361d6d6e2bc77c61e102161646505f574f4556ce0d41140793c128dd4c4276f122e3db6e7e
6640*3700a0774d337ef70b9f0b877395e1950b46296dd9cee979e2e6b85509a382695a308a9c427cba
6641*53ab9ae3d28637bceef5a86b6a819ed3303f398a871f7d24ca455a5baa35bdfc83b616b8513733
6642*01020fa18e93e3c66eae011815b4a9681b0ebb32126ec3ed1fb50fcafabb5af1c0a73e2bbfdf15
6643*595c02186327a104d0bd6b88827fa375573b8c718d89c21c070dfbd65f86cede7ed4e91a5e7a61
6644*3f7ef0dc0ff852e58251e81a1fc6a876efee9e40e8017d4a572ec534ad1518636f3bb970c4b4b8
6645*eb67ee0301b0b1bb1d1ffbf4ef26db5ed2c3290f8c49b475efc20fce0fd4d386514133e8500930
6646*e672390c6ddd8681c12dd8b071137afbd6a3adbd03e7cecd63667212e363af61e4e42b387eec28
6647*6639178c84ae94557863859616f4b637e1c9c71fc14b2fbd14ed514530f253ffe0fd94191197bd
6648*17ab6d186574b76ebb0ab77ef0a3d872e535d0f2de7d549c17dd00366fa525cf4e8de3c1affe1e
6649*5e387480d16fbcc8d89c37b0695d073efa953f4825da02970ec624451bd8324400a0a1a1019b36
6650*6ec4d163c7126857138cd1c65a2e873beefe247ee4cd6f45634b5bd84b32559d9d38859ef51b60
6651*58407d0eb8fd5d3f2ed4128d71d3c68d585e5ec6e898f061765509234bc2d93b0d6dd982919111
6652*2cda17dd8823ae5fa95660940d52bb6e7c1d3ef90b9f417d53216caa786cd5d96b43ebfa24f83e
6653*fd95574f4658a40d63024d8e7b1a37be0be5b8eaf997d8b092609431686e69c1677ee537b079db
6654*35616b4560654b34c7ab03469e364000618e1b72aa0d18233d4a2f765cbf13bff0cb5f40ae7447
6655*c172019be8e6d9c2b154198cac6a49f7f02f206247dc68e378c7126a93a79a333ae5aaabafc503
6656*bffa4520972f7b84651022e8a7766154d10c5e0516fe0584225d950ca3943601b65e79153efbf9
6657*df01c9e54372e902ebf499c128651668f3e319b856c1fe5b0b304af68e2bb60ce197bff8fb403e
6658*5f76602587e4ab4a6914150a63b436095432222efb7adcea8451a57cf6f35f0672dee77725804d
6659*e507a93501a3fc5022226e3857a84618fdaea29cf1d6db3eec9e9f5da9089bd7c28f9b758b009c
6660*d8e3ab601895b45d3c1911d7178955bbf1fabbc430ca967c3e8fdd1ff8c88aa7046671c99da455
6661*0fa3829b72c405aa07c628097fcef8c94f3d004dafc74a97c98951f63b5b85308a0d821b8fce65
6662*05396ef5c1a8526e7ae35b938f254679f1d0016624507e17d50823201305a5fa908ab87e8f4a86
6663*51461b20d0751d39bd7ec5d384babc86ff7dea4989c385d509a3488f7ad42a02bf80f0eaab0f46
6664*6635a75cbb73d78a430b009777b7e3d8d1a3911b5a7a6c550e639212f8058420e2324653493046
6665*eb7995bb6e785d5cd5d8a5b12e87bd4ffe17e6cf09ef48b8aa61a4fa60e4b8c26b1552bde9dd0a
6666*c228576b97e16ddbe38e2076d9d8dd86fbeef86de58dad7c10a358d8539a2e905c1813142ae24a
6667*45d0b4611468cad5cae8d29ec48afdd8f958a5bdb90edff9e77fc4a953a718a36194550a6350dd
6668*2f1dfe05846347650ad507a3ca50e6cfcca057b5df98a5ad51c740771b6efbca5712edcd6a11c6
6669*2485f14854f91cb7926054d17ee2b14730bceb8de5d93ebed2d6a463fba65e7cfceebb31373727
6670*1e9e5bb17a61f46bba6b4e0a8bc8df9ca50da3bc77b98fa61d8ffd070ea0b95ec7c292f203e2a4
6671*4b5b531daedad8833beffa381e798471c38e04859edab4815c39189314c62f2064c75059308a0d
6672*688b99e969743769585cd6605ae96ef8bc06f4af6dc5655d6db8e3cebbf0e8638f49fb962f3a56
6673*078c3c6d27a8fa236e8eb2f39fcce52d3ec02317ff0b095dff80a3b57dbb8e48ed70f9dbbff926
6674*b65ed685c8073d2a94ae42036edaba016df51a3ef2b18fe1d1c71e638e97b788c61b2ea56d21a1
6675*2b2f4bcfb28c665c6dc751469b26c32eeec34bc40fd608f4cf5e911abad820f9c7563667fcea1f
6676*fd31aedb711daede79134e4ecdc3881979350085461d837d5d686d6ec037bef197f8d297bf5c7a
6677*409e6af17d70538b602b1c19c3b5a968063f25f6e3a242613276370c83958351499310fcdcfdf7
6678*e33fbeff3dbcfecacd7875620663b30b52006b1a5068d0d1ddde82beae36d4e93afeefff9ec667
6679*7ee99770f8f061eee83318d9ab11d56e9b177989ef71513ed76a8591db0747fbdcb973b8f927df
6680*897bf67c02f7ecd9834dbd5d3873ee0216168bb8b058c4e2b201d322a8d7f368a8cba3b1be0e4d
6681*f575e86c6b413e9fc399b9393cf4d043f8f6b7bf83ff7cfcf1b4462b1a722ada9502a3b46530dd
6682*04a0f5f4ad2700d0d8d080814d9b70f8c811be806ad79700c6b8a5bdbd1d7b3e7137def1f6b763
6683*6060006d6d6d219bf9f9798c8d8fe399a79fc177fff55ff1e4534fc130446726563b8cf14582cd
6684*c3c3c33879f2241617edc748693dbd7d36b88d8d4c70ab1946bfbaaa7457672706060650281430
6685*363e86b1b1719c3f7f1eac192f570a50eb30aa580c0fd1e09672dcd2d11cd27b405f25c218a5e7
6686*2fd33333989ee13ee530be7606a38a336526c87125875015307a9aee5a992323a325154d896a39
6687*4d4911b93e920d90b1df8af6089d55007ce7cfd224278351b41a4fbf966094d7f6f6542570d5b7
6688*7c15c3e8abca6094722e2f8cae79f41bf59e016157518f44f57fa596bcd41e8c6a7d6430a6a917
6689*74a61e892ae340ad9531ea66302a39d7048c2209278df55d8f4b1fb39523c70daed6028c722669
6690*c328e19c368c3134838e5cf758bacec199031391152a338c02b10c46d93eaa134651ad773daebd
6691*4ee5b8ee92c128adcd37cb6094aa95d526ce7f6ec4f5725cf1256aab0b46b66906a354ad92b6ec
6692*b7a2779500000208494441541140e9525a2fe2d2392e8fdc0c4659fd4b08632ced4b09a382b94b
6693*2d23c7e5a7b9f18f826a0146db257d4dbfe3aa8451499bbe209e7963e70cc6f89a7ec70c46092b
6694*d9e30837da9622ae7b201627c7951f1fc7b4fa60144a940b4625edea8131e6060620ba0593ec27
6695*4175301505a3e79cc11861b50230f23ded88ea5cc108701ed097c128e790c118ed2284316e21a2
6696*07f4ad5218992d3509a3f200ca0ea348890081af7c399982e790c1a86c95b6768dc298449b1d71
6697*3318c396198c4cd73461e4b9fa6fbf241d71fd8ae9c1c877ca60149bd7128c490a15719db82bea
6698*505c3218958ad454c7dcc05508235bdb992402a845dc0c46a592c128a14dc4cd1c093f0fd43767
6699*a1dbde55118c800a90198c894b0a30ca3b1004bf21a3afc7a50432183318cb0ca3824b3021605f
6700*ab80ea8651ec592e18d3d3f6aa2b1046a153c259118d551471c3d72a54008c3e970c469e7635c1
6701*28ef4d02af0511974a816b0246765506a392b994a13a8c628360541144dc969602d6adeb653b2a
6702*8caa16604c948726ed3a965315c2a8505a5b5ba9b0aa3535371300c8e572e8e9e9e1769ac198b0
6703*eb584e29242f11915cad679641fc398d7edbb4c5e9d3a761591600406b6c6a964228ee68ca0f63
6704*0abbea6ada4d338dab17c6b87de85242198c114ed500a3bca6741f29c3a8a2e9fd02822f157304
6705*aca6f2c218d1bda4a6b8a12260a48c6a074695f2ffb8cbbd2492de9dad0000000049454e44ae42
6706*6082
6707addfile ./contrib/musicplayer/src/resources/images/album_cover_3.png
6708binary ./contrib/musicplayer/src/resources/images/album_cover_3.png
6709oldhex
6710*
6711newhex
6712*89504e470d0a1a0a0000000d494844520000012c0000012c0806000000797d8e75000000047342
6713*4954080808087c086488000000097048597300000ddb00000ddb01995f140f0000001974455874
6714*536f667477617265007777772e696e6b73636170652e6f72679bee3c1a0000200049444154789c
6715*ed9d797824477df7bfdd33d2e8be565a69b5da4bbb6b6ce313db188231c609e18ac11c36873184
6716*c380c107040281e409a72140de246fc843c0604802210fd809e1b20dbc1c068cf101c6dec580ed
6717*f55e5aed4a7b49ab7b35d3f5fe319a511f55d555d5553d3d527df7697ba6abeaf7fb764df747d5
6718*353dddcecbaf7e3d416a22dcb7122d0d89505f2688924031513424910bc1abed2409ac94dd7178
6719*a5ecc6aad69cf82abe1c59dbb1d949b8e96bec8d5633af3d61d63e2b0b21c5da6108c91cb26ad9
6720*79108a64b710524b9f4108c934e3004b6d8f587d108a89a42989e22e1052ad2114ac610242d4b8
6721*0c5908c928c11ea8d15f1e243e9a859099241642e2928190701e0b21f59a46fcc507cd9bed170b
6722*21b59a1642c1741642e2aa3f080945590a433d25dcbc69239ce81e4c3790e2943d5d598250ad83
6723*06c3323fc29886f674ac06e9b95a99108a0bb36bf79ec8ba3cada50360efde7d2002a78b12f9f5
6724*44cad44808081ca2358190585f590849a4b72321f5289abc6dddb209344fcc392c424879d1939f
6725*9a5ca4485306c9da32105233af0a21e2e9c82ef70d99279a25cb1012aaa043624956238474f861
6726*ce6111a1f0f50c21b3f3427110e25d4694068484ff1459080927b110d21386a0ec8fe691328745
6727*428b516f096b5b080573d40784624b33e08f59cb4248298c2e6bf4534206af78b2101297859029
6728*590871221909a3692b85d773af74b7101297859029590871221909631642c9c250e7b096075782
6729*100290a99f6e641942e5ca2948715e2843108ad434e62d79e0d501214d916242c4cc6105c626a1
6730*961983908673620b2153b210e2443212a6de202417883e2795a74229e1fc9585908c2c84c495a5
6731*53b25500a198308a7b472231e7b0c2ccb21092918590b82c8494031b08530b0831c3524f091d4a
6732*a903380e9b3c164212e92d84d4a35808690993290809562dd3877a4a189d5eaf4c7889ec301642
6733*a664211413497b88950921f9daba4312ce3bd904d46f091dd8df8f999185504c24ed212c84cc84
6734*548350022da5c8d3c0447cff440399d50a84905c5309ad400885c258089909592b0831cb687358
6735*542811301be8938590b82c8494821a0a6321249c50b22909bc0eb2a93cb4caebdd060b21715908
6736*29053514a67e2124d958459a201455f8fcce09bd8e9eff093c84c242485c16424a410d85b11092
6737*4e2ad84c1542125e58a784b44123730ecb42483d8a859096301642d249059ba5002129d18955fe
6738*96901658fb1c9658300b213d612c84cc84b41052f312d79885a0b012ce6159087122690f632164
6739*26a485909a97b8c6267a51ee067e3c331642ca61346da5962899869044d8550fa1726045a50ba1
6740*782d670dcc6111dfff09741ee8f4e489a2580869096321249d54b09985105f7159597358dae6af
6741*926ff6ea8090a6481642eab2103224b5ac32088ab9acc1424839b08110e2192c84c49b5908f1a5
6742*0f42520d29c47240b9ac8159db5f6a21a42d8485905452c16616427cd5084214457b9f54d73bb4
6743*53c240551fdd44efd620e52e41984c4128264cbd40285ab55e210444af92160eac280b219e829f
6744*80be195dfa1c16b5aaa68cfaa3ea8b6421a4260b21435a1d1062ad8f9491d853420153ab0242f2
6745*b57587ac1708959b8a9e925908996d15df384d08495526a09ee2d127dd43bcb210321372654288
6746*f69e1954d98df690896421245e593412bd1ef59450627c2561403d8c85907042c9a616426c5908
6747*895736ff095532702e6b1047566c16b9a2c4b57587b41052776324acb22c84c42ba70721da8ae8
6748*1961f90d630e4b268b5051e2daba435a08a9bb311256591642e2956b0b2181daa132d61c5678bd
6749*ef9cb07e2124d958558a29b205217ee37a8150a296c2104a94452dc28a81507209cc6159088937
6750*b31062cb4248bcf2ea82509cfc8c4a7e5983bf8972550b21715908b164212493323b10a2948290
6751*f21216ffb784164294f77abdc435b610d296452d8a8590b0e220a443ec1bf851e7af2c84c46521
6752*c49285904cca950f2119511ff3b57c3f774d86b204a1726045650942ea59571f84a422298b30df
6753*706ba6aef89fe3d5dedff26f99835e049e9ac38aaadacc42882f0b21f18a1642d5ec19e82b6e76
6754*f18ef4d5899bc322a1ba82db682114270b21f1ca1642010719f6270fa1e462ce61f11f556f2164
6755*212453d94228e020c3fe6a01216111129ec32aeffac4f75a2c90b203fd2113c94248bcb28550c0
6756*4186fd651d4281b7be178ccb1a1860529e73b710e2c9fe7443265d960ef448695a36e8d923e933
6757*0422168434887e4a28d1191642164216427ab45a212423e695eefc392c134a1942318d2d846452
6758*66ed400f94a665839edd42485db45342b3262d84c42b5b0805b2d795b70c1dea7508a16a51a01a
6759*7db8a4701d96859078650ba140f6baf296a1437d8540285ee1db6747e7d7f3f4b0c4b7b0652164
6760*4616427a946908010183197366084209b4e427328745208a2b5a50769185904ccaac1de881d2b4
6761*6cd0b35b08a92ba31062142ace61094328a6b2a02c84ccc84228812c84c4a5f89465d6395e5891
6762*cb1a1c000e292fe5320b2189daa9ca4228812c84c4a509423a94a73d0e7a399da6392c0b216165
6763*194240d85fedfd04642124ae0c41881e99ce9fc8b784c273581642c2b2104a200b2171651e42c9
6764*95a7eeab4c62a50ca20c1fe81642f27acab9e7e0e52f7949f5bde779f8c0473e8a62b158435721
6765*5908a51e9916913387c5ab6ee0dbbc0c1fe82bef9e42294ae05aa14263013d3d3d8175eb0707b1
6766*77df3e83c660215483c8fa22062329dfc0cf42488fea1d42326a686c88acdbb861831ab02c8452
6767*8f6c0a42f42116a1f66900588133427a7d4e867495e553b2d50421193d69dbb6c8bae12d9bf1b3
6768*bbef5e32a3e94b1e0ba11a4414809006f99e4be8cfa0f794505416428acaf24f37620efaadc3c3
6769*d4fb1e456521947ec47420242a02a2f0a87ad924750321a0d67e02ca328400a55332dafabebe3e
6770*b86e0e9ee719f112db543d6baa5157328458ebe59f4bc84a6221a4a61508a1a8e44642aeeba2bf
6771*7f2d0e1e3c24ec254e1642da022b897b5b2a4e51e0a7cf84be275581e5df3f828fd989c992822c
6772*8412a8061092d5a6a10d38387a905bc742485b6025e98050b2fc65e5e9f307e6e7b02c8412284b
6773*df9025180955b479cb66dc73df7dc9bd00c836da2c84f82948e055c0dfd24bf5e712d20c5808a9
6774*6b854048a665a5ee29946f0fd5a3cae7d71ac542483c29afaf18e3a55860651a4240c060c69c65
6775*0b424066be210bc75b37b80e4d4d05cccfcf6b8fad25529d4208d0b3e768839006e5698f832684
6776*54979ac842485c1981906854dadf3fd77171e693cfc0fdbf7a4031aaa82c84d8296a07a138f9fb
6777*caf8650dcb592d8484556710d211f6ecb3cfc27d0c600907cad0814529e46a354388520830064c
6778*c9e6b02c84c4b50a20141fd5614ef99c71c693e1baeed2f5587502a17205a6cccc0bd52984ca15
6779*122b0a2c42b884aba92c84ea382a59ba2b243d6e7353334ed9b60dbfffc31fb465643bb1101255
6780*1a109211e3b286147d5808a51eb9667342845fed9cb3ce4e04ac6c7d43965d08952d64c91f2d61
6781*f96bc270896b263f612e24bc54ad4597e547fd882c6a7e228bbf99c422d941da23eb8be8ab19db
6782*5f7236799f120170d6d96752d673fe85f625bf1f27bc4069cf09f51ddf0bb36f92ed2cc252ed2b
6783*33fed809a2de18bb16a5b9f81c961d09a51e595f44c27d5b4bf9adac59d38bf5ebd7636464845a
6784*37f5919040b169657f24c42835e4cdb52321ae21ed91f5450cd5161d0919da9184feba53dc8775
6785*e92597981d0945fa2ad2381b7d95819150c4a9dae1a9d50ff5315fcb10d2203b12528f64ae1384
6786*a47b4ec801fb91279582a75e7001fee71bdfc0cccc4ca86e4c67d4695f995019cad1c4956b2e49
6787*b0323d06a57d1247f12aef35ced2cee37904a552f40e1e79faee25301252908590d6c04ad20da1
6788*c422c1970d8d055c7cf1c5b8fd8edbebb6aff4e446043ad5d161b82e1183905ea927a1e38304ca
6789*884762aec30a148a8fed2c84b4065652ad21443984e2eb11c67a00175f7c31eebcf34e7824c13d
6790*b218aaf548a8cc201f847cdb183e40eb1f42ba45580fa130750a9fe5881642fc141a0e74075c50
6791*55d4ddd58df3cf3f1ff709dec1a1d610aaa6a9fe87044ea9fc90b2108a64e1ac8fee2c9c39aca4
6792*09d565211454a621a4c547b4fcd24b2fc5bdf7ddab16c080fca7630118ad88d331dd124cc2393c
6793*09e85e199735104408a7417aa25908f153d47eb4c133c0b3e02fdbbc7933ce78f219d8b973a751
6794*3f91774b4789178693f1be5bf910d2a1c43f7eb6105ad66a8490545f09555d7e75e595afc0238f
6795*3c2279bf777692ea9fe00a8cbce88849bfd4e2660a42a1aae6ac45c658911ad411969ef1958510
6796*3fc50a819094a2bd490355e565ffdab578ce73fe14dffbde1de27688ef7f64792e49efef622d84
6797*9492c836a39d12ca874b652b8594e4c0b2b7f30814a62e1ea8fc2f9fffbce7e1eebb7f8ea9a929
6798*769cca3c124902260b21a5249a9ad106e3b4ea6eb078696251e3efc76495e42a60d1df8ec5fda6
6799*2db8885e311df593e5be32e38d9d64a9c7422949b019c552537333ae78f995e5b2a56df13c0f9e
6800*e7a1542ac12b95e0791e88e75160c5db68762764e18aeee031e97b69ecf054eb2bae6de5c3835d
6801*937959834ef1a75ac1edddd5f64bfa247d6546ec849192186fcb9f2509fc2faee9054f7d2a7efc
6802*931fe1f1c71f8f4fc2901d09e96996cc6bf22d55be5b43dcefb584474264e5ff923e495f99f1c7
6803*4e10f5c6195d28792391ffc535755c076fbdf66de8ecea0c4612197154bc2a4b62e4c11b5d7076
6804*41637e049b253b3c14fd28868800cb42484c16420a7e9ce5be1009efafd3d9d9891baebf11b95c
6805*de4248a0593d4148269b6bef2914f66a21a4e28796a43cdf54fe116bb15842e557287116583637
6806*6edc84375d734d726f71073763179493e241cfb39de8f050f4a31846724f617888aecfb37efa1c
6807*fa1505d537eb9d68515a2299f2c74e182949c59b5a92f008a7fcde0774856ccc32b2fcbff3cebb
6808*007f76d908bef3ed6f711b9be93ac5a8c6763f4d5b2918462e9b196f795a5999652463077a387d
6809*96bcf11312e61b53d203216ef4ea68533d5f201ea7205c7ed9652fc6c8c87e3cf8eb0713e58dc9
6810*9ea8997a8fac3e0831eb50fe08b9fed30aa1b1a6c0a85155a2a763f57663333ddee406d9d55631
6811*def813d3ce726642a2971250cf37c53f1866972c15b0ca5dd7c51bdef8660cad5fafb5afe2aa8a
6812*9e8eb1b734e181251842f4102635f016dfe5ec7f00812bea4755d982103809ea074200df5b3c84
6813*f8b391e5b6049e5744a95484572ac2f34a28df0a85768484dec67861765b281cafd79a9a9a70dd
6814*0def404b4b736c5ff1bab55e2044e9664eb6f4bdf19bf12124ec078a9735d42f842426a70d7889
6815*3d0438def82092b94575d88307423c78a4b40c28af08af542affd68e76608717d1ae8b3e9630d0
6816*65225def2f5fd3db8beb6f78079c4a609657ca56333e018925c6a4e0225e357d6ffc66fa2024e3
6817*cd5dae1b4c682124e625f610e078330721d6d1515e88e7c12b954fef8ac5d2d2699e07cf23f210
6818*92ea2b875a552407abceb66da7e0ead7fd39ef13905814372bdccdc255d3f7c66f561b08b14346
6819*9dd19f4ba8b6a70aba1728d19e3736a35c4be5a632d7ed0b26a1ec84e13555a857a025ad041f08
6820*ad29118fcaace38b71d1339f859191fdf8e1fffb41726f89ab6ada79b575b9a18349392ca1bca2
6821*d7a395277b547d4c5a0ba14816a56ab2d62a60e25f62a01942a251893e50f9f5f22b5e89032323
6822*f8fdef7e27103d2647829aa6c2ac0c082987ad8a3187c51bc48a4f4eeb1ba9a98d2933753a5639
6823*522b2f798b74d791f23c945742a9b4183f494e8bcab31de38ff3094496b8ed616e73a80bc3cae7
6824*f378ebb5d7614d6fafc4dea2b65fc56ca26297f3ff291f4ccafed4dc99f1b22c976e867d3025e9
6825*3b1deeeb1642c1aa09fbca0b00aa1400945817d77c629a1195552812b1a5ad0def78e7bbd0d8d0
6826*90cc5f824d0c16af0c0871dd69db1dd80dfdff5c0b21dad1ec7b19b728779b5c5f9547b31ebc52
6827*09a56269e95b3c8fef55a8f7e5bd24da2446135ebcb8bef5c7e85f3788eb6e7c879ab7188fd1c3
6828*c7424875a3e2bcb152085cd6a0e6de4248e293a65621205ee59bbc224ac5124a250f1e617fb85a
6829*bc2408139fc58984e5e58c73c72a3fedf43370d555afe56e62dce1b3e22024e32741101508c978
6830*63dcd39d1e3a5313d3a1aacad66492686a1629aafe70932070f9882e2f09c2c8658ca9cdba0e2b
6831*d43c2ea7c831f5ac4bff18074647f0e31ffd30aeb69c943f02c27997405a02f18368da64c98674
6832*b4e5a957351030463f7e5908718b44ba87a00c27cf8bce3fc9aa5610d2114623a896eb38b8f255
6833*afc1d8a14378e491df260c2cd6305b10e207aa0d8492b70c9d12dad331ae5787bd447fe1c20aea
6834*81782578a562f91b3dafc886955a170b6cbd625f69eaf2707b91cf9b5beedb77fccae7f278cbdb
6835*aec7dab5fd12de8285a99f8e2508a43a2fa4ee4d7da3845b8656b83a2054dd61580bd3769c143b
6836*84673bbce88050ac1fb27475f9f255e5fe9fbd0876714c36f59dc74c97b30ef1e8f689d86115d2
6837*40e557734b2bdef9aef7a0a9a910e3cc4228e94609b7944a115ce1f28e10bdd70a25ec104eb59a
6838*402866b38847e0153d94168b4bdfea952115f7ed5d309b196fea5d2e7a98b3fc3881ddcc751d0c
6839*f5af416f573bda9a9bd090cf4572b3b6270e54fef66bfad6e2c6bff84bc071e2f7554d5dce6b68
6840*2124ea23ea873e8745adca935c6d91669122992933553f8a61aadd5b9d8f8a3b9434485b976bf2
6841*2312920299ee8eb6c07bcf23985b58c0dcfc49ccce2fe0c4cc5cf529ccb4f6a2a9b76e7f12def0
6842*c66b70cbe76f8e89a0123d412fa6df50bca5540a03fb11252ae7a739f2d8a266e015d7018462eb
6843*e99a3497491adb2c4508493416f9b85dd7416b73135a9b9b0000a592872393277074721a8b8ba5
6844*44f62efca38b71e0c001dc79fb77855ad68025ca8dcdb0251d08d10a595ffa712e6b10cdb0f220
6845*145b9b90f2f550228f505fe110920a13be590301fef8792fc0d6adc3d8be751bb66d1dc6b6adc3
6846*d83a3c8c96961600402ee7a2bfa70bfd3d5d98989ac6f8f129cccd9f94caeddff12f7fd995181d
6847*3d80877e2379b7520b21f5a84a1b421f30e559bc0a102ec31092ab9ac09b1f5216422a2123ab1d
6848*00274e4ce1c1071fc2830f3e542d731c07ebd60de0f4534fc54b2f7f312e38ff3c0040577b1bba
6849*dadb303bbf80f16393989c9ee3a7a6e476dd1cdef496b7e3e31ffd0046470e286e54650b0cb75a
6850*71104aae3ca1c0a83a39ad3b61d620141766e9024e8f3392b210526f4a18c1082138303a8a03a3
6851*a3f8c18f7e84adc3c378d59557e0f9cffd53140a05b43415b079702d4ecccc62ffa1a3582c79a1
6852*f67c0f85a626bcf35d7f850ffcf57b313b3bcb70a7b64dfa2aaf1e083133d0fee02c0fbdfc0b28
6853*ef5951c517f1aa124135795b6e531e45154b25144b25943c0fc9bf1d4be88fd330fe9fa0334d5d
6854*2edb50a4e6ae279ec0473ffe09bce0c52fc5673e7b33c60f1f060074b4b6e0499b07d1d1529eef
6855*8abbd8d97f6945677737def5def7c3097c532cb61511afc295b575b2b857125c346d8894bf8057
6856*22bad0f3d27f4b28e15bbcaaa6ce48102658e47356bdf34111c5521125af04046ecd2221657ffe
6857*9eaa1f08c97d454fbfe3a8a8bfc9c9497cf1dfff032f7ae9cbf1371ff810c60f1f463e97c3f050
6858*3f86fabae1baf4b98bc035603e6dd8b419afbcea6a7a3a296f5a3a59bc6b08ea1442327ee87259
6859*75c5c26aea8c04618245fcc68e6f2104d5a7c094024f81d1e9cfdf532b154212fec2f5022bc537
6860*ac582ce2ceef7f1faf7ccd6bf1c31fff0400d0dbdd8153370da0b990f745a483caaf8b9ffd1c0c
6861*0f6fa378d5d6d1625b48b00a20c4f6d3d0d080cece0eac1b18c0f0962dcc7a2e3da486ce48b00d
6862*325efc108a530052c5a2f015e7bc0d5bf51092e82ba0dcd7fe8b67690d45534d9e3881f7bcefaf
6863*f1e19b3e8ed9b939141a1b71eae6f5e8ed6a431ca82a079bebbab8e6daeb964667c21d2de69560
6864*5543284e8d8d8d58bb762db66fdb866d5bb7a27f6d3f72b93c26264f60cfdebd81d3c3cac2b8ac
6865*81b5c5cade024de3e022f5a5242b896f55e5624ed1db0527d84ccd41f88112a5506eacd690b57b
6866*33012911f99bdffe367efde083b8e9c31fc4934f3f1d1bfad720e7ba38746c325a9b127b4d6f1f
6867*ae7aedebf1e57fbb253ebda437d38afb822154db940d7a364a3ac771d0dede86eeae2eb4b4b460
6868*7e7e1e478e1cc1ccec2c161616626396e7b014411a2ce237961909319308fa23a88ca48a281517
6869*e17925788437792ef1f742cb1f1d7ee34c8c84041a4ab5e2fdada05696f3b37f6404afbfe62df8
6870*c297fe0d9ee761b0af1b033d1dcb6908ffe07ec6c5cfc6b6eda7d89150626fc43738082edd5d9d
6871*d8be6d2b06d7adc3e2e222f6ecdd8bdd7bf6e0d8f1e342b00228d76105378fbf913a47422a0dc3
6872*612a9720f07f1aa3d38f78104d9b6cbca1542bc9ca0e758f49706087de174b257ce67337e3fe07
6873*1ec03ffefda730d8d703003878f4446c1cc77571cdb5d7e37defbe11a512fb8a7a65afe63a36b1
6874*e2bd25f3d3d4d484750303682c34e2e891a3383e3121d3c7f32018057010c0a1c82961f912aca5
6875*89691957862014db9a60e95b3ecafdcc13cb424863e5482b697c09a6bbff815fe3ba1bdf814fff
6876*d33f72a1150ed7d5b306af7dc335f8d2e73f2b946735434844b95c0e6bfbfad0d5d58513274e60
6877*ffc8088ac562247de565b158c4d4e4719c989c58dcb079f83cd775471e7ae0bee3fe98fcc77cd5
6878*084222612b579d4b8fa6621cd50b84a45ad6f8c0229cd724bc5238125fbf79e8615c77e33bf02f
6879*fff79f96a0e5e0e0d1c9d82817fed13371cfcf7f8adf3df288563fbac4dfddd3f7c34a5f686ac2
6880*86a121785e097bf6eec3ecec0cb5c9c985054c4e1cc7898909cc4c4f5556374c1effd53421e478
6881*b83ef3b286e8b6072b897e3b26047ac1a572ca572c2e3d4a3df0e9b11b2a7f4326e14d5343f196
6882*5229d4fdb024e22ff2ed1835907e6f153df4f00ebcfd861b313333539dd3e24521001cc7c5ebaf
6883*799b113fccbc84b710eebc90093f4173cb4b9cb7cabfb6b6566cdeb41127a626f1c4eedd54582d
6884*cccf63efaec7f1fb9d0fe3e0c87e3fac2a7a12cd4ee04af75a40282e1001599e405f7a9c15ddab
6885*6996a86f94704ba914660ef4387f425fd1732b8492294a7452faa18777e2daebcbd05abfb6076d
6886*cd05fa36fbec75f5acc1332fbe44dd5cacbfec400831def84758546b7a7a3034b41e636363181b
6887*1b8f9c011517177160df1e3cfac84e4c4e4406507ed18125dc35da8e0f7643ff3f8f78282e8da6
6888*4a5e29f2b4183dded4374ab8a5540a6d9d2cee35b4c36ada10ba1cdf537328d54c7d3bb663e74e
6889*5c7bfd0d989999c1d6f56b91737d5f8e332c3fefb2cb25fdd527842487225c0d0c0ca0b7b717fb
6890*f6efc7f189898037e2118c8d1ec0ef773e8ca3870f8b4ce53046585a8e0f764399911081ffc2ce
6891*1248c953f4a6b6515287a4f68a723b0b3752f8e0d6b321caa20327ead5545f01c08e9dbfc50def
6892*7c1772ae83cd033dcb06687e01f4ae1dc0d9e73e65d54348d45bef9a35686f6dc5eeddbb313d3d
6893*13f0b6585cc4138ffd01630747b937120889754a28e8c8b7e83e1d231e293f776fd177f5b9841f
6894*5e56a956c295cd1c587129f543487d67161f0dc5e5d27f60b1bcfceac1dfe0e62fdc82ee8e36f4
6895*b4b7309d5474f9cb5ea1cd53244965df0ff555962014ee4096b78ece76ac59d3837dfbf763e1e4
6896*c940c8f9b939ecfafdef68735471a202ab7a5983bf0bfcdd23b4910a2240f9e931e2c4554b295c
6897*d9cc4ec08d6a664394153f4aafd63469839e515347de7ccb17f1f4a75d8833cf3813d373235858
6898*ba8b292dc2e0d0266cdd7e0a763df6688c39dedbf4fb8a9f5eafbb969616ac1b5887fd2323980f
6899*5dfc79627202fbf7ec86a7765ddb7ac7715a092181197b970aa6447fa0f90d08f15f81ee09b652
6900*1d40981969c4a5acff9190192f015f953f941c3f264ec73ccfc3fbfee66f3137378badebfbe0f0
6901*1e4ce10057bce22a20c65bea23218452088e8474bb6b6c68c086a1211c3a740833d333013f1347
6902*8f62efaec7556155d129e115eccb1a0252db992b6b3c02943c0f8bc5228aa5e804badcb16be6c0
6903*8a4b692194dc5bf9e7c595c306beff9af31334575e46470fe2a6bffb245a9b9b30d0d3ceab8acd
6904*db4e41ffba75e6bdf993c64128022203eee20e0242303030808989e398989808549e9999c1c8be
6905*3d3a5c444e0ba56fe017aded3096f213504ac5e53b23e839d0c5b5322124e3478fe2a0283c1272
6906*947fccc531175cb8fe7cffbe7bc71db8fdceef61b0b71bf99c1b09b76cd9c52baf7a9d267fd987
6907*10eb8008af69efe840636303c6c60f07d69f5c3c897dbb7741ed82ee8822c0ca57b623ba5d8ca1
6908*72ac8ff27553e5d33d7d078d507aa974fabd453264cc4f209bbe8ed42fc27d4b5da3a28f7efcef
6909*70ce5967a1bfab1d078e4e2e832aa4534e3fa37cea58e9b4409d6083d47b2e9250dc81aa57d775
6910*31d0df8fd183070360f23c0ffb9ed885e2e2a262e488a2232c02da1347213dda20c443c92ba2b8
6911*747704d98e632e7cf00b79d339fa303312d2b39b6b1b0999102d2ccb1f77aca1cfdfcccc0c3efc
6912*b18f615d6f17f2aecb0c9dcb37e09c73cfabcb9190894f796d5f1f666767313d351df07260df1e
6913*cc05ee919f58945342ea81455b47dfccea247aa91878ec9585d02a80108229e24ec72a3e96fbd2
6914*1fc0ac37d6e9d82feef9251edaf130d676b7319b03c0854f7f86513fb58690a8b7c6a53b831e3a
6915*7428907166661a93c7b957aeab8832e92eb199fe2e2c791e164bbe49740b214ddeea08428437d6
6916*a0f9737ca052dc020108c98e843efbb9cf07e6b26875b69e72aab41fde41912508450f587698ae
6917*ee6e1c9f98c462b118587f6834c923d3986a731ca7c3bf224feb10ff3606d713108f722b976a2b
6918*b30afb89a96dca063d5b6cba74fdf0d2d377c91a2a929eb08b0ce89e7befc5c33b76a07fdd1046
6919*8ed0ef9dd5ded583b6b6364c4fb12f804ca517397d95288c801cc74157672776efd913587f6272
6920*027333f4bb3168503b80ea87c2ff694ee5af0129a1545aacdebd53e7df81fa1d0905471b69f889
6921*1a5c5ef48e84f47aa3fef5ab548b8c8488196731a38dcfde7c33067bbbd19063fcf8c37170d1b3
6922*2e4d612444f7c73e75e184d1e1d597bbb3a3037373b338b9b0505d473c0fe36646571505ae3b61
6923*5cd6505ebceafc54f8562e7c651942f1fe2c8444bc850facb88b29c38eb438143dc8054ec7eebe
6924*e797d8b173077ada9ba9294080739e72bea2b7187f358490c0015a554f77378e1d0bce534d1e3f
6925*2249cce10000200049444154267c7b6345052617297f4eca23aa62713170cfa99502a15aff8875
6926*a54148d6596c7d8d1092fd84fff5739fc79aceb6a04f5fa3a18d5b561c8462c32e2d85a602dc5c
6927*0e53d3d3017f13fc5bc4e8506084559dc32264e97ee89e87528950b6cdd001c311bf6fd3f7c34a
6928*1f75921d6fe1153571c6485a0602db512a5e7d49eefec53dd8f3c4e3c83574a0588a666f283461
6929*cbf030763fb18b1746a337333da012b5a5a515d333d381d65eb184d9e9696dbe180a9e1212af84
6930*52b13c3f4598f353fa3aae5e4642d1d1d0ea1a0925f1c31e6d44af7437f2092b792b2fdffcd677
6931*d0c1bac91f80d39e7c6672af064742220bb725c34b4b73136667660355a7a64e54cfc00c2a082c
6932*d9f9299a562284f85f849bf1569f1062639bd78304949506bc8593c479fcc9cf7e86ee8ed648fd
6933*8a7a7afb18fe04212471ac99865079e106aaaaa5a505b3a18b42a74e449ffb6840c15342562d7e
6934*df1aa72a5f84f7365bde6aee2ec68f70334d0a80ca4128915a569d5ec7c6c670747c148e5300ed
6935*b66c43434352d0094b5bcb1476a44a8ac6860610029cf4fde4861082e913fcc7a7695270d2dd8e
6936*84e4bd85ff4aa5753b0f513f494742897cf2bc3924109c441a70c2e8f22a3002fae94f7f869642
6937*03b57977cf9af84d16f6cadde9434b928d56f55a7ed5dcd28cd99999809f85b939a57bd9298875
6938*598301651942117ff50921a401a1883f716fd29bade255f3e9d84feefa29bada9aa85e9b5b5a57
6939*0484e2bd2dfb6bc837e064e84ea2c5a2b61f38c749ec949029c27b6b102ea222cc37e9bb8b2414
6940*77908a574e5f29858895c4ed65129c7631430ad6dbf1dbdf02a522b55d4363c188b7b0e43210ea
6941*4b5d725d37f2a4668d776488130558948d5cfe836421c44fb8da21241fb812df01121ffcda5afb
6942*5f12821d3b1e42d7d02991723727ff379e9242d95b5af2a7745c17a5c5934b63f972e1628d80e5
6943*66fe742ce2cf80bbc8d899774a115ce486e33afcc50ce3c54f56f4f8943c1d13899fac4f7da582
6944*a73cb4403fbdebae60b8ca1b3778adb59cd7d01a456faaa2edbdd57fa163ceef25e738c11b7022
6945*d553c2c0a4bbfa9f0b9a08f34dfa7f242209c51da4e295d357ca617429a5d331f1e391505f9a12
6946*01b0eb89dd701d2c7d53e8c30d011a1a1b7d733a24da3845d1ff2c310b95e4baeed26f8897555c
6947*2c326a6b97e41c56962014496a21241ed44c0f48458d4c6155461adaec30259782e0f0e171e45d
6948*072797be090b7eb4c4587f467285d7d4e0a0735db73cc20a98a8c9d18f7cb8e3cb7f45fc9737a4
6949*200b21c5a0198010ab654c1082f2550e493a461642328d8f1d3d869ceb8220f8d5bdeb38916fcc
6950*14b2a3d6108a8a505e95e5380e4a9e17e0413e4fbfecc38002f7f3a1de0f4b8b2c841483d62f84
6951*a4a21150bf3434092119953c0fae130d98cf3928140a585858a86b08c9f82b168b68c8e731e75b
6952*976f480d58811f2bcacd61d50d84d4b3ae3a08250b249f31744c730f7aca5bd3f2a7f38a4584a9
6953*9a775d9c3c7932bdb38f88f44048460b2717d0582800be9b17e6f37aa7bf390a8eb0a810aacc2e
6954*723e140b210b212d7fb26a724d93585f2d164b8013bcf2c70131708577fa101217c1c2c202dada
6955*da026672350316a5b3886fd12e0b21f5965986500d0eace8ec2bfbad8a4a0420fe011601bc92e8
6956*d7f9d98610b38452343fbf809e9e9e4059cd46585aface4248bde5aa829013084bbbeedd348464
6957*441c3792f7e4c23c0052f71092d1c9930b2834066fb9d358685abadcc1f8ef09c3232c862243ac
6958*0c41083002a29509a150ed1a8f84c2e96b37175475c07ee7441f2a353b3595c6592ccd4db024c5
6959*6ef33c8252a9847c3e8762b17cfd95e300ad6ded69dc622634e94edbf2c015d3946203ae96f36a
6960*0ea9aba585102b7bd480a01f73b639ee0493160a4d91dbcb1000c78e1e49628c6ba036ec164b3a
6961*3f3f879696564c4e2e03aab53d1560d14758e1bf82dae6b02c84d46ad72984cc29398464b461d3
6962*266af883074729b5eb134280b8bf89c913e8eaea0c02abad0d8ee3981e256b98c3b21052ab6d21
6963*14485dbecc8931395d63400eae1f8a58701d078ffefe1100a40620d20f21194d4d9dc0e0ba01e4
6964*f3f9a5d34202d775d1dcd252be57963931e6b0fc5b2979ff20962c84cc28ab10a21988cc59f95f
6965*38698f40f8c9fc5e86366c8c94b716f23874684ca3e7da4288928559e2790427a64ea0b3b30347
6966*8f1ead566defe84c1958923da10d611642acecd1355986908cb7d02d91f56d96fe53b2f5435160
6967*75b535616c7c5cd94ba4668d2124539500387e7c120303fd3872e468757d5b47278e1f3da2f473
6968*254145af74a7fd15149fc3b21012c81e5db35220644cb59d17ea5dbb36e2c4599cc7f8f818b75d
6969*bd414846333333c8e7f2d59f2601e5df19f6f4adc5a1032392d184253287e54356962124df38b1
6970*2c84d455b9ee8a04de97df656d4ea8bbb73f50a3c17570dfbdbf34e8b33610924d3831711c7d7d
6971*bd18195906545b7b3b9a9a9b313f37476b9c540160b9ac1bad25fd9a90482c9135a23736d3f4e9
6972*443d89ddd8ccd48dd678ee22ff02fe421fa3716f517f01af91bea38ce67dbb9c492f516fec5d6c
6973*70fd06905c43206c7b73237e7ef72f8cf9e116c51c026a1fb39ab723478ea2ada515ad2dad81f5
6974*6bfad6d29224d5342124f0681ea9ebebe53b44b9716245d3115e61ca229c77b415698b6d40e708
6975*432c9478425dde9efbc217455277b735e19e5ffe52ca0fafaa998f588f375ed562a98483636358
6976*b76e1d1edfb5ab7a494353730bda3a3a743ffaebd1f00ae6650d74625b08f14528af582bd2563a
6977*10e28afa0c0a7ef25acc093df9eca7045739c0b1f183983c31590308494436c3ab48cb8989e3e8
6978*eeea44ef9a1e1c3eb27c21eddafe012c2e9cc4c2c2bc72f490fe105ee17bcc176bfcc9198b6a10
6979*65b0ce38dd89f1634c5467547fccd33163fed8c3789e37da0c80693fd45165dcee96c89bda298f
6980*ebb8e8ece90b4469cabbf8f9cf7faef1744cc25f5c52fa66487c12927db5a4d18307d1dbdb8b86
6981*7c43b5c8715c0cac5f8f5c2e27dd1b0c458095a7b84f2c7a87702ba42c4279c55a91b6d806b27c
6982*d5b48837022761f74ab4e654e545b9f8d23f89f8ec6c2908ce5f09fad3b319c65a8a345d985fc0
6983*b163c731383888bd7bf7569be5f20de81f1cc2c1917d3aae80a78db0c444f91bce1c6da43132e3
6984*39b42321753f516f6c3fcade027d2df1d79d5714b3cb897ec4cf78d6a5913aedcd8d78f0370fc6
6985*788b492ab619315ed55baa36e5551b3f3c8e7c431eeb0607e07f7e545373137afbfb29bd2bade8
6986*082be88ff84ca6784f77a608e5156b45da621ba8f7915072d1933881220202876bdb9c5576e48d
6987*5bb607de37e41c3cfac8c3b14f8951f79a602b159bf29b8907f53c82bd7bf6607878186bfb8a18
6988*3f7cb85ad6dede09d771313e760844fd1634d14977ea1e2cf2a748591642e2124f5a4b10c955a3
6989*fd89d42189488caa83431be03404effbd4d7d98caffdef2f147c1aa18962334dbd4c09b3b858c4
6990*9e3d7b31bc650b16178b387efc78b5acb5ad1deb1b1a7168f480ca730c0f104222bff9d174db40
6991*0b2171ad3c0889da24a1ffc73fb83e398464aa3dff452f09bc771c6073ff1afcf4673f97f7239a
6992*54b9993908c96a6161017bf6edc3e64d9bb0582c626a6aaa1ab8a1d088c18d1b317e7054f6c2d2
6993*c8e920c00556709845c245359585504c162dd54c58a5cfcb243390cce772ebf39e7651a0a4b3b9
6994*010f3ffc1076efd9ad2d69bd40881e861d78767606fb47f663c3d010460f8e626262f936343937
6995*8781c1214c1c3f86c9e3c744a79ae8c0aadeecd517c33fd99a9e2c8462b268a966c6aa68d4e098
6996*8a709aea8290a8ce3ee73c3434b506d66de8ebc2473ef38f09fbb47e212419085327a670e0c028
6997*d6af5f8fd6e6568c1e3c588593e338e8ee59838e8e4e1c3f7654e4c67f8c1196d1832de6a34c1d
6998*44750aa198aab585503203129f88d104575cf5bac0fb42de05595cc01ddffbbebe833cb530e99e
6999*bafa35393989f9f9796cdcb0015bb70e63dfbe7d813b39b8f91cd6ac5d8b8eae2e1c3b7a0473ec
7000*5bd3c89e12b264211493454bd57a82908a5782e80dfc8c26e4346b6e69c1ba4d5b03e5ebbadbf0
7001*b55b6f959f2c5e81108a0b1c4e31bf308fc79fd88575ebd661ebd6ad38303a1ab8532900343436
7002*a27f60108b274f6276661ab33333e12be479c00aa724bec542c8424846ec960eef2a0663c7637c
7003*8d575dfd7a10dfe9aaeb0083bd1df8afaf7d2d91b77827f50321d9105ec9c381910398e99ac1d0
7004*e07a747574627c7c1c73f3c19fed343436a2b3b1079ddd3d28158b989d9dc1eccccc625f7f7f7e
7005*cbb6edcdbb1f7f2c30539fa74d8099bb5851b0a68550adabea69c96a5659efb0eba4392f74e133
7006*9f1d58ddd3d684bbeeba0b07470f8a86e0ae91f1a25f1a2094a0f1c4c404e6e6e630d0df8faddb
7007*8631353585f1f1c398a37c6398cbe7d0ded181f68e8e06023c4a006cdeb67d02c0288083000e25
7008*bcacc142484bc274aaea6999b47348f0a5a6c3891a5f44e73ff569c8175a02eb36f577e3931ffe
7009*6a726f2b1442b2411616e6b177df5e149a9ad0d7d787e1e1614c4d4f97475cac4b1d96c3762d2d
7010*a703d4392c125a38562d846a5d554f4b6393e6228105ea186159f9dd4b5f7575606d6b218723e3
7011*8770f72f7e414fbc2a20241e4826ddfcdc3cf6efdb8ff142017dbdbdd8ba6518274f9ec4d4f434
7012*a6a7a731333313fb60d67ce463244ba8d27e4a18c8a2a5aa85909184c64fc7fc678449c3d12024
7013*aab6f6760c6cd81268b5bea7035fbcf93320e187122674b75221a4e26d61610123070e606c7c1c
7014*5d9d9d686d6b454ff7101cc7c1ecec2ca6a7a731373fbf74016a50797d50b21032de32c310120d
7015*23029824109269f6eaabdf10986c2fe45d74b71570dbff7c4328b08550b2408b8b2771f8c8611c
7016*3e72188ee3a0b9b9196d6d6d686d6bc3dafe7eecd8b933d226660e2bcb1092889c650825689a15
7017*088987895e384a641368dc992eb8e892c0fb2dfd5df8f677be53be519f8a2c84d45a1180108299
7018*9959cccccc0200ce3ce30c6aa3c829e1727862216421241126fdbe92091a4ef1cc4b2e45aeb1b9
7019*fabea5318781359df8f72f7fc590b7d5072175b1e7d00357ba13df0bc2e79582017dd524ab6a6b
7020*6921a41c885ae4d0f749e1a0caee08f0b257bd2eb06adbe01adc71e79d78e491df29fbd15233cb
7021*10520f9ba461406a8faa97316021a4ae3a829088e8975de98390a8366f194657ef40b54947731e
7022*9dad4df8d4fff907a9401642fa87c6c4f77f424991a7570f34e1471634a0267313aef2cdb20ca1
7023*0481533c1d23e115fcfbf7193bd05fffd6ebfc16b07d7d1fbef6f55bb17bf76e0b218310924e41
7024*2156700e2b8c3746600ba1b4c2641f4252a23daa3e8503cb5fb3b5b50d9bb69f5e5dd7dd5a40de
7025*21f8a74fffb3c6cb780c9d2bac040809578e99c3a28dad0c0c8e53686621a41a34cde381807603
7026*3ff3a7637f7ecdb5a85ccae038c0f6a15e7cf1965b303e7698d258cd9b54cb5505a16422843b87
7027*2587ac403315332682a616c642483410afefd2383ece7fc625d5d76b3b9a313f378bcfde7cb374
7028*c07a8150246a0621c45a4f2be35cd6c07e2b6c2641a9a62409c25808e908145753fd6092cff8e2
7029*97bd02ced223e8730eb06d7d2ffeee139fa85e516d21a447eaa7d6e1869c53c2483dc6002bde8b
7030*860e5915100a06aefdf1606802203cd7a01c4c6d43fdad9e7ff9cbabafd775b7627c6c0cfff1e5
7031*ff5438c02c84245aeab451fe9690c6b520af2c845402d7fe334e01421272141e552fdd825178ce
7032*53ce47535b170020ef3a181eecc5bbdff35edfdd302d84245aeab4c14e41826f01ff3ddda9b505
7033*8c19619985908e403a8e8fe6e6166c3ff5546cdd760a86366e42cf9a3e2c2e9ec4dcdc2c666766
7034*303b338d2387c7f1ebfbefc5d8a143b259c5bc6ae8f4e7bce032b8a4847c430336ac69c7a38f3d
7035*866ffcefff2a05b710d293226eca88c626fa6f092578c54e17bf46319026691a3f1a9e7749944e
7036*d2db868d9b70c5ab5f8b0d9bb7a0bda30b0d4dcd70dc5c340c25ee2bdef07638c4c3ecd42446f7
7037*efc137befe55ec78f837f116533a1dfbd447ff16777ef73b38edb46d0080d7bfff3d815b995808
7038*e949617ade5aea211416426682d4e6f828072a140a78e92b5e8d675efa5cb475ade1a78b99d324
7039*8e8be68e6e6c7d7237defda173515c98c5afeef9196ef9d77f09dfaf9b97459bfc519f7ee18538
7040*edb4d30000f7de771f7ef8a31fa7ee2790c142484994392c12f827ac550121f140b58490889e7e
7041*d1c578d1cb5e81f59bb7018ecb8f14032a96f285165c78c973f1d48b9e8d7ffd878f316ac96dbc
7042*c8688fa69e9e1e7cf14b5f0200dc7adb7f4be71551b20b4fd305519620444de93bc3f373883187
7043*c508a24d1642a603b15abde9add7e1a24b9f8b5c63537c3b455085e5e41bf1f6f77c10289d64ce
7044*98aa251177f3dddb6fc7776fbf5db87e3543367650a114998750a04864068b3687c56a97706ea1
7045*f623ded503219190679d732eaeffcbbf46535ba75873a9d382781100c83546d7abd34add8b8590
7046*ba12404887a46ee06721a43f50120889442e140a78f7fb3f8853cfb9402c856650c54b4f060ba1
7047*04aa3184b80aa5a79e120acd605908a9b54af1c0fa93e73e1fafb9e63ae41a0af191520755bc2c
7048*8412a88e20442d5cfa6d4eb86a9e56b73ae195c290cac074859140b1ad6a3ffcacaaa7b70feffb
7049*e04d18d8301c9f4dd33c95aae4a094250889d5d0aaba8550b0828853167e242e6bb010d2183979
7050*44468aa73eed8f70fd5f7d008e9b936e5f9b5dde42885db4b220945884f9e3e78a050543358650
7051*6ccb3a8490606b3cef852fc26bde7c43e03730d9055542499dc25a08854c0815a605217659b490
7052*797b9900ae2c84d423a674ca73d5ebde88e7bfecd5523e3270e8046521a4ae7a8190b0037aaf4a
7053*dfad41dd8064cb0c4128123563f32e2f7fd5d55558d51ba8887ac76a48ce2bca402fad3808c9e0
7054*8a2ec6650deab3ee16427a243a197dd1b32ec1e5af7c5ddd816a599a5dc584ab3988ea0642e22e
7055*8c79a51ca09c392c463ba942230dc5a3d62984282da96bb73fe954bcf52fde0fe2bf6f4bdd804a
7056*421642e2aa4308b1eb09ce61556e4f6a6f6c2690a146c02e140af89b9bfe01a87c1b58afa06298
7057*ac1708c556d5250da3869a434832a4f81c16db8d365908254bf1be0f7e0cb94253fd820a9587e8
7058*a4e8d642485c86209454fc47d54ba6b010d293829f91e0e9175d8c6d679c2bd92e7b725dea2d48
7059*e5b402212457534222139d4943ea0ac438c58bf92da18590b2242124aa7c3e8f6bffe2fd8126f5
7060*06aa8a72f47b265b08c9a89e20a421729e76d0b3e7b02c84c4339af1f3ba37bea5fadbc07a0555
7061*456ef5c1aa1642fca02b1b42ac88f4392c8da7844c0389c2a40ba25a404826e5c57ffac2ba0755
7062*450e29e99dc35a6510d216254508a9355e6e1d7b4a281043a57592c6d229b20ea16011bbf0052f
7063*7a09728dcd060cd546dec9d9f84a16429a82641742ccf2b839ac4a3961d7e76590a9aca6150821
7064*19bdf88a57c757aa234d1c1e4b7460ad3a08450299d9cfd56765cc1f77d4392c9f03e306563b84
7065*4495cfe7d1deddbb624e071b720e76fce1774865feca4228d3100aa4f3fd5f610e2b6156a12829
7066*1f82198210c50053cfbaf43920d070194046d4d9dc88bbefb927c18114bb424f582d812c84f815
7067*38dffc85243e876521a4a6f84f4ba8ea45975caac34d6634d4d781fbef7f20b8d242a86e2004c4
7068*4d2f9af153bdd2dd1f9e7e59838590cf8070055d4e376ed9ae2952edd556c863d7638f62766646
7069*3986859019d5024232e23ce62be169212b24b3a8f69d21fa8d542a4e43499a5a5ad3c89a8a867a
7070*3bf0f95bff23b2de42c88cb20e21bf027358894e09e332508b32d0191986904421dc7c1e5e06ba
7071*33a99a1b5c74b634e22b5ffdaa5c9f5b080553720bb3b5a3043f3a516ff47a746011df422dcef2
7072*2959962024ee22ae1621f53fe1ee38c0e99bfaf1f5db6ec5e4e4a4b1392b2d112d84b852835072
7073*f9e6b002b3581078d0975ead4208892a979378564886b5bebb156dcd057ce10bb724dac92d84cc
7074*a856101253d90fff51f57a72c416ae4608f10307b3948a8bc83940296bfb90849a1b5c6c1d5a8b
7075*2f7ff92b78ecb1c722e5164266540f10aabe23c1d77ae6b024e6616a0fa2ec4248468ee3209773
7076*502a666d87135343cec1d95bd763f4c0017ce8231f51bb2ec7b02c8474890d211dca537284e6b0
7077*2c84d881f564118992775d9c44494bbe3495731c9cb565004d8d79bcf35defc2f4d4546ab92d84
7078*74c92c846494a73d0e9ac0c01c960608c9d5949081c95f9d5fd11342d09403047e2a9c29390e70
7079*fac65eb4b734e10bb77c1177ddf5d3c431eb164240c6fc6507424b0e28efcbebfcde98cf2554ca
7080*215ea85853421987906c646f7e1a40fddca921e700a76de8c59ace36fce0073fc0df7ee003ccba
7081*1642ba947508f94a62bcb16ebe109dc322a145d0807a4d0919fa1a5c4b1443de2a3a78601f3a36
7082*9c8ac53a98796fc839386bf300da5b9bf0eb071fc49bdef256144bbed3d9da1f49015908c9481d
7083*423ae4067e8743c2b40a2efc520ee7788a3426ec45228b16af86bca9f8fcf14f7e82c19e36e1b8
7084*b55273838bf3b60fa1bdb509bb763d81ab5e7335e66667437d655ed18f8e3097e86799ae3b9eb7
7085*74ac718ef9183f697b732d846a07216a4486973beeb8139bfa7bd0d4e00afb48530e80755d2d78
7086*eaa99bd0d498c7030f3c80175e76198e1e3daa2d878590ba9f2c4328b2f80c84fd28ddad81b142
7087*d9bafe40667a5a39aa864f7e7c7c1cf7dd7f3f4e39ed0cecd8336e680bd554c8bb386d431fbada
7088*5b0000dff9ce7771eddbdf8e858585d8b6d1dd2a4b5b06f81dd6de1adb40edbd01dc238408d58a
7089*8de652471b21cad564241409c4f93310f1272e65af717e34ff9922006efad8c7d1d3d18a27adef
7090*01eb8133692ae73a18ea69c385a76e44577b0b3ccfc33f7ffad378e39bdf8cf985858c8e8400ff
7091*a7ca1f099114ac657924c4f7171c09f1fda91d9d512f428faaafd8d6a2153e12924ec92d0c96de
7092*7befbdf8fad76fc595575e01c775f187912335f94174ce7530d0d582e1c15ee4dcf229eaeeddbb
7093*71dd0d37e2befbee4bdf10807a19090119f7a730124a2656960a14836be98faa87240d2d848229
7094*b985c9fc7cf0c31fc6d39e7621366edc88b6a646fc6edf38a6178a89628ac801d0d298436f670b
7095*36aeed412e57061521045ff8c22df8c84d37616e6e4e73560b2171d50384244c30468eec39ac00
7096*b12c844c424846478e1cc18b2f7f09fee7bf6fc3962d5b70fe933660f4f004f61e9ec442d1d39a
7097*cb0fa9f5bd5d686c58de5d0821b8fd8e3bf0c94ffd3d1e79e41189a81642e25a611012afc6ac97
7098*a77e2aa4f21ff9aeb0103223bf9b91030770d98b2fc77ffde75770e6996762b0af0b837d5d383a
7099*398d91c393989c3ba97caac8835445dffbfef7f1894f7e0a3b76ec88b8cb46b7b14dd4de9f1884
7100*626a6a94b89f84d52423d1f923f42da185901905cfa4e5bc8d8d8de139cf7d1edef4c637e2afde
7101*fb1eb4b5b5614d6779210026a766716c6a16276617b0582aa1e41194bcf2c4683ee7a021e7a231
7102*9f43a12187a6c606343736a0b9a9012d8546b86ef4d289871f7e18dffcd6b7f1cd6f7d0b7bf6ec
7103*49b2d98ab21012575621245995764a283d876521c4551208c9aa542ae17337df8c6f7eeb5bb8ee
7104*ed6fc395575e89eeae2e3800bada5baa9719a8ca3ca42c84c4951c429255f544514cc8e28fb376
7105*dd2009ef1d679e792676eedc09dafd6874c942489796fd140a055cf66797e125975f8ef32f381f
7106*3dddddc25116171771686c0cfbf7edc30f7ff4e30490b21012d7ea8390a8ce39e76cfce6370f45
7107*5ad3e7b0146521a44b413fa2f6e6e71770eb6db7e1d6db6e03006cdbb60d4f39f75cf4f7f7a3bb
7108*bb0b5d9d5d28341530363686d1d183181d1dc5e8c1f2ff0f1f3eccf903652124ae2c4148225282
7109*84ea4de35a9248b5d8392c0b215d528350123dfef8e378fcf1c785fc044a6ade7516425a226512
7110*42825519e784d139acc095ab35df732d84a46421242e0b21232d3574086f0e5deb29a1a8a890cc
7111*8c2c84c46521a425d22a80507c18da5784d116c99f4bc83253fba3c9270b2171590869896421a4
7112*9e8433c4e202cb424846750aa150b1859099a416426acdc2cad3e6a902bfa44f551642e2b210d2
7113*16c942c8445065f19e29a1ed9430ce42f555860ff4da7b03440f7c0b2133492d84d49ac587d5e3
7114*2701b02c84c4652154cba416426acde2c31af423368745aaffaffd650d1642e2b21032d2d24248
7115*a6610a228c392c8309b9a53507519d4048c2848590b6241261b275d0671d42f477c6e6b02c84c4
7116*652164a4a585904cc314c482904433b153c2706b7aaa7a81504c4d8d4a7e4ab63a2024d13acb10
7117*4ad8941d72054248ba325fd57bbafb2194fa3301ca59858b2c84f427b5104ad6941dd242482d28
7118*7dc09437ffc40db1220b21fd492d8492356587b4103211948baba5370a73582b0f429255f544b1
7119*1032155831a4859089a0dc9a7161d87358e112b2bc5808194f682194ac293ba4859089a0892024
7120*25ca292148f4633533879525084944aac931986e87c487c8ce81af0ca172e3144428afa49a6954
7121*d620943c10f59eee5a0c5808a9b7b41092699c822c844c070acf57b1064c94392c125ad4bc5908
7122*694b2211c64248258985909940b449f3a4623f973019af626421241ec642482589859099409128
7123*5ac2d2825000045d8faa672655afa6b7a985103ba4859089a016429abc85c2c45cd660fe40b710
7124*526b161f365b7e58496a0f21f1c016427a02897c99c799c3e28cb12c844c0455968590fec01642
7125*7a02e9bba280f8fe4f3925143d7db410526b161f365b7e78498453661942e261d20892690801ba
7126*40a46f87e05ed6c02eb310e287cd961f5e120b21ed412c847484659f12d20cc44cbb5b08c9344c
7127*41164286835808690a2b9a81306a2fcf6149f04ad09b74a9ee66f161b3e58797a45e20145b3343
7128*07faea80106064a7509e3292435658f4392ca914d93ae82d846464219420b25a948cf9e185d40f
7129*a1045a4ac1b80e8bfd981dd1c03ab52221245d597fd07a8150244ad6bc65cc0f2f641621c42a93
7130*b8ac4122b0a22c84cc04b510b210126e68423129c4cfd6e87352fc5342c9edb3103213d442c842
7131*48aab10971521899326228f6067e164266825a085908493536a18c4048264d9e0624525d12f5b8
7132*a22c841264518b92356f19f3c30b69212421891454fea032c20aaf27be459b08e59554338db210
7133*528caa1e25637e78212d84242405a1e47e842e6be05988be926aa65116428a51d5a364cc0f2fa4
7134*8590845286908cb837f093326321a41e454bd85500a150580b2109651842d4748133bce50adc39
7135*2c0ba104512c8494c25a0849a8deb1cda45200000204494441542024586179b8449bc38aa59ba8
7136*c41a5808e9096421944016426a4a08211dd272033f0b213d812c8412c842485d8a07b0d11330c6
7137*a08979033fee0c56c60e2c0b213d612d84246421242ea9a07c62057f4bb8f48610667d8984c994
7138*650801ba406421249edd42a8fe21245e93851fc1cb1a2c8424a2e808c20d6b2124210b21711986
7139*506291b87bba0ba45a1d10028cec2284fb56bca109294348ac466209a658f510920a5c1b0831d7
7140*530e5ee6a47b0057193be82d84146521a42e0b217129f69548bd3cf5e0979ac3b210126e684216
7141*42eab2101297160889a760cf61319b2acfba2f47b11052534c8a7a9917b2109209bcf22124dd90
7142*845788dec0cfdfcc42484d1642eab2101257d621241c893e60ca03510855e6bbe4e1942508c9d7
7143*5692f22959762054ae9a3288ea1642e2952d84f48b3e8765da808590ba2c84c46521a4a9a1f9fd
7144*8835951e16e79450720ecb42485d2b1042f1a58ab210d2d4b0361012abbcf446e6b286b3cf3ebb
7145*ae2164a8a111a5eec6424853ba1a4328d2385bfbb509394dcd2deac32813b22321355908694ab7
7146*fa2094782494a2a4bf25549285909a12cc2fd60b84a4c32a07499e65754048ba7562f1d3074b63
7147*9f9a239e45a828b6548b2c84c46521a4a9b18590787a752f41605908a92b4bdf90651942b181b2
7148*34396d21249e3e1d2f9ccb1a2c842c8474a5b41012afbcfa20242a4208fe3fb1ed16c7a87a91db
7149*0000000049454e44ae426082
7150addfile ./contrib/musicplayer/src/resources/images/album_covers.svg
7151hunk ./contrib/musicplayer/src/resources/images/album_covers.svg 1
7152+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
7153+<!-- Created with Inkscape (http://www.inkscape.org/) -->
7154+
7155+<svg
7156+   xmlns:dc="http://purl.org/dc/elements/1.1/"
7157+   xmlns:cc="http://creativecommons.org/ns#"
7158+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
7159+   xmlns:svg="http://www.w3.org/2000/svg"
7160+   xmlns="http://www.w3.org/2000/svg"
7161+   xmlns:xlink="http://www.w3.org/1999/xlink"
7162+   version="1.1"
7163+   width="640.36694"
7164+   height="300"
7165+   id="svg2">
7166+  <defs
7167+     id="defs4">
7168+    <linearGradient
7169+       id="linearGradient4099">
7170+      <stop
7171+         id="stop4101"
7172+         style="stop-color:#bfd1de;stop-opacity:1"
7173+         offset="0" />
7174+      <stop
7175+         id="stop4103"
7176+         style="stop-color:#bfd1de;stop-opacity:0"
7177+         offset="1" />
7178+    </linearGradient>
7179+    <linearGradient
7180+       id="linearGradient4085">
7181+      <stop
7182+         id="stop4087"
7183+         style="stop-color:#000000;stop-opacity:1"
7184+         offset="0" />
7185+      <stop
7186+         id="stop4089"
7187+         style="stop-color:#565f63;stop-opacity:1"
7188+         offset="1" />
7189+    </linearGradient>
7190+    <style
7191+       type="text/css"
7192+       id="style6" />
7193+    <linearGradient
7194+       x1="572.88373"
7195+       y1="345.02939"
7196+       x2="542.60852"
7197+       y2="13.83673"
7198+       id="linearGradient3048"
7199+       xlink:href="#linearGradient4085"
7200+       gradientUnits="userSpaceOnUse"
7201+       gradientTransform="translate(-7.0703125e-5,-1.9335938e-5)" />
7202+    <linearGradient
7203+       x1="572.88373"
7204+       y1="345.02939"
7205+       x2="542.60852"
7206+       y2="13.83673"
7207+       id="linearGradient3050"
7208+       xlink:href="#linearGradient4099"
7209+       gradientUnits="userSpaceOnUse" />
7210+    <linearGradient
7211+       x1="624.71539"
7212+       y1="213.83673"
7213+       x2="573.34576"
7214+       y2="114.75416"
7215+       id="linearGradient3052"
7216+       xlink:href="#linearGradient4099"
7217+       gradientUnits="userSpaceOnUse" />
7218+    <linearGradient
7219+       x1="572.88373"
7220+       y1="345.02939"
7221+       x2="542.60852"
7222+       y2="13.83673"
7223+       id="linearGradient3068"
7224+       xlink:href="#linearGradient4085"
7225+       gradientUnits="userSpaceOnUse"
7226+       gradientTransform="translate(-7.0703125e-5,-1.9335938e-5)" />
7227+    <linearGradient
7228+       x1="572.88373"
7229+       y1="345.02939"
7230+       x2="542.60852"
7231+       y2="13.83673"
7232+       id="linearGradient3070"
7233+       xlink:href="#linearGradient4099"
7234+       gradientUnits="userSpaceOnUse" />
7235+    <linearGradient
7236+       x1="624.71539"
7237+       y1="213.83673"
7238+       x2="573.34576"
7239+       y2="114.75416"
7240+       id="linearGradient3072"
7241+       xlink:href="#linearGradient4099"
7242+       gradientUnits="userSpaceOnUse" />
7243+    <linearGradient
7244+       x1="572.88373"
7245+       y1="345.02939"
7246+       x2="542.60852"
7247+       y2="13.83673"
7248+       id="linearGradient3088"
7249+       xlink:href="#linearGradient4085"
7250+       gradientUnits="userSpaceOnUse"
7251+       gradientTransform="translate(-7.0703125e-5,-1.9335938e-5)" />
7252+    <linearGradient
7253+       x1="572.88373"
7254+       y1="345.02939"
7255+       x2="542.60852"
7256+       y2="13.83673"
7257+       id="linearGradient3090"
7258+       xlink:href="#linearGradient4099"
7259+       gradientUnits="userSpaceOnUse" />
7260+    <linearGradient
7261+       x1="624.71539"
7262+       y1="213.83673"
7263+       x2="573.34576"
7264+       y2="114.75416"
7265+       id="linearGradient3092"
7266+       xlink:href="#linearGradient4099"
7267+       gradientUnits="userSpaceOnUse" />
7268+    <linearGradient
7269+       x1="624.71539"
7270+       y1="213.83673"
7271+       x2="573.34576"
7272+       y2="114.75416"
7273+       id="linearGradient3991"
7274+       xlink:href="#linearGradient4099"
7275+       gradientUnits="userSpaceOnUse"
7276+       gradientTransform="matrix(0.21333333,0,0,0.21333333,4.1490624,144.33833)" />
7277+    <linearGradient
7278+       x1="572.88373"
7279+       y1="345.02939"
7280+       x2="542.60852"
7281+       y2="13.83673"
7282+       id="linearGradient3996"
7283+       xlink:href="#linearGradient4099"
7284+       gradientUnits="userSpaceOnUse"
7285+       gradientTransform="matrix(0.21333333,0,0,0.21333333,4.1490624,144.33833)" />
7286+    <linearGradient
7287+       x1="572.88373"
7288+       y1="345.02939"
7289+       x2="542.60852"
7290+       y2="13.83673"
7291+       id="linearGradient3999"
7292+       xlink:href="#linearGradient4085"
7293+       gradientUnits="userSpaceOnUse"
7294+       gradientTransform="matrix(0.21333333,0,0,0.21333333,4.1490473,144.33833)" />
7295+    <linearGradient
7296+       x1="624.71539"
7297+       y1="213.83673"
7298+       x2="573.34576"
7299+       y2="114.75416"
7300+       id="linearGradient4012"
7301+       xlink:href="#linearGradient4099"
7302+       gradientUnits="userSpaceOnUse"
7303+       gradientTransform="matrix(0.58,0,0,0.58,-57.340102,74.629683)" />
7304+    <linearGradient
7305+       x1="572.88373"
7306+       y1="345.02939"
7307+       x2="542.60852"
7308+       y2="13.83673"
7309+       id="linearGradient4017"
7310+       xlink:href="#linearGradient4099"
7311+       gradientUnits="userSpaceOnUse"
7312+       gradientTransform="matrix(0.58,0,0,0.58,-57.340102,74.629683)" />
7313+    <linearGradient
7314+       x1="572.88373"
7315+       y1="345.02939"
7316+       x2="542.60852"
7317+       y2="13.83673"
7318+       id="linearGradient4020"
7319+       xlink:href="#linearGradient4085"
7320+       gradientUnits="userSpaceOnUse"
7321+       gradientTransform="matrix(0.58,0,0,0.58,-57.340143,74.629672)" />
7322+    <linearGradient
7323+       x1="624.71539"
7324+       y1="213.83673"
7325+       x2="573.34576"
7326+       y2="114.75416"
7327+       id="linearGradient4033"
7328+       xlink:href="#linearGradient4099"
7329+       gradientUnits="userSpaceOnUse"
7330+       gradientTransform="translate(-13.761468,0.23718262)" />
7331+    <linearGradient
7332+       x1="572.88373"
7333+       y1="345.02939"
7334+       x2="542.60852"
7335+       y2="13.83673"
7336+       id="linearGradient4038"
7337+       xlink:href="#linearGradient4099"
7338+       gradientUnits="userSpaceOnUse"
7339+       gradientTransform="translate(-13.761468,0.23718262)" />
7340+    <linearGradient
7341+       x1="572.88373"
7342+       y1="345.02939"
7343+       x2="542.60852"
7344+       y2="13.83673"
7345+       id="linearGradient4041"
7346+       xlink:href="#linearGradient4085"
7347+       gradientUnits="userSpaceOnUse"
7348+       gradientTransform="translate(-13.761539,0.23716328)" />
7349+    <linearGradient
7350+       x1="572.88373"
7351+       y1="345.02939"
7352+       x2="542.60852"
7353+       y2="13.83673"
7354+       id="linearGradient4052"
7355+       xlink:href="#linearGradient4085"
7356+       gradientUnits="userSpaceOnUse"
7357+       gradientTransform="matrix(0.58,0,0,0.58,-57.340143,74.629672)" />
7358+    <linearGradient
7359+       x1="572.88373"
7360+       y1="345.02939"
7361+       x2="542.60852"
7362+       y2="13.83673"
7363+       id="linearGradient4054"
7364+       xlink:href="#linearGradient4099"
7365+       gradientUnits="userSpaceOnUse"
7366+       gradientTransform="matrix(0.58,0,0,0.58,-57.340102,74.629683)" />
7367+    <linearGradient
7368+       x1="624.71539"
7369+       y1="213.83673"
7370+       x2="573.34576"
7371+       y2="114.75416"
7372+       id="linearGradient4056"
7373+       xlink:href="#linearGradient4099"
7374+       gradientUnits="userSpaceOnUse"
7375+       gradientTransform="matrix(0.58,0,0,0.58,-57.340102,74.629683)" />
7376+    <linearGradient
7377+       x1="572.88373"
7378+       y1="345.02939"
7379+       x2="542.60852"
7380+       y2="13.83673"
7381+       id="linearGradient4058"
7382+       xlink:href="#linearGradient4085"
7383+       gradientUnits="userSpaceOnUse"
7384+       gradientTransform="translate(-13.761539,0.23716328)" />
7385+    <linearGradient
7386+       x1="572.88373"
7387+       y1="345.02939"
7388+       x2="542.60852"
7389+       y2="13.83673"
7390+       id="linearGradient4060"
7391+       xlink:href="#linearGradient4099"
7392+       gradientUnits="userSpaceOnUse"
7393+       gradientTransform="translate(-13.761468,0.23718262)" />
7394+    <linearGradient
7395+       x1="624.71539"
7396+       y1="213.83673"
7397+       x2="573.34576"
7398+       y2="114.75416"
7399+       id="linearGradient4062"
7400+       xlink:href="#linearGradient4099"
7401+       gradientUnits="userSpaceOnUse"
7402+       gradientTransform="translate(-13.761468,0.23718262)" />
7403+    <linearGradient
7404+       x1="572.88373"
7405+       y1="345.02939"
7406+       x2="542.60852"
7407+       y2="13.83673"
7408+       id="linearGradient4064"
7409+       xlink:href="#linearGradient4085"
7410+       gradientUnits="userSpaceOnUse"
7411+       gradientTransform="matrix(0.21333333,0,0,0.21333333,4.1490473,144.33833)" />
7412+    <linearGradient
7413+       x1="572.88373"
7414+       y1="345.02939"
7415+       x2="542.60852"
7416+       y2="13.83673"
7417+       id="linearGradient4066"
7418+       xlink:href="#linearGradient4099"
7419+       gradientUnits="userSpaceOnUse"
7420+       gradientTransform="matrix(0.21333333,0,0,0.21333333,4.1490624,144.33833)" />
7421+    <linearGradient
7422+       x1="624.71539"
7423+       y1="213.83673"
7424+       x2="573.34576"
7425+       y2="114.75416"
7426+       id="linearGradient4068"
7427+       xlink:href="#linearGradient4099"
7428+       gradientUnits="userSpaceOnUse"
7429+       gradientTransform="matrix(0.21333333,0,0,0.21333333,4.1490624,144.33833)" />
7430+  </defs>
7431+  <metadata
7432+     id="metadata7">
7433+    <rdf:RDF>
7434+      <cc:Work
7435+         rdf:about="">
7436+        <dc:format>image/svg+xml</dc:format>
7437+        <dc:type
7438+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
7439+        <dc:title></dc:title>
7440+      </cc:Work>
7441+    </rdf:RDF>
7442+  </metadata>
7443+  <g
7444+     transform="translate(-101.30906,-27.362183)"
7445+     id="layer1">
7446+    <path
7447+       d="m 124.50779,-68.462854 2.00559,0 c 0.5014,3.202538 1.12816,5.899417 1.96382,8.090625 0.79389,2.149075 1.7549,4.003175 2.79948,5.562304 1.08637,1.559134 2.71592,3.539652 4.88866,5.983693 2.17273,2.444041 3.88585,4.508835 5.22292,6.236521 4.0112,5.183057 6.0168,10.618949 6.0168,16.265528 0,5.815137 -2.42344,12.894436 -7.35387,21.237889 l -1.33707,0 c 0.62675,-1.474853 1.37885,-3.160396 2.21452,-5.056638 0.79388,-1.938381 1.5042,-3.666066 2.08916,-5.225195 0.54319,-1.559129 1.00281,-3.118263 1.33707,-4.677392 0.29248,-1.559129 0.45962,-3.076125 0.45962,-4.593116 0,-2.401903 -0.45962,-4.803806 -1.42064,-7.205713 -0.96101,-2.444043 -2.33986,-4.677394 -4.05298,-6.742189 -1.7549,-2.064794 -3.71872,-3.708203 -5.93324,-5.0145 -2.21451,-1.264158 -4.5126,-1.980518 -6.89425,-2.106932 l 0,47.700973 c 0,2.69687 -0.79388,5.14092 -2.46521,7.24785 -1.62955,2.10693 -3.7605,3.75034 -6.35107,4.88809 -2.54879,1.13774 -5.09757,1.68554 -7.64635,1.68554 -2.42344,0 -4.47082,-0.63208 -6.10037,-1.8541 -1.58777,-1.22202 -2.42344,-3.03398 -2.42344,-5.39375 0,-2.48618 0.83567,-4.80381 2.46522,-6.95288 1.62955,-2.10692996 3.71872,-3.79248 6.22572,-5.0145 2.507,-1.222021 4.93043,-1.8541 7.2703,-1.8541 3.2591,0 5.59897,0.632079 7.01961,1.93838 l 0,-43.065725 0,-22.080663 z"
7448+       id="path25"
7449+       style="fill:#131516;stroke:#131516;stroke-width:0.27168813" />
7450+    <g
7451+       transform="translate(0,3e-6)"
7452+       id="g4043">
7453+      <path
7454+         d="m 674.31195,176.21539 a 71.559631,71.559631 0 1 1 -143.11926,0 71.559631,71.559631 0 1 1 143.11926,0 z"
7455+         transform="matrix(2.0512821,0,0,2.0512821,-644.739,-184.10529)"
7456+         id="path3866"
7457+         style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" />
7458+      <path
7459+         d="m 441.67603,27.362183 0,299.999997 300,0 0,-120.625 c -0.13394,0.002 -0.27188,0 -0.40625,0 -16.21386,0 -29.34375,-13.16114 -29.34375,-29.375 0,-16.21386 13.12989,-29.34375 29.34375,-29.34375 0.13437,0 0.27231,-0.002 0.40625,0 l 0,-120.656247 -300,0 z"
7460+         id="rect3759"
7461+         style="fill:url(#linearGradient4058);fill-opacity:1;fill-rule:nonzero;stroke:none" />
7462+      <path
7463+         d="m 441.67603,27.362183 0,141.874997 c 56.98929,-26.12874 171.71101,-69.10948 300,-63.53125 l 0,-78.343747 -300,0 z"
7464+         id="rect4107"
7465+         style="opacity:0.06956524;fill:url(#linearGradient4060);fill-opacity:1;fill-rule:nonzero;stroke:none" />
7466+      <path
7467+         d="m 597.28592,89.247504 4.33176,0 c 1.08294,6.90699 2.43662,12.723406 4.24152,17.449236 1.71466,4.63496 3.79029,8.63374 6.04642,11.99635 2.34637,3.36262 5.86593,7.63405 10.55868,12.90517 4.69274,5.27112 8.39279,9.72431 11.28064,13.45045 8.66353,11.17842 12.99529,22.90213 12.99529,35.08024 0,12.54164 -5.23422,27.80973 -15.88314,45.80425 l -2.88784,0 c 1.35368,-3.18085 2.97809,-6.8161 4.78299,-10.90577 1.71466,-4.18055 3.24882,-7.90669 4.51225,-11.2693 1.17319,-3.36261 2.16589,-6.72523 2.88785,-10.08784 0.63171,-3.36261 0.99269,-6.63435 0.99269,-9.90608 0,-5.18024 -0.99269,-10.36048 -3.06833,-15.54073 -2.07564,-5.27112 -5.05373,-10.08784 -8.75377,-14.54103 -3.7903,-4.45319 -8.03182,-7.99757 -12.81481,-10.81489 -4.78299,-2.72644 -9.74647,-4.27143 -14.89044,-4.54407 l 0,102.8778 c 0,5.81641 -1.71466,11.08753 -5.32446,15.63161 -3.51956,4.54407 -8.12206,8.08845 -13.71726,10.54225 -5.50495,2.45379 -11.0099,3.63525 -16.51485,3.63525 -5.23422,0 -9.65623,-1.36322 -13.17578,-3.99878 -3.42932,-2.63556 -5.23422,-6.54346 -5.23422,-11.63283 0,-5.362 1.8049,-10.36048 5.32446,-14.99543 3.51956,-4.54408 8.03181,-8.17934 13.44652,-10.8149 5.41471,-2.63556 10.64892,-3.99878 15.70265,-3.99878 7.03912,0 12.09284,1.36322 15.16118,4.18055 l 0,-92.88085 0,-47.621876 z"
7468+         id="path4095"
7469+         style="fill:#ffffff;fill-opacity:1;stroke:none" />
7470+      <path
7471+         d="m 597.28592,86.49521 4.33176,0 c 1.08294,6.90699 2.43662,12.72341 4.24152,17.44924 1.71466,4.63496 3.79029,8.63374 6.04642,11.99635 2.34637,3.36262 5.86593,7.63405 10.55868,12.90517 4.69274,5.27112 8.39279,9.72431 11.28064,13.45045 8.66353,11.17842 12.99529,22.90213 12.99529,35.08024 0,12.54164 -5.23422,27.80973 -15.88314,45.80425 l -2.88784,0 c 1.35368,-3.18085 2.97809,-6.8161 4.78299,-10.90577 1.71466,-4.18055 3.24882,-7.90669 4.51225,-11.2693 1.17319,-3.36261 2.16589,-6.72523 2.88785,-10.08784 0.63171,-3.36261 0.99269,-6.63435 0.99269,-9.90608 0,-5.18024 -0.99269,-10.36048 -3.06833,-15.54073 -2.07564,-5.27112 -5.05373,-10.08784 -8.75377,-14.54103 -3.7903,-4.45319 -8.03182,-7.99757 -12.81481,-10.81489 -4.78299,-2.72644 -9.74647,-4.27143 -14.89044,-4.54407 l 0,102.8778 c 0,5.81641 -1.71466,11.08753 -5.32446,15.63161 -3.51956,4.54407 -8.12206,8.08845 -13.71726,10.54225 -5.50495,2.45379 -11.0099,3.63525 -16.51485,3.63525 -5.23422,0 -9.65623,-1.36322 -13.17578,-3.99878 -3.42932,-2.63556 -5.23422,-6.54346 -5.23422,-11.63283 0,-5.362 1.8049,-10.36048 5.32446,-14.99543 3.51956,-4.54408 8.03181,-8.17934 13.44652,-10.8149 5.41471,-2.63556 10.64892,-3.99878 15.70265,-3.99878 7.03912,0 12.09284,1.36322 15.16118,4.18055 l 0,-92.88085 0,-47.62188 z"
7472+         id="path4081"
7473+         style="fill:#dcdfe1;fill-opacity:1;stroke:none" />
7474+      <path
7475+         d="m 597.28592,86.49521 4.33176,0 c 1.08294,6.90699 2.43662,12.72341 4.24152,17.44924 1.71466,4.63496 3.79029,8.63374 6.04642,11.99635 2.34637,3.36262 5.86593,7.63405 10.55868,12.90517 4.69274,5.27112 8.39279,9.72431 11.28064,13.45045 8.66353,11.17842 12.99529,22.90213 12.99529,35.08024 0,12.54164 -5.23422,27.80973 -15.88314,45.80425 l -2.88784,0 c 1.35368,-3.18085 2.97809,-6.8161 4.78299,-10.90577 1.71466,-4.18055 3.24882,-7.90669 4.51225,-11.2693 1.17319,-3.36261 2.16589,-6.72523 2.88785,-10.08784 0.63171,-3.36261 0.99269,-6.63435 0.99269,-9.90608 0,-5.18024 -0.99269,-10.36048 -3.06833,-15.54073 -2.07564,-5.27112 -5.05373,-10.08784 -8.75377,-14.54103 -3.7903,-4.45319 -8.03182,-7.99757 -12.81481,-10.81489 -4.78299,-2.72644 -9.74647,-4.27143 -14.89044,-4.54407 l 0,102.8778 c 0,5.81641 -1.71466,11.08753 -5.32446,15.63161 -3.51956,4.54407 -8.12206,8.08845 -13.71726,10.54225 -5.50495,2.45379 -11.0099,3.63525 -16.51485,3.63525 -5.23422,0 -9.65623,-1.36322 -13.17578,-3.99878 -3.42932,-2.63556 -5.23422,-6.54346 -5.23422,-11.63283 0,-5.362 1.8049,-10.36048 5.32446,-14.99543 3.51956,-4.54408 8.03181,-8.17934 13.44652,-10.8149 5.41471,-2.63556 10.64892,-3.99878 15.70265,-3.99878 7.03912,0 12.09284,1.36322 15.16118,4.18055 l 0,-92.88085 0,-47.62188 z"
7476+         id="path4097"
7477+         style="fill:url(#linearGradient4062);fill-opacity:1;stroke:none" />
7478+      <path
7479+         d="m 446.70728,32.393433 0,0.5 0,288.968747 0,0.5 0.5,0 288.96875,0 0.5,0 0,-0.5 0,-111.77928 -1,0 0,111.27928 -287.96875,0 0,-287.968747 287.96875,0 0,111.165417 1,0 0,-111.665417 0,-0.5 -0.5,0 -288.96875,0 z m 289.6875,110.874997 c -15.05,2.13547 -26.8673,13.87884 -29.15625,28.90625 -2.86367,18.80066 10.35559,36.38633 29.15625,39.25 0.18348,-0.70778 0.13018,-0.88099 -0.34404,-1.32569 -17.97896,-3.02771 -30.59883,-19.67848 -27.84346,-37.76806 2.19676,-14.42223 13.61626,-25.472 27.98681,-27.73939 0.33574,-0.17932 0.27182,-0.50329 0.20069,-1.32311 z"
7480+         id="path40"
7481+         style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;opacity:0.2;color:#000000;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.86486489;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans" />
7482+    </g>
7483+    <g
7484+       transform="translate(5.5045872,63.000003)"
7485+       id="g4022">
7486+      <path
7487+         d="m 329.3578,169.79338 a 48.623852,48.623852 0 1 1 -97.2477,0 48.623852,48.623852 0 1 1 97.2477,0 z"
7488+         transform="matrix(1.7358491,0,0,1.7358491,-193.49812,-117.3735)"
7489+         id="path3864"
7490+         style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" />
7491+      <path
7492+         d="m 206.81365,90.362183 0,173.999997 174,0 0,-69.9625 c -0.0777,0.001 -0.15769,0 -0.23563,0 -9.40404,0 -17.01937,-7.63346 -17.01937,-17.0375 0,-9.40404 7.61533,-17.01937 17.01937,-17.01937 0.0779,0 0.15794,-10e-4 0.23563,0 l 0,-69.980627 -174,0 z"
7493+         id="path3056"
7494+         style="fill:url(#linearGradient4052);fill-opacity:1;fill-rule:nonzero;stroke:none" />
7495+      <path
7496+         d="m 206.81365,90.362183 0,82.287497 c 33.05379,-15.15467 99.59238,-40.0835 174,-36.84812 l 0,-45.439377 -174,0 z"
7497+         id="path3058"
7498+         style="opacity:0.06956524;fill:url(#linearGradient4054);fill-opacity:1;fill-rule:nonzero;stroke:none" />
7499+      <path
7500+         d="m 297.06738,126.25567 2.51242,0 c 0.62811,4.00605 1.41324,7.37958 2.46009,10.12056 0.9945,2.68827 2.19836,5.00757 3.50692,6.95788 1.36089,1.95032 3.40224,4.42775 6.12403,7.485 2.72179,3.05725 4.86782,5.6401 6.54278,7.80126 5.02484,6.48348 7.53726,13.28324 7.53726,20.34654 0,7.27415 -3.03584,16.12964 -9.21222,26.56646 l -1.67494,0 c 0.78513,-1.84489 1.72729,-3.95333 2.77413,-6.32534 0.9945,-2.42472 1.88432,-4.58588 2.6171,-6.5362 0.68045,-1.95031 1.25622,-3.90063 1.67496,-5.85094 0.36639,-1.95032 0.57576,-3.84793 0.57576,-5.74553 0,-3.00454 -0.57576,-6.00908 -1.77963,-9.01362 -1.20387,-3.05725 -2.93117,-5.85095 -5.07719,-8.4338 -2.19837,-2.58285 -4.65846,-4.63859 -7.43259,-6.27264 -2.77413,-1.58133 -5.65295,-2.47743 -8.63646,-2.63556 l 0,59.66913 c 0,3.37351 -0.9945,6.43076 -3.08818,9.06633 -2.04135,2.63556 -4.7108,4.6913 -7.95601,6.11451 -3.19287,1.42319 -6.38574,2.10844 -9.57862,2.10844 -3.03584,0 -5.60061,-0.79067 -7.64195,-2.31929 -1.989,-1.52863 -3.03585,-3.79521 -3.03585,-6.74704 0,-3.10996 1.04685,-6.00908 3.08819,-8.69735 2.04135,-2.63557 4.65845,-4.74402 7.79898,-6.27264 3.14053,-1.52863 6.17638,-2.3193 9.10754,-2.3193 4.08269,0 7.01385,0.79067 8.79348,2.42472 l 0,-53.87089 0,-27.62069 z"
7501+         id="path3060"
7502+         style="fill:#ffffff;fill-opacity:1;stroke:none" />
7503+      <path
7504+         d="m 297.06738,124.65934 2.51242,0 c 0.62811,4.00605 1.41324,7.37958 2.46009,10.12056 0.9945,2.68828 2.19836,5.00757 3.50692,6.95788 1.36089,1.95032 3.40224,4.42775 6.12403,7.485 2.72179,3.05725 4.86782,5.6401 6.54278,7.80126 5.02484,6.48349 7.53726,13.28324 7.53726,20.34654 0,7.27415 -3.03584,16.12964 -9.21222,26.56647 l -1.67494,0 c 0.78513,-1.8449 1.72729,-3.95334 2.77413,-6.32535 0.9945,-2.42472 1.88432,-4.58588 2.6171,-6.53619 0.68045,-1.95032 1.25622,-3.90064 1.67496,-5.85095 0.36639,-1.95032 0.57576,-3.84792 0.57576,-5.74553 0,-3.00454 -0.57576,-6.00908 -1.77963,-9.01362 -1.20387,-3.05725 -2.93117,-5.85095 -5.07719,-8.4338 -2.19837,-2.58285 -4.65846,-4.63859 -7.43259,-6.27263 -2.77413,-1.58134 -5.65295,-2.47743 -8.63646,-2.63557 l 0,59.66913 c 0,3.37352 -0.9945,6.43077 -3.08818,9.06633 -2.04135,2.63556 -4.7108,4.6913 -7.95601,6.11451 -3.19287,1.4232 -6.38574,2.10844 -9.57862,2.10844 -3.03584,0 -5.60061,-0.79067 -7.64195,-2.31929 -1.989,-1.52862 -3.03585,-3.79521 -3.03585,-6.74704 0,-3.10996 1.04685,-6.00908 3.08819,-8.69735 2.04135,-2.63557 4.65845,-4.74402 7.79898,-6.27264 3.14053,-1.52863 6.17638,-2.3193 9.10754,-2.3193 4.08269,0 7.01385,0.79067 8.79348,2.42472 l 0,-53.87089 0,-27.62069 z"
7505+         id="path3062"
7506+         style="fill:#dcdfe1;fill-opacity:1;stroke:none" />
7507+      <path
7508+         d="m 297.06738,124.65934 2.51242,0 c 0.62811,4.00605 1.41324,7.37958 2.46009,10.12056 0.9945,2.68828 2.19836,5.00757 3.50692,6.95788 1.36089,1.95032 3.40224,4.42775 6.12403,7.485 2.72179,3.05725 4.86782,5.6401 6.54278,7.80126 5.02484,6.48349 7.53726,13.28324 7.53726,20.34654 0,7.27415 -3.03584,16.12964 -9.21222,26.56647 l -1.67494,0 c 0.78513,-1.8449 1.72729,-3.95334 2.77413,-6.32535 0.9945,-2.42472 1.88432,-4.58588 2.6171,-6.53619 0.68045,-1.95032 1.25622,-3.90064 1.67496,-5.85095 0.36639,-1.95032 0.57576,-3.84792 0.57576,-5.74553 0,-3.00454 -0.57576,-6.00908 -1.77963,-9.01362 -1.20387,-3.05725 -2.93117,-5.85095 -5.07719,-8.4338 -2.19837,-2.58285 -4.65846,-4.63859 -7.43259,-6.27263 -2.77413,-1.58134 -5.65295,-2.47743 -8.63646,-2.63557 l 0,59.66913 c 0,3.37352 -0.9945,6.43077 -3.08818,9.06633 -2.04135,2.63556 -4.7108,4.6913 -7.95601,6.11451 -3.19287,1.4232 -6.38574,2.10844 -9.57862,2.10844 -3.03584,0 -5.60061,-0.79067 -7.64195,-2.31929 -1.989,-1.52862 -3.03585,-3.79521 -3.03585,-6.74704 0,-3.10996 1.04685,-6.00908 3.08819,-8.69735 2.04135,-2.63557 4.65845,-4.74402 7.79898,-6.27264 3.14053,-1.52863 6.17638,-2.3193 9.10754,-2.3193 4.08269,0 7.01385,0.79067 8.79348,2.42472 l 0,-53.87089 0,-27.62069 z"
7509+         id="path3064"
7510+         style="fill:url(#linearGradient4056);fill-opacity:1;stroke:none" />
7511+      <path
7512+         d="m 209.73177,93.072183 0,0.29 0,166.999997 0,0.29 0.29,0 167.60188,0 0.29,0 0,-0.29 0,-64.02198 -0.58,0 0,63.73198 -167.02188,0 0,-166.419997 167.02188,0 0,64.684067 0.58,0 0,-64.974067 0,-0.29 -0.29,0 -167.60188,0 z m 168.01875,64.515627 c -8.729,1.23857 -15.58303,8.04973 -16.91062,16.76562 -1.66093,10.90439 6.00624,21.10407 16.91062,22.765 0.10642,-0.41051 0.0755,-0.51097 -0.19954,-0.7689 -10.4278,-1.75607 -17.74732,-11.41352 -16.14921,-21.90547 1.27412,-8.3649 7.89743,-14.77376 16.23235,-16.08885 0.19473,-0.104 0.15766,-0.29191 0.1164,-0.7674 z"
7513+         id="path3066"
7514+         style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;opacity:0.35080644;color:#000000;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.86486489;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans" />
7515+    </g>
7516+    <g
7517+       transform="translate(0,113.23718)"
7518+       id="g4001">
7519+      <path
7520+         d="m 158.93684,178.85826 a 28.057219,28.057219 0 1 1 -56.11444,0 28.057219,28.057219 0 1 1 56.11444,0 z"
7521+         transform="matrix(1.1098266,0,0,1.1098266,-11.944626,-16.376658)"
7522+         id="path3862"
7523+         style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" />
7524+      <path
7525+         d="m 101.30906,150.125 0,64 64,0 0,-25.73334 c -0.0286,4.3e-4 -0.058,0 -0.0867,0 -3.45895,0 -6.26,-2.80771 -6.26,-6.26666 0,-3.45896 2.80105,-6.26 6.26,-6.26 0.0287,0 0.0581,-4.3e-4 0.0867,0 l 0,-25.74 -64,0 z"
7526+         id="path3076"
7527+         style="fill:url(#linearGradient4064);fill-opacity:1;fill-rule:nonzero;stroke:none" />
7528+      <path
7529+         d="m 101.30906,150.125 0,30.26666 c 12.15772,-5.57413 36.63168,-14.74335 64,-13.55333 l 0,-16.71333 -64,0 z"
7530+         id="path3078"
7531+         style="opacity:0.06956524;fill:url(#linearGradient4066);fill-opacity:1;fill-rule:nonzero;stroke:none" />
7532+      <path
7533+         d="m 134.50584,163.3272 0.92411,0 c 0.23102,1.47349 0.51981,2.71433 0.90485,3.7225 0.3658,0.98879 0.8086,1.84187 1.28991,2.55922 0.50056,0.71736 1.25139,1.6286 2.25251,2.75311 1.00112,1.1245 1.79047,2.07452 2.40654,2.86943 1.84822,2.38473 2.77233,4.88578 2.77233,7.48378 0,2.67555 -1.11663,5.93274 -3.3884,9.77157 l -0.61608,0 c 0.28879,-0.67858 0.63533,-1.4541 1.02038,-2.32656 0.36579,-0.89185 0.69308,-1.68676 0.96261,-2.40412 0.25028,-0.71735 0.46205,-1.43471 0.61607,-2.15207 0.13477,-0.71736 0.21178,-1.41533 0.21178,-2.1133 0,-1.10512 -0.21178,-2.21023 -0.65458,-3.31535 -0.4428,-1.12451 -1.07813,-2.15208 -1.86747,-3.10209 -0.8086,-0.95001 -1.71346,-1.70615 -2.73383,-2.30718 -1.02037,-0.58164 -2.07924,-0.91123 -3.17662,-0.9694 l 0,21.94727 c 0,1.24083 -0.3658,2.36534 -1.13589,3.33474 -0.75084,0.9694 -1.73271,1.72554 -2.92635,2.24901 -1.17439,0.52348 -2.34878,0.77552 -3.52317,0.77552 -1.11663,0 -2.05999,-0.29082 -2.81083,-0.85307 -0.73159,-0.56225 -1.11663,-1.39594 -1.11663,-2.48167 0,-1.14389 0.38504,-2.21024 1.13588,-3.19903 0.75084,-0.9694 1.71346,-1.74492 2.86859,-2.30717 1.15514,-0.56226 2.27177,-0.85308 3.3499,-0.85308 1.50168,0 2.57981,0.29082 3.23439,0.89185 l 0,-19.81458 0,-10.15933 z"
7534+         id="path3080"
7535+         style="fill:#ffffff;fill-opacity:1;stroke:none" />
7536+      <path
7537+         d="m 134.50584,162.74004 0.92411,0 c 0.23102,1.47349 0.51981,2.71433 0.90485,3.72251 0.3658,0.98879 0.8086,1.84186 1.28991,2.55922 0.50056,0.71736 1.25139,1.6286 2.25251,2.7531 1.00112,1.12451 1.79047,2.07452 2.40654,2.86943 1.84822,2.38473 2.77233,4.88579 2.77233,7.48379 0,2.67554 -1.11663,5.93274 -3.3884,9.77157 l -0.61608,0 c 0.28879,-0.67858 0.63533,-1.4541 1.02038,-2.32657 0.36579,-0.89185 0.69308,-1.68676 0.96261,-2.40411 0.25028,-0.71736 0.46205,-1.43472 0.61607,-2.15208 0.13477,-0.71735 0.21178,-1.41532 0.21178,-2.11329 0,-1.10512 -0.21178,-2.21024 -0.65458,-3.31536 -0.4428,-1.1245 -1.07813,-2.15207 -1.86747,-3.10208 -0.8086,-0.95002 -1.71346,-1.70615 -2.73383,-2.30718 -1.02037,-0.58164 -2.07924,-0.91124 -3.17662,-0.9694 l 0,21.94726 c 0,1.24083 -0.3658,2.36534 -1.13589,3.33474 -0.75084,0.96941 -1.73271,1.72554 -2.92635,2.24902 -1.17439,0.52347 -2.34878,0.77552 -3.52317,0.77552 -1.11663,0 -2.05999,-0.29082 -2.81083,-0.85308 -0.73159,-0.56225 -1.11663,-1.39593 -1.11663,-2.48167 0,-1.14389 0.38504,-2.21023 1.13588,-3.19902 0.75084,-0.9694 1.71346,-1.74493 2.86859,-2.30718 1.15514,-0.56225 2.27177,-0.85307 3.3499,-0.85307 1.50168,0 2.57981,0.29082 3.23439,0.89185 l 0,-19.81458 0,-10.15934 z"
7538+         id="path3082"
7539+         style="fill:#dcdfe1;fill-opacity:1;stroke:none" />
7540+      <path
7541+         d="m 134.50584,162.74004 0.92411,0 c 0.23102,1.47349 0.51981,2.71433 0.90485,3.72251 0.3658,0.98879 0.8086,1.84186 1.28991,2.55922 0.50056,0.71736 1.25139,1.6286 2.25251,2.7531 1.00112,1.12451 1.79047,2.07452 2.40654,2.86943 1.84822,2.38473 2.77233,4.88579 2.77233,7.48379 0,2.67554 -1.11663,5.93274 -3.3884,9.77157 l -0.61608,0 c 0.28879,-0.67858 0.63533,-1.4541 1.02038,-2.32657 0.36579,-0.89185 0.69308,-1.68676 0.96261,-2.40411 0.25028,-0.71736 0.46205,-1.43472 0.61607,-2.15208 0.13477,-0.71735 0.21178,-1.41532 0.21178,-2.11329 0,-1.10512 -0.21178,-2.21024 -0.65458,-3.31536 -0.4428,-1.1245 -1.07813,-2.15207 -1.86747,-3.10208 -0.8086,-0.95002 -1.71346,-1.70615 -2.73383,-2.30718 -1.02037,-0.58164 -2.07924,-0.91124 -3.17662,-0.9694 l 0,21.94726 c 0,1.24083 -0.3658,2.36534 -1.13589,3.33474 -0.75084,0.96941 -1.73271,1.72554 -2.92635,2.24902 -1.17439,0.52347 -2.34878,0.77552 -3.52317,0.77552 -1.11663,0 -2.05999,-0.29082 -2.81083,-0.85308 -0.73159,-0.56225 -1.11663,-1.39593 -1.11663,-2.48167 0,-1.14389 0.38504,-2.21023 1.13588,-3.19902 0.75084,-0.9694 1.71346,-1.74493 2.86859,-2.30718 1.15514,-0.56225 2.27177,-0.85307 3.3499,-0.85307 1.50168,0 2.57981,0.29082 3.23439,0.89185 l 0,-19.81458 0,-10.15934 z"
7542+         id="path3084"
7543+         style="fill:url(#linearGradient4068);fill-opacity:1;stroke:none" />
7544+      <path
7545+         d="m 103.13526,152.2286 0,0.10667 0,59.72947 0,0.10666 0.10667,0 60.67358,0 0.10667,0 0,-0.10666 0,-22.95932 -0.21333,0 0,22.85265 -60.46025,0 0,-59.51614 60.46025,0 0,22.68502 0.21333,0 0,-22.79168 0,-0.10667 -0.10667,0 -60.67358,0 z m 60.82692,22.62306 c -3.21067,0.45557 -5.73169,2.96082 -6.22,6.16667 -0.61092,4.01081 2.20919,7.76242 6.22,8.37333 0.0391,-0.15099 0.0278,-0.18794 -0.0734,-0.28281 -3.83551,-0.64591 -6.52775,-4.19808 -5.93993,-8.05719 0.46864,-3.07674 2.9048,-5.43402 5.97052,-5.91773 0.0716,-0.0383 0.058,-0.10737 0.0428,-0.28227 z"
7546+         id="path3086"
7547+         style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4.6875;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans" />
7548+    </g>
7549+  </g>
7550+</svg>
7551addfile ./contrib/musicplayer/src/resources/images/next.png
7552binary ./contrib/musicplayer/src/resources/images/next.png
7553oldhex
7554*
7555newhex
7556*89504e470d0a1a0a0000000d494844520000000c0000000c080600000056755ce7000000197445
7557*5874536f6674776172650041646f626520496d616765526561647971c9653c0000006d49444154
7558*78da6258b566fd7920d6672012300215ff87b21bc242021bb12902aaa9c7a601042e00710250e3
7559*45340d70354c6886198034219b880e98708837e0f21bba93b06a8662bc363090ea24580018a087
7560*1c0b2e67e00a62162ca662042b2e0d384d45f63403a9490320c000c18231b06a136a8100000000
7561*49454e44ae426082
7562addfile ./contrib/musicplayer/src/resources/images/next_active.png
7563binary ./contrib/musicplayer/src/resources/images/next_active.png
7564oldhex
7565*
7566newhex
7567*89504e470d0a1a0a0000000d494844520000000c0000000c080600000056755ce7000000197445
7568*5874536f6674776172650041646f626520496d616765526561647971c9653c0000005549444154
7569*78da62f8ffffff7920d6672012300215ff87b21b1819191bb129022aa947e62003acb6212b40d7
7570*0003f5a46a40b18d580d70db90394c0ca4026a3989684f130c561624719c11079243d64d52d200
7571*083000b0c621774561a5bd0000000049454e44ae426082
7572addfile ./contrib/musicplayer/src/resources/images/play.png
7573binary ./contrib/musicplayer/src/resources/images/play.png
7574oldhex
7575*
7576newhex
7577*89504e470d0a1a0a0000000d49484452000000280000007808060000008070b09a000000047342
7578*4954080808087c086488000000097048597300000dd700000dd70142289b780000001974455874
7579*536f667477617265007777772e696e6b73636170652e6f72679bee3c1a000013cd49444154789c
7580*ad5b7b9015d599ff9dc7edee3b330e0c030e8f0b988cbb8802098b24881544a694adac60b4023e
7581*8810145722082aec4682c435d172b792182ba58b0fd06cb1a861acb5c4ec26d9dd02190bb59652
7582*b22b6aca9555c60b0c0cc3639899dbddb7fb9cfd63fab4e79edb7def9d49beaa53ddd3ddf79c5f
7583*ffbe73bef33d7ac8f9f3e7311c3977eedcc50016039805603c21640221643c0048298f49298f02
7584*3806e05d00bb478c18f1c970c621430178f6ecd9b100d612426ee49c4fe59c8331064208082125
7585*cf4a2921a544188608820041107c24a5fc17004f8e1c39b2eb4f0af0cc99331700d8c818db68db
7586*761de77cf0c71aa8248026d82008e0fbfe4018863f05f0d3a6a6a6aa83570578faf4e92594d2ad
7587*b66d37673299188879d4cf4d703a4805d4f3bc1e21c4f7468d1ad53e2c803d3d3d04c08f6ddbde
7588*6c5956ac46bd99004dd1c19920a594f03c0fbeef3f0a604b7373b34cea2311e0a953a7ea09212f
7589*398eb388730e4a6922b0a4b9970630ad158b45789ef7ba94f296d1a347f79b7d50f34277773721
7590*84bcec38ce22b500cadeaa02d83496d35e88730ec7711611425eeeeeee2e1bac0c2080476ddbbe
7591*4eb156099cd94ca6d31836af11426059d675001ea908f0c489134b2ccbda446912eee48e4d7069
7592*204db0665f94526432991f9c3871624922c0aeaeae0b38e74f33c6e29bfa6a343bafa4dea4395b
7593*8b30c6c0397fbaababeb8232808490ef673299512630536a9977d5d84a123526e77c1421e4fb25
7594*008f1d3b369631767f357069809398d3ef992fa68f93643329a5f71f3b766c6c0c90527a2f632c
7595*6b9a854a800f1e3c8842a15006b29a7d4c33e2fa35c6589610b25e0778a3092cad3325fbf6edc3
7596*0d37de88679f7bae0c4412b86afd9af69110722300d07c3e7f31a5f4cf86dae185175e88f3e7cf
7597*63c78e1d58b478313a3a3aaab25e4bffea0529a57f1e61fb82bda174aa240802747777e3079b37
7598*63c58a15f8fcf3cf13770cf5fb5ac78940de4029a5b3cd9b95e649d220524af8be8f3f7cfc316e
7599*bbed366cd9b2059ee7559dcb9558040042c8e514c0f8b41f24814beb0c00a410182814b077ef5e
7600*2cbefe7abcf8d24b15f7e14a20a3f3f114404bda83953aafa4aa200cd1dbdb8be79e7d164b6fba
7601*09efbdf75eea0b579906632980b1d5549ad659352906014e9c3c8907366dc2baf5ebd1dddd5d91
7602*c5040d8ee3e6e095409ae7b588140242084ccce54029ad49e5fa185c4ad925a56cd5ec4f458042
7603*889267aac955f3e661fdfaf5686e6e2e5938d534145d3fc6a594c700b49ae875104208a4793869
7604*d2fae52fe3fe0d1bf0951933502814e0791e841035b1a7b5e31cc0d16af32109641a838d8d8df8
7605*deead558bc78310a85020606065241a501d6fa3fcaa594ef4a296faec59428f5266d658c312cf9
7606*f6b771c71d77803186bebebeb2fe2a81d3ff1642a8dfbccb01bc26a5fc896268386a9d33670eee
7607*bbf75eb4b4b4c0755d846158115c2de62b3abec6274c98f0bf478f1efd08c054730525a9553128
7608*a5c4a4499370effaf5b8fcf2cbd1dfdf8f81818112e0b52c840a203fcce5729ff0a8af57a59453
7609*15bd3a285dadba2f376fde3cdc7aebad705d174991611ab85ad41db5578128eccce7f36309219f
7610*1042ea6b0980d47d7dc527c95019d480f603b83897cb755100c8e5725d001e371f4e6aea7e1886
7611*f1df959e359fa9654503f85984e98bc03d9fcf5f00e03021644c2d716ed23189bd4aea5553c8b8
7612*d60da03597cb9d07b4a029bab026eded2ab152893dd37c549a7b91ac51e04a004620db013c6a02
7613*4bfa3be97a2590d5541dc9a311865838ca650b806952caeb4d9529f3a29b1a5dcc88cd5477da8a
7614*8ee4b568ecd23e934c443e9faf07b013c0f5fac0fa792d71af390f938e1ab865b95cae7af20800
7615*a2076f00f0a8eaacd2aaabb6e22ba814d118372481036a4860e6f3f925009e0230a6ecc743c818
7616*244837061744c50466d5cd36eaa015c08f0094bc658d5b9629fd515fadd5c001434ca2e7f3f9b1
7617*00d66250fd97d6fcc341f910c0ab009e5446b8161912405df2f97c4919426bc060f941b57701ec
7618*cee572c32a432499999aa4b3b3b35e08318610d22ca51c01a04e08c100104a699d94720421c48b
7619*76a6fa5c2e37ac7186c4e05b6fbd75919472939472a194727252360bf8626118fbeb1142c8ef28
7620*a58fcd9d3bf7b33f29c077de79a74908f18810e27642884329856a00121ddb249324844018862e
7621*63ec05dff71fbcfaeaab4fffd100f7efdf7f9794f21f28a52328a5608c95a47b4d1695a4eddbca
7622*0b12429c638c3d3077eedca78705309fcf93cececead52cabb1863508d520abd3491c4a2527118
7623*8689e0c2308c1b21e4d9c99327afcee572b5d7490e1e3cd85828147603b84ad5e3a2fc71095013
7624*a42eba8a7540aa76a79f03e820842cbef2ca2bcf99fd94ade27c3e4f0a85c26e42c8559cf31854
7625*26932901a900aaa4bbae629d41210438e708820042080441004a697c24842008827952caddf97c
7626*7ebec96419c0cecece7f54e032990cf4a302c739af9aee9552c6ccea20f5978bd80300044130ef
7627*c891234fe772b9bbf47e4a54fcf6db6faf96526eb52c2b06a637a5de6a796813a83a4a3958fa52
7628*2a2e168b28168bf07d3fbece395f3d67ce9c67ca0046a6e4ff38e7232dcb826559c86432b02c2b
7629*56b15e7daae66e5572b5822088c1e9207ddf471004e73299cc97e6cc9973062855f18f1963234d
7630*d638e750d54e05a6160675a3ad3bba524aa87ab3fe029a511f21847804c09a98c103070e4c0ec3
7631*f00f9c73c7719c9839d5aa9519861a76aaf3a8c00ddff7e1799ecea4cb189b7ac515577c460120
7632*0cc30728a58ebe52d3c029b372f4e85184615866b44df3631ef54592a4ada83952ca077415ffa5
7633*5a00fac349c1bb62ecf5d75fc7ecaf7d0d19ce316bd6ac54f6f4bf5553aa9652c2b2ac446fbd58
7634*2c2e0400d6d6d63603c0265da5aa99ace8ecfce217bfc0cf1f7f1c4d4d4dc85816c230c4a851a3
7635*4a58ae56f9d45b42983bb2ababeb554a29bd39896653a5a69a0821f07d1f4f3df514d6ad5b87ff
7636*3a7000fbf7efc7f9f3e7535fccec47357d87d2370321c4cd9c1032d3bc99a4dea4bf95e9e93975
7637*0a3fd8b409b366cdc2da7bee8163dbf8fad7bf0ec658995a4d75ab7b994c2636e40a4710043339
7638*21a4459b9c65fbac0e4e37338c31f008208b9e3f74e810d6ae5983c58b17833286fafa7acc983e
7639*3d11a4298a49b547178b4510425ab8cac528d529f6aacd1fc61832ea59eda528a5f8b7dffc061d
7640*6fbe89952b57a2bfbf1f93274dc2f8f1e313d9532d93c9c4ce836251083186534a47eb5b581a38
7641*9d45f5324ac571d380168b456cdbb60d13274ec4aa55ab70e4f3cf3163fa74d4d7d59530a7b36a
7642*9a2400a3b9c94a926ad36c21d700324a13c19e3c79128f3df61866cf9e8d712d2d68f8d2974a80
7643*2970a633ac1aa7949e2284e4d218ab04b08c41a3314a315028005262de37be81d6d6d68a73d124
7644*87527a8a13426280fa22a8a662ce79ea1c54aed4a953a7b06ad52aac5dbb16d96cb66cbb4b1263
7645*43e8e194d293690f993b88ba0700445331d3d42aa5c4a9ee6ecc9f3f1f0f3ffc30264c98909630
7646*2a11fd9e06f004c760607dad7eb3961d80192a2684e0dcd9b3b8f0c20bf1e28b2fe28a2bae484b
7647*b3a50234934f8490f72821e465750128cfeaa7aa5cdbb7dd4201bde7cee1c1071f44474707e6ce
7648*9d5b114c922465c084102fd2a953a7fe0f21a4d3445f4d2821088200c78f1dc3b7bef52d1c3870
7649*00cb972f8fdd7c7d5ad4225a38aaa2c1cee9d3a7bfcf23f4bf1542fcb51e1aea5f2025c9c48913
7650*d1dbdb8bf65dbbd0dada3a6440bae8c0d4f88490df0291c37ae8d0a149b66d7f9ccd666dc771a0
7651*5aa5b9d8d7d787c6c6c632372c6dbfadd43ccf83ebbaf1b15028789ee74d993973e6110a00d3a6
7652*4deb0cc370bb0a5cd4d1147df0868686b23c6092e75c4d92e26500cfcf9c39f308a025303399cc
7653*e620087a4d90692074d0464c91c8629228b75f6fbeeff78661b8593d13036c6d6d3d5b2c1637a8
7654*1050b54a9da73197042ce99e8a47f43109217f337dfaf433650001e0d24b2fdd562c16b7eaa1a0
7655*ebbaa94970d30baef68c2e66d8592c161186e1d353a74e7d567fae2ca9327efcf8359ee775441f
7656*c0c611975e034e63aad24250f7c33084e779ea035b3d92eb983061c2dd269e32808d8d8df2dcb9
7657*738b060606de705d376651753894d569828b3ea82d01e8ba2e5cd7ddc7185bd4d8d858369f52d3
7658*6fbdbdbde4f8f1e34f5a967577369b8d43513389544d947d538b409f3e11d0adb95c6e4d12b88a
7659*00957cf4d147776432999f398e33c20cac92fc477dcb542a552ebc3a46e07a8510f75f72c925db
7660*2b8d5f530af8f0e1c3238bc5e2df5996b5dab66ddb4c24e9ae98ce9cbe332980aeeb7a41103c63
7661*59d643adadad67ab8d3da424fac71f7f3c514af9b794d2459cf3c92ab164eebffaca56f6340882
7662*cf8410bf0e82e027d3a64debac75cc61d7493ef8e083698cb1258490bfa0948e2384b400181ddd
7663*3e25a53c1104c17100ef0921da2fbbecb243c31967d87592f7df7fdff17dbf290882116118d609
7664*213261181200608c6528a5758cb1119cf326cbb29ccb2ebb6c58e30c89c15ffdea57adaeeb3eec
7665*baee0221c4383302d4455773f425c971c771f6388ef3d04d37dd74f84f0a70d7ae5dcdaeebfe6c
7666*6060e0164288a5672292c0e9119b5ac92a3f2da5f4ebeaea5e721c67c3d2a54b7bfe68803b77ee
7667*bcafafafef47849006ddb4e82646813217090881d4cc8d021901ed6b6868f8e1b265cb7e3e2c80
7668*bb76eda2aeebfed3c0c0c07794fda39482710e9a14b7500ac5a3b98be8056e95ed5706bbaeaeee
7669*9f1dc759b174e9d244373e11e0ae5dbb46f6f5f5fd7b1004b3952931eb2395f28709b145ecbaa9
7670*e28ee65e81737ea0a1a1e1daa54b9796d9c53280edededb4afafef9d2008669730c71818e7e009
7671*60930cb519a5e98ea902ac8e9ee7299073962c5952c264999929140abfd4c1e9b511fd3ca9e294
7672*0630a9c24408898f00e079de6cd7757f0960792a833b77eebcafbfbfff71bd4ea280e9451dc526
7673*ab854129213470dace525633f17d1f0d0d0df7dd72cb2d4f94016c6f6f6f3e7bf6ec678cb10633
7674*b9adb2feba27a3a7e92a89623108020461883002a67bd186a7d33772e4c88b962c59d253a262cf
7675*f37e4a296dd0d5a897239252c4b5489c48620c2c0810180b4b673c62bdc1f3bc9f00b81d881cd6
7676*f6f6f656d7756f8dd3af3a7b5ac549011d4e0c4c0829eb471df5f9cd1883ebbacb5e79e595d698
7677*41cff31e228458716252abd3595aa7ba3a3dcf43c6b250152a21f133849012f3a433a7e62ce71c
7678*524acbf3bc1f02584101c0f7fd365d157a09563735ba3a3ef8f043504ae1ba6ee53c4e048c68aa
7679*658c95943b32d151b7b74110b401006b6d6d9de579de0635e76cdb866559254773ce1142f0daee
7680*ddd8be7d3b265f7411c68d1b8762b19858bed081c68c2638b9a1e689034010048d9f7efae9afb9
7681*10e2b69839cd94e87344b1664eec8181013cf1c41398386912d6dd730f9aa3428efa4d25b55342
7682*60dbf620b8304426caf033c6e2125bb158fc0e0dc3f0ab3ab5ba19d11744996ba5cd9dce2347b0
7683*71e3463cb76d1b42213030305031534b0989992c2922191884105fa552cad1f1feaa81d3cd49da
7684*9e1b0641497bb3a303b7af5c89bd7bf7220843f4f5f7c740cadc322921012491a3762829e5182a
7685*84685613576f8abdc47430210885403108ca9ae7fb78fe8517b06ad52a7c72f8709cb1aab48074
7686*428c15decca5944d31f5caa0320692b243c481b810081332604ace9c3e8d87b66cc1942953b061
7687*e346148300f5757565c56c0550f733d50b08219a68d9fcd04a08496a55f755a6a05a3b74e81056
7688*7ef7bb78e1f9e7d1d3d35336964a279b5502f50c678c9da1948ed5e75a9203a027278141731044
7689*f99a4a22c210239b9a306dda348c1d3b367e495d28ca1761c4e669ce18eb01305677dd9326b632
7690*333ac04a2a1642c0b22c2c5fbe1ccb962d83e33825e18129a626010c02e49c7787061304286330
7691*0940b1584cbc4729455b5b1bd6ac5983969696448fa7acef84ad8f317692673299834110cc57ea
7692*d319531dcb0874124813d8942953b061c306cc983123be6eb29fd48ff9516e1886c866b3bfe7d9
7693*6c7647a150b84f75a43f18771a4568fa80261ba3468dc2dd77df8d6f7ef39b3565bdca006ae180
7694*aad55896b5832e5cb8f02063eca859a730d59e26b66d63c58a15686f6fc775d75d17aff0a14868
7695*808bb6bca3d75e7bed410e008ee3fca7ebba2bf49841455f69aa696969c1fcf9f3b16edd3ae472
7696*b9125043f117f5149d8a5b8410b06dfb3f80c81fcc66b30f7b9e77731004b6f24a941db32c2bb1
7697*e365cb96a1bebebe0454b5b9962409796a1042bc6c36fb2320f2a8afb9e69a4febebeb7724c407
7698*830bc118941052024ebf3e14d103f820622f0ae6775c73cd359fc60001c0b6edef33c67af56046
7699*25d1875fe4aa0cbca404119d534a7b6ddb2efd1f7700686b6b3b5d5f5fbf390c43785afd42b55a
7700*07ade539424859ffbeef430881fafafacd6d6d6df1c7b7251674e1c2854fd6d5d56d2f4609ee28
7701*031f7ff895387805a0959833fbf77d1fd96c76fbc2850b9fd49f2dcb2cd8b67da79472aaeffb73
7702*cdad4e4809cb88eac820925490bae8a508139ce3386fd9b67d67d94b26258ff6ecd933c2f7fd7f
7703*f57dff4a15a7388e3318e4d836ac2834188aa805a0d74794662ccbda6f59d65f2d58b0a0ec23db
7704*d4f4dbdebd7b491004cf140a853b1963b06d1bb66d7f112b1bb16c92e8b64d65b2f4624eb49d3d
7705*c739bfebeaabaf1e5e9d64cf9e3ddf735df7efa5948d2a5c8cbf698df23366a63529b3aa5b86c8
7706*d6f53a8ef3c082050bb6561abfa614f01b6fbc312a0cc3477cdf5f298470f4cf96f50c17b4bddb
7707*dc369511a694ba9665bdc0187b70fefcf97ffca7f2baecdbb7ef2221c4a62008160a2126ab4ddd
7708*4ca49b15d0e8058e70ce7f47297decaaabaefaacd631875d27e9e8e8f88a10e26629e55785102d
7709*52ca3142886600a094f61042ba29a5270821bfa794be3c6fdebcff1ece38ff0f8edc041b2aa64d
7710*f20000000049454e44ae426082
7711addfile ./contrib/musicplayer/src/resources/images/play.svg
7712hunk ./contrib/musicplayer/src/resources/images/play.svg 1
7713+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
7714+<!-- Created with Inkscape (http://www.inkscape.org/) -->
7715+
7716+<svg
7717+   xmlns:dc="http://purl.org/dc/elements/1.1/"
7718+   xmlns:cc="http://creativecommons.org/ns#"
7719+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
7720+   xmlns:svg="http://www.w3.org/2000/svg"
7721+   xmlns="http://www.w3.org/2000/svg"
7722+   xmlns:xlink="http://www.w3.org/1999/xlink"
7723+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
7724+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
7725+   width="40"
7726+   height="120"
7727+   id="svg2"
7728+   version="1.1"
7729+   inkscape:version="0.47 r22583"
7730+   sodipodi:docname="play.svg"
7731+   inkscape:export-filename="/home/josip/bin/tahoe-lafs/contrib/musicplayer/src/resources/images/play.png"
7732+   inkscape:export-xdpi="90"
7733+   inkscape:export-ydpi="90">
7734+  <defs
7735+     id="defs4">
7736+    <linearGradient
7737+       id="linearGradient8237">
7738+      <stop
7739+         id="stop8239"
7740+         offset="0"
7741+         style="stop-color:#2d2d2d;stop-opacity:0;" />
7742+      <stop
7743+         style="stop-color:#383838;stop-opacity:0;"
7744+         offset="0.46525377"
7745+         id="stop8241" />
7746+      <stop
7747+         id="stop8243"
7748+         offset="1"
7749+         style="stop-color:#262626;stop-opacity:1;" />
7750+    </linearGradient>
7751+    <linearGradient
7752+       id="linearGradient8129">
7753+      <stop
7754+         style="stop-color:#686868;stop-opacity:0;"
7755+         offset="0"
7756+         id="stop8131" />
7757+      <stop
7758+         id="stop8137"
7759+         offset="0.5"
7760+         style="stop-color:#686868;stop-opacity:0;" />
7761+      <stop
7762+         style="stop-color:#686868;stop-opacity:1;"
7763+         offset="1"
7764+         id="stop8133" />
7765+    </linearGradient>
7766+    <linearGradient
7767+       inkscape:collect="always"
7768+       id="linearGradient8045">
7769+      <stop
7770+         style="stop-color:#000000;stop-opacity:1"
7771+         offset="0"
7772+         id="stop8047" />
7773+      <stop
7774+         style="stop-color:#343535;stop-opacity:1"
7775+         offset="1"
7776+         id="stop8049" />
7777+    </linearGradient>
7778+    <linearGradient
7779+       inkscape:collect="always"
7780+       id="linearGradient7977">
7781+      <stop
7782+         style="stop-color:#e5e5e5;stop-opacity:1;"
7783+         offset="0"
7784+         id="stop7979" />
7785+      <stop
7786+         style="stop-color:#d7d7d7;stop-opacity:1"
7787+         offset="1"
7788+         id="stop7981" />
7789+    </linearGradient>
7790+    <linearGradient
7791+       inkscape:collect="always"
7792+       id="linearGradient7951">
7793+      <stop
7794+         style="stop-color:#ffffff;stop-opacity:1;"
7795+         offset="0"
7796+         id="stop7953" />
7797+      <stop
7798+         style="stop-color:#ffffff;stop-opacity:0;"
7799+         offset="1"
7800+         id="stop7955" />
7801+    </linearGradient>
7802+    <inkscape:perspective
7803+       sodipodi:type="inkscape:persp3d"
7804+       inkscape:vp_x="0 : 526.18109 : 1"
7805+       inkscape:vp_y="0 : 1000 : 0"
7806+       inkscape:vp_z="744.09448 : 526.18109 : 1"
7807+       inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
7808+       id="perspective10" />
7809+    <filter
7810+       inkscape:collect="always"
7811+       id="filter7971"
7812+       color-interpolation-filters="sRGB">
7813+      <feGaussianBlur
7814+         inkscape:collect="always"
7815+         stdDeviation="2.1654676"
7816+         id="feGaussianBlur7973" />
7817+    </filter>
7818+    <radialGradient
7819+       inkscape:collect="always"
7820+       xlink:href="#linearGradient7951"
7821+       id="radialGradient8017"
7822+       gradientUnits="userSpaceOnUse"
7823+       gradientTransform="matrix(1.1891384,0,0,1.1891384,-29.316449,-56.053463)"
7824+       cx="155"
7825+       cy="289.22653"
7826+       fx="155"
7827+       fy="289.22653"
7828+       r="43" />
7829+    <clipPath
7830+       clipPathUnits="userSpaceOnUse"
7831+       id="clipPath8021">
7832+      <path
7833+         sodipodi:type="arc"
7834+         style="fill:url(#linearGradient8025);fill-opacity:1;stroke:none"
7835+         id="path8023"
7836+         sodipodi:cx="155"
7837+         sodipodi:cy="296.36218"
7838+         sodipodi:rx="43"
7839+         sodipodi:ry="43"
7840+         d="m 198,296.36218 a 43,43 0 1 1 -86,0 43,43 0 1 1 86,0 z"
7841+         transform="matrix(1.0086516,0,0,1.0086516,-0.77262799,5.2970921)" />
7842+    </clipPath>
7843+    <linearGradient
7844+       inkscape:collect="always"
7845+       xlink:href="#linearGradient7977"
7846+       id="linearGradient8025"
7847+       gradientUnits="userSpaceOnUse"
7848+       x1="198.26703"
7849+       y1="339.4957"
7850+       x2="198.26703"
7851+       y2="309.34891" />
7852+    <linearGradient
7853+       inkscape:collect="always"
7854+       xlink:href="#linearGradient7977"
7855+       id="linearGradient8027"
7856+       gradientUnits="userSpaceOnUse"
7857+       x1="198.26703"
7858+       y1="339.4957"
7859+       x2="198.26703"
7860+       y2="309.34891" />
7861+    <linearGradient
7862+       inkscape:collect="always"
7863+       xlink:href="#linearGradient7977"
7864+       id="linearGradient8041"
7865+       gradientUnits="userSpaceOnUse"
7866+       x1="160.67363"
7867+       y1="287.52835"
7868+       x2="158.46223"
7869+       y2="312.41592" />
7870+    <radialGradient
7871+       inkscape:collect="always"
7872+       xlink:href="#linearGradient7951"
7873+       id="radialGradient8043"
7874+       gradientUnits="userSpaceOnUse"
7875+       gradientTransform="matrix(1.1891384,0,0,1.1891384,-29.316449,-56.053463)"
7876+       cx="155"
7877+       cy="289.22653"
7878+       fx="155"
7879+       fy="289.22653"
7880+       r="43" />
7881+    <linearGradient
7882+       inkscape:collect="always"
7883+       xlink:href="#linearGradient8045"
7884+       id="linearGradient8051"
7885+       x1="143.63651"
7886+       y1="299.68353"
7887+       x2="160.13068"
7888+       y2="306.98428"
7889+       gradientUnits="userSpaceOnUse" />
7890+    <radialGradient
7891+       inkscape:collect="always"
7892+       xlink:href="#linearGradient8129"
7893+       id="radialGradient8135"
7894+       cx="155"
7895+       cy="296.36218"
7896+       fx="153.96471"
7897+       fy="292.49844"
7898+       r="43"
7899+       gradientUnits="userSpaceOnUse"
7900+       gradientTransform="matrix(1.4885599,0,0,1.4885599,-75.726792,-144.79069)" />
7901+    <inkscape:perspective
7902+       id="perspective8191"
7903+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
7904+       inkscape:vp_z="1 : 0.5 : 1"
7905+       inkscape:vp_y="0 : 1000 : 0"
7906+       inkscape:vp_x="0 : 0.5 : 1"
7907+       sodipodi:type="inkscape:persp3d" />
7908+    <linearGradient
7909+       inkscape:collect="always"
7910+       xlink:href="#linearGradient7977"
7911+       id="linearGradient8225"
7912+       gradientUnits="userSpaceOnUse"
7913+       x1="160.67363"
7914+       y1="287.52835"
7915+       x2="158.46223"
7916+       y2="312.41592" />
7917+    <radialGradient
7918+       inkscape:collect="always"
7919+       xlink:href="#linearGradient8045"
7920+       id="radialGradient8227"
7921+       gradientUnits="userSpaceOnUse"
7922+       gradientTransform="matrix(1.1891384,0,0,1.1891384,-29.316449,-56.053463)"
7923+       cx="155"
7924+       cy="289.22653"
7925+       fx="155"
7926+       fy="289.22653"
7927+       r="43" />
7928+    <linearGradient
7929+       inkscape:collect="always"
7930+       xlink:href="#linearGradient8045"
7931+       id="linearGradient8229"
7932+       gradientUnits="userSpaceOnUse"
7933+       x1="146.0701"
7934+       y1="306.98428"
7935+       x2="160.13068"
7936+       y2="306.98428" />
7937+    <radialGradient
7938+       inkscape:collect="always"
7939+       xlink:href="#linearGradient8237"
7940+       id="radialGradient8231"
7941+       gradientUnits="userSpaceOnUse"
7942+       gradientTransform="matrix(1.4398708,0,0,1.4398707,-68.179991,-130.36105)"
7943+       cx="155"
7944+       cy="296.36218"
7945+       fx="155"
7946+       fy="289.86111"
7947+       r="43" />
7948+    <filter
7949+       inkscape:collect="always"
7950+       id="filter8233"
7951+       color-interpolation-filters="sRGB">
7952+      <feGaussianBlur
7953+         inkscape:collect="always"
7954+         stdDeviation="2.15"
7955+         id="feGaussianBlur8235" />
7956+    </filter>
7957+  </defs>
7958+  <sodipodi:namedview
7959+     id="base"
7960+     pagecolor="#f3f3f3"
7961+     bordercolor="#d7d7d7"
7962+     borderopacity="1"
7963+     inkscape:pageopacity="0"
7964+     inkscape:pageshadow="2"
7965+     inkscape:zoom="1"
7966+     inkscape:cx="11.334097"
7967+     inkscape:cy="82.921351"
7968+     inkscape:document-units="px"
7969+     inkscape:current-layer="layer1"
7970+     showgrid="false"
7971+     inkscape:snap-global="false"
7972+     inkscape:showpageshadow="false"
7973+     showborder="true"
7974+     borderlayer="false"
7975+     inkscape:window-width="1280"
7976+     inkscape:window-height="741"
7977+     inkscape:window-x="0"
7978+     inkscape:window-y="26"
7979+     inkscape:window-maximized="1" />
7980+  <metadata
7981+     id="metadata7">
7982+    <rdf:RDF>
7983+      <cc:Work
7984+         rdf:about="">
7985+        <dc:format>image/svg+xml</dc:format>
7986+        <dc:type
7987+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
7988+        <dc:title></dc:title>
7989+      </cc:Work>
7990+    </rdf:RDF>
7991+  </metadata>
7992+  <g
7993+     inkscape:label="Layer 1"
7994+     inkscape:groupmode="layer"
7995+     id="layer1"
7996+     transform="translate(-331.68859,-262.98427)">
7997+    <g
7998+       id="g8211"
7999+       transform="matrix(0.46511627,0,0,0.46511627,280.32014,220.20089)">
8000+      <path
8001+         transform="matrix(1,0,0,-1,-1.5578274,603.34646)"
8002+         d="m 198,296.36218 c 0,23.74825 -19.25176,43 -43,43 -23.74824,0 -43,-19.25175 -43,-43 0,-23.74824 19.25176,-43 43,-43 23.74824,0 43,19.25176 43,43 z"
8003+         sodipodi:ry="43"
8004+         sodipodi:rx="43"
8005+         sodipodi:cy="296.36218"
8006+         sodipodi:cx="155"
8007+         id="path8213"
8008+         style="fill:url(#linearGradient8225);fill-opacity:1;stroke:none"
8009+         sodipodi:type="arc" />
8010+      <path
8011+         sodipodi:type="arc"
8012+         style="opacity:0.1072797;fill:url(#radialGradient8227);fill-opacity:1;stroke:none;filter:url(#filter8233)"
8013+         id="path8215"
8014+         sodipodi:cx="155"
8015+         sodipodi:cy="296.36218"
8016+         sodipodi:rx="43"
8017+         sodipodi:ry="43"
8018+         d="m 198,296.36218 c 0,23.74825 -19.25176,43 -43,43 -23.74824,0 -43,-19.25175 -43,-43 0,-23.74824 19.25176,-43 43,-43 23.74824,0 43,19.25176 43,43 z"
8019+         transform="matrix(0.99142262,0,0,-0.99142262,-0.79182653,607.2467)"
8020+         clip-path="url(#clipPath8021)" />
8021+      <path
8022+         sodipodi:type="star"
8023+         style="fill:#090909;fill-opacity:1;stroke:none"
8024+         id="path8217"
8025+         sodipodi:sides="3"
8026+         sodipodi:cx="153.44217"
8027+         sodipodi:cy="303.18256"
8028+         sodipodi:r1="15.206906"
8029+         sodipodi:r2="7.6034532"
8030+         sodipodi:arg1="1.5707963"
8031+         sodipodi:arg2="2.6179939"
8032+         inkscape:flatsided="true"
8033+         inkscape:rounded="0"
8034+         inkscape:randomized="0"
8035+         d="m 153.44217,318.38946 -13.16957,-22.81036 26.33914,0 -13.16957,22.81036 z"
8036+         transform="matrix(0,-1.2147699,1.2147699,0,-217.35179,495.14899)" />
8037+      <path
8038+         transform="matrix(0,-1.2147699,1.2147699,0,-217.35179,493.38122)"
8039+         d="m 153.44217,318.38946 -13.16957,-22.81036 26.33914,0 -13.16957,22.81036 z"
8040+         inkscape:randomized="0"
8041+         inkscape:rounded="0"
8042+         inkscape:flatsided="true"
8043+         sodipodi:arg2="2.6179939"
8044+         sodipodi:arg1="1.5707963"
8045+         sodipodi:r2="7.6034532"
8046+         sodipodi:r1="15.206906"
8047+         sodipodi:cy="303.18256"
8048+         sodipodi:cx="153.44217"
8049+         sodipodi:sides="3"
8050+         id="path8219"
8051+         style="opacity:0.60536397;fill:url(#linearGradient8229);fill-opacity:1;stroke:none"
8052+         sodipodi:type="star" />
8053+      <path
8054+         id="path8221"
8055+         d="m 141.71875,291 0,1.75 26.15625,15.125 1.53125,-0.90625 L 141.71875,291 z"
8056+         style="fill:#e0e1e1;fill-opacity:1;stroke:none" />
8057+      <path
8058+         sodipodi:type="arc"
8059+         style="fill:url(#radialGradient8231);fill-opacity:1;stroke:none"
8060+         id="path8223"
8061+         sodipodi:cx="155"
8062+         sodipodi:cy="296.36218"
8063+         sodipodi:rx="43"
8064+         sodipodi:ry="43"
8065+         d="m 198,296.36218 c 0,23.74825 -19.25176,43 -43,43 -23.74824,0 -43,-19.25175 -43,-43 0,-23.74824 19.25176,-43 43,-43 23.74824,0 43,19.25176 43,43 z"
8066+         transform="matrix(1,0,0,-1,-1.5578274,603.34646)" />
8067+    </g>
8068+    <g
8069+       transform="matrix(0.46511627,0,0,0.46511627,280.32014,180.20089)"
8070+       id="g8029">
8071+      <path
8072+         sodipodi:type="arc"
8073+         style="fill:url(#linearGradient8041);fill-opacity:1;stroke:none"
8074+         id="path8031"
8075+         sodipodi:cx="155"
8076+         sodipodi:cy="296.36218"
8077+         sodipodi:rx="43"
8078+         sodipodi:ry="43"
8079+         d="m 198,296.36218 c 0,23.74825 -19.25176,43 -43,43 -23.74824,0 -43,-19.25175 -43,-43 0,-23.74824 19.25176,-43 43,-43 23.74824,0 43,19.25176 43,43 z"
8080+         transform="matrix(1,0,0,-1,-1.5578274,603.34646)" />
8081+      <path
8082+         clip-path="url(#clipPath8021)"
8083+         transform="matrix(0.99142262,0,0,-0.99142262,-0.79182653,607.2467)"
8084+         d="m 198,296.36218 c 0,23.74825 -19.25176,43 -43,43 -23.74824,0 -43,-19.25175 -43,-43 0,-23.74824 19.25176,-43 43,-43 23.74824,0 43,19.25176 43,43 z"
8085+         sodipodi:ry="43"
8086+         sodipodi:rx="43"
8087+         sodipodi:cy="296.36218"
8088+         sodipodi:cx="155"
8089+         id="path8033"
8090+         style="opacity:0.9425287;fill:url(#radialGradient8043);fill-opacity:1;stroke:none;filter:url(#filter7971)"
8091+         sodipodi:type="arc" />
8092+      <path
8093+         transform="matrix(0,-1.2147699,1.2147699,0,-217.35179,495.14899)"
8094+         d="m 153.44217,318.38946 -13.16957,-22.81036 26.33914,0 -13.16957,22.81036 z"
8095+         inkscape:randomized="0"
8096+         inkscape:rounded="0"
8097+         inkscape:flatsided="true"
8098+         sodipodi:arg2="2.6179939"
8099+         sodipodi:arg1="1.5707963"
8100+         sodipodi:r2="7.6034532"
8101+         sodipodi:r1="15.206906"
8102+         sodipodi:cy="303.18256"
8103+         sodipodi:cx="153.44217"
8104+         sodipodi:sides="3"
8105+         id="path8035"
8106+         style="fill:#090909;fill-opacity:1;stroke:none"
8107+         sodipodi:type="star" />
8108+      <path
8109+         sodipodi:type="star"
8110+         style="fill:url(#linearGradient8051);fill-opacity:1;stroke:none"
8111+         id="path8037"
8112+         sodipodi:sides="3"
8113+         sodipodi:cx="153.44217"
8114+         sodipodi:cy="303.18256"
8115+         sodipodi:r1="15.206906"
8116+         sodipodi:r2="7.6034532"
8117+         sodipodi:arg1="1.5707963"
8118+         sodipodi:arg2="2.6179939"
8119+         inkscape:flatsided="true"
8120+         inkscape:rounded="0"
8121+         inkscape:randomized="0"
8122+         d="m 153.44217,318.38946 -13.16957,-22.81036 26.33914,0 -13.16957,22.81036 z"
8123+         transform="matrix(0,1.2147699,1.2147699,0,-217.35179,120.58736)" />
8124+      <path
8125+         style="fill:#e0e1e1;fill-opacity:1;stroke:none"
8126+         d="m 141.71875,291 0,1.75 26.15625,15.125 1.53125,-0.90625 L 141.71875,291 z"
8127+         id="path8039" />
8128+      <path
8129+         transform="matrix(1,0,0,-1,-1.5578274,603.34646)"
8130+         d="m 198,296.36218 c 0,23.74825 -19.25176,43 -43,43 -23.74824,0 -43,-19.25175 -43,-43 0,-23.74824 19.25176,-43 43,-43 23.74824,0 43,19.25176 43,43 z"
8131+         sodipodi:ry="43"
8132+         sodipodi:rx="43"
8133+         sodipodi:cy="296.36218"
8134+         sodipodi:cx="155"
8135+         id="path8127"
8136+         style="opacity:0.83524906;fill:url(#radialGradient8135);fill-opacity:1;stroke:none"
8137+         sodipodi:type="arc" />
8138+    </g>
8139+    <g
8140+       id="g8008"
8141+       transform="matrix(0.46511627,0,0,0.46511627,280.32014,140.20089)">
8142+      <path
8143+         transform="matrix(1,0,0,-1,-1.5578274,603.34646)"
8144+         d="m 198,296.36218 c 0,23.74825 -19.25176,43 -43,43 -23.74824,0 -43,-19.25175 -43,-43 0,-23.74824 19.25176,-43 43,-43 23.74824,0 43,19.25176 43,43 z"
8145+         sodipodi:ry="43"
8146+         sodipodi:rx="43"
8147+         sodipodi:cy="296.36218"
8148+         sodipodi:cx="155"
8149+         id="path7167"
8150+         style="fill:url(#linearGradient8027);fill-opacity:1;stroke:none"
8151+         sodipodi:type="arc" />
8152+      <path
8153+         sodipodi:type="arc"
8154+         style="fill:url(#radialGradient8017);fill-opacity:1;stroke:none;filter:url(#filter7971)"
8155+         id="path7941"
8156+         sodipodi:cx="155"
8157+         sodipodi:cy="296.36218"
8158+         sodipodi:rx="43"
8159+         sodipodi:ry="43"
8160+         d="m 198,296.36218 c 0,23.74825 -19.25176,43 -43,43 -23.74824,0 -43,-19.25175 -43,-43 0,-23.74824 19.25176,-43 43,-43 23.74824,0 43,19.25176 43,43 z"
8161+         transform="matrix(0.99142262,0,0,0.99142262,-0.79182653,7.0289736)"
8162+         clip-path="url(#clipPath8021)" />
8163+      <path
8164+         sodipodi:type="star"
8165+         style="fill:#ffffff;fill-opacity:1;stroke:none"
8166+         id="path7985"
8167+         sodipodi:sides="3"
8168+         sodipodi:cx="153.44217"
8169+         sodipodi:cy="303.18256"
8170+         sodipodi:r1="15.206906"
8171+         sodipodi:r2="7.6034532"
8172+         sodipodi:arg1="1.5707963"
8173+         sodipodi:arg2="2.6179939"
8174+         inkscape:flatsided="true"
8175+         inkscape:rounded="0"
8176+         inkscape:randomized="0"
8177+         d="m 153.44217,318.38946 -13.16957,-22.81036 26.33914,0 -13.16957,22.81036 z"
8178+         transform="matrix(0,-1.2147699,1.2147699,0,-217.35179,495.14899)" />
8179+      <path
8180+         transform="matrix(0,-1.2147699,1.2147699,0,-217.35179,493.38122)"
8181+         d="m 153.44217,318.38946 -13.16957,-22.81036 26.33914,0 -13.16957,22.81036 z"
8182+         inkscape:randomized="0"
8183+         inkscape:rounded="0"
8184+         inkscape:flatsided="true"
8185+         sodipodi:arg2="2.6179939"
8186+         sodipodi:arg1="1.5707963"
8187+         sodipodi:r2="7.6034532"
8188+         sodipodi:r1="15.206906"
8189+         sodipodi:cy="303.18256"
8190+         sodipodi:cx="153.44217"
8191+         sodipodi:sides="3"
8192+         id="path7975"
8193+         style="fill:#323333;fill-opacity:1;stroke:none"
8194+         sodipodi:type="star" />
8195+      <path
8196+         id="path8003"
8197+         d="m 141.71875,291 0,1.75 26.15625,15.125 1.53125,-0.90625 L 141.71875,291 z"
8198+         style="fill:#3a3b3b;fill-opacity:1;stroke:none" />
8199+    </g>
8200+    <path
8201+       sodipodi:type="star"
8202+       style="fill:#33519d;fill-opacity:1;stroke:none"
8203+       id="path8061"
8204+       sodipodi:sides="3"
8205+       sodipodi:cx="153.44217"
8206+       sodipodi:cy="303.18256"
8207+       sodipodi:r1="15.206906"
8208+       sodipodi:r2="7.6034532"
8209+       sodipodi:arg1="1.5707963"
8210+       sodipodi:arg2="2.6179939"
8211+       inkscape:flatsided="true"
8212+       inkscape:rounded="0"
8213+       inkscape:randomized="0"
8214+       d="m 153.44217,318.38946 -13.16957,-22.81036 26.33914,0 -13.16957,22.81036 z"
8215+       transform="matrix(0,-0.54932746,0.54932746,0,91.365104,403.15218)" />
8216+  </g>
8217+</svg>
8218addfile ./contrib/musicplayer/src/resources/images/previous.png
8219binary ./contrib/musicplayer/src/resources/images/previous.png
8220oldhex
8221*
8222newhex
8223*89504e470d0a1a0a0000000d494844520000000c0000000c080600000056755ce7000000197445
8224*5874536f6674776172650041646f626520496d616765526561647971c9653c0000006f49444154
8225*78da62642012ac5ab35e1f482d600132ea61826121818d381483d43480d82c3006143462331588
8226*0d60624c789c0032f502b262980d0c844c45064cc4988a53033100450334940ca0b610670350d3
8227*452036440b3dc24ec2651b0b2e9360b6012943e48863243569000418005c3728cdb5c9bd650000
8228*000049454e44ae426082
8229addfile ./contrib/musicplayer/src/resources/images/previous_active.png
8230binary ./contrib/musicplayer/src/resources/images/previous_active.png
8231oldhex
8232*
8233newhex
8234*89504e470d0a1a0a0000000d494844520000000c0000000c080600000056755ce7000000197445
8235*5874536f6674776172650041646f626520496d616765526561647971c9653c0000005a49444154
8236*78da62602012fcffff5f1f88cf3302897a9820232363230ec520350d300e1ce03215450d2e0d20
8237*53ff6301181ab0998a53032e5391011303a980222791e56942c1ca028f102c001891178194214a
8238*c4919a3400020c008f8b24f4b5e305de0000000049454e44ae426082
8239hunk ./contrib/musicplayer/src/services/albumCover.js 3
8240 //#require <services/services.js>
8241 //#require "services/lastfm.js"
8242-//#require "libs/util/Goal.js"
8243 
8244 (function () {
8245 var lastfm = da.service.lastFm,
8246hunk ./contrib/musicplayer/src/services/albumCover.js 6
8247-    Goal = da.util.Goal,
8248-    Artists = da.db.DEFAULT.views.Artist.view;
8249+    Artists = da.db.DocumentTemplate.Artist.view();
8250 
8251 function fetchAlbumCover(search_params, album, callback) {
8252   lastfm.album.getInfo(search_params, {
8253hunk ./contrib/musicplayer/src/services/albumCover.js 12
8254     success: function (data) {
8255       var urls = data.album.image ? data.album.image : null,
8256-          n = urls.length;
8257+          n = urls.length,
8258+          url;
8259       
8260hunk ./contrib/musicplayer/src/services/albumCover.js 15
8261-      while(n--)
8262-        urls[n] = urls[n]["#text"];
8263+      while(n--) {
8264+        url = urls[n]["#text"];
8265+        if(!url || !url.length)
8266+          url = "resources/images/album_cover_" + n + ".png";
8267+       
8268+        urls[n] = url;
8269+      }
8270       
8271hunk ./contrib/musicplayer/src/services/albumCover.js 23
8272-      album.set({
8273+      album.update({
8274         album_cover_urls: urls,
8275         lastfm_id:        data.album.id,
8276         mbid:             data.album.mbid.length ? data.album.mbid : null
8277hunk ./contrib/musicplayer/src/services/albumCover.js 28
8278       });
8279-      album.save();
8280       
8281       // fun fact: typeof /.?/ === "function"
8282hunk ./contrib/musicplayer/src/services/albumCover.js 30
8283-      if(urls && typeof callback === "function")
8284+      if(urls && callback)
8285         callback(urls);
8286hunk ./contrib/musicplayer/src/services/albumCover.js 32
8287+     
8288+      delete callback;
8289+      delete urls;
8290+      delete data;
8291+      delete search_params;
8292+    },
8293+    failure: function () {
8294+      if(!callback)
8295+        return;
8296+     
8297+      callback([
8298+        "resources/images/album_cover_0.png",
8299+        "resources/images/album_cover_1.png",
8300+        "resources/images/album_cover_2.png",
8301+        "resources/images/album_cover_3.png"
8302+      ]);
8303+     
8304+      delete callback;
8305     }
8306   });
8307 }
8308hunk ./contrib/musicplayer/src/services/albumCover.js 58
8309  *  da.service.albumCover(song[, callback]) -> undefined
8310  *  - song (da.db.DocumentTemplate): song whose album art needs to be fetched
8311  *  - callback (Function): called once album cover is fetched, with first
8312- *    argument being the URL of the album cover.
8313- *
8314+ *    argument being an array of four URLs.
8315+ * 
8316  *  #### Notes
8317hunk ./contrib/musicplayer/src/services/albumCover.js 61
8318- *  The URL will be saved to the `song` under 'cover_art' propety.
8319+ *  Fetched URLs will be saved to the `song` under 'album_cover_urls' propety.
8320  **/
8321 da.service.albumCover = function (album, callback) {
8322   var search_params = {};
8323hunk ./contrib/musicplayer/src/services/albumCover.js 65
8324-  if(album.get("lastfm_id"))
8325-    search_params.id = album.get("lastfm_id");
8326-  else if(album.get("mbid"))
8327+  if(album.get("mbid"))
8328     search_params.mbid = album.get("mbid");
8329   
8330hunk ./contrib/musicplayer/src/services/albumCover.js 68
8331-  if(!search_params.id && !search_params.mbid)
8332+  if(!search_params.mbid)
8333     search_params = {
8334       album: album.get("title"),
8335       artist: Artists.getRow(album.get("artist_id")).title
8336hunk ./contrib/musicplayer/src/workers/indexer.js 35
8337     queue = [];
8338 
8339 this.da = {};
8340-importScripts("env.js");
8341 
8342hunk ./contrib/musicplayer/src/workers/indexer.js 36
8343+//#require "libs/vendor/mootools-1.2.4-core-server.js"
8344+//#require "libs/vendor/mootools-1.2.4-request.js"
8345+//#require <libs/util/util.js>
8346+//#require "libs/util/BinaryFile.js"
8347+//#require "libs/util/ID3.js"
8348+
8349+var ID3 = da.util.ID3;
8350 /**
8351  *  Indexer.onMessage(event) -> undefined
8352  *  - event (Event): DOM event.
8353hunk ./contrib/musicplayer/src/workers/indexer.js 59
8354 
8355 function getTags(cap) {
8356   if(!cap) return false;
8357-  var parser = new da.util.ID3({
8358+  var parser = new ID3({
8359     url: "/uri/" + encodeURIComponent(cap),
8360     
8361     onSuccess: function (tags) {
8362hunk ./contrib/musicplayer/src/workers/indexer.js 88
8363 var finish_timeout;
8364 function checkQueue() {
8365   if(!queue.length)
8366-    finish_timeout = setTimeout(finish, 30*60*1000);
8367+    finish_timeout = setTimeout(finish, 3*60*1000);
8368   else {
8369     clearTimeout(finish_timeout);
8370     setTimeout(function () {
8371hunk ./contrib/musicplayer/src/workers/indexer.js 92
8372-      getTags(queue.shift());
8373+      getTags(queue[0]);
8374     }, 444);
8375   }
8376 }
8377hunk ./contrib/musicplayer/src/workers/scanner.js 11
8378 
8379 var window = this,
8380     document = {},
8381-    queue = 0;
8382+    queue = [];
8383 
8384 this.da = {};
8385hunk ./contrib/musicplayer/src/workers/scanner.js 14
8386-importScripts("env.js");
8387+
8388+//#require "libs/vendor/mootools-1.2.4-core-server.js"
8389+//#require "libs/vendor/mootools-1.2.4-request.js"
8390+//#require "libs/TahoeObject.js"
8391+
8392+var TahoeObject = da.util.TahoeObject;
8393 
8394 /**
8395  *  Scanner.scan(object) -> undefined
8396hunk ./contrib/musicplayer/src/workers/scanner.js 25
8397  *  - object (TahoeObject): an Tahoe object.
8398  * 
8399- *  Traverses the `object` until it finds a file, whose cap is then reported to main thread via `postMessage`.
8400+ *  Traverses the `object` until it finds a file.
8401+ *  File's cap is then reported to the main thread via `postMessage`.
8402+ * 
8403  **/
8404 function scan (obj) {
8405hunk ./contrib/musicplayer/src/workers/scanner.js 30
8406-  queue++;
8407   obj.get(function () {
8408hunk ./contrib/musicplayer/src/workers/scanner.js 31
8409+    queue.erase(obj.uri);
8410+   
8411     if(obj.type === "filenode") {
8412hunk ./contrib/musicplayer/src/workers/scanner.js 34
8413-      return postMessage(obj.uri);
8414+      postMessage(obj.uri);
8415+      checkQueue();
8416+      return;
8417     }
8418     
8419     var n = obj.children.length;
8420hunk ./contrib/musicplayer/src/workers/scanner.js 49
8421         scan(child);
8422     }
8423     
8424-    if(!--queue)
8425-      postMessage("**FINISHED**");
8426+    checkQueue();
8427   });
8428 }
8429 
8430hunk ./contrib/musicplayer/src/workers/scanner.js 53
8431+function checkQueue() {
8432+  if(queue.length)
8433+    setTimeout(function() {
8434+      scan(new TahoeObject(queue.shift()))
8435+    }, 444);
8436+  else
8437+    postMessage("**FINISHED**");
8438+}
8439+
8440 /**
8441  *  Scanner.onmessage(event) -> undefined
8442  *  - event.data (String): Tahoe cap pointing to root directory from which scanning should begin.
8443hunk ./contrib/musicplayer/src/workers/scanner.js 67
8444  **/
8445 onmessage = function (event) {
8446-  scan(new TahoeObject(event.data));
8447+  queue.push(event.data);
8448
8449+  if(queue.length < 2)
8450+    scan(new TahoeObject(event.data));
8451 };
8452hunk ./contrib/musicplayer/tests/initialize.js 20
8453   'test_NavigationController',
8454   'test_Dialog',
8455   'test_SettingsController',
8456
8457   'test_ProgressBar',
8458hunk ./contrib/musicplayer/tests/initialize.js 22
8459-  'test_SegmentedProgressBar'
8460+  'test_SegmentedProgressBar',
8461+  'test_PlayerController'
8462 ]);
8463hunk ./contrib/musicplayer/tests/test_NavigationController.js 34
8464   this.test_navigationBehaviour = [
8465     {"params": {"xpath": "//div[@id='Artists_column_container']/div/div[2]/a[2]/span"},
8466       "method": "click"},
8467-    {"params": {"xpath": "//div[@id='Albums_column_container']/div/div[2]/a/span"},
8468-     "method": "click"},
8469-    {"params": {"xpath": "//div[@id='Albums_column_container']/a/span", "validator": "Albums"},
8470-     "method": "asserts.assertText"}
8471+    {"params": {"xpath": "//div[@id='Albums_column']/div[2]/a[@title='Urgency']"},
8472+     "method": "click"}
8473   ];
8474   
8475   this.test_activeColumns = function () {
8476addfile ./contrib/musicplayer/tests/test_PlayerController.js
8477hunk ./contrib/musicplayer/tests/test_PlayerController.js 1
8478+var test_PlayerController = new function () {
8479+  var Song   = da.db.DocumentTemplate.Song,
8480+      // Songs column was set as only one in test_NavigationController
8481+      Songs  = null,
8482+      Player = da.controller.Player,
8483+      self   = this;
8484
8485+  this.setup = function () {
8486+    Songs = da.controller.Navigation.activeColumns[1].column;
8487+   
8488+    self.songs = [
8489+      Song.findById(Songs.getItem(0).id),
8490+      Song.findById(Songs.getItem(1).id),
8491+      Song.findById(Songs.getItem(2).id)
8492+    ];
8493+  };
8494
8495+  this.test_play = function () {
8496+    jum.assertTrue("Should return 'undefined if nothing is playing",
8497+      !Player.nowPlaying()
8498+    );
8499+   
8500+    var play_event_fired = false;
8501+    function test_playEvent (song) {
8502+      jum.assertEquals("first argument to callback function should be playing song",
8503+        self.songs[0].id,
8504+        song.id
8505+      );
8506+     
8507+      Player.removeEvent("play", test_PlayEvent);
8508+      delete test_playEvent;
8509+    }
8510+   
8511+    Player.addEvent("play", test_playEvent);
8512+    Player.play(self.songs[0]);
8513+   
8514+    jum.assertTrue("'play' event should have been fired",
8515+      play_event_fired
8516+    );
8517+  };
8518
8519+  this.test_getNext = function () {
8520+    jum.assertTrue("there shouldn't be a next song, since there is no playlist",
8521+      !Player.getNext()
8522+    );
8523+   
8524+    var playlist = self.songs.map(function (s) { return s.id });
8525+    Player.setPlaylist(playlist);
8526+    Player.play(self.songs[1]);
8527+   
8528+    jum.assertEquals("next song in playlist should be returned",
8529+      playlist[2],
8530+      Player.getNext()
8531+    );
8532+  };
8533
8534+  this.test_getPrev = function () {
8535+    jum.assertEquals("previous song in playlist should have been returned",
8536+      self.songs[0].id,
8537+      Player.getPrev()
8538+    );
8539+   
8540+    Player.play(self.songs[2]);
8541+    jum.assertEquals("previous song in playlist should have been returned",
8542+      self.songs[1].id,
8543+      Player.getPrev()
8544+    );
8545+  };
8546
8547+  this.test_queue = function () {
8548+    jum.assertTrue("there shouldn't be anything left in the playlist",
8549+      !Player.getNext()
8550+    );
8551+   
8552+    Player.queue(self.songs[0].id);
8553+   
8554+    jum.assertEquals("next song should be from queue",
8555+      self.songs[0].id,
8556+      Player.getNext()
8557+    );
8558+  };
8559
8560+  return this;
8561+};
8562hunk ./contrib/musicplayer/tests/test_ProgressBar.js 27
8563   // in order to fix browser inconsistencies.
8564   this.test_incrementation = function () {
8565     self.pb.setProgress(0.5);
8566-   
8567-    jum.assertEqualArrays(BLACK,        getPixel(1));
8568-    jum.assertEqualArrays(BLACK,        getPixel(49));
8569+
8570+    jum.assertEqualArrays(BLACK,        getPixel(  1));
8571+    jum.assertEqualArrays(BLACK,        getPixel( 49));
8572     jum.assertEqualArrays(TRANSPARENT,  getPixel(100));
8573     
8574     self.pb.options.foreground = "rgba(255, 0, 0, 255)";
8575hunk ./contrib/musicplayer/tests/test_ProgressBar.js 35
8576     self.pb.setProgress(0.7);
8577     
8578-    jum.assertEqualArrays(BLACK,        getPixel(1));
8579-    jum.assertEqualArrays(RED,          getPixel(52));
8580-    jum.assertEqualArrays(RED,          getPixel(69));
8581-    jum.assertEqualArrays(TRANSPARENT,  getPixel(100));
8582+    jun.assertEqualArrays(BLACK,        getPixel(  1));
8583+    jun.assertEqualArrays(RED,          getPixel( 52));
8584+    jun.assertEqualArrays(RED,          getPixel( 69));
8585+    jun.assertEqualArrays(TRANSPARENT,  getPixel(100));
8586   };
8587   
8588   this.test_decrementation = function () {
8589hunk ./contrib/musicplayer/tests/test_ProgressBar.js 46
8590     self.pb.setProgress(0.6);
8591     
8592     jum.assertEqualArrays(RED,          getPixel(52));
8593-    jum.assertEqualArrays(RED,          getPixel(58));
8594+    jum.assertEqualArrays(RED,          getPixel(55));
8595     jum.assertEqualArrays(TRANSPARENT,  getPixel(60));
8596     jum.assertEqualArrays(TRANSPARENT,  getPixel(70));
8597   };
8598hunk ./contrib/musicplayer/tests/test_ProgressBar.js 52
8599   
8600   this.teardown = function () {
8601-    //this.pb.destroy();
8602+    this.pb.destroy();
8603   };
8604   
8605   return this;
8606hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 24
8607   }
8608   
8609   this.test_incrementation = function () {
8610-    var ctx = self.pb.ctx;
8611-   
8612     self.pb.setProgress("g", 0.6);
8613     self.pb.setProgress("r", 0.3);
8614hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 26
8615-    self.pb.setProgress("b", 1);
8616+    self.pb.setProgress("b",   1);
8617     
8618hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 28
8619-    jum.assertEqualArrays(RED,    getPixel(2));
8620+    jum.assertEqualArrays(RED,    getPixel( 2));
8621     jum.assertEqualArrays(RED,    getPixel(20));
8622     jum.assertEqualArrays(RED,    getPixel(29));
8623     
8624hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 44
8625   this.test_incrementingMiddleSegment = function () {
8626     self.pb.setProgress("g", 0.8);
8627     
8628-    jum.assertEqualArrays(RED,    getPixel(2));
8629+    jum.assertEqualArrays(RED,    getPixel( 2));
8630     jum.assertEqualArrays(RED,    getPixel(29));
8631     
8632     jum.assertEqualArrays(GREEN,  getPixel(31));
8633hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 57
8634   this.test_incrementingFirstSegment = function () {
8635     self.pb.setProgress("r", 0.9);
8636     
8637-    jum.assertEqualArrays(RED,    getPixel(2));
8638+    jum.assertEqualArrays(RED,    getPixel( 2));
8639     jum.assertEqualArrays(RED,    getPixel(66));
8640     jum.assertEqualArrays(RED,    getPixel(79));
8641     
8642hunk ./src/allmydata/test/test_musicplayer.py 107
8643 
8644     return d
8645 
8646-class ChromeTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Chrome):
8647-  pass
8648-
8649-#class FirefoxTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Firefox):
8650+#class ChromeTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Chrome):
8651 #  pass
8652hunk ./src/allmydata/test/test_musicplayer.py 109
8653+
8654+class FirefoxTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Firefox):
8655+  pass
8656}
8657[updated-docs
8658josip.lisec@gmail.com**20100731231211
8659 Ignore-this: cbfb98d6ebeed2f2826250eb43d912ff
8660 * Added NOTES file with instructions for running the tests.
8661 * Fixed typos in INSTALL
8662] {
8663hunk ./contrib/musicplayer/INSTALL 26
8664 (And if you're one of those who prefer to do it by-hand (and keyboard),
8665 this file isn't a place for you.)
8666 
8667-== Battle for Configuration File ==
8668+== Battle for the Configuration File ==
8669 Player's configuration file is a real beast on its own,
8670 and in order to edit it we must prepare ourselves really good,
8671 otherwise, we're doomed (actually, only you are )!
8672addfile ./contrib/musicplayer/NOTES
8673hunk ./contrib/musicplayer/NOTES 1
8674+=== Running the tests ===
8675+== Prerequsites ==
8676+= Windmill=
8677+In order to run the tests make sure you have the latest Windmill[1][2]
8678+(as of July 30th 2010 it means you'll have to build it from the trunk because
8679+it contains a crutial fix)
8680+
8681+You can obtain the latest copy of Windmill by running the following commands
8682+from your shell:
8683+  $ cd ~/bin
8684+  $ git pull git://github.com/windmill/windmill
8685+  $ python setup.py build
8686+  $ sudo python setup.py install
8687+
8688+= Working browser =
8689+You'll also need a working browser, the tests are intented to be ran in
8690+Google Chrome (not Chromium) and/or Firefox.
8691+
8692+By default Google Chrome will be used.
8693+
8694+If you do not have Google Chrome installed on your computer, to use Firefox
8695+you'll have to edit the src/allmydata/tests/test_musicplayer.py file.
8696+
8697+== Building the project ==
8698+Components needed by da.controller.Player and da.controller.CollectionScanner tests
8699+are only available after we've built the from project. (Web workers and SoundManger's Flash files).
8700+
8701+  $ cd <directory with this file>
8702+  $ python manage.py build
8703+
8704+== Running the tests ==
8705+To run the music player-specific tests simply:
8706+
8707+  $ cd <Tahoe-LAFS source directory>
8708+  $ python setup.py test -s allmydata.test.test_musicplayer
8709+
8710+If you have any Google Chrome winows open, remember to close them before
8711+running the tests, otherwise Windmill won't be able to work its magick.
8712+
8713+== Problematic tests ==
8714+* test_ProgressBar, test_SegmentedProgressBar: these might or might not fail,
8715+  the reason is that browsers don't render the graphics precisely the same.
8716+  They will probably (partially) fail in Firefox.
8717+* test_ID3v1: This test usually fails on Linux version of Google Chrome.
8718+  Reasons are still unknown.
8719+
8720+== Debugging tips ==
8721+* Chrome is definitely faster, but Firefox gives better error messages.
8722+  To view more detailed messages than those presented by Windmill type
8723+  following into Web Inspector's or Firebugs' console:
8724
8725+  `windmill.jsTests.testFailures`
8726+*
8727+
8728+=== Documentation ===
8729+Player's code is fully annotated with PDoc[2] syntax, which can then generate
8730+lovely web site for easier navigation.
8731+
8732+To generate the API documentation you'll need latest version of PDoc:
8733+
8734+  $ sudo gem install pdoc
8735+
8736+After you've installed PDoc just run:
8737+
8738+  $ cd <directory with this file>
8739+  $ python manage.py docs
8740+  $ open-in-web-browser docs/index.html
8741+
8742+Note: You will be probably greeted with few 'missing dependencies' errors
8743+from PDoc, just install them manually and you're good to go.
8744+
8745+[1] http://getwindmill.com - Windmill
8746+[2] http://github.com/windmill - Windmill's Github repository
8747+[3] http://pdoc.org/ - PDoc's website
8748}
8749[added-songcontext-and-services
8750josip.lisec@gmail.com**20100731231316
8751 Ignore-this: e8cd467b7d5681d1b9fbf9decb8b1f4b
8752 * Added da.controller.SongContext with default contexts:
8753   * 'Artist' - shows basic information about the artist
8754     the currently playing song,
8755   * 'Recommendations' - shows similar artists and songs,
8756   * 'Music Videos' - presents search results from YouTube
8757     of currently playing song.
8758 * Added da.service.* APIs which are used by contexts above
8759   * da.service.lastFm - interface to the Last.fm services
8760   * da.service.artistInfo
8761   * da.service.recommendations
8762   * da.service.musicVideo
8763 * UTF-8 related fixes to ID3v2 parser
8764] {
8765hunk ./contrib/musicplayer/manage.py 12
8766 
8767 CLOSURE_COMPILER_PATH = 'tools/closure-compiler-20100514/compiler.jar'
8768 
8769+class ClosureCompiler:
8770+  def __init__(self, input_files, output, warnings = 'QUIET'):
8771+    self.input = input_files
8772+    self.output = output
8773+    self.warnings = warnings
8774+
8775+  def compile(self, compression = 'WHITESPACE'):
8776+    print 'Compressing %s...' % self.output
8777+   
8778+    if compression == 'NONE':
8779+      output_file = open(self.output, 'a')
8780+      for filename in self.input:
8781+        f = open(filename)
8782+        output_file.write(f.read())
8783+        output_file.write('\n')
8784+        f.close()
8785+
8786+      output_file.close()
8787+    else:
8788+      args = [
8789+        'java',
8790+        '-jar',                 CLOSURE_COMPILER_PATH,
8791+        '--warning_level',      self.warnings,
8792+        '--compilation_level',  compression,
8793+        '--js_output_file',     self.output]
8794+     
8795+      for filename in self.input:
8796+        args.append('--js')
8797+        args.append(filename)
8798+     
8799+      subprocess.call(args)
8800
8801+  def syntax_check(self):
8802+    args = [
8803+      'java',
8804+      '-jar',             CLOSURE_COMPILER_PATH,
8805+      '--js_output_file', '_tmp.out',
8806+      '--dev_mode',       'START_AND_END']
8807+   
8808+    for filename in self.input:
8809+      args.append('--js')
8810+      args.append(filename)
8811+   
8812+    r = subprocess.call(args)
8813+    os.remove('_tmp.out')
8814+    return r == 0
8815+
8816 class JSDepsBuilder:
8817   """
8818   Looks for
8819hunk ./contrib/musicplayer/manage.py 80
8820     for (dirname, dirs, files) in os.walk(self.root):
8821       for filename in files:
8822         if filename.endswith('.js'):
8823-          self.detect_requires(os.path.join(dirname, filename))
8824+           self.detect_requires(os.path.join(dirname, filename))
8825   
8826   def detect_requires(self, path):
8827     reqs = []
8828hunk ./contrib/musicplayer/manage.py 95
8829       if not os.path.isdir(req_path) and not req_path.endswith('.js'):       
8830         reqs[i] += '.js'
8831     
8832-    #if len(reqs):
8833-    #  print '%s depends on:' % os.path.basename(path)
8834-    #  print '\t', '\n\t'.join(reqs)
8835-
8836     self.files[path] = reqs
8837 
8838hunk ./contrib/musicplayer/manage.py 97
8839-  def parse(self, path):
8840+  def parse(self, path, syntax_check = False):
8841     if path in self.included:
8842       return ''
8843     if not path.endswith('.js'):
8844hunk ./contrib/musicplayer/manage.py 104
8845       # TODO: If path points to a directory, require all the files within that directory.
8846       return ''
8847     
8848+    if syntax_check:
8849+      compiler = ClosureCompiler([path], None)
8850+      if not compiler.syntax_check():
8851+        raise Exception('There seems to be a syntax problem. Fix it.')
8852+   
8853     def insert_code(match):
8854       req_path = os.path.join(self.root, match.group(1))
8855       if not req_path in self.included:
8856hunk ./contrib/musicplayer/manage.py 115
8857         if not os.path.isfile(req_path):
8858           raise Exception('%s requires non existing file: %s' % (path, req_path))
8859           
8860-        return self.parse(req_path)
8861+        return self.parse(req_path, syntax_check)
8862     
8863     script_file = open(path, 'r')
8864     script = script_file.read()
8865hunk ./contrib/musicplayer/manage.py 161
8866       shutil.rmtree('build')
8867     
8868     shutil.copytree('src/resources', 'build/resources')
8869-    shutil.copytree('src/plugins', 'build/plugins')
8870     shutil.copy('src/config.example.json', 'build/')
8871     shutil.copy('src/index.html', 'build/')
8872     
8873hunk ./contrib/musicplayer/manage.py 176
8874     
8875     self._make_js('Application.js', 'build/js/app.js')
8876     
8877-    #deps.write_to_file('build/app.js')
8878-    #self._compress('build/js/app.js', ['build/app.js'])
8879-    #os.remove('build/app.js')
8880-   
8881     for worker in os.listdir('src/workers'):
8882       if worker.endswith('.js'):
8883         self._make_js('workers/' + worker, 'build/js/workers/' + worker)
8884hunk ./contrib/musicplayer/manage.py 179
8885-        #deps.write_to_file('build/worker_' + worker, root_file = 'workers/' + worker)
8886     
8887     print 'Done!'
8888   
8889hunk ./contrib/musicplayer/manage.py 182
8890-  #def _compile_js(self, source, output_file, files = None, join = True):
8891-  #  js_files = files
8892-  #  if not js_files:
8893-  #    js_files = []
8894-  #    for filename in os.listdir(source):
8895-  #      if filename.endswith('.js'):
8896-  #        js_files.append(os.path.join(source, filename))
8897-  #       
8898-  #    js_files.sort()
8899-  #  else:
8900-  #    js_files = [os.path.join(source, path) for path in files]
8901-  # 
8902-  #  if join:
8903-  #    self._compress(output_file, js_files)
8904-  #  else:
8905-  #    for js_file in js_files:
8906-  #      self._compress(output_file + os.path.basename(js_file), [js_file])
8907
8908   def _make_js(self, root, output):
8909     tmp_file = mkstemp()[1]
8910     self.deps.write_to_file(tmp_file, root_file = root)
8911hunk ./contrib/musicplayer/manage.py 185
8912-    self._compress(output, [tmp_file])
8913+    compiler = ClosureCompiler([tmp_file], output)
8914+    compiler.compile(self.compilation_level)
8915     os.remove(tmp_file)
8916hunk ./contrib/musicplayer/manage.py 188
8917
8918-  def _compress(self, output_file, files):
8919-    print 'Compressing %s...' % output_file
8920-   
8921-    if self.compilation_level == 'NONE':
8922-      output_file = open(output_file, 'a')
8923-      for filename in files:
8924-        f = open(filename)
8925-        output_file.write(f.read())
8926-        output_file.write('\n')
8927-        f.close()
8928-
8929-      output_file.close()
8930-    else:
8931-      args = [
8932-        'java',
8933-        '-jar',                 CLOSURE_COMPILER_PATH,
8934-        '--warning_level',      'QUIET',
8935-        '--compilation_level',  self.compilation_level,
8936-        '--js_output_file',     output_file]
8937-     
8938-      for filename in files:
8939-        args.append('--js')
8940-        args.append(filename)
8941-
8942-      subprocess.call(args)
8943 
8944 class Watch(Build):
8945   description = 'watches src directory for changes and runs build command when they occur'
8946hunk ./contrib/musicplayer/manage.py 283
8947     pass
8948     
8949   def run(self):
8950-    args = [
8951-      'java',
8952-      '-jar',             CLOSURE_COMPILER_PATH,
8953-      '--js_output_file', '_tmp.out',
8954-      '--dev_mode',       'START_AND_END']
8955-   
8956+    test_files = []
8957     for filename in os.listdir('tests'):
8958       if filename.endswith('.js'):
8959hunk ./contrib/musicplayer/manage.py 286
8960-        args.append('--js')
8961-        args.append('tests/' + filename)
8962+        test_files.append('tests/' + filename)
8963     
8964hunk ./contrib/musicplayer/manage.py 288
8965-    r = subprocess.call(args)
8966-    if r == 0:
8967-      print 'Everything seems to be okay! Yay!'
8968-   
8969-    os.remove('_tmp.out')
8970+    compiler = ClosureCompiler(test_files, None)
8971+    if compiler.syntax_check():
8972+      print 'Everything seems okay. Yay!'
8973 
8974 setup(
8975   name = 'tahoe-music-player',
8976hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 75
8977       this._goal.checkpoint("scanner");
8978       return;
8979     }
8980+    if(cap.debug) {
8981+      console.log("SCANNER", cap.msg, cap.obj);
8982+      return;
8983+    }
8984     
8985     if(da.db.DEFAULT.views.Song.view.findRow(cap) === -1)
8986       this.indexer.postMessage(cap);
8987hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 90
8988       return;
8989     }
8990     
8991+    if(event.data.debug) {
8992+      console.log("INDEXER", event.data.msg, event.data.obj);
8993+      return;
8994+    }
8995+   
8996     // Lots of async stuff is going on, a short summary would look something like:
8997     // 1. find or create artist with given name and save its id
8998     //    to artist_id.
8999hunk ./contrib/musicplayer/src/controllers/Player.js 65
9000     });
9001     
9002     da.app.addEvent("ready", this.initializeUI.bind(this));
9003+   
9004+    this.preloadImages()
9005   },
9006   
9007   initializeUI: function () {
9008hunk ./contrib/musicplayer/src/controllers/Player.js 179
9009    **/
9010   play: function (song) {
9011     var np = this.nowPlaying;
9012-    if(!song || (song && np.song && np.song.id === song.id))
9013+    if(song && np.song && np.song.id === song.id)
9014       return;
9015     
9016     this.elements.play.addClass("active");
9017hunk ./contrib/musicplayer/src/controllers/Player.js 184
9018     
9019+    if(!song && np.sound.paused) {
9020+      np.sound.resume();
9021+      return;
9022+    }
9023+   
9024     if(this.sounds[song.id]) {
9025       np.sound.stop();
9026       this.sounds[song.id].play();
9027hunk ./contrib/musicplayer/src/controllers/Player.js 209
9028       
9029       onload: function () {
9030         this._loading.remove(song.id);
9031+       
9032         if(!song.get("duration"))
9033           song.update({duration: this.duration});
9034       },
9035hunk ./contrib/musicplayer/src/controllers/Player.js 224
9036       },
9037       
9038       whileplaying: function () {
9039-        if(Player.nowPlaying.sound === this)
9040-          Player.progress_bar.setProgress("track", this.position/this.durationEstimate);
9041+        if(Player.nowPlaying.sound === this) {
9042+          Player.progress_bar.setProgress("track", this.position / this.durationEstimate);
9043+          Player.progress_bar.toElement().title = this.position + "/" + this.durationEstimate; 
9044+        }
9045       },
9046       
9047       onfinish: function () {
9048hunk ./contrib/musicplayer/src/controllers/Player.js 338
9049       this.play(song);
9050     
9051     if(this.nowPlaying.sound.paused) {
9052-      this.nowPlaying.sound.resume();
9053+      this.play();
9054     } else
9055       this.pause();
9056   },
9057hunk ./contrib/musicplayer/src/controllers/Player.js 422
9058   },
9059   
9060   /**
9061+   *  Player#preloadImages() -> undefined
9062+   **/
9063+  preloadImages: function () {
9064+    var images = [
9065+      "next", "next_active", "previous", "previous_active", "play",
9066+        "album_cover_1", "album_cover_2", "album_cover_3"
9067+      ],
9068+      n = images.length,
9069+      i = null;
9070+
9071+    while(n--) {
9072+      i = new Image();
9073+      i.src = "resources/images/" + images[n] + ".png";
9074+    }
9075+
9076+    delete images;
9077+    delete n;
9078+  },
9079
9080+  /**
9081    *  Player#free() -> undefined
9082    *
9083    *  Frees memory taken by loaded songs. This method is ran about every
9084hunk ./contrib/musicplayer/src/controllers/Player.js 460
9085       sound = this.sounds[id];
9086       if(this.sounds.hasOwnProperty(id)
9087         && this.nowPlaying.song.id !== id
9088-        && (
9089-          sound._last_played < eight_mins_ago || !sound.loaded
9090-        ))
9091+        && (sound._last_played >= eight_mins_ago || !sound.loaded))
9092       {
9093hunk ./contrib/musicplayer/src/controllers/Player.js 462
9094+        console.log("Freed sound ", id, sound._last_played);
9095         sound.destruct();
9096         delete this.sounds[id];
9097       }
9098addfile ./contrib/musicplayer/src/controllers/SongContext.js
9099hunk ./contrib/musicplayer/src/controllers/SongContext.js 1
9100+//#require "controllers/Player.js"
9101+
9102+(function () {
9103+var Song = da.db.DocumentTemplate.Song,
9104+    Player = da.controller.Player;
9105+
9106+var CONTEXTS = {},
9107+    TAB_SUFFIX = "_context_tab",
9108+    _TS_L = -TAB_SUFFIX.length,
9109+    TAB_CONTAINER_SUFFIX = "_context_tab_container";
9110+
9111+/** section: Controllers
9112+ * SongContext
9113+ **/
9114+var SongContext = {
9115+  /**
9116+   *  SongContext.active -> Object
9117+   **/
9118+  active: null,
9119
9120+  initialize: function () {
9121+    this.el = new Element("div", {
9122+      id: "song_context"
9123+    });
9124+    this.tabs = new Element("div", {
9125+      id: "context_tabs",
9126+      "class": "no_selection"
9127+    });
9128+   
9129+    for(var id in CONTEXTS)
9130+      this.tabs.grab(new Element("a", {
9131+        id:       id + TAB_SUFFIX,
9132+        "class":  "tab",
9133+        href:     "#",
9134+        html:     CONTEXTS[id].title
9135+      }));
9136+    this.tabs.addEvent("click:relay(.tab)", function () {
9137+      SongContext.show(this.id.slice(0, _TS_L));
9138+    });
9139+   
9140+    this.loading_screen = new Element("div", {
9141+      id:       "song_context_loading",
9142+      html:     "Loading...",
9143+      "class":  "no_selection",
9144+      style:    "display: none"
9145+    });
9146+   
9147+    $("player_pane").adopt(this.tabs, this.loading_screen, this.el);
9148+   
9149+    Player.addEvent("play", function (song) {
9150+      if(SongContext.active)
9151+        SongContext.active.update(song);
9152+    });
9153+     
9154+    window.addEvent("resize", function () {
9155+      SongContext.el.style.height = (
9156+        window.getHeight() - $("song_info_block").getHeight() - SongContext.tabs.getHeight()
9157+      ) + "px";
9158+    });
9159+    window.fireEvent("resize");
9160+    this.initialized = true;
9161+  },
9162
9163+  /**
9164+   *  SongContext#show(id) -> undefined
9165+   *  - id (String): id of the context.
9166+   **/
9167+  show: function (id) {   
9168+    this.hide();
9169+   
9170+    var context = CONTEXTS[id];
9171+    if(!context.__initialized) {
9172+      var container = new Element("div", {id: id + TAB_CONTAINER_SUFFIX});
9173+      context.initialize(container);
9174+      container.hide();
9175+      this.el.grab(container);
9176+     
9177+      delete container;
9178+      context.__initialized = true;
9179+    }
9180+   
9181+    $(id + TAB_SUFFIX).addClass("active");
9182+    this.active = context;
9183+   
9184+    da.controller.SongContext.showLoadingScreen();
9185+    $(id + TAB_CONTAINER_SUFFIX).show();
9186+    context.show();
9187+    context.update(Player.nowPlaying());
9188+  },
9189
9190+  /**
9191+   *  SongContext#hide() -> undefined
9192+   *
9193+   *  Hides active tab.
9194+   *
9195+   **/
9196+  hide: function () {
9197+    if(!this.active)
9198+      return;
9199+   
9200+    this.active.hide();
9201+    $(this.active.id + TAB_CONTAINER_SUFFIX).hide();
9202+    $(this.active.id + TAB_SUFFIX).removeClass("active");
9203+  },
9204
9205+  /**
9206+   *  SongContext#addTab(id) -> undefined
9207+   *  - id (String): id of the context.
9208+   *
9209+   **/
9210+  addTab: function (id) {
9211+    this.tabs.grab(new Element("a", {
9212+      id:       id + TAB_SUFFIX,
9213+      "class":  "tab",
9214+      href:     "#",
9215+      html:     CONTEXTS[id].title
9216+    }));
9217+  }
9218+};
9219+
9220+da.app.addEvent("ready", function () {
9221+  SongContext.initialize();
9222+});
9223+
9224+/**
9225+ * da.controller.SongContext
9226+ **/
9227+da.controller.SongContext = {
9228+  /**
9229+   *  da.controller.SongContext.register(context) -> undefined
9230+   *  - context.id (String): id of the context. Note that the "root" element of
9231+   *    the context also has to have the same id.
9232+   *  - context.title (String): human-frendly name of the context.
9233+   *  - context.initialize (Function): function called only once, with the container
9234+   *    element as the first argument. All DOM nodes should be added to that element.
9235+   *  - context.show (Function): called every time context's tab is activated.
9236+   *  - context.hide (Function): called when context's tab gets hidden.
9237+   *  - context.update (Function): called when another song starts playing.
9238+   *    The first argument of the function is the new song ([[da.db.DocumentTemplate.Song]]).
9239+   *
9240+   *  #### Example
9241+   *      da.controller.SongContext.register({
9242+   *        id: "artist-info",
9243+   *        initialize: function (container) {
9244+   *          this.el = new Element("div", {id: "artist-info", html: "Hai world!"});
9245+   *          this._shown = false;
9246+   *          // Try to limit youself by putting all needed nodes into
9247+   *          // container element.
9248+   *          container.grab(this.el);
9249+   *
9250+   *        },
9251+   *        show: function () {
9252+   *          // Called everytime this tab is activated.
9253+   *          if(!this._shown) {
9254+   *            this.el.position({relativeTo: this.el.parent()});
9255+   *            this._shown = true;
9256+   *          }
9257+   *        },
9258+   *        hide: function () {
9259+   *          // Called when tab is hidden, use this to stop updating document
9260+   *          // nodes, etc.
9261+   *        },
9262+   *        update: function (song) {
9263+   *          // Called when new song starts playing.
9264+   *        }
9265+   *      }));
9266+   *
9267+   *  #### Notes
9268+   *  When the context is activated for the first time, functions all called in
9269+   *  following order:
9270+   *  * `initialize(container)`
9271+   *  * `show`
9272+   *  * `update(song)`
9273+   *
9274+   *  `show` and `hide` methods should not implement hiding of the "root" element,
9275+   *  rather, adding/removing obsolete events and/or start/stop updating nodes.
9276+   *
9277+   **/
9278+  register: function (context) {
9279+    if(CONTEXTS[context.id])
9280+      return;
9281+   
9282+    CONTEXTS[context.id] = context;
9283+   
9284+    if(SongContext.initialized)
9285+      SongContext.addTab(context.id);
9286+  },
9287
9288+  /**
9289+   *  da.controller.SongContext.show(id) -> undefined
9290+   *  - id (String): id of the tab/context.
9291+   **/
9292+  show: function (id) {
9293+    var active = SongContext.active;
9294+    if((active && active.id === id) || !CONTEXTS[id])
9295+        return;
9296+   
9297+    delete active;
9298+    SongContext.show(id);
9299+  },
9300
9301+  /**
9302+   *  da.controller.SongContext.showLoadingScreen() -> undefined
9303+   **/
9304+  showLoadingScreen: function () {
9305+    var screen = SongContext.loading_screen,
9306+        el = SongContext.el;
9307+    screen.style.width = el.getWidth() + "px";
9308+    screen.style.height = el.getHeight() + "px";
9309+    screen.show();
9310+   
9311+    delete screen;
9312+    delete el;
9313+  },
9314
9315+  /**
9316+   *  da.controller.SongContext.hideLoadingScreen() -> undefined
9317+   **/
9318+  hideLoadingScreen: function () {
9319+    SongContext.loading_screen.hide();
9320+  }
9321+};
9322+
9323+da.app.fireEvent("ready.controller.SongContext", [], 1);
9324+
9325+})();
9326+
9327+//#require "controllers/default_contexts.js"
9328hunk ./contrib/musicplayer/src/controllers/controllers.js 13
9329 if(typeof da.controller === "undefined")
9330   da.controller = {};
9331 
9332+//#require "controllers/Settings.js"
9333 //#require "controllers/Navigation.js"
9334 //#require "controllers/Player.js"
9335hunk ./contrib/musicplayer/src/controllers/controllers.js 16
9336-//#require "controllers/Settings.js"
9337+//#require "controllers/SongContext.js"
9338 //#require "controllers/CollectionScanner.js"
9339hunk ./contrib/musicplayer/src/controllers/default_columns.js 168
9340   initialize: function (options) {
9341     this.parent(options);
9342     
9343-    this._el.style.width = "300px";
9344-    this._playlist = [];
9345-   
9346     this.addEvent("click", function (item, event, el) {
9347       da.controller.Player.setPlaylist(this._playlist);
9348       da.controller.Player.play(Song.findById(item.id));
9349hunk ./contrib/musicplayer/src/controllers/default_columns.js 171
9350-    });
9351+    }.bind(this), true);
9352   },
9353   
9354   view: {
9355hunk ./contrib/musicplayer/src/controllers/default_columns.js 187
9356     }
9357   },
9358   
9359-  mapReduceUpdated: function (result) {
9360-    this.parent(result);
9361-   
9362+  mapReduceFinished: function (values) {
9363+    this.parent(values);
9364+    this.createPlaylist();
9365+  },
9366
9367+  mapReduceUpdated: function (values) {
9368+    this.parent(values);
9369+    this.createPlaylist();
9370+  },
9371
9372+  createPlaylist: function () {
9373     var n = this.options.totalCount,
9374         playlist = new Array(n);
9375     
9376hunk ./contrib/musicplayer/src/controllers/default_columns.js 202
9377     while(n--)
9378-      playlist = this._rows[n].id;
9379+      playlist[n] = this._rows[n].id;
9380     
9381     this._playlist = playlist;
9382     delete playlist;
9383addfile ./contrib/musicplayer/src/controllers/default_contexts.js
9384hunk ./contrib/musicplayer/src/controllers/default_contexts.js 1
9385+//#require "services/artistInfo.js"
9386+//#require "services/recommendations.js"
9387+//#require "services/musicVideo.js"
9388+//#require "libs/ui/Dialog.js"
9389+//#require "doctemplates/Song.js"
9390+//#require "controllers/Player.js"
9391+
9392+(function () {
9393+var SongContext           = da.controller.SongContext,
9394+    Song                  = da.db.DocumentTemplate.Song,
9395+    Player                = da.controller.Player,
9396+    Dialog                = da.ui.Dialog,
9397+    fetchArtistInfo       = da.service.artistInfo,
9398+    fetchRecommendations  = da.service.recommendations;
9399+
9400+SongContext.register({
9401+  id: "artist_info",
9402+  title: "Artist",
9403
9404+  initialize: function (container) {
9405+    this.el = container;
9406+    var els = {
9407+      photo_wrapper:  new Element("div", {id: "artist_photo_wrapper"}),
9408+      photo:          new Element("img", {id: "artist_photo"}),
9409+      photo_chooser:  new Element("div", {id: "artist_photo_chooser"}),
9410+      zoomed_photo:   new Element("img", {id: "artist_photo_zoomed"}),
9411+      bio:            new Element("div", {id: "artist_bio"}),
9412+      stats:          new Element("div", {id: "artist_stats"}),
9413+      top_songs:      new Element("ol",  {id: "artist_top_tracks", "class": "context_column"}),
9414+      top_albums:     new Element("ol",  {id: "artist_top_albums", "class": "context_column middle_context_column"}),
9415+      events:         new Element("ul",  {id: "artist_events",     "class": "context_column"})
9416+    };
9417+   
9418+    var clear = new Element("div", {"class": "clear"});
9419+   
9420+    els.stats.adopt(els.top_songs, els.top_albums, els.events, clear);
9421+    els.photo_wrapper.adopt(els.photo, els.photo_chooser);
9422+    this.el.adopt(els.photo_wrapper, els.bio, clear.clone(), els.stats);
9423+   
9424+    els.photo_chooser.addEvent("click:relay(a)", function (event) {
9425+      var index = event.target.retrieve("photo_index");
9426+      if(typeof index === "number")
9427+        this.switchPhoto(index);
9428+    }.bind(this));
9429+   
9430+    this.photo_zoom = new Dialog({
9431+      html: els.zoomed_photo
9432+    });
9433+   
9434+    els.photo.addEvent("click", function (event) {
9435+      this.elements.zoomed_photo.src = this.active_photo.original;
9436+      this.photo_zoom.show();
9437+    }.bind(this));
9438+   
9439+    els.top_songs.addEvent("click:relay(li)", function (event) {
9440+      var index;
9441+      if(event.target.nodeName.toLowerCase() === "a")
9442+        index = event.target.parentNode.retrieve("list_position")
9443+      else
9444+        index = event.target.retrieve("list_position");
9445+     
9446+      if(typeof index !== "number")
9447+        return;
9448+     
9449+      var song_data = this.artist_info.top_tracks[index];
9450+      // TODO: Make an API for this type of things, it should also
9451+      //        update the navigation columns to show the
9452+      song_data.artist_id = this._current_artist.id;
9453+      Song.findFirst({
9454+        properties: song_data,
9455+        onSuccess: function (song) {
9456+          Player.play(song);
9457+        }
9458+      });
9459+     
9460+    }.bind(this));
9461+   
9462+    this.elements = els;
9463+    this._current_artist = {id: null};
9464+    delete els;
9465+    delete clear;
9466+  },
9467
9468+  show: $empty,
9469+  hide: $empty,
9470
9471+  update: function (song) {
9472+    song.get("artist", function (artist) {
9473+      if(this._current_artist.id === artist.id)
9474+        return !!SongContext.hideLoadingScreen();
9475+     
9476+      this._current_artist = artist;
9477+      fetchArtistInfo(artist, function (info) { 
9478+        this.artist_info = info;
9479+        var els = this.elements;
9480+       
9481+        els.bio.innerHTML = info.bio.summary;
9482+        els.photo.title = artist.get("title");
9483+        this.switchPhoto(0);
9484+       
9485+        this.updatePhotoChooser();
9486+        this.updateLists();
9487+       
9488+        SongContext.hideLoadingScreen();
9489+   
9490+        delete els;
9491+      }.bind(this));
9492+    }.bind(this));
9493+  },
9494
9495+  updatePhotoChooser: function () {
9496+    var pc      = this.elements.photo_chooser,
9497+        info    = this.artist_info,
9498+        photos  = info.photos,
9499+        n       = photos.length,
9500+        bullets = new Array(n);
9501+   
9502+    pc.dispose();
9503+    pc.empty();
9504+   
9505+    while(n--)
9506+      bullets[n] = (new Element("a", {
9507+        html: "&bull;",
9508+        href: "#"
9509+      })).store("photo_index", n);
9510+   
9511+    bullets.push(new Element("a", {
9512+      html: "+",
9513+      href: info.more_photos_url,
9514+      title: "More photos of " + this._current_artist.get("title"),
9515+      target: "_blank"
9516+    }));
9517+   
9518+    pc.adopt(bullets);
9519+    this.elements.photo_wrapper.grab(pc);
9520+   
9521+    delete pc;
9522+    delete bullets;
9523+    delete photos;
9524+    delete info;
9525+  },
9526
9527+  updateLists: function () {
9528+    var els         = this.elements,
9529+        info        = this.artist_info,
9530+        events      = info.events,
9531+        n;
9532+   
9533+    this.renderList("Top Songs",  $A(info.top_tracks || []), els.top_songs);
9534+    this.renderList("Top Albums", $A(info.top_albums || []), els.top_albums);
9535+    this.renderList("Events",     $A(info.events || []),     els.events);
9536+   
9537+    var max_height = Math.max(
9538+      els.top_songs.getHeight(),
9539+      els.top_albums.getHeight(),
9540+      els.events.getHeight()
9541+    );
9542+    els.top_albums.style.height = max_height + "px";
9543+   
9544+    delete els;
9545+    delete info;
9546+    delete events;
9547+  },
9548
9549+  renderList: function (title, items, el) {
9550+    var n = items.length;
9551+    if(!n) {
9552+      el.empty();
9553+      return;
9554+    }
9555+   
9556+    var item;
9557+    while(n--) {
9558+      item = items[n];
9559+      items[n] = (new Element("li"))
9560+        .store("list_position", n)
9561+        .grab(new Element("a", {
9562+          html:   item.title,
9563+          title:  item.title,
9564+          href:   item.url ? item.url : "#",
9565+          target: item.url ? "_blank" : ""
9566+        }));
9567+    }
9568+
9569+    el.empty().adopt(items);
9570+    (new Element("li", {
9571+      "class": "title",
9572+      "html":  title
9573+    })).inject(el, "top");
9574+   
9575+    delete el;
9576+    delete items;
9577+    delete item;
9578+  },
9579
9580+  switchPhoto: function (n) {
9581+    this.active_photo = this.artist_info.photos[n];
9582+    this.elements.photo.src = this.active_photo.extralarge;
9583+  }
9584+});
9585+
9586+SongContext.register({
9587+  id: "recommendations",
9588+  title: "Recommendations",
9589
9590+  initialize: function (container) {
9591+    this.el = container;
9592+    var els = {
9593+      artists_title:  new Element("h4", {html: "Artist you might like"}),
9594+      artists:        new Element("div", {id: "recommended_artists"}),
9595+      songs_title:    new Element("h4", {html: "Songs you should check out"}),
9596+      songs:          new Element("ul", {id: "recommended_songs", "class": "context_column"})
9597+    };
9598+     
9599+    this.el.adopt(els.artists_title, els.artists, els.songs_title, els.songs);
9600+    this.elements = els;
9601+    delete els;
9602+  },
9603
9604+  show: $empty,
9605+  hide: $empty,
9606
9607+  update: function (song) {
9608+    fetchRecommendations(song, function (rec) {
9609+      this.updateArtists($A(rec.artists || []));
9610+      this.updateSongs($A(rec.songs || []));
9611+      delete rec;
9612+      SongContext.hideLoadingScreen();
9613+    }.bind(this));
9614+  },
9615
9616+  updateArtists: function (recommendations) {
9617+    this.elements.artists.empty();
9618+    if(!recommendations.length)
9619+      return !!this.elements.artists_title.hide();
9620+    else
9621+      this.elements.artists_title.show();
9622+   
9623+    recommendations = recommendations.slice(0, 5);
9624+    var n = recommendations.length, rec;
9625+    while(n--) {
9626+      rec = recommendations[n];
9627+      recommendations[n] = (new Element("a", {href: "#"}))
9628+        .grab(new Element("img", {
9629+          src:    rec.image,
9630+          title:  rec.title
9631+        }))
9632+        .appendText(rec.title);
9633+    }
9634+   
9635+    this.elements.artists.adopt(recommendations);
9636+    delete recommendations;
9637+    delete rec;
9638+  },
9639
9640+  updateSongs: function (recommendations) {
9641+    this.elements.songs.empty();
9642+    if(!recommendations.length)
9643+      return !!this.elements.songs_title.hide();
9644+    else
9645+      this.elements.songs_title.show();
9646+   
9647+    var n = recommendations.length, rec;
9648+    while(n--) {
9649+      rec = recommendations[n];
9650+      recommendations[n] = (new Element("li"))
9651+        .grab(new Element("a", {
9652+          href: "#",
9653+          html: "<strong>{title}</strong> by {artist}".interpolate(rec)
9654+        }));
9655+    }
9656+   
9657+    this.elements.songs.adopt(recommendations);
9658+    delete recommendations;
9659+    delete rec;
9660+  }
9661+});
9662+
9663+SongContext.register({
9664+  id: "videos",
9665+  title: "Music Videos",
9666
9667+  initialize: function (container) {
9668+    this.el = container;
9669+    this.video = new Element("iframe", {
9670+      id:           "youtube_music_video",
9671+      type:         "text/html",
9672+      width:        640,
9673+      height:       385,
9674+      frameborder:  0,
9675+      "class":      "youtube-player"
9676+    });
9677+    this.search_results = new Element("ul", {
9678+      id: "video_search_results",
9679+      "class": "context_column no_selection"
9680+    });
9681+   
9682+    this.search_results.addEvent("click:relay(li)", function (event, el) {
9683+      var video_id = el.retrieve("video_id");
9684+     
9685+      if(typeof video_id === "undefined")
9686+        return;
9687+     
9688+      this.video.src = "http://www.youtube.com/embed/" + video_id;
9689+      this.video_dialog.show();
9690+    }.bind(this));
9691+   
9692+    this.video_dialog = new Dialog({
9693+      html: this.video,
9694+      onShow: function () {
9695+        Player.pause()
9696+      },
9697+      onHide: function () {
9698+        this.video.src = "about:blank";
9699+        setTimeout(function () {
9700+          Player.play();
9701+        }, 1000);
9702+      }.bind(this)
9703+    });
9704+   
9705+    this.el.grab(this.search_results);
9706+  },
9707
9708+  show: $empty,
9709+  hide: $empty,
9710
9711+  update: function (song) {
9712+    SongContext.showLoadingScreen();
9713+    da.service.musicVideo(song, this.updateSearchResults.bind(this));
9714+  },
9715
9716+  updateSearchResults: function (results) {
9717+    this.search_results.empty();
9718+   
9719+    if(!results)
9720+      return;
9721+   
9722+    var n = results.length, video;
9723+   
9724+    while(n--) {
9725+      video = results[n];
9726+      results[n] = (new Element("li"))
9727+        .store("video_id", video.id)
9728+        .grab((new Element("a")).adopt(
9729+          new Element("img", {
9730+            src:   video.thumbnail.sqDefault,
9731+            title: video.title
9732+          }),
9733+          new Element("strong", {
9734+            html:     video.title
9735+          }),
9736+//          new Element("small", {
9737+//            html:     (new Date(video.duration)).format("%M:%S")
9738+//          }),
9739+          new Element("p", {
9740+            html: video.description.slice(0, 110) + "&hellip;"
9741+          })
9742+        ));
9743+    }
9744+   
9745+    this.search_results.adopt(results);
9746+    SongContext.hideLoadingScreen();
9747+    delete results;
9748+    delete video;
9749+  }
9750+});
9751+
9752+})();
9753hunk ./contrib/musicplayer/src/doctemplates/doctemplates.js 12
9754 //#require "doctemplates/Artist.js"
9755 //#require "doctemplates/Album.js"
9756 //#require "doctemplates/Song.js"
9757-//#require "doctemplates/Playlist.js"
9758 
9759hunk ./contrib/musicplayer/src/index_devel.html 65
9760     <script src="libs/vendor/LastFM.js" type="text/javascript" charset="utf-8"></script>
9761     <script src="services/lastfm.js" type="text/javascript" charset="utf-8"></script>
9762     <script src="services/albumCover.js" type="text/javascript" charset="utf-8"></script>
9763+    <script src="services/artistInfo.js" type="text/javascript" charset="utf-8"></script>
9764+    <script src="services/recommendations.js" type="text/javascript" charset="utf-8"></script>
9765+    <script src="services/musicVideo.js" type="text/javascript" charset="utf-8"></script>
9766     
9767     <script src="controllers/controllers.js" type="text/javascript" charset="utf-8"></script>
9768     <script src="controllers/Navigation.js" type="text/javascript" charset="utf-8"></script>
9769hunk ./contrib/musicplayer/src/index_devel.html 75
9770     <script src="controllers/Player.js" type="text/javascript" charset="utf-8"></script>
9771     <script src="controllers/Settings.js" type="text/javascript" charset="utf-8"></script>
9772     <script src="controllers/CollectionScanner.js" type="text/javascript" charset="utf-8"></script>
9773+    <script src="controllers/SongContext.js" type="text/javascript" charset="utf-8"></script>
9774+    <script src="controllers/default_contexts.js" type="text/javascript" charset="utf-8"></script>
9775   </head>
9776   <body>
9777     <div id="loader">Loading...</div>
9778hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 36
9779     options = options || {};
9780     this.data = data;
9781     this.offset = options.offset || 0;
9782-    this.length = options.length || 0;
9783+    this.length = options.length || data.length || 0;
9784     this.bigEndian = options.bigEndian || false;
9785     
9786hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 39
9787-    if(typeof data === "string") {
9788-      this.length = this.length || data.length;
9789-    } else {
9790+    //if(typeof data === "string") {
9791+    //  this.length = this.length || data.length;
9792+    //} else {
9793       // In this case we're probably dealing with IE,
9794       // and in order for this to work, VisualBasic-script magic is needed,
9795       // for which we don't have enough of mana.
9796hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 45
9797-      throw Exception("We're lacking some mana. Please use different browser.");
9798-    }
9799+    //  throw Exception("We're lacking some mana. Please use different browser.");
9800+    //}
9801   },
9802   
9803   /**
9804hunk ./contrib/musicplayer/src/libs/util/ID3.js 72
9805   _onFileFetched: function (data) {
9806     var parser = this.parsers[0];
9807     if(parser && parser.test(data)) {
9808-      this.parser = (new parser(data, this.options, this.request));
9809-      delete this.parsers;
9810+      try {
9811+        this.parser = (new parser(data, this.options, this.request));
9812+      } catch(e) {
9813+        this.options.onFailure("parserFailure", e);
9814+        delete e;
9815+      } finally {
9816+        delete this.parsers;
9817+      }
9818     } else
9819       this._getFile(this.parsers.shift());
9820   },
9821hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 44
9822  *
9823  *  Contains know ID3v2 frame types.
9824  **/
9825-var BinaryFile = da.util.BinaryFile,
9826-    CACHE = [],
9827+var BinaryFile    = da.util.BinaryFile,
9828+    CACHE         = [],
9829+    GENRE_REGEXP  = /^\(\d+\)/,
9830+    BE_BOM        = "\xFE\xFF",
9831+    LE_BOM        = "\xFF\xFE",
9832+    UNSYNC_PAIR   = /(\uF7FF\0)/g,
9833+FFLAGS = {
9834+  ALTER_TAG_23:   0x8000,
9835+  ALTER_FILE_23:  0x4000,
9836+  READONLY_23:    0x2000,
9837+  COMPRESS_23:    0x0080,
9838+  ENCRYPT_23:     0x0040,
9839+  GROUP_23:       0x0020,
9840+
9841+  ALTER_TAG_24:   0x4000,
9842+  ALTER_FILE_24:  0x2000,
9843+  READONLY_24:    0x1000,
9844+  GROUPID_24:     0x0040,
9845+  COMPRESS_24:    0x0008,
9846+  ENCRYPT_24:     0x0004,
9847+  UNSYNC_24:      0x0002,
9848+  DATALEN_24:     0x0001
9849+},
9850+
9851 FrameType = {
9852   /**
9853    *  da.util.ID3v2Parser.frameTypes.text(offset, size) -> String
9854hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 74
9855    **/
9856   text: function (offset, size) {
9857     var d = this.data;
9858+   
9859     if(d.getByteAt(offset) === 1) {
9860       // Unicode is being used, and we're trying to detect Unicode BOM.
9861       // (we don't actually care if it's little or big endian)
9862hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 78
9863-      if(d.getByteAt(offset + 1) + d.getByteAt(offset + 2) === 255 + 254) {
9864-        offset += 2;
9865-        size -= 2;
9866-      }
9867+      var test_string = d.getStringAt(offset, 5),
9868+          bom_pos = test_string.indexOf(LE_BOM);
9869+      if(bom_pos === -1)
9870+        bom_pos = test_string.indexOf(BE_BOM);
9871+      if(bom_pos === -1)
9872+        window._ts = test_string;
9873+       
9874+      offset += bom_pos + 1;
9875+      size -= bom_pos + 1;
9876+     
9877+      console.log("Unicode BOM detected", [bom_pos, d.getStringAt(offset, size)]);
9878+     
9879+      //if(d.getByteAt(offset + 1) + d.getByteAt(offset + 2) === 255 + 254) {
9880+      //  offset += 2;
9881+      //  size -= 2;
9882+      //}
9883     }
9884     
9885     return d.getStringAt(offset + 1, size - 1).strip();
9886hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 133
9887   
9888   ignore: $empty
9889 },
9890+
9891 FRAMES = {
9892   // ID3v2.4 tags
9893hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 136
9894-  SEEK: $empty,
9895+  //SEEK: $empty,
9896   
9897   // ID3v2.3 tags
9898   TRCK: function (offset, size) {
9899hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 141
9900     var data = FrameType.text.call(this, offset, size);
9901-    return +data.split("/")[0]
9902+    return +data.split("/")[0];
9903   },
9904   TIT1: FrameType.text,
9905   TIT2: FrameType.text,
9906hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 150
9907   TPE2: FrameType.text,
9908   TALB: FrameType.text,
9909   TYER: FrameType.textNumeric,
9910-  TIME: $empty,
9911+  //TIME: $empty,
9912   TCON: function (offset, size) {
9913     // Genre, can be either "(123)Genre", "(123)" or "Genre".
9914     var data = FrameType.text.call(this, offset, size);
9915hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 154
9916-    return +((data.match(/^\(\d+\)/) || " ")[0].slice(1, -1));
9917+    return +((data.match(GENRE_REGEXP) || " ")[0].slice(1, -1));
9918   },
9919   USLT: FrameType.unsyncedLyrics,
9920   WOAR: FrameType.link,
9921hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 179
9922   WAR: FRAMES.WOAR,
9923   WXX: FRAMES.WXXX
9924 });
9925+
9926 var ID3v2Parser = new Class({
9927   /**
9928    *  new da.util.ID3v2Parser(data, options, request)
9929hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 221
9930       'majorVersion', 'minorVersion', "flags", "size"
9931     ]);
9932     this.version = 2 + (this.header.majorVersion/10) + this.header.minorVersion;
9933-    this.header.size = this.unsync(this.header.size) + 10;
9934+    this.header.size = this.unpad(this.header.size, 7) + 10;
9935     
9936     this.parseFlags();
9937     
9938hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 249
9939   parseFlags: function () {
9940     var flags = this.header.flags;
9941     this.flags = {
9942-      unsync_all:   flags & 0x80,
9943-      extended:     flags & 0x40,
9944-      experimental: flags & 0x20,
9945-      footer:       flags & 0x10
9946+      unsync_all:   !!(flags & 0x80),
9947+      extended:     !!(flags & 0x40),
9948+      experimental: !!(flags & 0x20),
9949+      footer:       !!(flags & 0x10)
9950     };
9951   },
9952hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 255
9953-
9954
9955   /**
9956    *  da.util.ID3v2Parser#parseFrames() -> undefined
9957    *  Calls proper function for parsing frames depending on tag's version.
9958hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 261
9959    **/
9960   parseFrames: function () {
9961+    console.log("parsing ID3v" + this.version + " tag", this.options.url);
9962+   
9963     if(this.version >= 2.3)
9964       this.parseFrames_23();
9965     else
9966hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 278
9967    **/
9968   parseFrames_23: function () {
9969     if(this.version >= 2.4 && this.flags.unsync_all)
9970-      this.data.data = this.unsync(0, this.header.size);
9971-
9972+      this.unsync();
9973+   
9974     var offset = 10,
9975         ext_header_size = this.data.getStringAt(offset, 4),
9976         tag_size = this.header.size;
9977hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 283
9978-
9979+   
9980     // Some tagging software is apparently know for setting
9981     // "extended header present" flag but then ommiting it from the file,
9982     // which means that ext_header_size will be equal to name of a frame.
9983hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 289
9984     if(this.flags.extended && !FRAMES[ext_header_size]) {
9985       if(this.version >= 2.4)
9986-        ext_header_size = this.unsync(ext_header_size) - 4;
9987+        ext_header_size = this.unpad(ext_header_size) - 4;
9988       else
9989         ext_header_size = this.data.getLongAt(10);
9990       
9991hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 296
9992       offset += ext_header_size;
9993     }
9994     
9995+    var foffset, frame_name, frame_size, frame_size_ajd, frame_flags;
9996     while(offset < tag_size) {
9997hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 298
9998-      var foffset     = offset,
9999-          frame_name  = this.data.getStringAt(foffset, 4),
10000-          frame_size  = this.unsync(foffset += 4, 4),
10001-          frame_flags = [this.data.getByteAt(foffset += 4), this.data.getByteAt(foffset += 1)];
10002-      foffset++; // frame_flags
10003+      foffset     = offset;
10004+      frame_name  = this.data.getStringAt(foffset, 4);
10005+      frame_size  = frame_size_adj = this.unpad(foffset += 4, 4);
10006+      frame_flags = this.data.getShortAt(foffset += 5);
10007+      foffset += 1;
10008       
10009hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 304
10010-      if(!frame_size)
10011+      if(!frame_size) {
10012         break;
10013hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 306
10014+      }
10015       
10016hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 308
10017-      if(FRAMES[frame_name] && frame_size)
10018-        this.frames[frame_name] = FRAMES[frame_name].call(this, foffset, frame_size);
10019+      if(this.version >= 2.4) {
10020+        if(frame_flags & (FFLAGS.COMPRESS_24 | FFLAGS.DATALEN_24)) {
10021+          foffset         += 4;
10022+          frame_size      -= 1;
10023+          frame_size_adj  -= 5;
10024+        }
10025+        //if(!this.flags.unsync_all && (frame_flags & FFLAGS.UNSYNC_24))
10026+        //  this.unsync(offset, frame_size_);
10027+      } else {
10028+        if(frame_flags & FFLAGS.COMPRESS_23) {
10029+          console.log("Compressed frame. Sorry.", self.options.url);
10030+          foffset += 4;
10031+        }
10032+      }
10033       
10034hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 323
10035-      //console.log(frame_name, this.frames[frame_name], [foffset, frame_size]);
10036+      if(FRAMES[frame_name])
10037+        this.frames[frame_name] = FRAMES[frame_name].call(this, foffset, frame_size_adj);
10038+     
10039+      //console.log("parsed frame", [frame_name, this.frames[frame_name]]);
10040       offset += frame_size + 10;
10041     }
10042   },
10043hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 337
10044    **/
10045   parseFrames_22: function () {
10046     var offset = 10,
10047-        tag_size = this.header.size;
10048+        tag_size = this.header.size,
10049+        foffset, frame_name, frame_size;
10050     
10051     while(offset < tag_size) {
10052hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 341
10053-      var foffset = offset,
10054-          frame_name = this.data.getStringAt(foffset, 3),
10055-          frame_size = (new BinaryFile(
10056-            "\0" + this.data.getStringAt(foffset += 3, 3),
10057-            {bigEndian:true}
10058-          )).getLongAt(0);
10059+      foffset = offset;
10060+      frame_name = this.data.getStringAt(foffset, 3);
10061+      frame_size = (new BinaryFile(
10062+        "\0" + this.data.getStringAt(foffset += 3, 3),
10063+        {bigEndian:true}
10064+      )).getLongAt(0);
10065       foffset += 3;
10066       
10067       if(!frame_size)
10068hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 361
10069   },
10070 
10071   /**
10072-   *  da.util.ID3v2Parser#unsync(offset, length[, bits = 7]) -> Number
10073-   *  da.util.ID3v2Parser#unsync(string) -> Number
10074-   *  - offset (Number): offset from which so start unsyncing.
10075-   *  - length (Number): length string to unsync.
10076+   *  da.util.ID3v2Parser#unpad(offset, length[, bits = 8]) -> Number
10077+   *  da.util.ID3v2Parser#unpad(string, bits) -> Number
10078+   *  - offset (Number): offset from which so start unpadding.
10079+   *  - length (Number): length string to unpad.
10080    *  - bits (Number): number of bits used.
10081hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 366
10082-   *  - string (String): String to unsync.
10083-   * 
10084-   *  Performs unsyncing process defined in ID3 specification.
10085+   *  - string (String): String to unpad.
10086    **/
10087hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 368
10088-  unsync: function (offset, length, bits) {
10089-    bits = bits || 7;
10090+  unpad: function (offset, size, bits) {
10091+    bits = bits || 8;
10092     var mask = (1 << bits) - 1,
10093         bytes = [],
10094         numeric_value = 0,
10095hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 377
10096     
10097     if(typeof offset === "string") {
10098       data = new BinaryFile(offset, {bigEndian: true});
10099-      length = offset.length;
10100+      if(size)
10101+        bits = size;
10102+      size = offset.length;
10103       offset = 0;
10104     }
10105     
10106hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 383
10107-    if(length) {
10108-      for(var n = offset, m = offset + length; n < m; n++)
10109+    if(size) {
10110+      for(var n = offset, m = n + size; n < m; n++)
10111         bytes.push(data.getByteAt(n) & mask);
10112       
10113       bytes.reverse();
10114hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 403
10115   },
10116   
10117   /**
10118+   *  da.util.ID3v2Parser#unsync() -> undefined
10119+   *  da.util.ID3v2Parser#unsync(offset, length) -> undefined
10120+   *
10121+   *  Unsyncs the data as per ID3 specification.
10122+   *
10123+   **/
10124+  unsync: function (n, m) {
10125+    console.log("unsyncing file", this.options.url);
10126+   
10127+    if(arguments.length) {
10128+      var data = this.data.data,
10129+          part = data
10130+            .slice(n, m)
10131+            .replace(UNSYNC_PAIR, String.fromCharCode(0xF7FF));
10132+     
10133+      this.data.data = data.slice(0, n).concat(part, data.slice(n + m));
10134+    } else
10135+      this.data.data = this.data.data.replace(UNSYNC_PAIR, String.fromCharCode(0xF7FF));
10136+   
10137+    this.data.length = this.data.data.length;
10138+  },
10139
10140+  /**
10141    *  da.util.ID3v2Parser#simplify() -> Object
10142    * 
10143    *  Returns humanised version of data parsed from frames.
10144hunk ./contrib/musicplayer/src/libs/vendor/SoundManager.js 19
10145  *
10146  **/
10147 
10148-(function (window) {
10149+// SoundManager depends on too much of window.* functions,
10150+// thus making it in-efficient to load it inside another closure
10151+// with da.vendor as alias for window.
10152 //#require "libs/vendor/soundmanager/script/soundmanager2.js"
10153hunk ./contrib/musicplayer/src/libs/vendor/SoundManager.js 23
10154-})(da.vendor);
10155+
10156+da.vendor.SoundManager = SoundManager;
10157+da.vendor.soundManager = soundManager;
10158+//delete SoundManager;
10159+//delete soundmanager;
10160 
10161 var url = location.protocol + "//" + location.host + location.pathname.split("/").slice(0, -1).join("/"),
10162     path = location.pathname.contains("devel.html") ? "/libs/vendor/soundmanager/swf/" : "/resources/flash/";
10163hunk ./contrib/musicplayer/src/libs/vendor/SoundManager.js 33
10164 
10165 $extend(da.vendor.soundManager, {
10166-  useHTML5Audio:  false,
10167+  useHTML5Audio:  true,
10168   url:            url + path,
10169   debugMode:      false,
10170   debugFlash:     false
10171hunk ./contrib/musicplayer/src/resources/css/app.css 133
10172 }
10173 
10174 .dialog {
10175+  display: block;
10176   margin: 50px auto 0 auto;
10177   background: #fff;
10178   border: 1px solid #ddd;
10179hunk ./contrib/musicplayer/src/resources/css/app.css 220
10180   z-index: 1002;
10181 }
10182 
10183+/** Tooltips **/
10184+.tooltip {
10185+       color: #fff;
10186+       width: 139px;
10187+       z-index: 13000;
10188+}
10189+
10190+.tool-title {
10191+       font-weight: bold;
10192+       font-size: 11px;
10193+       margin: 0;
10194+       color: #9FD4FF;
10195+       padding: 8px 8px 4px;
10196+       background: url(bubble.png) top left;
10197+}
10198+
10199+.tool-text {
10200+       font-size: 11px;
10201+       padding: 4px 8px 8px;
10202+       background: url(bubble.png) bottom right;
10203+}
10204+
10205 /*** Navigation columns ***/
10206 .column_container {
10207   float: left;
10208hunk ./contrib/musicplayer/src/resources/css/app.css 343
10209   margin-left: 20px;
10210 }
10211 
10212-.navigation_column .active_column_item, .menu_item:hover, #next_song:hover, #prev_song:hover {
10213+.navigation_column .active_column_item, .menu_item:hover, #next_song:hover, #prev_song:hover,
10214+#video_search_results a:active, #video_search_results a:focus {
10215   background-color: #33519d !important;
10216   text-shadow: #000 0 1px 0;
10217   color: #fff !important;
10218hunk ./contrib/musicplayer/src/resources/css/app.css 379
10219   display: block;
10220   
10221   -webkit-box-shadow: rgba(0, 0, 0, 0.5) 0 1px 3px;
10222+  -moz-box-shadow: rgba(0, 0, 0, 0.5) 0 1px 3px;
10223+  -o-box-shadow: rgba(0, 0, 0, 0.5) 0 1px 3px;
10224+  box-shadow: rgba(0, 0, 0, 0.5) 0 1px 3px;
10225 }
10226 
10227 /*** Menus ***/
10228hunk ./contrib/musicplayer/src/resources/css/app.css 505
10229   width: 150px;
10230   text-align: right;
10231   display: inline-block;
10232+  margin: 0 5px 0 0;
10233 }
10234 
10235 #settings_controls .setting_box label.no_indent {
10236hunk ./contrib/musicplayer/src/resources/css/app.css 513
10237   text-align: left;
10238 }
10239 
10240+#settings_controls input[type="text"], #settings_controls input[type="password"] {
10241+  width: 250px;
10242+}
10243+
10244 #settings_controls .settings_footer {
10245   border-top: 1px solid #c0c0c0;
10246   text-align: right;
10247hunk ./contrib/musicplayer/src/resources/css/app.css 563
10248 #song_info_block {
10249   width: 100%;
10250   background: #f3f3f3;
10251-  border-bottom: 1px solid #ddd;
10252 }
10253 
10254 #song_details {
10255hunk ./contrib/musicplayer/src/resources/css/app.css 606
10256   margin: 10px;*/
10257   max-width: 150px;
10258   border: 1px solid #fff;
10259-  outline: 1px solid #ddd;
10260   
10261   -webkit-box-shadow: #c0c0c0 0 3px 5px;
10262   -moz-box-shadow: #c0c0c0 0 3px 5px;
10263hunk ./contrib/musicplayer/src/resources/css/app.css 611
10264   -o-box-shadow: #c0c0c0 0 3px 5px;
10265   box-shadow: #c0c0c0 0 3px 5px;
10266
10267+  -webkit-border-radius: 3px;
10268+  -moz-border-radius: 3px;
10269+  -o-border-radius: 3px;
10270+  border-radius: 3px;
10271 }
10272 
10273 #song_title {
10274hunk ./contrib/musicplayer/src/resources/css/app.css 674
10275   color: #000;
10276   background: rgba(255, 255, 255, 0.8) no-repeat;
10277   text-shadow: #fff 0 1px 0;
10278+  padding: 1px 0;
10279   
10280   overflow: hidden;
10281   text-overflow: ellipsis;
10282hunk ./contrib/musicplayer/src/resources/css/app.css 681
10283   white-space: nowrap;
10284   
10285   -webkit-box-shadow: rgba(0, 0, 0, 0.4) 0 1px 3px;
10286+  -moz-box-shadow: rgba(0, 0, 0, 0.4) 0 1px 3px;
10287+  -o-box-shadow: rgba(0, 0, 0, 0.4) 0 1px 3px;
10288+  box-shadow: rgba(0, 0, 0, 0.4) 0 1px 3px;
10289
10290+  -webkit-border-radius: 3px;
10291+  -moz-border-radius: 3px;
10292+  -o-border-radius: 3px;
10293+  border-radius: 3px;
10294 }
10295 
10296 #next_song:hover, #prev_song:hover {
10297hunk ./contrib/musicplayer/src/resources/css/app.css 697
10298 
10299 #next_song:active, #prev_song:active {
10300   text-shadow: #000 0 -1px 0;
10301+  padding-top: 3px;
10302+  padding-bottom: 1px;
10303 }
10304 
10305 #next_song {
10306hunk ./contrib/musicplayer/src/resources/css/app.css 724
10307 #prev_song:hover {
10308   background-image: url(../images/previous_active.png);
10309 }
10310+
10311+/*** Song Context ***/
10312+#context_tabs {
10313+  background: #ddd;
10314+  border: 1px solid #c0c0c0;
10315+  border-left: 0;
10316+  border-right: 0;
10317+  width: 100%;
10318+  text-align: center;
10319+  padding: 0;
10320+  text-shadow: #fff 0 1px 0;
10321
10322+  -webkit-box-shadow: #c0c0c0 0px -1px 7px inset;
10323+}
10324+
10325+#context_tabs a.tab {
10326+  text-decoration: none;
10327+  display: inline-block;
10328+  cursor: default;
10329+  padding: 2px 7px;
10330+  margin: 0 0 -1px 0;
10331+  outline: 0;
10332+  border-left: 1px solid #c0c0c0;
10333+}
10334+
10335+#context_tabs a.tab:last-child {
10336+  border-right: 1px solid #c0c0c0;
10337+}
10338+
10339+#context_tabs a.tab:active, #context_tabs a.tab:focus, #context_tabs a.tab.active {
10340+  -webkit-box-shadow: #c0c0c0 0px 1px 5px inset;
10341+  -moz-box-shadow: #c0c0c0 0px 1px 5px inset;
10342+  -o-box-shadow: #c0c0c0 0px 1px 5px inset;
10343+  box-shadow: #c0c0c0 0px 1px 5px inset;
10344+}
10345+
10346+#context_tabs a.tab.active {
10347+  background: #fff;
10348+}
10349+
10350+#song_context {
10351+  overflow: auto;
10352+}
10353+
10354+#song_context_loading {
10355+  background: rgba(255, 255, 255, 0.7);
10356+  position: absolute;
10357+  text-align: center;
10358+  padding-top: 20px;
10359+  font-size: 1.5em;
10360+}
10361+
10362+/** Artist context **/
10363+#artist_photo_wrapper {
10364+  float: left;
10365+  margin: 10px;
10366+  width: 200px;
10367+}
10368+
10369+#artist_photo_wrapper:hover #artist_photo_chooser {
10370+  visibility: visible;
10371+}
10372+
10373+#artist_photo {
10374+  max-width: 200px;
10375
10376+  -webkit-border-radius: 4px;
10377+  -moz-border-radius: 4px;
10378+  -o-border-radius: 4px;
10379+  border-radius: 4px;
10380+}
10381+
10382+#artist_photo_chooser {
10383+  visibility: hidden;
10384+  text-align: center;
10385+  position: relative;
10386+  margin: -30px auto 0 auto;
10387+  font-size: 1.1em;
10388+  background: rgba(0, 0, 0, 0.7);
10389+  width: 90%;
10390+  overflow: hidden;
10391
10392+  -webkit-border-radius: 4px;
10393+  -moz-border-radius: 4px;
10394+  -o-border-radius: 4px;
10395+  border-radius: 4px;
10396+}
10397+
10398+#artist_photo_chooser a {
10399+  display: inline-block;
10400+  padding: 0 5px;
10401+  cursor: default;
10402+  color: #fff;
10403+  outline: 0;
10404+}
10405+
10406+#artist_photo_chooser a:hover, #artist_photo_chooser a:focus {
10407+  text-shadow: #fff 0 0 5px;
10408+}
10409+
10410+#artist_bio {
10411+  padding-top: 10px;
10412+  width: 270px;
10413+  float: left;
10414+}
10415+
10416+#artist_bio a {
10417+  color: #00f;
10418+  text-decoration: underline;
10419+}
10420+
10421+#artist_stats {
10422+  margin: 30px 0 0 5px;
10423+}
10424+
10425+.context_column {
10426+  width: 32%;
10427+  float: left;
10428+  margin-bottom: 0;
10429+}
10430+
10431+.context_column li {
10432+  list-style: none;
10433+  padding: 2px;
10434+  margin: 0;
10435+}
10436+
10437+.context_column li.title {
10438+  margin-bottom: 10px;
10439+  font-weight: bold;
10440+  text-align: center;
10441+}
10442+
10443+.context_column li:nth-child(2n+3) {
10444+  background: #f3f3f3;
10445+}
10446+
10447+.context_column li a {
10448+  display: block;
10449+  overflow: hidden;
10450+  text-overflow: ellipsis;
10451+  white-space: nowrap;
10452+  cursor: default;
10453+}
10454+
10455+.middle_context_column {
10456+  border: 1px solid #ddd;
10457+  border-top: 0;
10458+  border-bottom: 0;
10459+  margin: 0 2px;
10460+  padding: 0 2px;
10461+}
10462+
10463+#recommended_artists {
10464+  width: 98%;
10465+  margin: 5px 0 30px 5px;
10466+  overflow: hidden;
10467+  white-space: nowrap;
10468+}
10469+
10470+#recommended_artists a {
10471+  text-align: center;
10472+  display: inline-block;
10473+  margin: 0 2px;
10474+  font-size: 0.9em;
10475
10476+  -webkit-border-radius: 4px;
10477+  -moz-border-radius: 4px;
10478+  -o-border-radius: 4px;
10479+  border-radius: 4px;
10480+}
10481+
10482+#recommended_artists img {
10483+  display: block;
10484+  margin: 0 auto;
10485+  padding: 0;
10486
10487+  -webkit-border-radius: 4px;
10488+  -moz-border-radius: 4px;
10489+  -o-border-radius: 4px;
10490+  border-radius: 4px;
10491+}
10492+
10493+#recommended_songs {
10494+  float: none;
10495+  width: 98%;
10496+  margin: auto;
10497+}
10498+
10499+#recommended_songs li:nth-child(2n+1), #video_search_results li:nth-child(2n+1) {
10500+  background: #f3f3f3;
10501+}
10502+
10503+#recommendations_context_tab_container h4 {
10504+  font-size: 1.1em;
10505+  margin: 5px 0 0 5px;
10506+}
10507+
10508+#video_search_results {
10509+  width: 100%;
10510+  margin: 0;
10511+  overflow: hidden;
10512+  white-space: nowrap;
10513+}
10514+
10515+#video_search_results li {
10516+  list-style: none;
10517+  border-bottom: 1px solid #ddd;
10518+  min-height: 90px;
10519+  padding: 0;
10520+  overflow: hidden;
10521+}
10522+
10523+#video_search_results a {
10524+  white-space: normal;
10525+}
10526+
10527+#video_search_results img {
10528+  float: left;
10529+  margin: 0 5px 0 0;
10530+  width: 120px;
10531+  height: 90px;
10532+}
10533+
10534+#video_search_results strong {
10535+  display: block;
10536+}
10537+
10538+#video_search_results small {
10539+  color: #c0c0c0;
10540+  font-size: 0.9em;
10541+  display: block;
10542+}
10543+
10544+#video_search_results p {
10545+  display: block;
10546+  margin: 5px 0 0 0;
10547+  float: left;
10548+  width: 360px;
10549+}
10550+
10551hunk ./contrib/musicplayer/src/services/albumCover.js 1
10552-//#require <services/services.js>
10553 //#require "services/lastfm.js"
10554 
10555 (function () {
10556addfile ./contrib/musicplayer/src/services/artistInfo.js
10557hunk ./contrib/musicplayer/src/services/artistInfo.js 1
10558+//#require "services/lastfm.js"
10559+//#require "doctemplates/Artist.js"
10560+//#require "libs/util/Goal.js"
10561+
10562+(function () {
10563+var lastfm = da.service.lastFm,
10564+    Artist = da.db.DocumentTemplate.Artist,
10565+    Goal = da.util.Goal;
10566+
10567+var CACHE = {};
10568+
10569+function fetchArtistInfo(search_params, artist, callback) {
10570+  if(CACHE[artist.id]) {
10571+    if(callback)
10572+      callback(CACHE[artist.id]);
10573+   
10574+    return;
10575+  }
10576+
10577+  var info = {},
10578+      fetch_data = new Goal({
10579+        checkpoints: ["bio", "photos", "events", "toptracks", "topalbums"],
10580+        onFinish: function () {
10581+          CACHE[artist.id] = info;
10582+         
10583+          if(callback)
10584+            callback(info);
10585+         
10586+          delete info;
10587+          delete fetch_data;
10588+        }
10589+        /*,
10590+        after: {
10591+          mbid: function () {
10592+            fetchArtistLinks(info.mbid, function (links) {
10593+              if(links)
10594+                info.links = links;
10595+             
10596+              fetch_data.checkpoint("links");
10597+            });
10598+          }
10599+        } */
10600+      });
10601+
10602+  lastfm.artist.getInfo(search_params, {
10603+    success: function (data) {
10604+      data = data.artist;
10605+      if(data.mbid && data.mbid.length) {
10606+        info.mbid = data.mbid;
10607+        search_params.mbid = data.mbid;
10608+      }
10609+     
10610+      info.bio = data.bio;
10611+      fetch_data.checkpoint("bio");
10612+    },
10613+    failure: function () {
10614+      fetch_data.checkpoint("bio");
10615+    }
10616+  });
10617
10618+  lastfm.artist.getImages(search_params, {
10619+    success: function (data) {
10620+      data = data.images;
10621+      if(!data.image)
10622+        return fetch_data.checkpoint("photos");
10623+     
10624+      if($type(data.image) !== "array")
10625+        data.image = [data.image];
10626+     
10627+      var images = data.image.slice(0, 10),
10628+          n = images.length,
10629+          sizes, m;
10630+     
10631+      while(n--) {
10632+        sizes = images[n].sizes.size;
10633+        m = sizes.length;
10634+        images[n] = {};
10635+        while(m--)
10636+          images[n][sizes[m].name] = sizes[m]["#text"];
10637+      }
10638+     
10639+      info.photos = images;
10640+      info.more_photos_url = data.image[0].url;
10641+     
10642+      delete images;
10643+      delete data;
10644+     
10645+      fetch_data.checkpoint("photos");
10646+    },
10647+    failure: function () {
10648+      fetch_data.checkpoint("photos");
10649+    }
10650+  });
10651
10652+  lastfm.artist.getEvents(search_params, {
10653+    success: function (data) {
10654+      data = data.events;
10655+      if(!data.event)
10656+        return fetch_data.checkpoint("events");
10657+     
10658+      if($type(data.event) !== "array")
10659+        data.event = [data.event];
10660+     
10661+      var events = data.event.slice(0, 10),
10662+           n = events.length, event;
10663+     
10664+      while(n--) {
10665+        event = events[n];
10666+        if(+event.cancelled) {
10667+          delete events[n];
10668+          continue;
10669+        }
10670+       
10671+        var loc = event.venue.location;
10672+        events[n] = {
10673+          id: event.id,
10674+          title: [event.venue.name, loc.city, loc.country].join(", "),
10675+          time: event.startDate,
10676+          url: event.url
10677+        };
10678+      }
10679+     
10680+      info.events = events.clean();
10681+      delete events;
10682+      delete event;
10683+      delete data;
10684+     
10685+      fetch_data.checkpoint("events");
10686+    },
10687+    failure: function () {
10688+      fetch_data.checkpoint("events");
10689+    }
10690+  });
10691
10692+  lastfm.artist.getTopTracks(search_params, {
10693+    success: parseTop("track", info, fetch_data),
10694+    failure: function () {
10695+      fetch_data.checkpoint("toptracks");
10696+    }
10697+  });
10698
10699+  lastfm.artist.getTopAlbums(search_params, {
10700+    success: parseTop("album", info, fetch_data),
10701+    failure: function () {
10702+      fetch_data.checkpoint("topalbums");
10703+    }
10704+  });
10705+}
10706+
10707+function parseTop(what, info, goal) {
10708+  return function (data) {
10709+    data = data["top" + what + "s"];
10710+    if(!data[what])
10711+      return goal.checkpoint("top" + what + "s");
10712+   
10713+    if($type(data[what]) !== "array")
10714+      data[what] = [data[what]];
10715+   
10716+    var items = data[what].slice(0, 10),
10717+         n = items.length,
10718+         item;
10719+   
10720+    while(n--) {
10721+      item = items[n];
10722+      items[n] = {title: item.name};
10723+      if(item.mbid && item.mbid.length)
10724+        items[n].mbid = item.mbid;
10725+    }
10726+   
10727+    info["top_" + what + "s"] = items;
10728+    delete items;
10729+    delete item;
10730+    delete data;
10731+     
10732+    goal.checkpoint("top" + what + "s");
10733+  }
10734+}
10735+
10736+/*
10737+// Cross-domain XML: Fail (unless you use Flash)
10738+function fetchArtistLinks(mbid, callback) {
10739+  var req = new Request({
10740+    url: "http://musicbrainz.org/ws/1/artist/" + mbid,
10741+    onSuccess: function (_, data) {
10742+      window.dx = data;
10743+      console.log(data);
10744+     
10745+      //delete req;
10746+    },
10747+    onFailure: function () {
10748+      if(callback)
10749+        callback(false);
10750+     
10751+      //delete req;
10752+    }
10753+  });
10754
10755+  req.get({
10756+    type: "xml",
10757+    inc:  "url-rels"
10758+  });
10759+}
10760+*/
10761+
10762+/**
10763+ *  da.service.artistInfo(artist, callback) -> undefined
10764+ *  - artist (da.db.DocumentTemplate.Artist): artist whose info needs to be retireved.
10765+ *  - callback (Function): function called when informations are fetched.
10766+ **/
10767+da.service.artistInfo = function artistInfo (artist, callback) {
10768+  var search_params = {};
10769+  if(artist.get("mbid"))
10770+    search_params.mbid = artist.get("mbid");
10771+  else
10772+    search_params.artist = artist.get("title");
10773+   
10774+  fetchArtistInfo(search_params, artist, callback);
10775+};
10776+
10777+})();
10778hunk ./contrib/musicplayer/src/services/lastfm.js 1
10779+//#require <services/services.js>
10780 //#require "libs/vendor/LastFM.js"
10781 
10782 (function () {
10783addfile ./contrib/musicplayer/src/services/musicVideo.js
10784hunk ./contrib/musicplayer/src/services/musicVideo.js 1
10785+//#require <services/services.js>
10786+//#require <doctemplates/Song.js>
10787+
10788+(function () {
10789+var Song = da.db.DocumentTemplate.Song,
10790+    CACHE = {};
10791+
10792+/**
10793+ *  da.service.musicVideo(song, callback) -> undefined
10794+ *  - song (da.db.DocumentTemplate.Song): song who's music videos are needed.
10795+ *  - callback (Function): function to which results will be passed.
10796+ **/
10797+da.service.musicVideo = function (song, callback) {
10798+  if(CACHE[song.id])
10799+    return !!callback(CACHE[song.id]);
10800
10801+  song.get("artist", function (artist) {
10802+    var req = new Request.JSONP({
10803+      url: "http://gdata.youtube.com/feeds/api/videos",
10804+      data: {
10805+        v:        2,
10806+        alt:      "jsonc",
10807+        category: "Music",
10808+        format:   5,
10809+        orderby:  "relevance",
10810+        time:     "all_time",
10811+        q:        artist.get("title") + " " + song.get("title")
10812+      },
10813+      onSuccess: function (results) {
10814+        callback(results.data.items);
10815+        delete req;
10816+      },
10817+      onFailure: function () {
10818+        delete req;
10819+        callback(false);
10820+      }
10821+    });
10822+    req.send();
10823+  });
10824+};
10825+
10826+})();
10827addfile ./contrib/musicplayer/src/services/recommendations.js
10828hunk ./contrib/musicplayer/src/services/recommendations.js 1
10829+//#require "services/lastfm.js"
10830+//#require "libs/util/Goal.js"
10831hunk ./contrib/musicplayer/src/services/recommendations.js 4
10832+(function () {
10833+var DocumentTemplate  = da.db.DocumentTemplate,
10834+    Artist            = DocumentTemplate.Artist,
10835+    Album             = DocumentTemplate.Album,
10836+    Song              = DocumentTemplate.Song,
10837+    Goal              = da.util.Goal,
10838+    lastfm            = da.service.lastFm,
10839+   
10840+    CACHE  = {};
10841+
10842+function getSimilar (what, search_params, callback) {
10843+  console.log("getSimilar", what);
10844+  lastfm[what].getSimilar(search_params, {
10845+    success: function (data) {
10846+      data = data["similar" + what + "s"];
10847+      var items = data[what];
10848+     
10849+      if(!items || typeof items === "string")
10850+        return callback(false);
10851+     
10852+      if($type(items) !== "array")
10853+        items = [items];
10854+      else
10855+        items = items.slice(0, 10);
10856+     
10857+      var n = items.length, item;
10858+      while(n--) {
10859+        item = items[n];
10860+        items[n] = {
10861+          title: item.name,
10862+          image: item.image && item.image.length ? item.image[2]["#text"] : ""
10863+        };
10864+       
10865+        if(item.mbid && item.mbid.length)
10866+          items[n].mbid = item.mbid;
10867+       
10868+        if(what === "track") {
10869+          items[n].artist = item.artist.name;
10870+          if(item.artist.mbid && item.artist.mbid.length)
10871+            items[n].artist_mbid = item.artist.mbid;
10872+        }
10873+      }
10874+     
10875+      callback(items);
10876+      delete items;
10877+      delete data;
10878+    },
10879+    failure: function () {
10880+      callback(false);
10881+    }
10882+  });
10883+}
10884+
10885+/**
10886+ *  da.service.recommendations(song, callback) -> undefined
10887+ *  - song (da.db.DocumentTemplate.Song): songs for which recommendations are needed.
10888+ *  - callback (Function): called once recommended songs and albums are fetched.
10889+ **/
10890+da.service.recommendations = function (song, callback) {
10891+  var recommendations = {},
10892+      fetch_data = new Goal({
10893+        checkpoints: ["artists", "songs"],
10894+        onFinish: function () {
10895+          callback(recommendations);
10896+          delete recommendations;
10897+        }
10898+      });
10899
10900+  song.get("artist", function (artist) {
10901+    if(CACHE[artist.id]) {
10902+      recommendations.artists = CACHE[artist.id];
10903+      fetch_data.checkpoint("artists");
10904+    } else
10905+      getSimilar("artist", {artist: artist.get("title"), limit: 10}, function (data) {
10906+        if(data)
10907+          CACHE[artist.id] = recommendations.artists = data;
10908+        fetch_data.checkpoint("artists");
10909+      });
10910+   
10911+    if(CACHE[song.id]) {
10912+      recommendations.songs = CACHE[song.id];
10913+      fetch_data.checkpoint("songs");
10914+    } else { 
10915+      var song_search_params;
10916+      if(song.get("mbid"))
10917+        song_search_params = {mbid: song.get("mbid")};
10918+      else
10919+        song_search_params = {
10920+          track:  song.get("title"),
10921+          artist: artist.get("title")
10922+        };
10923+   
10924+      getSimilar("track", song_search_params, function (data) {
10925+        if(data)
10926+          CACHE[song.id] = recommendations.songs = data;
10927+   
10928+        fetch_data.checkpoint("songs");
10929+      });
10930+    }
10931+  });
10932
10933+};
10934+
10935+})();
10936hunk ./contrib/musicplayer/src/workers/indexer.js 36
10937 
10938 this.da = {};
10939 
10940+var console = {
10941+  log: function (msg, obj) {
10942+    postMessage({debug: true, msg: msg, obj: obj});
10943+  }
10944+};
10945+
10946 //#require "libs/vendor/mootools-1.2.4-core-server.js"
10947 //#require "libs/vendor/mootools-1.2.4-request.js"
10948 //#require <libs/util/util.js>
10949hunk ./contrib/musicplayer/src/workers/indexer.js 64
10950 };
10951 
10952 function getTags(cap) {
10953-  if(!cap) return false;
10954+  if(!cap)
10955+    return false;
10956
10957   var parser = new ID3({
10958     url: "/uri/" + encodeURIComponent(cap),
10959     
10960hunk ./contrib/musicplayer/src/workers/indexer.js 84
10961     },
10962     
10963     onFailure: function (calledBy) {
10964+      console.log("Failed to parse tags for", cap);
10965       parser.destroy();
10966       delete parser;
10967       
10968hunk ./contrib/musicplayer/src/workers/indexer.js 107
10969 }
10970 
10971 function finish () {
10972-  postMessage("**FINISHED**");
10973+  if(!queue.length)
10974+    postMessage("**FINISHED**");
10975+  else
10976+    checkQueue();
10977 }
10978hunk ./contrib/musicplayer/tests/initialize.js 5
10979 
10980 windmill.jsTest.register([
10981   'test_Goal',
10982
10983   'test_BinaryFile',
10984   'test_ID3',
10985   'test_ID3v1',
10986hunk ./contrib/musicplayer/tests/initialize.js 10
10987   'test_ID3v2',
10988+  'test_ID3v22',
10989+  'test_ID3v23',
10990+  'test_ID3v24',
10991
10992   'test_BrowserCouchDict',
10993   'test_BrowserCouch',
10994   'test_BrowserCouch_tempView',
10995hunk ./contrib/musicplayer/tests/initialize.js 20
10996   'test_BrowserCouch_liveView',
10997   'test_DocumentTemplate',
10998   
10999-  'test_CollectionScannerController',
11000   'test_Menu',
11001hunk ./contrib/musicplayer/tests/initialize.js 21
11002-  'test_NavigationController',
11003   'test_Dialog',
11004hunk ./contrib/musicplayer/tests/initialize.js 22
11005-  'test_SettingsController',
11006
11007   'test_ProgressBar',
11008   'test_SegmentedProgressBar',
11009hunk ./contrib/musicplayer/tests/initialize.js 24
11010-  'test_PlayerController'
11011
11012+  'test_CollectionScannerController',
11013+  'test_NavigationController',
11014+  'test_SettingsController',
11015+  'test_PlayerController',
11016+  'test_SongContextController'
11017 ]);
11018hunk ./contrib/musicplayer/tests/test_PlayerController.js 23
11019       !Player.nowPlaying()
11020     );
11021     
11022-    var play_event_fired = false;
11023-    function test_playEvent (song) {
11024-      jum.assertEquals("first argument to callback function should be playing song",
11025-        self.songs[0].id,
11026-        song.id
11027-      );
11028-     
11029-      Player.removeEvent("play", test_PlayEvent);
11030-      delete test_playEvent;
11031-    }
11032+    self.play_event_fired = false;
11033+    Player.addEvent("play", function () {
11034+      self.play_event_fired = true;
11035+      self.play_event_args = arguments;
11036+    });
11037     
11038hunk ./contrib/musicplayer/tests/test_PlayerController.js 29
11039-    Player.addEvent("play", test_playEvent);
11040     Player.play(self.songs[0]);
11041hunk ./contrib/musicplayer/tests/test_PlayerController.js 30
11042-   
11043-    jum.assertTrue("'play' event should have been fired",
11044-      play_event_fired
11045+  };
11046
11047+  this.test_playEvent = {
11048+    method: "waits.forJS",
11049+    params: {
11050+      js: function () { return !!self.play_event_fired }
11051+    }
11052+  };
11053
11054+  this.test_verifyPlayEventArgs = function () {
11055+    jum.assertEquals("first argument to callback function should be playing song",
11056+      self.songs[0].id,
11057+      self.play_event_args[0].id
11058     );
11059   };
11060   
11061hunk ./contrib/musicplayer/tests/test_PlayerController.js 46
11062+  this.test_verifySongInfo = [
11063+    {method: "asserts.assertText", params: {id: "song_title", validator: "Persona"}},
11064+    {method: "asserts.assertText", params: {id: "song_album_title", validator: "Urgency"}},
11065+    {method: "asserts.assertText", params: {id: "song_artist_name", validator: "Superhumanoids"}}
11066+  ];
11067
11068   this.test_getNext = function () {
11069     jum.assertTrue("there shouldn't be a next song, since there is no playlist",
11070       !Player.getNext()
11071hunk ./contrib/musicplayer/tests/test_PlayerController.js 67
11072     );
11073   };
11074   
11075+  this.test_prevNextButtons = [
11076+    {method: "asserts.assertText", params: {id: "prev_song", validator: "Persona"}},
11077+    {method: "asserts.assertText", params: {id: "next_song", validator: "Maps"}}
11078+  ];
11079
11080   this.test_getPrev = function () {
11081     jum.assertEquals("previous song in playlist should have been returned",
11082       self.songs[0].id,
11083hunk ./contrib/musicplayer/tests/test_PlayerController.js 96
11084       self.songs[0].id,
11085       Player.getNext()
11086     );
11087+   
11088+    Player.setPlaylist([self.songs[2].id]);
11089+    jum.assertEquals("next song should be from queue, ignoring the playlist",
11090+      self.songs[0].id,
11091+      Player.getNext()
11092+    );
11093   };
11094   
11095   return this;
11096hunk ./contrib/musicplayer/tests/test_ProgressBar.js 35
11097     self.pb.options.foreground = "rgba(255, 0, 0, 255)";
11098     self.pb.setProgress(0.7);
11099     
11100-    jun.assertEqualArrays(BLACK,        getPixel(  1));
11101-    jun.assertEqualArrays(RED,          getPixel( 52));
11102-    jun.assertEqualArrays(RED,          getPixel( 69));
11103-    jun.assertEqualArrays(TRANSPARENT,  getPixel(100));
11104+    jum.assertEqualArrays(BLACK,        getPixel(  1));
11105+    jum.assertEqualArrays(RED,          getPixel( 52));
11106+    jum.assertEqualArrays(RED,          getPixel( 69));
11107+    jum.assertEqualArrays(TRANSPARENT,  getPixel(100));
11108   };
11109   
11110   this.test_decrementation = function () {
11111addfile ./contrib/musicplayer/tests/test_SongContextController.js
11112hunk ./contrib/musicplayer/tests/test_SongContextController.js 1
11113+var test_SongContextController = new function () {
11114+  var SongContext = da.controller.SongContext,
11115+      self = this;
11116+     
11117+  this.setup = function () {
11118+    self.called = {
11119+      a_init:   0,
11120+      a_show:   0,
11121+      a_hide:   0,
11122+      a_update: 0,
11123+     
11124+      b_init:   0,
11125+      b_show:   0,
11126+      b_hide:   0,
11127+      b_update: 0
11128+    };
11129+   
11130+    SongContext.register({
11131+      id: "__a__",
11132+      title: "Click meh!",
11133+     
11134+      initialize: function (container) {
11135+        self.called.a_init++;
11136+        self.passed_container = $type(container) === "element";
11137+        self.a_container  = container;
11138+        container.grab(new Element("div", {
11139+          id: "__a__test_el",
11140+          html: "Hai!"
11141+        }));
11142+      },
11143+     
11144+      show: function () {
11145+        self.called.a_show++;
11146+      },
11147+     
11148+      hide: function () {
11149+        self.called.a_hide++;
11150+      },
11151+     
11152+      update: function (song) {
11153+        self.called.a_update++;
11154+        self.passed_song = song;
11155+      }
11156+    });
11157+   
11158+    SongContext.register({
11159+      id: "__b__",
11160+      title: "Click meh too!",
11161+      initialize: function () { self.called.b_init++; },
11162+      show:       function () { self.called.b_show++; },
11163+      hide:       function () { self.called.b_hide++; },
11164+      update:     function () { self.called.b_update++; }
11165+    });
11166+  },
11167
11168+  this.test_show = function () {
11169+    SongContext.show("__a__");
11170+    jum.assertEquals("initialize function should have been called once",
11171+      1, self.called.a_init
11172+    );
11173+    jum.assertTrue("dom nodes should have been inserted into the document",
11174+      !!$("__a__test_el")
11175+    );
11176+    jum.assertEquals("show function should have been called once",
11177+      1, self.called.a_show
11178+    );
11179+    jum.assertEquals("context should be visible",
11180+      "block", $("__a__test_el").getParent().style.display
11181+    );
11182+    jum.assertEquals("update function should have been called once",
11183+      1, self.called.a_update
11184+    );
11185+    jum.assertEquals("loading screen should be visible",
11186+      "block", $("song_context_loading").style.display
11187+    );
11188+  };
11189
11190+  this.test_updateArguments = function () {
11191+    jum.assertTrue("song should be passed to the update function",
11192+      self.passed_song instanceof da.db.DocumentTemplate.Song
11193+    );
11194+  };
11195
11196+  this.test_contextSwitch = function () {
11197+    SongContext.show("__b__");
11198+    jum.assertEquals("A's hide function should have been called",
11199+      1, self.called.a_hide
11200+    );
11201+    jum.assertEquals("B's hide function shouldn't have been called",
11202+      0, self.called.b_hide
11203+    );
11204+    jum.assertEquals("B's show function should have been called",
11205+      1, self.called.b_show
11206+    );
11207+  };
11208+}
11209hunk ./src/allmydata/test/test_musicplayer.py 70
11210     self.settings['JAVASCRIPT_TEST_DIR'] = '../contrib/musicplayer/tests'
11211     self.settings['SCRIPT_APPEND_ONLY'] = True
11212     
11213+    self.browser_debugging = True
11214     self.test_url = 'static/musicplayer/index_devel.html'
11215hunk ./src/allmydata/test/test_musicplayer.py 72
11216+   
11217     shutil.copytree('../contrib/musicplayer/src', self.public_html_path + '/musicplayer')
11218hunk ./src/allmydata/test/test_musicplayer.py 74
11219-    #os.makedirs(self.public_html_path + '/musicplayer/js/workers')
11220     shutil.copytree('../contrib/musicplayer/build/js/workers', self.public_html_path + '/musicplayer/js/workers')
11221     
11222     d = self.client.create_dirnode()
11223hunk ./src/allmydata/test/test_musicplayer.py 113
11224 
11225 class FirefoxTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Firefox):
11226   pass
11227+
11228hunk ./src/allmydata/test/tilting.py 34
11229     return d
11230   
11231   def _set_up_windmill(self):
11232-    self.browser_debugging = True
11233+    self.browser_debugging = False
11234     self.browser_name = 'firefox'
11235     self.test_url = '/'
11236     self._js_test_details = []
11237}
11238
11239Context:
11240
11241[add-music-players-full-deps
11242josip.lisec@gmail.com**20100710190714
11243 Ignore-this: 59d7b3890a1f9349bc8ac511af05b2b8
11244] 
11245[add-music-players-core-deps
11246josip.lisec@gmail.com**20100710185908
11247 Ignore-this: 58f5546ff75501f77d6b346ae99eebec
11248] 
11249[add-music-player
11250josip.lisec@gmail.com**20100710185704
11251 Ignore-this: c29dc0709640abd2e33cfd119b2681f
11252] 
11253[misc/build_helpers/run-with-pythonpath.py: fix stale comment, and remove 'trial' example that is not the right way to run trial.
11254david-sarah@jacaranda.org**20100726225729
11255 Ignore-this: a61f55557ad69a1633bfb2b8172cce97
11256] 
11257[docs/specifications/dirnodes.txt: 'mesh'->'grid'.
11258david-sarah@jacaranda.org**20100723061616
11259 Ignore-this: 887bcf921ef00afba8e05e9239035bca
11260] 
11261[docs/specifications/dirnodes.txt: bring layer terminology up-to-date with architecture.txt, and a few other updates (e.g. note that the MAC is no longer verified, and that URIs can be unknown). Also 'Tahoe'->'Tahoe-LAFS'.
11262david-sarah@jacaranda.org**20100723054703
11263 Ignore-this: f3b98183e7d0a0f391225b8b93ac6c37
11264] 
11265[docs: use current cap to Zooko's wiki page in example text
11266zooko@zooko.com**20100721010543
11267 Ignore-this: 4f36f36758f9fdbaf9eb73eac23b6652
11268 fixes #1134
11269] 
11270[__init__.py: silence DeprecationWarning about BaseException.message globally. fixes #1129
11271david-sarah@jacaranda.org**20100720011939
11272 Ignore-this: 38808986ba79cb2786b010504a22f89
11273] 
11274[test_runner: test that 'tahoe --version' outputs no noise (e.g. DeprecationWarnings).
11275david-sarah@jacaranda.org**20100720011345
11276 Ignore-this: dd358b7b2e5d57282cbe133e8069702e
11277] 
11278[TAG allmydata-tahoe-1.7.1
11279zooko@zooko.com**20100719131352
11280 Ignore-this: 6942056548433dc653a746703819ad8c
11281] 
11282Patch bundle hash:
1128322835c80e300cdec9f7c0f92ba86a06703c4fb7c