Ticket #1023: updates14082010.dpatch

File updates14082010.dpatch, 266.5 KB (added by josipl, at 2010-08-14T18:12:57Z)

Adds search and playlist editor, numerous improvements to column rendering and slightly improved UI.

Line 
11 patch for repository /home/josip/bin/tahoe-tmp:
2
3Sat Aug 14 19:29:19 CEST 2010  josip.lisec@gmail.com
4  * updates14082010
5  * Implemented search interface
6  * Implemented playlist manager
7  * Implemented playlist exporters (XSPF, PLS, M3U)
8  * Separated 'main menu' into two menus
9  * Fixed possible issues with da.ui.Column's performance
10  * More UTF-8 related fixes to da.util.ID3v2Parser
11
12New patches:
13
14[updates14082010
15josip.lisec@gmail.com**20100814172919
16 Ignore-this: 4a70a398132d7b306c0b29d5ed36fe54
17 * Implemented search interface
18 * Implemented playlist manager
19 * Implemented playlist exporters (XSPF, PLS, M3U)
20 * Separated 'main menu' into two menus
21 * Fixed possible issues with da.ui.Column's performance
22 * More UTF-8 related fixes to da.util.ID3v2Parser
23] {
24hunk ./contrib/musicplayer/INSTALL 1
25-=== Installing Music Player for Tahoe (codename 'Daaw') ===
26+= Installing Music Player for Tahoe (codename 'Daaw') =
27 
28 == Maths and Systems Theory quiz ==
29 If you already have a 'build' directory, feel free to skip this step.
30hunk ./contrib/musicplayer/INSTALL 14
31 and computing related courses.
32 
33 Just in case you haven't, you can type in next line into your shell:
34-  $ python manage.py build
35-  running build
36+  $ python manage.py roll
37+  running roll
38   Calculating dependencies...
39   Compressing <something>...
40   ...
41hunk ./contrib/musicplayer/INSTALL 19
42-  Done!
43+  You're ready to rock 'n' roll!
44 
45 Bravo, you're done! (just make sure you have a 'build' directory)
46 
47hunk ./contrib/musicplayer/INSTALL 29
48 == Battle for the Configuration File ==
49 Player's configuration file is a real beast on its own,
50 and in order to edit it we must prepare ourselves really good,
51-otherwise, we're doomed (actually, only you are )!
52+otherwise, we're doomed (actually, only you are)!
53 
54 Read next few steps carefully, the beast is just around the corner!
55 
56hunk ./contrib/musicplayer/INSTALL 33
57-1. Create two dirnodes on your Tahoe-LAFS server, one will be used for storing
58+1. Create two dirnodes on your Tahoe-LAFS server, one which will be used for storing
59    all your music files and the other one for syncing settings between multiple
60    computers.
61   
62hunk ./contrib/musicplayer/INSTALL 45
63    <top secret no.2>
64   
65    (make sure Tahoe-LAFS is running on your computer before issuing those commands)
66+   Pro tip: create a (S)FTP account with your music directory as it's home directory
67+   and upload your music files in batches without a sweat.
68 
69 2. Take a big breath, as we're about to open example configuration file!
70 
71hunk ./contrib/musicplayer/INSTALL 54
72    Now quickly, we have to replace her evil genes with a good ones,
73    find following line in her DNA sequence:
74   
75-      "music_cap": "<bad gene no.1>",
76+      "music_cap":    "<bad gene no.1>",
77       "settings_cap": "<bad gene no.2>"
78   
79   and quickly replace <bad gene no.1> with <top secret no.1> as well as <bad gene no.2>
80hunk ./contrib/musicplayer/INSTALL 66
81   knows how to re-sequence DNA of living beings, and we don't want others to
82   find out about that and use it in evil purposes, don't we?)
83   
84-  Now save the new genes under name of 'config.json'.
85+  Now save the new genes under the name of 'config.json'.
86 
87 == The Critical Step ==
88 After we've conquered the beast of configuration file we're ready to
89hunk ./contrib/musicplayer/INSTALL 70
90-upload the player to the Tahoe!
91+upload the player to the Tahoe-LAFS!
92 
93 To do that, just copy the 'build' directory to 'public_html' directory of your
94 Tahoe storage node (usually ~/.tahoe).
95hunk ./contrib/musicplayer/INSTALL 74
96-Note that 'public_html' directory is probably missing,
97-so you'll have to create it on your own.
98+Note that 'public_html' directory is probably missing, so you'll have to create it on your own.
99 
100hunk ./contrib/musicplayer/INSTALL 76
101+If you are on a UNIX-like operating system, you can do it with following command:
102   $ mkdir -p ~/.tahoe/public_html/musicplayer
103hunk ./contrib/musicplayer/INSTALL 78
104-  $ cp -r build/ ~/.tahoe/public_html/musicplayer
105+  $ cp -r build/ ~/.tahoe/public_html/musicplayer/
106
107+  Pro tip: instead of copying whole 'build' directory, make a symbolic link
108+  so that 'installing' a new version will be a breeze.
109+
110+or if you're using a Windows machine with Command prompt
111+  $ mkdir %HOMEDRIVE%%HOMEPATH%\.tahoe\public_html\musicplayer
112+  $ xcopy /S build\ %HOMEDRIVE%%HOMEPATH%\.tahoe\public_html\musicplayer\
113
114+(drag and drop also works)
115 
116 WARNING: If you don't perform next step exactly as
117 you're instructed, the whole process could fail and you'll
118hunk ./contrib/musicplayer/INSTALL 99
119 
120 == Fin ==
121 You can now upload your music to the <top secret no.1> dirnode and
122-launch music player by typing this URL into your web browser:
123+launch music player by typing this URI into your web browser:
124   http://localhost:3456/static/musicplayer
125 
126 If it appears that something isn't working, it probably means
127hunk ./contrib/musicplayer/INSTALL 103
128-that you haven't read 'The Critical Step' carefully enough.
129+that you haven't read 'The Critical Step' carefully enough, but
130+if you're sure you did it as instructed, please report all bugs you encounter
131+on the following address:
132+  http://tahoe-lafs.org/trac/tahoe-lafs/ticket/1023
133+or ask around for josipl on tahoe-lafs IRC channel (irc.freenode.net).
134 
135 We hope you're going to enjoy your music even more with Music Player for Tahoe-LAFS!
136hunk ./contrib/musicplayer/INSTALL 110
137+
138+Note: During the initial collection scan (or any other), it's suggested to
139+turn off Firebug or Web Inspector's XMLHttpRequest logging feature, for sake of performance.
140hunk ./contrib/musicplayer/NOTES 53
141   following into Web Inspector's or Firebugs' console:
142   
143   `windmill.jsTests.testFailures`
144-*
145 
146 === Documentation ===
147 Player's code is fully annotated with PDoc[2] syntax, which can then generate
148hunk ./contrib/musicplayer/manage.py 6
149 
150 import os, shutil, sys, subprocess, re
151 from time import sleep
152+import tempfile
153 from tempfile import mkstemp
154 from setuptools import setup
155 from setuptools import Command
156hunk ./contrib/musicplayer/manage.py 70
157   """
158   requires_re = re.compile('^//#require ["|\<](.+)["|\>]$', re.M)
159   
160-  def __init__(self, root_directory):
161+  def __init__(self, root_directory, syntax_check = False):
162     self.files        = {}
163     self.included     = []
164hunk ./contrib/musicplayer/manage.py 73
165-    self.root         = root_directory   
166+    self.root         = root_directory
167+    self.syntax_check = syntax_check
168     
169     self.scan()
170   
171hunk ./contrib/musicplayer/manage.py 99
172     
173     self.files[path] = reqs
174 
175-  def parse(self, path, syntax_check = False):
176+  def parse(self, path):
177     if path in self.included:
178       return ''
179     if not path.endswith('.js'):
180hunk ./contrib/musicplayer/manage.py 106
181       # TODO: If path points to a directory, require all the files within that directory.
182       return ''
183     
184-    if syntax_check:
185+    if self.syntax_check:
186       compiler = ClosureCompiler([path], None)
187       if not compiler.syntax_check():
188         raise Exception('There seems to be a syntax problem. Fix it.')
189hunk ./contrib/musicplayer/manage.py 117
190         if not os.path.isfile(req_path):
191           raise Exception('%s requires non existing file: %s' % (path, req_path))
192           
193-        return self.parse(req_path, syntax_check)
194+        return self.parse(req_path)
195     
196     script_file = open(path, 'r')
197     script = script_file.read()
198hunk ./contrib/musicplayer/manage.py 144
199   description = 'builds whole application into build directory'
200   user_options = [
201     ('compilation-level=', 'c', 'compilation level for Google\'s Closure compiler.'),
202+    ('syntax-check', 's', 'run syntax check on every individal file')
203   ]
204   
205   def initialize_options(self):
206hunk ./contrib/musicplayer/manage.py 149
207     self.compilation_level = 'SIMPLE_OPTIMIZATIONS'
208+    self.syntax_check = False
209     
210   def finalize_options(self):
211     compilation_levels = [
212hunk ./contrib/musicplayer/manage.py 167
213     shutil.copytree('src/resources', 'build/resources')
214     shutil.copy('src/config.example.json', 'build/')
215     shutil.copy('src/index.html', 'build/')
216+    shutil.copy('src/playlist_download.html', 'build/')
217+    shutil.copy('src/about.html', 'build/')
218+    shutil.copy('src/cache.manifest', 'build/')
219     
220     shutil.copytree('src/libs/vendor/soundmanager/swf', 'build/resources/flash')
221     shutil.copy('src/libs/vendor/persist-js/persist.swf', 'build/resources/flash')
222hunk ./contrib/musicplayer/manage.py 174
223     
224-    os.makedirs('build/js/libs')
225     os.makedirs('build/js/workers')
226     shutil.copy('src/libs/vendor/browser-couch/js/worker-map-reducer.js', 'build/js/workers/map-reducer.js')
227     
228hunk ./contrib/musicplayer/manage.py 178
229     print 'Calculating dependencies...'
230-    self.deps = JSDepsBuilder('src/')
231+    self.deps = JSDepsBuilder('src/', syntax_check = self.syntax_check)
232     
233     self._make_js('Application.js', 'build/js/app.js')
234     
235hunk ./contrib/musicplayer/manage.py 186
236       if worker.endswith('.js'):
237         self._make_js('workers/' + worker, 'build/js/workers/' + worker)
238     
239-    print 'Done!'
240+    print 'You\'re ready to rock \'n\' roll!'
241   
242   def _make_js(self, root, output):
243hunk ./contrib/musicplayer/manage.py 189
244-    tmp_file = mkstemp()[1]
245+    fd = mkstemp()
246+    os.close(fd[0])
247+    tmp_file = fd[1]
248     self.deps.write_to_file(tmp_file, root_file = root)
249     compiler = ClosureCompiler([tmp_file], output)
250     compiler.compile(self.compilation_level)
251hunk ./contrib/musicplayer/manage.py 303
252 setup(
253   name = 'tahoe-music-player',
254   cmdclass = {
255-    'build':    Build,
256+    'roll':     Build,
257     'install':  Install,
258     'watch':    Watch,
259     'tests':    VerifyTests,
260hunk ./contrib/musicplayer/src/Application.js 17
261 //#require "libs/db/PersistStorage.js"
262 //#require "libs/db/DocumentTemplate.js"
263 //#require "libs/util/Goal.js"
264+//#require "libs/ui/Menu.js"
265+//#require "libs/ui/Dialog.js"
266 
267 (function () {
268 var BrowserCouch    = da.db.BrowserCouch,
269hunk ./contrib/musicplayer/src/Application.js 23
270     PersistStorage  = da.db.PersistStorage,
271-    Goal            = da.util.Goal;
272+    Goal            = da.util.Goal,
273+    Menu            = da.ui.Menu,
274+    Dialog          = da.ui.Dialog;
275 
276 /** section: Controllers
277hunk ./contrib/musicplayer/src/Application.js 28
278- *  class da.app
279+ * App
280  *
281hunk ./contrib/musicplayer/src/Application.js 30
282- *  The main controller. All methods are public.
283+ * Private interface of the [[da.app]].
284  **/
285hunk ./contrib/musicplayer/src/Application.js 32
286-da.app = {
287-  /**
288-   *  da.app.caps -> Object
289-   *  Object with `music` and `settings` properties, ie. the contents of `config.json` file.
290-   **/
291-  caps: {},
292
293+var App = {
294   initialize: function () {
295     this.startup = new Goal({
296hunk ./contrib/musicplayer/src/Application.js 35
297-      checkpoints: ["domready", "settings_db", "caps", "data_db", "soundmanager"],
298+      checkpoints: ["domready", "settings_db", "caps", "data_db"],
299       onFinish: this.ready.bind(this)
300     });
301     
302hunk ./contrib/musicplayer/src/Application.js 42
303     BrowserCouch.get("settings", function (db) {
304       da.db.SETTINGS = db;
305       if(!db.getLength())
306-        this.loadInitialSettings();
307+        App.loadInitialSettings();
308       else {
309hunk ./contrib/musicplayer/src/Application.js 44
310-        this.startup.checkpoint("settings_db");
311-        this.getCaps();
312+        App.startup.checkpoint("settings_db");
313+        App.getCaps();
314       }
315hunk ./contrib/musicplayer/src/Application.js 47
316-    }.bind(this), new PersistStorage("tahoemp_settings"));
317+    }, new PersistStorage("tahoemp_settings"));
318     
319     BrowserCouch.get("data", function (db) {
320       da.db.DEFAULT = db;
321hunk ./contrib/musicplayer/src/Application.js 51
322-      this.startup.checkpoint("data_db");
323-    }.bind(this), new PersistStorage("tahoemp_data"));
324+      App.startup.checkpoint("data_db");
325+    }, new PersistStorage("tahoemp_data"));
326     
327hunk ./contrib/musicplayer/src/Application.js 54
328-    this.addEvent("ready.controller.CollectionScanner", function () {
329+    da.app.addEvent("ready.controller.CollectionScanner", function () {
330       if(!da.db.DEFAULT.getLength())
331         da.controller.CollectionScanner.scan();
332     });
333hunk ./contrib/musicplayer/src/Application.js 61
334   },
335   
336   loadInitialSettings: function () {
337-    new Request.JSON({
338+    var req = new Request.JSON({
339       url: "config.json",
340       noCache: true,
341       
342hunk ./contrib/musicplayer/src/Application.js 70
343           {id: "music_cap",     type: "Setting", group_id: "caps", value: data.music_cap},
344           {id: "settings_cap",  type: "Setting", group_id: "caps", value: data.settings_cap}
345         ], function () {
346-          this.startup.checkpoint("settings_db");
347+          App.startup.checkpoint("settings_db");
348           
349hunk ./contrib/musicplayer/src/Application.js 72
350-          this.caps.music = data.music_cap;
351-          this.caps.settings = data.settings_cap;
352+          da.app.caps.music = data.music_cap;
353+          da.app.caps.settings = data.settings_cap;
354           
355hunk ./contrib/musicplayer/src/Application.js 75
356-          this.startup.checkpoint("caps");
357+          App.startup.checkpoint("caps");
358           
359           if(!da.db.DEFAULT.getLength())
360hunk ./contrib/musicplayer/src/Application.js 78
361-            da.controller.CollectionScanner.scan();
362-        }.bind(this));
363-      }.bind(this),
364+            App.callController("CollectionScanner", "scan");
365+        });
366+      },
367       
368       onFailure: function () {
369hunk ./contrib/musicplayer/src/Application.js 83
370+        delete req;
371         alert("You're missing a config.json file! See docs on how to set it up.");
372hunk ./contrib/musicplayer/src/Application.js 85
373-        var showSettings = function () {
374-          da.controller.Settings.showGroup("caps");
375-        };
376         
377hunk ./contrib/musicplayer/src/Application.js 86
378-        if(da.controller.Settings)
379-          showSettings();
380-        else
381-          da.app.addEvent("ready.controller.Settings", showSettings);
382+        App.callController("Settings", "showGroup", ["caps"]);
383       }
384hunk ./contrib/musicplayer/src/Application.js 88
385-    }).get()
386+    });
387+   
388+    req.get();
389   },
390   
391   getCaps: function () {
392hunk ./contrib/musicplayer/src/Application.js 106
393       
394       finished: function (result) {
395         if(!result.rows.length)
396-          return this.loadInitialSettings();
397+          return App.loadInitialSettings();
398+       
399+        da.app.caps = {
400+          music:    result.getRow("music_cap"),
401+          settings: result.getRow("settings_cap")
402+        };
403         
404hunk ./contrib/musicplayer/src/Application.js 113
405-        this.caps.music = result.getRow("music_cap");
406-        this.caps.settings = result.getRow("settings_cap");
407-        if(!this.caps.music.length || !this.caps.music.length)
408-          this.loadInitialSettings();
409+        if(!da.app.caps.settings.length || !da.app.caps.music.length)
410+          App.loadInitialSettings();
411         else
412hunk ./contrib/musicplayer/src/Application.js 116
413-          this.startup.checkpoint("caps");
414-      }.bind(this),
415+          App.startup.checkpoint("caps");
416+      },
417       
418       updated: function (result) {
419         var music    = result.getRow("music_cap"),
420hunk ./contrib/musicplayer/src/Application.js 124
421             settings = result.getRow("settings_cap");
422         
423         if(music)
424-          da.controller.CollectionScanner.scan(this.caps.music = music);
425+          da.controller.CollectionScanner.scan(da.app.caps.music = music);
426         
427         if(settings)
428hunk ./contrib/musicplayer/src/Application.js 127
429-          this.caps.settings = settings;
430+          da.app.caps.settings = settings;
431         
432hunk ./contrib/musicplayer/src/Application.js 129
433-        this.startup.checkpoint("caps");
434-      }.bind(this)
435+        App.startup.checkpoint("caps");
436+      }
437     });
438   },
439   
440hunk ./contrib/musicplayer/src/Application.js 134
441+  callController: function(controller, method, args) {
442+    function callControllerMethod() {
443+      var c = da.controller[controller];
444+      c[method].apply(c, args);
445+      c = null;
446+    }
447+   
448+    if(da.controller[controller])
449+      callControllerMethod();
450+    else
451+      da.app.addEvent("ready.controller." + controller, callControllerMethod);
452+  },
453
454   /**
455    *  da.app.ready() -> undefined
456    *  fires ready
457hunk ./contrib/musicplayer/src/Application.js 155
458    **/
459   ready: function () {
460     $("loader").destroy();
461-    $("panes").setStyle("display", "block");
462+    $("panes").style.display = "block";
463+   
464+    this.setupMainMenu();
465+   
466+    da.app.fireEvent("ready");
467+   
468+    var about_iframe = new Element("iframe", {
469+      src: "about:blank",
470+      width: 400,
471+      height: 500
472+    });
473+    about_iframe.style.background = "#fff";
474+    this.about_dialog = new Dialog({
475+      html: about_iframe,
476+      onShow: function () {
477+        about_iframe.src = "about.html";
478+      },
479+      onHide: function () {
480+        about_iframe.src = "about:blank";
481+      }
482+    });
483+  },
484
485+  setupMainMenu: function () {
486+    var main_menu_button = new Element("a", {
487+      id:   "main_menu",
488+      // triangle
489+      html: "&#x25BC;",
490+      href: "#",
491+      events: {
492+        mousedown: function (event) {
493+          da.app.mainMenu.show(event);
494+        },
495+        click: function (event) {
496+          Event.stop(event);
497+        }
498+      }
499+    });
500     
501hunk ./contrib/musicplayer/src/Application.js 194
502-    this.fireEvent("ready");
503+    da.app.mainMenu = new Menu({
504+      items: {
505+        toggleShuffle: {html: "Turn shuffle on", id: "player_toggle_shuffle_menu_item", href: "#"},
506+        mute:     {html: "Mute", id: "player_mute_menu_item", href: "#"},
507+        _sep0:    Menu.separator,
508+       
509+        addToPl:  {html: "Add to playlist&hellip;", href: "#"},
510+        share:    {html: "Share&hellip;",           href: "#"},
511+       
512+        _sep1:    Menu.separator,
513+       
514+        search:   {html: "Search&hellip;",    href: "#"},
515+        upload:   {html: "Import&hellip;",    href: "#"},
516+        rescan:   {html: "Rescan collection", href: "#"},
517+        settings: {html: "Settings&hellip;",  href: "#"},
518+       
519+        _sep2:    Menu.separator,
520+       
521+        help:     {html: "Help",  href: "#"},
522+        about:    {html: "About", href:"#"}
523+      },
524+     
525+      position: {
526+        position: "bottomRight",
527+        edge:     "topRight",
528+        offset:   { y: -3 }
529+      },
530+     
531+      onShow: function () {
532+        main_menu_button.addClass("active_menu");
533+      },
534+      onHide: function () {
535+        main_menu_button.removeClass("active_menu");
536+      },
537+      onClick: function (item) {
538+        var fn = da.app.mainMenuActions[item];
539+        if(fn)
540+          fn();
541+        fn = null;
542+      }
543+    });
544+    document.body.grab(main_menu_button);
545+  }
546+};
547+
548+/**
549+ *  class da.app
550+ *
551+ *  The main controller.
552+ **/
553+da.app = {
554+  /**
555+   *  da.app.caps -> Object
556+   *  Object with `music` and `settings` properties, ie. the contents of `config.json` file.
557+   **/
558+  caps: {},
559
560+  /**
561+   *  da.app.mainMenu -> da.ui.Menu
562+   **/
563+  mainMenu: null,
564
565+  /**
566+   *  da.app.mainMenuActions -> Object
567+   *  Object's keys match [[da.app.mainMenu]] item keys.
568+   **/
569+  mainMenuActions: {
570+    toggleShuffle:  function () { da.controller.Player._toggleShuffle() },
571+    mute:           function () { da.controller.Player.toggleMute()     },
572+    addToPl:        function () { da.controller.Playlist.addSong()      },
573+    search:         function () { da.controller.Search.show()           },
574+    rescan:         function () { da.controller.CollectionScanner.scan(da.app.caps.music) },
575+    settings:       function () { da.controller.Settings.show()         },
576+    about:          function () { App.about_dialog.show()               }
577   }
578 };
579 $extend(da.app, new Events());
580hunk ./contrib/musicplayer/src/Application.js 272
581 
582-da.app.initialize();
583+App.initialize();
584 
585 window.addEvent("domready", function () {
586hunk ./contrib/musicplayer/src/Application.js 275
587-  da.app.startup.checkpoint("domready");
588+  App.startup.checkpoint("domready");
589 });
590 
591 })();
592addfile ./contrib/musicplayer/src/about.html
593hunk ./contrib/musicplayer/src/about.html 1
594+<!DOCTYPE html>
595+<html>
596+<!-- html manifest="cache.manifest" -->
597+  <head>
598+    <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
599+   
600+    <title>About Daaw</title>
601+    <link rel="stylesheet" href="resources/css/reset.css" type="text/css" media="screen" charset="utf-8"/>
602+    <link rel="stylesheet" href="resources/css/text.css"  type="text/css" media="screen" charset="utf-8"/>
603+   
604+    <style type="text/css">
605+      body {
606+        font-family: 'Droid Sans', sans-serif;
607+        padding: 0 10px;
608+        background: #fff;
609+        width: 380px;
610+        height: 500px;
611+        overflow: hidden;
612+      }
613+     
614+      header {
615+        background: #f3f3f3 url(resources/images/radio_pattern.png) 0 0 repeat;
616+        height: 65px;
617+        overflow: hidden;
618+        border-bottom: 1px solid #c0c0c0;
619+        padding: 0;
620+        margin: 0 -10px 10px -10px;
621+        text-shadow: #fff 0 1px 0;
622+        text-align: left;
623+       
624+        -webkit-box-shadow: #c0c0c0 0 0 5px inset;
625+        -moz-box-shadow: #c0c0c0 0 0 5px inset;
626+        box-shadow: #c0c0c0 0 0 5px inset;
627+      }
628+     
629+      h1 {
630+        background: url(resources/images/song_details_background.png) 0 0 repeat;
631+        display: inline-block;
632+        height: 50px;
633+        margin: 7px 0 0 5px;
634+        padding: 4px 0 0 0;
635+        width: 110px;
636+        overflow: hidden;
637+        white-space: nowrap;
638+        text-align: center;
639+      }
640+     
641+      h3 {
642+        font-size: 16px;
643+        margin-bottom: 0;
644+        margin-top: 5px;
645+      }
646+     
647+      small {
648+        font-size: 0.4em;
649+      }
650+     
651+      p {
652+        padding: 10px;
653+        margin: 0;
654+      }
655+     
656+      a {
657+        color: #33519d;
658+        text-decoration: none;
659+      }
660+     
661+      a:hover {
662+        background: #33519d;
663+        color: #fff;
664+     
665+        -webkit-border-radius: 2px;
666+        -moz-border-radius: 2px;
667+        border-radius: 2px;
668+      }
669+     
670+    </style>
671+  </head>
672+  <body>
673+    <header>
674+      <h1>Daaw<small>0.1</small></h1>
675+    </header>
676+   
677+    <p style="font-weight:bold">
678+      Daaw has been developed as part of <a href="http://socghop.appspot.com/gsoc/program/home/google/gsoc2010" target="_blank">Google Summer of Code 2010</a> by Josip Lisec and
679+      mentored by David-Sarah Hopwood under <a href="http://tahoe-lafs.org/" target="_blank">Tahoe-LAFS</a> project.
680+    </p>
681+    <p>
682+      Project is licenced under GNU General Public License version 2, or any later version.
683+    </p>
684+   
685+    <h3>Notices</h3>
686+    <p style="margin-top:0">
687+      <a href="http://www.last.fm/legal/terms" target="_blank"><img src="http://cdn.last.fm/flatness/badges/lastfm_red_small.gif" align="right"/></a>
688+      This application makes use of services provided by <a href="http://last.fm">Last.fm</a>.
689+    </p>
690+    <p>
691+      Some of the icons are derived from the <a href="http://somerandomdude.com/projects/iconic/" target="_blank">Iconic Icon Set</a>, while
692+      others are used without any modifications.
693+    </p>
694+    <p>
695+      <a href="http://validator.xspf.org/referrer/" target="_blank">
696+        <img src="http://svn.xiph.org/websites/xspf.org/images/banners/valid-xspf.png" style="border:0" align="right" title="This website produces valid XSPF playlist files."/>
697+      </a>
698+      All generated <a href="http://xspf.org/">XSPF</a> playlists are valid XSPF documents.
699+    </p>
700+   
701+    <h3>Links</h3>
702+    <ul>
703+      <li><a href="http://tahoe-lafs.org/trac/tahoe-lafs/ticket/1023" target="_blank">Bug tracker</a></li>
704+      <li><a href="http://tahoe-music-player.tumblr.com/" target="_blank">Blog</a></li>
705+    </ul>
706+  </body>
707+</html>
708hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 5
709 //#require "doctemplates/Song.js"
710 //#require "doctemplates/Artist.js"
711 //#require "doctemplates/Album.js"
712+//#require "doctemplates/Setting.js"
713 
714 (function () {
715 var DocumentTemplate  = da.db.DocumentTemplate,
716hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 12
717     Song              = DocumentTemplate.Song,
718     Artist            = DocumentTemplate.Artist,
719     Album             = DocumentTemplate.Album,
720-    Goal              = da.util.Goal;
721+    Setting           = DocumentTemplate.Setting,
722+    Goal              = da.util.Goal,
723+    GENRES            = da.util.GENRES;
724 
725 /** section: Controllers
726  *  class CollectionScanner
727hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 32
728    *  Starts a new scan using [[Application.caps.music]] as root directory.
729    **/
730   initialize: function (root) {
731+    root = root || da.app.caps.music;
732+    if(!root) {
733+      this.finished = true;
734+      return false;
735+    }
736+   
737     console.log("collection scanner started");
738     this.indexer = new Worker("js/workers/indexer.js");
739     this.indexer.onmessage = this.onIndexerMessage.bind(this);
740hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 45
741     this.scanner = new Worker("js/workers/scanner.js");
742     this.scanner.onmessage = this.onScannerMessage.bind(this);
743     
744-    this.scanner.postMessage(root || da.app.caps.music);
745+    this.scanner.postMessage(root);
746     
747     this.finished = false;
748     
749hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 63
750             this._found_files ? "Your patience has paid off." : "Make sure your files have proper ID3 tags."
751           ])
752         );
753+       
754+        Setting.findById("last_scan").update({value: new Date()});
755       }.bind(this)
756     });
757     
758hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 70
759     da.ui.ROAR.alert(
760       "Collection scanner started",
761-      "We're scanning your musical collection. Soon new artists and songs\
762-      should start popping up. Patience."
763+      "Your musical collection is being scanned. You should see new artists showing \
764+      up in the area above. Patience."
765     );
766   },
767   
768hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 86
769       this._goal.checkpoint("scanner");
770       return;
771     }
772+   
773     if(cap.debug) {
774       console.log("SCANNER", cap.msg, cap.obj);
775       return;
776hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 127
777                   title:      tags.title,
778                   track:      tags.track,
779                   year:       tags.year,
780-                  lyrics:     tags.lyrics,
781-                  genre:      tags.genere,
782+                  genre:      fixGenre(tags.genre),
783                   artist_id:  artist_id,
784hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 129
785-                  album_id:   album_id
786+                  album_id:   album_id,
787+                  plays:      0
788                 });
789                 
790                 delete links;
791hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 183
792   }
793 });
794 
795-var SCANNER;
796+function fixGenre (genre) {
797+  return typeof genre === "number" ? genre : (GENRES.contains(genre) ? GENRES.indexOf(genre) : genre);
798+}
799+
800+Setting.register({
801+  id:           "last_scan",
802+  group_id:     "CollectionScanner",
803+  representAs:  "text",
804+  title:        "Last scan",
805+  help:         "The date your collection was scanned.",
806+  value:        new Date(0)
807+});
808+
809+da.app.addEvent("ready", function () {
810+  var last_scan = Setting.findById("last_scan"),
811+       five_days_ago = (new Date()) - 5*24*60*60*1000;
812+  if((new Date(last_scan.get("value"))) < five_days_ago)
813+    da.controller.CollectionScanner.scan();
814+});
815+
816+var CS;
817 /**
818  * da.controller.CollectionScanner
819  * Public interface of [[CollectionScanner]].
820hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 214
821    *  Starts scanning music directory
822    **/
823   scan: function (cap) {
824-    if(!SCANNER || (SCANNER && SCANNER.finished))
825-      SCANNER = new CollectionScanner(cap);
826+    if(!CS || (CS && CS.finished))
827+      CS = new CollectionScanner(cap);
828+    else if(cap && cap.length)
829+      CS.scanner.postMessage(cap);
830   },
831   
832   /**
833hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 224
834    *  da.controller.CollectionScanner.isScanning() -> true | false
835    **/
836   isScanning: function () {
837-    return SCANNER && !SCANNER.finished;
838+    return CS ? !CS.finished : false;
839   }
840 };
841 
842hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 229
843 da.app.fireEvent("ready.controller.CollectionScanner", [], 1);
844+
845 })();
846hunk ./contrib/musicplayer/src/controllers/Navigation.js 69
847       this.createHeader();
848     
849     this.column = new Navigation.columns[this.column_name]({
850-      id: this.column_name,
851-      filter: options.filter,
852-      parentElement: this._el
853+      id:             this.column_name,
854+      filter:         options.filter,
855+      parentElement:  this._el,
856+      parentColumn:   this.parent_column ? this.parent_column.column : null
857     });
858     Navigation.adjustColumnSize(this.column);
859 
860hunk ./contrib/musicplayer/src/controllers/Navigation.js 108
861    **/
862   createHeader: function () {
863     this.header = new Element("a", {
864-      "class": "column_header",
865-      href: "#"
866+      "class":  "column_header",
867+      href:     "#"
868     });
869     
870     this.header.addEvent("click", function (event) {
871hunk ./contrib/musicplayer/src/controllers/Navigation.js 119
872     });
873     
874     this._el.grab(this.header.grab(new Element("span", {
875-      html: this.column_name,
876-      "class": "column_title"
877+      html:     Navigation.columns[this.column_name].title,
878+      "class":  "column_title"
879     })));
880     
881     return this;
882hunk ./contrib/musicplayer/src/controllers/Navigation.js 134
883    **/
884   createMenu: function () {
885     var filters = this.column.constructor.filters,
886-        items = {};
887+        items = {},
888+        column;
889     
890     if(!filters || !filters.length)
891       return false;
892hunk ./contrib/musicplayer/src/controllers/Navigation.js 141
893     
894     items[this.column_name] = {html: this.column.constructor.title, "class": "checked", href: "#"};
895-    for(var n = 0, m = filters.length; n < m; n++)
896-      items[filters[n]] = {html: filters[n], href: "#"};
897+    for(var n = 0, m = filters.length; n < m; n++) {
898+      column = Navigation.columns[filters[n]];
899+      if(!column.hidden)
900+        items[filters[n]] = {html: column.title, href: "#"};
901+    }
902     
903     this.menu = new Menu({
904       items: items
905hunk ./contrib/musicplayer/src/controllers/Navigation.js 155
906     
907     this.menu.addEvent("show", function () {     
908       var header = this.header;
909-      header.addClass("active");
910+      header.addClass("active_menu");
911       // adjusting menu's width to the width of the header
912       header.retrieve("menu").toElement().style.width = header.getWidth() + "px";
913     }.bind(this));
914hunk ./contrib/musicplayer/src/controllers/Navigation.js 161
915     
916     this.menu.addEvent("hide", function () {
917-      this.header.removeClass("active");
918+      this.header.removeClass("active_menu");
919     }.bind(this));
920     
921     if(filters && filters.length)
922hunk ./contrib/musicplayer/src/controllers/Navigation.js 209
923     if(element)
924       element.addClass("checked");
925     
926-    header.getElement(".column_title").set("text", filter_name);
927+    header.getElement(".column_title").set("text", Navigation.columns[filter_name].title);
928   },
929   
930   /**
931hunk ./contrib/musicplayer/src/controllers/Navigation.js 292
932   initialize: function () {
933     var root_column = new NavigationColumnContainer({columnName: "Root"});
934     root_column.menu.removeItem("Root");
935-    root_column.menu.addItems({
936-      separator_1: Menu.separator,
937-      search:     {html: "Search",    href:"#"},
938-      settings:   {html: "Settings",  href:"#", events: {click: da.controller.Settings.show}},
939-      help:       {html: "Help",      href:"#"}
940-    });
941     
942     var artists_column = new NavigationColumnContainer({
943       columnName: "Artists",
944hunk ./contrib/musicplayer/src/controllers/Navigation.js 302
945     root_column.header = artists_column.header;
946     
947     this._header_height = artists_column.header.getHeight();
948+    this._player_pane_width = $("player_pane").getWidth();
949     window.addEvent("resize", function () {
950       var columns = Navigation.activeColumns,
951           n = columns.length,
952hunk ./contrib/musicplayer/src/controllers/Navigation.js 312
953       while(n--)
954         columns[n].column._el.setStyles({
955           height: height,
956-          width: width
957-        });
958+          width:  width
959+        }).fireEvent("resize");
960       
961hunk ./contrib/musicplayer/src/controllers/Navigation.js 315
962-      //$("navigation_pane").style.width = width*3 + "px";
963       $("navigation_pane").style.height = window.getHeight() + "px";
964     }.bind(this));
965     
966hunk ./contrib/musicplayer/src/controllers/Navigation.js 328
967    *  Adjusts column's height to window.
968    **/
969   adjustColumnSize: function (column) {
970-    column._el.setStyles({
971-      height: window.getHeight() - this._header_height,
972+    var el = column.toElement();
973+    el.style.height = (window.getHeight() - this._header_height) + "px";
974       // -1 for te right border
975hunk ./contrib/musicplayer/src/controllers/Navigation.js 331
976-      width: (window.getWidth() - $("player_pane").getWidth())/3 - 1
977-    });
978+    el.style.width = ((window.getWidth() - this._player_pane_width)/3 - 1) + "px";
979+    el.fireEvent("resize");
980+    el = null;
981   },
982   
983   /**
984hunk ./contrib/musicplayer/src/controllers/Navigation.js 337
985-   *  da.controller.Navigation.registerColumn(name, filters, column) -> undefined
986-   *  - name (String): name of the column.
987-   *  - filters (Array): names of the columns which can accept filter created (with [[da.ui.NavigationColumn#createFilter]]) by this one.
988+   *  da.controller.Navigation.registerColumn(id[, title], filters, column) -> undefined
989+   *  - id (String): id of the column.
990+   *  - title (String): name of the column. Defaults to the value `id`, if not provided.
991+   *  - filters (Array): names of the columns which can accept filter created
992+   *    (with [[da.ui.NavigationColumn#createFilter]]) by this one.
993    *  - column (da.ui.NavigationColumn): column class.
994    * 
995hunk ./contrib/musicplayer/src/controllers/Navigation.js 344
996-   *  `name` (renamed to `title`) and `filters` will be added to `column` as static methods.
997+   *  #### Notes
998+   *  `title` and `filters` will be added to `column` as class properties.
999+   *  If the `id` begins with an underscore, the column will be considered private
1000+   *  and it won't be visible in the menus.
1001    **/
1002hunk ./contrib/musicplayer/src/controllers/Navigation.js 349
1003-  registerColumn: function (name, filters, col) {
1004+  registerColumn: function (id, title, filters, col) {
1005+    if(arguments.length === 3) {
1006+      col     = filters;
1007+      filters = title;
1008+      title   = id;
1009+    }
1010     col.extend({
1011hunk ./contrib/musicplayer/src/controllers/Navigation.js 356
1012-      title: name,
1013-      filters: filters || []
1014+      title:   title,
1015+      filters: filters || [],
1016+      hidden:  id[0] === "_"
1017     });
1018     
1019hunk ./contrib/musicplayer/src/controllers/Navigation.js 361
1020-    this.columns[name] = col;
1021-    if(name !== "Root")
1022-      this.columns.Root.filters.push(name);
1023+    this.columns[id] = col;
1024+    if(id !== "Root")
1025+      this.columns.Root.filters.push(id);
1026     
1027     // TODO: If Navigation is initialized
1028     // then Root's menu has to be updated.
1029hunk ./contrib/musicplayer/src/controllers/Player.js 55
1030   },
1031   _loading: [],
1032   
1033+  play_mode: null,
1034
1035   /**
1036    *  new Player()
1037    *  Sets up soundManager2 and initializes player's interface.
1038hunk ./contrib/musicplayer/src/controllers/Player.js 62
1039    **/
1040   initialize: function () {
1041-    soundManager.onready(function () {
1042-      da.app.startup.checkpoint("soundmanager");
1043-    });
1044+    //soundManager.onready(function () {
1045+    //  da.app.startup.checkpoint("soundmanager");
1046+    //});
1047     
1048     da.app.addEvent("ready", this.initializeUI.bind(this));
1049     
1050hunk ./contrib/musicplayer/src/controllers/Player.js 79
1051     window.fireEvent("resize");
1052     
1053     this.progress_bar = new SegmentedProgressBar(150, 5, {
1054-      track: "#33519d",
1055+      track: "#339D4C",
1056       load:  "#C1C6D4"
1057     });
1058hunk ./contrib/musicplayer/src/controllers/Player.js 82
1059+    var load_grad = this.progress_bar.ctx.createLinearGradient(0, 0, 0, 5);
1060+    load_grad.addColorStop(0, "#6f7074");
1061+    load_grad.addColorStop(1, "#cfccd7");
1062+    this.progress_bar.segments.load.options.foreground = load_grad;
1063+    load_grad = null;
1064+   
1065+    var track_grad = this.progress_bar.ctx.createLinearGradient(0, 0, 0, 5);
1066+    track_grad.addColorStop(0, "#339D4C");
1067+    track_grad.addColorStop(1, "#326732");
1068+    this.progress_bar.segments.track.options.foreground = track_grad;
1069+    track_grad = null;
1070     
1071     this.progress_bar.toElement().id = "track_progress";
1072     this.progress_bar.toElement().addEvents({
1073hunk ./contrib/musicplayer/src/controllers/Player.js 96
1074-      // Has some issues in Firefox - the object's width also gets scaled
1075-      /*
1076-      mouseenter: function () {
1077-        this.tween("height", 15);
1078-      },
1079-      mouseleave: function () {
1080-        this.tween("height", 5);
1081-      },
1082-      */
1083       mouseup: function (e) {
1084         var sound = Player.nowPlaying.sound;
1085         if(!sound)
1086hunk ./contrib/musicplayer/src/controllers/Player.js 103
1087         
1088         var p = e.event.offsetX / this.getWidth();
1089         sound.setPosition(sound.durationEstimate * p);
1090+      },
1091+      mouseover: function () {
1092+        Player.elements.position.fade("in");
1093       }
1094     });
1095     
1096hunk ./contrib/musicplayer/src/controllers/Player.js 114
1097       wrapper:        new Element("div",  {id: "song_details"}),
1098       cover_wrapper:  new Element("div",  {id: "song_album_cover_wrapper"}),
1099       album_cover:    new Element("img",  {id: "song_album_cover"}),
1100-      song_title:     new Element("h2",   {id: "song_title"}),
1101-      album_title:    new Element("span", {id: "song_album_title"}),
1102-      artist_name:    new Element("span", {id: "song_artist_name"}),
1103-      controls:       new Element("div",  {id: "player_controls", "class": "no_selection"}),
1104-      play:           new Element("a",    {id: "play_button", href: "#"}),
1105-      next:           new Element("a",    {id: "next_song", href: "#"}),
1106-      prev:           new Element("a",    {id: "prev_song", href: "#"})
1107+      song_title:     new Element("h2",   {id: "song_title",        html: "Unknown"}),
1108+      album_title:    new Element("span", {id: "song_album_title",  html: "Unknown"}),
1109+      artist_name:    new Element("span", {id: "song_artist_name",  html: "Unknown"}),
1110+      controls:       new Element("div",  {id: "player_controls",   "class": "no_selection"}),
1111+      play:           new Element("a",    {id: "play_button",       href: "#"}),
1112+      next:           new Element("a",    {id: "next_song",         href: "#"}),
1113+      prev:           new Element("a",    {id: "prev_song",         href: "#"}),
1114+      position:       new Element("span", {id: "song_position",     href: "#"})
1115     };
1116     
1117     var play_wrapper = new Element("div", {id: "play_button_wrapper"});
1118hunk ./contrib/musicplayer/src/controllers/Player.js 126
1119     play_wrapper.grab(els.play);
1120-    els.controls.adopt(els.prev, play_wrapper, els.next, this.progress_bar.toElement());
1121+    els.controls.adopt(
1122+      els.prev, play_wrapper, els.next,
1123+      this.progress_bar.toElement(), els.position
1124+    );
1125     
1126     els.wrapper.grab(els.song_title);
1127     els.wrapper.appendText("from ");
1128hunk ./contrib/musicplayer/src/controllers/Player.js 143
1129   
1130     this._el.adopt(els.info_block);
1131     
1132+    els.position.set("tween", {duration: "short", link: "cancel"});
1133+   
1134     this._el.style.visibility = "hidden";
1135     this._visible = false;
1136     
1137hunk ./contrib/musicplayer/src/controllers/Player.js 148
1138-    var next = els.next, prev = els.prev;
1139+    // We're using them in mouseover event,
1140+    // to avoid creating another closure.
1141+    var next = els.next,
1142+        prev = els.prev;
1143     els.play.addEvents({
1144       click: function () {
1145         Player.playPause();
1146hunk ./contrib/musicplayer/src/controllers/Player.js 165
1147       }
1148     });
1149     
1150-    var hideNextPrev = function () {
1151-      next.fade("out");
1152-      prev.fade("out");
1153-    };
1154+    next.addEvent("click", function () { Player.playNext() });
1155+    next.set("tween", {duration: "short", link: "cancel"});
1156     
1157hunk ./contrib/musicplayer/src/controllers/Player.js 168
1158-    els.next.addEvents({
1159-      click:   function () { Player.playNext() }
1160-    });
1161-    els.next.set("tween", {duration: "short", link: "cancel"});
1162+    prev.addEvent("click", function () { Player.playPrev() });
1163+    prev.set("tween", {duration: "short", link: "cancel"});
1164     
1165hunk ./contrib/musicplayer/src/controllers/Player.js 171
1166-    els.prev.addEvents({
1167-      click:    function () { Player.playPrev() }
1168+    els.controls.addEvent("mouseleave", function () {
1169+      next.fade("out");
1170+      prev.fade("out");
1171+      Player.elements.position.fade("out");
1172     });
1173hunk ./contrib/musicplayer/src/controllers/Player.js 176
1174-    els.prev.set("tween", {duration: "short", link: "cancel"});
1175     
1176hunk ./contrib/musicplayer/src/controllers/Player.js 177
1177-    els.controls.addEvent("mouseleave", hideNextPrev);
1178+    this.play_mode = "normal";
1179     
1180     this.elements = els;
1181     delete els;
1182hunk ./contrib/musicplayer/src/controllers/Player.js 182
1183     delete play_wrapper;
1184+    delete play_modes;
1185   },
1186   
1187   /**
1188hunk ./contrib/musicplayer/src/controllers/Player.js 214
1189       np.sound.stop();
1190     
1191     this._loading.push(song.id);
1192+    var _np_update_buffer = +new Date();
1193     this.sounds[song.id] = soundManager.createSound({
1194       id: song.id,
1195       url: "/uri/" + encodeURIComponent(song.id),
1196hunk ./contrib/musicplayer/src/controllers/Player.js 223
1197       stream: true,
1198       
1199       onload: function () {
1200-        this._loading.remove(song.id);
1201-       
1202         if(!song.get("duration"))
1203           song.update({duration: this.duration});
1204hunk ./contrib/musicplayer/src/controllers/Player.js 225
1205+       
1206+        if(!Player.progress_bar.ticks)
1207+          Player.progress_bar.ticks = Math.round(this.duration / (60 * 1000));
1208+       
1209+        Player._loading.remove(song.id);
1210       },
1211       
1212       onplay: function () {
1213hunk ./contrib/musicplayer/src/controllers/Player.js 237
1214       },
1215       
1216       whileloading: function () {
1217+        // It will usually take less time to load the song than to complete the
1218+        // playback so we're not buffering the updates here.
1219         if(Player.nowPlaying.sound === this)
1220           Player.progress_bar.setProgress("load", this.bytesLoaded/this.bytesTotal);
1221       },
1222hunk ./contrib/musicplayer/src/controllers/Player.js 244
1223       
1224       whileplaying: function () {
1225-        if(Player.nowPlaying.sound === this) {
1226-          Player.progress_bar.setProgress("track", this.position / this.durationEstimate);
1227-          Player.progress_bar.toElement().title = this.position + "/" + this.durationEstimate; 
1228+        var d = +new Date();
1229+        if(d - _np_update_buffer > 1000 && Player.nowPlaying.sound === this) {
1230+          var pb = Player.progress_bar;
1231+          pb.setProgress("track", this.position / this.durationEstimate);
1232+          Player.elements.position.set("text",
1233+            (new Date(this.position)).format("%M:%S") + " of " + (new Date(this.durationEstimate)).format("%M:%S")
1234+          );
1235+          pb = null;
1236+          _np_update_buffer = d;
1237         }
1238       },
1239       
1240hunk ./contrib/musicplayer/src/controllers/Player.js 257
1241       onfinish: function () {
1242+        song.update({plays: song.get("plays") + 1});
1243+       
1244         if(Player.nowPlaying.sound === this)
1245           Player.playbackFinished();
1246       }
1247hunk ./contrib/musicplayer/src/controllers/Player.js 270
1248     if(!this._visible) {
1249       this._visible = true;
1250       this._el.style.visibility = "visible";
1251+      this.elements.position.position({
1252+        relativeTo: $("track_progress"),
1253+        position:   "centerBottom",
1254+        edge:       "centerTop",
1255+        offset:     {y: 2}
1256+      });
1257     }
1258   },
1259   
1260hunk ./contrib/musicplayer/src/controllers/Player.js 327
1261       delete els;
1262     });
1263     
1264+    if(song.get("duration"))
1265+      this.progress_bar.ticks = Math.round(song.get("duration") / (60*1000));
1266+   
1267     da.controller.Player.fireEvent("play", [song]);
1268     song = null;
1269     
1270hunk ./contrib/musicplayer/src/controllers/Player.js 479
1271   },
1272   
1273   /**
1274+   *  Player.switchPlayMode(mode) -> undefined
1275+   *  - mode (String): `normal` or `shuffle`.
1276+   **/
1277+  switchPlayMode: function (mode) {
1278+    var old = this.play_mode;
1279+    this.play_mode = mode;
1280+   
1281+    if(old === "shuffle" || mode === "shuffle") {
1282+      var np = this.nowPlaying.song;
1283+      if(old === "shuffle") {
1284+        this.playlist = this._normalised_playlist || [];
1285+        if(np)
1286+          this._playlist_pos = this.playlist.indexOf(np.id);
1287+        else
1288+          this._playlist_pos = 0;
1289+
1290+        delete this._normalised_playlist;
1291+      } else if(mode === "shuffle") {
1292+        this._normalised_playlist = this.playlist;
1293+        // .shuffle() modifies the array itself
1294+        this.playlist = $A(this.playlist).shuffle();
1295+       
1296+        // moving now playing song to the beginning of the playlist
1297+        if(np)
1298+          this.playlist.erase(np.id).unshift(np.id);
1299+       
1300+        this._playlist_pos = 0;
1301+      }
1302+     
1303+      delete np;
1304+    }
1305+   
1306+   
1307+    $("player_toggle_shuffle_menu_item").set("text",
1308+      mode === "shuffle" ? "Turn shuffle off" : "Turn shuffle on"
1309+    );
1310+   
1311+    this.updateNextPrev();
1312+  },
1313
1314+  /**
1315    *  Player#free() -> undefined
1316    *
1317    *  Frees memory taken by loaded songs. This method is ran about every
1318hunk ./contrib/musicplayer/src/controllers/Player.js 526
1319    *  eight minutes and it destroys all SMSound objects which were played
1320    *  over eight minutes ago, ie. we're caching only about two songs in memory.
1321    *
1322-   *  #### Links
1323+   *  #### External resources
1324    *  * (The Universality of Song Length?)[http://a-candle-in-the-dark.blogspot.com/2010/02/song-length.html]
1325    *
1326    **/
1327hunk ./contrib/musicplayer/src/controllers/Player.js 537
1328     for(var id in this.sounds) {
1329       sound = this.sounds[id];
1330       if(this.sounds.hasOwnProperty(id)
1331-        && this.nowPlaying.song.id !== id
1332-        && (sound._last_played >= eight_mins_ago || !sound.loaded))
1333-      {
1334-        console.log("Freed sound ", id, sound._last_played);
1335+      && (this.nowPlaying.song.id !== id)
1336+      && (sound._last_played >= eight_mins_ago || !sound.loaded)) {
1337+        console.log("Freed sound", id, sound._last_played);
1338         sound.destruct();
1339         delete this.sounds[id];
1340       }
1341hunk ./contrib/musicplayer/src/controllers/Player.js 545
1342     }
1343     
1344-    delete sound;
1345+    sound = null;
1346   }
1347 };
1348 
1349hunk ./contrib/musicplayer/src/controllers/Player.js 551
1350 Player.initialize();
1351 
1352-// Check is performed every four minutes
1353-setTimeout(function () {
1354+setInterval(function () {
1355   Player.free();
1356 }, 8*60*1000);
1357 
1358hunk ./contrib/musicplayer/src/controllers/Player.js 649
1359     if(!playlist || $type(playlist) !== "array")
1360       return false;
1361     
1362+    if(Player.play_mode === "shuffle") {
1363+      Player._normalised_playlist = $A(playlist);
1364+      playlist.shuffle();
1365+    }
1366+   
1367     Player.playlist = playlist;
1368hunk ./contrib/musicplayer/src/controllers/Player.js 655
1369-    Player._playlist_pos = 0;
1370+    if(Player.nowPlaying.song)
1371+      Player._playlist_pos = playlist.indexOf(Player.nowPlaying.song.id)
1372+    else
1373+      Player._playlist_pos = 0;
1374+   
1375+    Player.updateNextPrev();
1376+  },
1377
1378+  /**
1379+   *  da.controller.Player.getPlaylist() -> [String, ...]
1380+   *  Returns an array with ids of the songs belonging to the playlist.
1381+   **/
1382+  getPlaylist: function () {
1383+    return Player.playlist;
1384   },
1385   
1386   /**
1387hunk ./contrib/musicplayer/src/controllers/Player.js 676
1388    **/
1389   nowPlaying: function () {
1390     return Player.nowPlaying.song;
1391+  },
1392
1393+  /**
1394+   *  da.controller.Player#setPlayMode(mode) -> undefined
1395+   *  - mode (String): `normal`, `shuffle` or `repeat`. (all lowercase)
1396+   **/
1397+  setPlayMode: function (mode) {
1398+    var old = Player.play_mode;
1399+    if(!mode || mode === old)
1400+      return false;
1401+   
1402+    Player.switchPlayMode(mode);
1403+  },
1404
1405+  /**
1406+   *  da.controller.Player#toggleMute() -> Boolean
1407+   *  Returns `true` if the sound volume will be set to 0, `false` otherwise.
1408+   **/
1409+  toggleMute: function () {
1410+    var muted = Player.nowPlaying.sound.muted;
1411+    da.vendor.soundManager[muted ? "unmute" : "mute"]();
1412+    $("player_mute_menu_item").set("text", muted ? "Mute" : "Unmute");
1413+   
1414+    return !muted;
1415+  },
1416
1417+  _toggleShuffle: function () {
1418+    if(Player.play_mode === "shuffle")
1419+      this.setPlayMode("normal");
1420+    else
1421+      this.setPlayMode("shuffle");
1422   }
1423 };
1424 $extend(da.controller.Player, new Events());
1425addfile ./contrib/musicplayer/src/controllers/Playlist.js
1426hunk ./contrib/musicplayer/src/controllers/Playlist.js 1
1427+//#require "libs/ui/Dialog.js"
1428+//#require "libs/ui/Menu.js"
1429+//#require "libs/util/PlaylistExporters.js"
1430+//#require "doctemplates/Playlist.js"
1431+//#require "doctemplates/Song.js"
1432+
1433+(function () {
1434+var Playlist        = da.db.DocumentTemplate.Playlist,
1435+    Song            = da.db.DocumentTemplate.Song,
1436+    Dialog          = da.ui.Dialog,
1437+    Menu            = da.ui.Menu,
1438+    playlistExport  = da.util.playlistExporter,
1439+    SONG_PREFIX     = "playlist_song_";
1440+
1441+/** section: Controllers
1442+ *  class PlaylistEditor
1443+ **/
1444+var PlaylistEditor = new Class({
1445+  /**
1446+   *  new PlaylistEditor(playlist)
1447+   *  - playlist (da.db.DocumentTemplate.Playlist): playlist wich will be edited.
1448+   **/
1449+  initialize: function (playlist) {
1450+    this.playlist = playlist;
1451+   
1452+    var playlist_details = (new Element("div", {
1453+        id: "playlist_details"
1454+      })).adopt(
1455+        new Element("label", {html: "Name of the playlist:", "for": "playlist_title"}),
1456+        new Element("input", {type: "text", value: playlist.get("title"), id: "playlist_title"}),
1457+       
1458+        new Element("label", {html: "Description:", "for": "playlist_description"}),
1459+        (new Element("textarea", {id: "playlist_description", value: playlist.get("description")}))
1460+      ),
1461+      songs = (new Element("ol", {
1462+        id: "playlist_songs",
1463+        "class": "navigation_column"
1464+      })),
1465+      footer = (new Element("div", {
1466+        "class": "footer"
1467+      })).adopt(
1468+        new Element("button", {
1469+          id: "playlist_delete",
1470+          html: "Delete this playlist",
1471+          events: {
1472+            click: this.destroyPlaylist.bind(this)
1473+          }
1474+        }),
1475+        new Element("button", {
1476+          id: "playlist_add_more_songs",
1477+          html: "Add more songs",
1478+          events: {
1479+            click: this.showSearchDialog.bind(this)
1480+          }
1481+        }),
1482+        new Element("button", {
1483+          id: "playlist_export",
1484+          html: "Export &#x25BC;",
1485+          events: {
1486+            mousedown: function (event) {
1487+              this.export_menu.show(event);
1488+            }.bind(this)
1489+          }
1490+        }),
1491+        new Element("input", {
1492+          type: "submit",
1493+          id: "playlist_save",
1494+          value: "Save",
1495+          events: {
1496+            click: this.save.bind(this)
1497+          }
1498+        })
1499+      ),
1500+      song_ids  = $A(playlist.get("song_ids")),
1501+      n         = song_ids.length;
1502+   
1503+    while(n--)
1504+      song_ids[n] = this.renderItem(Song.findById(song_ids[n]));
1505+   
1506+    songs.adopt(song_ids);
1507+    songs.addEvent("click:relay(.action)", this.removeSong.bind(this));
1508+   
1509+    this._sortable = new Sortables(songs, {
1510+      opacity:    0,
1511+      revert:     false,
1512+      clone:      true,
1513+      constrain:  true
1514+    });
1515+   
1516+    this._el = (new Element("div", {
1517+      id: "playlist_editor_" + playlist.id,
1518+      "class": "playlist_editor"
1519+    })).adopt(playlist_details, songs, footer);
1520+   
1521+    this.dialog = new Dialog({
1522+      title: "Edit playlist",
1523+      html: (new Element("div", {
1524+        "class": "playlist_editor_wrapper no_selection"
1525+      })).grab(this._el),
1526+      show:               true,
1527+      closeButton:        true,
1528+      hideOnOutsideClick: false,
1529+      destroyOnHide:      true,
1530+      onHide:             this.destroy.bind(this)
1531+    });
1532+   
1533+    var export_formats = {};
1534+    for(var format in playlistExport)
1535+      export_formats[format] = {html: "." + format, href: "#"};
1536+   
1537+    this.export_menu = new Menu({
1538+      items: export_formats,
1539+      position: {
1540+        position:   "center",
1541+        edge:       "center",
1542+        relativeTo: "playlist_export"
1543+      },
1544+      onClick: this.exportPlaylist.bind(this)
1545+    });
1546+  },
1547
1548+  /**
1549+   *  PlaylistEditor#removeSong(event, element) -> undefined
1550+   **/
1551+  removeSong: function (event, element) {
1552+    var el = element.parentNode;
1553+    this._sortable.removeItems(el);
1554+    el.set("slide", {duration: 360, mode: "horizontal"});
1555+    el.slide("out").fade("out").get("slide").chain(function () {
1556+      el.destroy();
1557+      delete el;
1558+    }.bind(this));
1559+  },
1560
1561+  /**
1562+   *  PlaylistEditor#destroyPlaylist(event, element) -> undefined
1563+   **/
1564+  destroyPlaylist: function (event, element) {
1565+    if(!confirm("Are you sure?"))
1566+      return;
1567+   
1568+    this.playlist.destroy();
1569+    this.destroy();
1570+  },
1571
1572+  /**
1573+   *  PlaylistEditor#save() -> undefined
1574+   **/
1575+  save: function () {
1576+    var ids = this._sortable.serialize(),
1577+         _pref_l = SONG_PREFIX.length,
1578+         n = ids.length;
1579+   
1580+    while(n--)
1581+      if(ids[n])
1582+        ids[n] = ids[n].slice(_pref_l);
1583+   
1584+    this.playlist.update({
1585+      title:       $("playlist_title").value,
1586+      description: $("playlist_description").value,
1587+      song_ids:    ids.clean()
1588+    });
1589+  },
1590
1591+  exportPlaylist: function (format) {
1592+    var exporter = playlistExport[format];
1593+    if(!exporter)
1594+      return false;
1595+   
1596+    exporter(this.playlist);
1597+  },
1598
1599+  renderItem: function (song) {
1600+    return new Element("li", {
1601+      id:   SONG_PREFIX + song.id,
1602+      "class": "column_item"
1603+    }).adopt(
1604+      new Element("a", {
1605+        title:    "Remove this song from the playlist",
1606+        href:     "#",
1607+        "class":  "action"
1608+      }),
1609+      new Element("span", {html: song.get("title")})
1610+    );
1611+  },
1612
1613+  showSearchDialog: function () {
1614+    var addSearchResult = this.addSearchResult.bind(this);
1615+   
1616+    da.controller.Search.search("/.*?/", ["Song"], {
1617+      onComplete: function (results, column) {
1618+        console.log("Hacking search results");
1619+
1620+        column.removeEvents("click");
1621+        column.addEvent("click", addSearchResult);
1622+       
1623+      }.bind(this)
1624+    });
1625+  },
1626
1627+  addSearchResult: function (item) {
1628+    if($("playlist_song_" + item.id))
1629+      return false;
1630+   
1631+    item = this.renderItem(Song.findById(item.id));
1632+    $("playlist_songs").grab(item);
1633+    this._sortable.addItems(item);
1634+  },
1635
1636+  destroy: function () {
1637+    this.export_menu.destroy();
1638+    this.playlist = null;
1639+    delete this.dialog;
1640+    delete this.export_menu;
1641+    delete this._el;
1642+  }
1643+});
1644+
1645+/** section: Controllers
1646+ *  class AddToPlaylistDialog
1647+ **/
1648+var AddToPlaylistDialog = new Class({
1649+  /**
1650+   *  new AddToPlaylistDialog(song)
1651+   *  - song (da.db.DocumentTemplate.Song): song which will be added to selected playlist.
1652+   **/
1653+  /**
1654+   *  AddToPlaylistDialog#song -> da.db.DocumentTemplate.Song
1655+   **/
1656+  initialize: function (song) {
1657+    this.song = song;
1658+   
1659+    var playlist_selector = new Element("select", {id: "playlist_selector"}),
1660+        playlists = Playlist.view().rows,
1661+        n = playlists.length;
1662+   
1663+   
1664+    while(n--)
1665+      playlist_selector.grab(new Element("option", {
1666+        value: playlists[n].id,
1667+        html: playlists[n].value.title
1668+      }));
1669+   
1670+    playlist_selector.grab(new Element("option", {
1671+      value: "_new_playlist",
1672+      html: "New playlist"
1673+    }));
1674+   
1675+    playlist_selector.addEvent("change", this.selectionChange.bind(this));
1676+   
1677+    this._new_playlist_form = (new Element("div", {id: "add_to_new_pl"})).adopt(
1678+      new Element("label",    {html: "Title:", "for": "add_to_new_pl_title"}),
1679+      new Element("input",    {id:   "add_to_new_pl_title", type: "text"}),
1680+      new Element("label",    {html: "Description:", "for": "add_to_new_pl_description"}),
1681+      new Element("textarea", {id:   "add_to_new_pl_description"})
1682+    );
1683+   
1684+    this._el = (new Element("form", {
1685+      id: "add_to_pl_dialog"
1686+    }).adopt(
1687+      new Element("div",    {id: "add_to_pl_playlists"}).adopt(
1688+        new Element("label",  {html: "Choose an playlist:", "for": "playlist_selector"}),
1689+        playlist_selector,
1690+        this._new_playlist_form
1691+      ),
1692+      (new Element("div", {"class": "footer"})).grab(
1693+        new Element("input", {
1694+          type: "submit",
1695+          value: "Okay",
1696+          events: {
1697+            click: this.save.bind(this)
1698+          }
1699+        })
1700+      )
1701+    ));
1702+   
1703+    this._el.addEvent("submit", this.save.bind(this));
1704+   
1705+    var title = (new Element("div", {"class": "dialog_title no_selection"})).adopt(
1706+      new Element("img",  {
1707+        src: "resources/images/album_cover_1.png",
1708+        id: "add_to_pl_album_cover"
1709+      }),
1710+      new Element("span", {html: song.get("title"), "class": "title"})
1711+    );
1712+   
1713+    this.dialog = new Dialog({
1714+      title: title,
1715+      html: (new Element("div", {"class": "add_to_pl_wrapper"})).grab(this._el),
1716+      closeButton:    true,
1717+      show:           true,
1718+      destroyOnHide:  true
1719+    });
1720+   
1721+    this.song.get("album", function (album) {
1722+      title.appendText("from ").grab(
1723+        new Element("span", {html: album.get("title")})
1724+      );
1725+     
1726+      var album_covers = album.get("album_cover_urls");
1727+      if(album_covers && album_covers[1])
1728+        $("add_to_pl_album_cover").src = album_covers[1];
1729+    });
1730+   
1731+    this.song.get("artist", function (artist) {
1732+      title.appendText(" by ").adopt(
1733+        new Element("span", {html: artist.get("title")}),
1734+        new Element("div", {"class": "clear"})
1735+      );
1736+    });
1737+   
1738+    this._playlist_selector = playlist_selector;
1739+    playlist_selector = null;
1740+  },
1741
1742+  /**
1743+   *  AddToPlaylistDialog#save([event]) -> undefined
1744+   **/
1745+  save: function (event) {
1746+    if(event)
1747+      Event.stop(event);
1748+   
1749+    var playlist_id = this._playlist_selector.value;
1750+    if(playlist_id === "_new_playlist") {
1751+      var title = $("add_to_new_pl_title");
1752+      if(!title.value.length)
1753+        return title.focus();
1754+       
1755+      Playlist.create({
1756+        title:        title.value,
1757+        description:  $("add_to_new_pl_description").value,
1758+        song_ids:     [this.song.id]
1759+      });
1760+    } else {
1761+      var playlist = Playlist.findById(playlist_id);
1762+      playlist.get("song_ids").include(this.song.id);
1763+      playlist.save();
1764+      playlist = null;
1765+    }
1766+   
1767+    this.destroy();
1768+  },
1769
1770+  /**
1771+   *  AddToPlaylistDialog#selectionChange() -> undefined
1772+   *  Called on `change` event by playlist selector.
1773+   **/
1774+  selectionChange: function () {
1775+    if(this._playlist_selector.value === "_new_playlist")
1776+      this._new_playlist_form.show();
1777+    else
1778+      this._new_playlist_form.hide();
1779+  },
1780
1781+  /**
1782+   *  AddToPlaylistDialog#destroy() -> undefined
1783+   **/
1784+  destroy: function () {
1785+    this.song = null;
1786+    this.dialog.destroy();
1787+    delete this.dialog;
1788+    delete this._el;
1789+    delete this._playlist_selector;
1790+    delete this._new_playlist_form;
1791+  }
1792+});
1793+
1794+/**
1795+ * da.controller.Playlist
1796+ **/
1797+da.controller.Playlist = {
1798+  /**
1799+   *  da.controller.Playlist.edit(playlist) -> undefined
1800+   *  - playlist (da.db.DocumentTemplate.Playlist): playlist which will be edited.
1801+   **/
1802+  edit: function (playlist) {
1803+    new PlaylistEditor(playlist);
1804+  },
1805
1806+  /**
1807+   *  da.controller.Playlist.addSong([song]) -> undefined
1808+   *  - song (da.db.DocumentTemplate.Song): song which will be added to an playlist.
1809+   *    If not provided, [[da.controller.Player.nowPlaying]] will be used.
1810+   **/
1811+  addSong: function (song) {
1812+    if(!song)
1813+      song = da.controller.Player.nowPlaying();
1814+    if(!song)
1815+      return false;
1816+   
1817+    new AddToPlaylistDialog(song);
1818+  }
1819+};
1820+
1821+da.app.fireEvent("ready.controller.Playlist", [], 1);
1822+})();
1823addfile ./contrib/musicplayer/src/controllers/Search.js
1824hunk ./contrib/musicplayer/src/controllers/Search.js 1
1825+//#require "libs/ui/Dialog.js"
1826+//#require "controllers/default_columns.js"
1827+
1828+(function () {
1829+var Dialog      = da.ui.Dialog,
1830+    SongsColumn = da.controller.Navigation.columns.Songs,
1831+    Playlist    = da.db.DocumentTemplate.Search,
1832+    ACTIVE      = null;
1833+
1834+/** section: Controllers
1835+ *  class SearchResults < da.controller.Navigation.columns.Songs
1836+ **/
1837+var SearchResults = new Class({
1838+  Extends: SongsColumn,
1839
1840+  options: {
1841+    id:         "search_results",
1842+    rowHeight:  50
1843+  },
1844
1845+  view: {
1846+    id:        null,
1847+    temporary: true,
1848+   
1849+    map: function (doc, emit) {
1850+      var type = doc.type,
1851+          filters = this.options.filters,
1852+          query = this.options.query;
1853+     
1854+      // we have to emit every document because the filters
1855+      // represent OR operation, ie. if user selected ["Title", "Album"]
1856+      // it means that only one of those filters have to be satisfied
1857+      // in order fot the song to show up in the search results.
1858+     
1859+      emit(type, type === "Song" ? {
1860+        id:         doc.id,
1861+        title:      doc.title,
1862+        artist_id:  doc.artist_id,
1863+        album_id:   doc.album_id,
1864+        track:      doc.track,
1865+        match:      query.test(doc.title)
1866+      } : {
1867+        id:     doc.id,
1868+        title:  doc.title,
1869+        match:  query.test(doc.title)
1870+      });
1871+    },
1872+   
1873+    reduce: function (key, values, rereduce) {
1874+      var query = this.options.query;
1875+     
1876+      if(key !== "Song") {
1877+        var _values = {},
1878+            n = values.length,
1879+            val;
1880+       
1881+        while(n--) {
1882+          val = values[n];
1883+          _values[val.id] = val;
1884+        }
1885+       
1886+        return _values;
1887+      } else {
1888+        var n = values.length,
1889+            val;
1890+       
1891+        while(n--) {
1892+          val = values[n];
1893+          values[n] = {
1894+            id:   val.id,
1895+            key:  val.title,
1896+            value: val
1897+          };
1898+        }
1899+         
1900+        return values;
1901+      }
1902+    }
1903+  },
1904
1905+  mapReduceFinished: function (view) {
1906+    var songs = view.getRow("Song");
1907+    if(!songs)
1908+      return this.parent({rows: []});
1909+   
1910+    var n = songs.length,
1911+        filters = this.options.filters,
1912+        query  = this.options.query,
1913+        matches;
1914+   
1915+    this._albums = view.getRow("Album");
1916+    this._artists = view.getRow("Artist");
1917+   
1918+   
1919+    while(n--) {
1920+      song = songs[n].value;
1921+      matches = [];
1922+      if(filters.contains("Song"))
1923+        matches.push(song.match);
1924+      if(filters.contains("Album"))
1925+        matches.push(this._albums[song.album_id].match);
1926+      if(filters.contains("Artist"))
1927+        matches.push(this._artists[song.artist_id].match);
1928+     
1929+      var m = matches.length, false_count = 0;
1930+      while(m--)
1931+        if(!matches[m])
1932+          false_count++;
1933+           
1934+      if(false_count === matches.length)
1935+        delete songs[n];
1936+    }
1937+
1938+    songs = songs.clean();
1939+    this.parent({
1940+      rows: songs
1941+    });
1942+   
1943+    this._finished = true;
1944+    Search.search_field.disabled = false;
1945+   
1946+    this.fireEvent("complete", [songs, this], 1);
1947+  },
1948+
1949+  renderItem: function (index) {
1950+    var item    = this.getItem(index),
1951+        data    = item.value,
1952+        query   = this.options.query,
1953+        artist  = this._artists[data.artist_id].title,
1954+        album   = this._albums[data.album_id].title;
1955+   
1956+    return (new Element("a", {
1957+      id:       "search_results_column_item_" + item.id,
1958+      href:     "#",
1959+      title:    "{0} by {1}".interpolate([data.title, artist]),
1960+      "class":  index % 2 ? "even" : "odd"
1961+    }).adopt([
1962+      new Element("span", {html: index + 1,   "class": "result_number"}),
1963+      new Element("span", {
1964+        html: data.title.replace(query, underline),
1965+        "class": "title"
1966+      }),
1967+      new Element("span", {
1968+        html: "{0}from <i>{1}</i> by <i>{2}</i>".interpolate([
1969+          data.track ? "track no." + data.track + " " : "",
1970+          album.replace(query, underline), artist.replace(query, underline)
1971+        ]),
1972+        "class": "subtitle"
1973+      })
1974+    ]));
1975+  },
1976
1977+  compareFunction: function (a, b) {
1978+    a = a.key + a.id;
1979+    b = b.key + b.id;
1980+   
1981+    if(a < b) return -1;
1982+    if(a > b) return 1;
1983+    return 0;
1984+  }
1985+});
1986+
1987+function underline (str) {
1988+  return "<u>" + str + "</u>";
1989+}
1990+
1991+
1992+/** section: Controllers
1993+ * class Search
1994+ **/
1995+var Search = ({
1996+  /**
1997+   *  Search.query -> String
1998+   *  Current search query
1999+   **/
2000+  query: "",
2001
2002+  /**
2003+   *  Search.active_filters -> [String, ]
2004+   *  List of active filters, possible values are:
2005+   *  * `Song`,
2006+   *  * `Album` or
2007+   *  * `Artist`
2008+   *
2009+   **/
2010+  active_filters: ["Song"],
2011
2012+  /**
2013+   *  Search.results_column -> SearchResults
2014+   **/
2015+  results_column: null,
2016
2017+  initialize: function () {
2018+    this._el = new Element("div", {id: "search_dialog"});
2019+    this.search_field = new Element("input", {
2020+      type:         "text",
2021+      id:           "search_field",
2022+      placeholder:  "Search..."
2023+    });
2024+    var header = (new Element("form", {
2025+      id:       "search_header",
2026+      action:   "#",
2027+      "class":  "dialog_title"
2028+    })).adopt([
2029+      this.search_field,
2030+      (new Element("div", {
2031+        id: "search_by_filters",
2032+        "class": "button_group"
2033+      })).adopt([
2034+        new Element("button", {id: "search_filter_Song",       html: "Song title", "class": "active"}),
2035+        new Element("button", {id: "search_filter_Album",      html: "Album"}),
2036+        new Element("button", {id: "search_filter_Artist",     html: "Artist"})
2037+      ])
2038+    ]);
2039+
2040+    function searchFromField (event) {
2041+      if(event)
2042+        Event.stop(event);
2043+     
2044+      Search.search(Search.search_field.value, Search.active_filters);
2045+    }
2046+
2047+    header.addEvent("submit", searchFromField);
2048+   
2049+    var _search_buffer;
2050+    this.search_field.addEvent("keyup", function (event) {
2051+      clearTimeout(_search_buffer);
2052+      _search_buffer = setTimeout(searchFromField, 360);
2053+    });
2054+    this.search_field.addEvent("mousedown", function (event) {
2055+      // since the title is draggable, the text field would never
2056+      // get focus.
2057+      event.stopPropagation();
2058+    });
2059+   
2060+    this._el.grab(header);
2061+    var _sf_l = "search_filter_".length;
2062+    header.addEvent("click:relay(button)", function (event, button) {
2063+      var filter = button.id.slice(_sf_l);
2064+      if(Search.active_filters.contains(filter))
2065+        Search.deactivateFilter(filter);
2066+      else
2067+        Search.activateFilter(filter);
2068+     
2069+      Search.query = null;
2070+      Search.search_field.focus();
2071+      Search.search(Search.search_field.value);
2072+    });
2073+   
2074+    this.dialog = new Dialog({
2075+      title:              header,
2076+      html:               (new Element("div", {id: "search_dialog_wrapper"})).grab(this._el),
2077+      closeButton:        true,
2078+      draggable:          true,
2079+      hideOnOutsideClick: false,
2080+     
2081+      onShow: function () {
2082+        Search.search_field.focus();
2083+      },
2084+     
2085+      onHide: function () {
2086+        if(this.results_column)
2087+          this.results_column.destroy();
2088+       
2089+        delete this.results_column;
2090+      }
2091+    });
2092+   
2093+    this.initialized = true;
2094+    delete header;
2095+  },
2096
2097+  /**
2098+   *  Search.show() -> undefined
2099+   **/
2100+  show: function () {
2101+    this.dialog.show();
2102+  },
2103
2104+  /**
2105+   *  Search.search(query[, filters, options]) -> undefined | false
2106+   *  - query (String | RegExp): search query.
2107+   *  - filter (String): one of the filters. See [[Search.active_filter]] for
2108+   *    possible values, this also the default value.
2109+   *  - options (Function): passed to the [[SearchResults]] class.
2110+   *
2111+   *  `false` will be returned in cases when search won't be started:
2112+   *  * if the last query was the same as the new one,
2113+   *  * if the last search hasn't finished,
2114+   *  * there are no active filters.
2115+   * 
2116+   *  #### Notes
2117+   *  If the `query` is a [[String]], modifications to it will be applied
2118+   *  in order to get semi-fuzzy search.
2119+   * 
2120+   *  #### See also
2121+   *  * [Autocomplete fuzzy matching](http://www.dustindiaz.com/autocomplete-fuzzy-matching/)
2122+   *
2123+   **/
2124+  search: function (query, filters, options) {
2125+    if(this.query === query || query.length < 3)
2126+      return false;
2127+    if(!filters || !filters.length)
2128+      filters = this.active_filters;
2129+    if(!filters.length)
2130+      return false;
2131+   
2132+    this.query = query;
2133+    if(this.results_column) {
2134+      if(!this.results_column._finished)
2135+        return false;
2136+     
2137+      this.results_column.destroy();
2138+      delete this.results_column;
2139+    }
2140+   
2141+    if(!(query instanceof RegExp))
2142+      if(query[0] === "/" && query.slice(-1) === "/")
2143+        query = new RegExp(query.slice(1,-1), "ig")
2144+      else if(query.contains(" "))
2145+        query = new RegExp("(" + query.escapeRegExp() + ")", "ig");
2146+      else
2147+        query = new RegExp(query.replace(/\W/g, "").split("").join("\\w*"), "ig");
2148
2149+    console.log("searching for", query, filters);
2150+    this.search_field.disabled = true;
2151+   
2152+    // This is a small hack which allows playlist editor
2153+    // add drag&drop controls, as the options persist between
2154+    // calls.
2155+    if(options)
2156+      this.column_options = options;
2157+    else
2158+      options = this.column_options;
2159+   
2160+    if(!options.parentElement)
2161+      options.parentElement = this._el;
2162+    this.results_column = new SearchResults($extend(options, {
2163+      query:          query,
2164+      filters:        filters
2165+    }));
2166+  },
2167
2168+  /**
2169+   *  Search.saveAsPlaylist() -> undefined
2170+   *  Saves search results as a new playlist and opens [[PlaylistEditor#edit]] dialog.
2171+   **/
2172+  saveAsPlaylist: function () {
2173+    if(!this.results_column || !this.results_column.finished)
2174+      return;
2175+   
2176+    var songs   = this.results_column._rows,
2177+        n       = songs.length,
2178+        song_ids = new Array(n);
2179+   
2180+    while(n--)
2181+      song_ids[n] = songs[n].id;
2182+   
2183+    Playlist.create({
2184+      title: "Search results",
2185+      song_ids: song_ids
2186+    }, function (playlist) {
2187+      da.controller.Playlist.edit(playlist);
2188+    });
2189+  },
2190
2191+  /**
2192+   *  Search.activateFilter(filter) -> false | undefined
2193+   *  - filter (String): filter to activate. See [[Search.active_filter]] for
2194+   *    possible values.
2195+   **/
2196+  activateFilter: function (filter) {
2197+    if(this.active_filters.contains(filter))
2198+      return false;
2199+   
2200+    $("search_filter_" + filter).addClass("active");
2201+    this.active_filters.push(filter);
2202+  },
2203
2204+  /**
2205+   *  Search.deactivateFilter(filter) -> false | undefined
2206+   *  - filter (String): filter to deactivate.
2207+   **/
2208+  deactivateFilter: function (filter) {
2209+    if(!this.active_filters.contains(filter))
2210+      return false;
2211+   
2212+   $("search_filter_" + filter).removeClass("active");
2213+   this.active_filters.erase(filter);
2214+  },
2215
2216+  /**
2217+   *  Search.destroy() -> undefined
2218+   **/
2219+  destroy: function () {
2220+    this.dialog.destroy();
2221+    delete this.dialog;
2222+   
2223+    if(this.results_column)
2224+      this.results_column.destroy();
2225+    delete this.results_column;
2226+    delete this._el;
2227+    delete this.search_field;
2228+  }
2229+});
2230+
2231+/**
2232+ * da.controller.Search
2233+ **/
2234+da.controller.Search = {
2235+  /**
2236+   *  da.controller.Search.show() -> undefined
2237+   * 
2238+   *  Shows search overlay.
2239+   *
2240+   **/
2241+  show: function () {
2242+    if(!Search.initialized)
2243+      Search.initialize();
2244+   
2245+    Search.column_options = {};
2246+    Search.show();
2247+  },
2248+  /**
2249+   *  da.controller.Search.search(searchTerm[, filters][, options]) -> undefined
2250+   *  - searchTerm (String): the query.
2251+   *  - filters (Array): filters to use, [[Search.active_filters]].
2252+   *  - options (Object): options passed to [[SearchResults]] class.
2253+   *  - options.onComplete (Function): function called with search results as first
2254+   *    argument and instance of the class as the second argument.
2255+   **/
2256+  search: function (term, filters, options) {
2257+    this.show();
2258+    Search.search_field.value = term;
2259+    Search.search(term, filters, options || {});
2260+  }
2261+};
2262+
2263+})();
2264hunk ./contrib/musicplayer/src/controllers/Settings.js 14
2265  *  This is private class.
2266  *  Public interface is accessible via [[da.controller.Settings]].
2267  **/
2268-   
2269-var Dialog = da.ui.Dialog,
2270-    Setting = da.db.DocumentTemplate.Setting;
2271+
2272+var Dialog            = da.ui.Dialog,
2273+    NavigationColumn  = da.ui.NavigationColumn,
2274+    Setting           = da.db.DocumentTemplate.Setting;
2275 
2276 var GROUPS = [{
2277hunk ./contrib/musicplayer/src/controllers/Settings.js 20
2278-    id: "caps",
2279-    title: "Caps",
2280-    description: "Tahoe caps for your music and configuration files."
2281-  }, {
2282-    id: "lastfm",
2283-    title: "Last.fm",
2284-    description: 'Share the music your are listening to with the world via <a href="http://last.fm" target="_blank">Last.fm</a>.'
2285-  }
2286-];
2287+  id: "caps",
2288+  title: "Caps",
2289+  description: "Tahoe caps for your music and configuration files."
2290+}];
2291 
2292 // Renderers are used to render the interface elements for each setting (ie. the input boxes, checkboxes etc.)
2293 // Settings and renderers are bound together via "representAs" property which
2294hunk ./contrib/musicplayer/src/controllers/Settings.js 99
2295     this.dialog = new Dialog({
2296       title: "Settings",
2297       html:  new Element("div", {id: "settings"}),
2298-      hideOnOutsideClick: false
2299+      hideOnOutsideClick: false,
2300+      closeButton: true
2301     });
2302     this._el = $("settings");
2303     this.column = new GroupsColumn({
2304hunk ./contrib/musicplayer/src/controllers/Settings.js 263
2305 }
2306 
2307 var GroupsColumn = new Class({
2308-  Extends: da.ui.NavigationColumn,
2309+  Extends: NavigationColumn,
2310 
2311   view: null,
2312 
2313hunk ./contrib/musicplayer/src/controllers/SongContext.js 27
2314     });
2315     this.tabs = new Element("div", {
2316       id: "context_tabs",
2317-      "class": "no_selection"
2318+      "class": "button_group no_selection"
2319     });
2320     
2321     for(var id in CONTEXTS)
2322hunk ./contrib/musicplayer/src/controllers/SongContext.js 31
2323-      this.tabs.grab(new Element("a", {
2324-        id:       id + TAB_SUFFIX,
2325-        "class":  "tab",
2326-        href:     "#",
2327-        html:     CONTEXTS[id].title
2328+      this.tabs.grab(new Element("button", {
2329+        id:   id + TAB_SUFFIX,
2330+        html: CONTEXTS[id].title
2331       }));
2332hunk ./contrib/musicplayer/src/controllers/SongContext.js 35
2333-    this.tabs.addEvent("click:relay(.tab)", function () {
2334+    this.tabs.addEvent("click:relay(button)", function () {
2335       SongContext.show(this.id.slice(0, _TS_L));
2336     });
2337     
2338hunk ./contrib/musicplayer/src/controllers/SongContext.js 48
2339     
2340     $("player_pane").adopt(this.tabs, this.loading_screen, this.el);
2341     
2342+    function adjustDimensions () {
2343+      SongContext.el.style.height = (
2344+        window.getHeight() - $("song_info_block").getHeight() - SongContext.tabs.getHeight()
2345+      ) + "px";
2346+    }
2347+   
2348     Player.addEvent("play", function (song) {
2349hunk ./contrib/musicplayer/src/controllers/SongContext.js 55
2350+      adjustDimensions();
2351+     
2352       if(SongContext.active)
2353         SongContext.active.update(song);
2354     });
2355hunk ./contrib/musicplayer/src/controllers/SongContext.js 61
2356     
2357-    window.addEvent("resize", function () {
2358-      SongContext.el.style.height = (
2359-        window.getHeight() - $("song_info_block").getHeight() - SongContext.tabs.getHeight()
2360-      ) + "px";
2361-    });
2362+    window.addEvent("resize", adjustDimensions);
2363     window.fireEvent("resize");
2364hunk ./contrib/musicplayer/src/controllers/SongContext.js 63
2365+   
2366     this.initialized = true;
2367   },
2368   
2369hunk ./contrib/musicplayer/src/controllers/SongContext.js 115
2370    *
2371    **/
2372   addTab: function (id) {
2373-    this.tabs.grab(new Element("a", {
2374-      id:       id + TAB_SUFFIX,
2375-      "class":  "tab",
2376-      href:     "#",
2377-      html:     CONTEXTS[id].title
2378+    this.tabs.grab(new Element("button", {
2379+      id:   id + TAB_SUFFIX,
2380+      html: CONTEXTS[id].title
2381     }));
2382   }
2383 };
2384hunk ./contrib/musicplayer/src/controllers/controllers.js 17
2385 //#require "controllers/Navigation.js"
2386 //#require "controllers/Player.js"
2387 //#require "controllers/SongContext.js"
2388+//#require "controllers/Search.js"
2389+//#require "controllers/Playlist.js"
2390 //#require "controllers/CollectionScanner.js"
2391hunk ./contrib/musicplayer/src/controllers/default_columns.js 1
2392+//#require "controllers/Player.js"
2393 //#require "libs/ui/NavigationColumn.js"
2394 //#require "services/albumCover.js"
2395 
2396hunk ./contrib/musicplayer/src/controllers/default_columns.js 7
2397 (function () {
2398 var Navigation        = da.controller.Navigation,
2399+    Player            = da.controller.Player,
2400     NavigationColumn  = da.ui.NavigationColumn,
2401     Album             = da.db.DocumentTemplate.Album,
2402     Song              = da.db.DocumentTemplate.Song,
2403hunk ./contrib/musicplayer/src/controllers/default_columns.js 11
2404+    Playlist          = da.db.DocumentTemplate.Playlist,
2405     fetchAlbumCover   = da.service.albumCover;
2406 
2407 /** section: Controller
2408hunk ./contrib/musicplayer/src/controllers/default_columns.js 49
2409  * 
2410  *  Displays artists.
2411  **/
2412-var the_regex = /^the\s*/i;
2413+var THE_REGEX = /^the\s*/i;
2414 Navigation.registerColumn("Artists", ["Albums", "Songs"], new Class({
2415   Extends: NavigationColumn,
2416   
2417hunk ./contrib/musicplayer/src/controllers/default_columns.js 58
2418     map: function (doc, emit) {
2419       // If there are no documents in the DB this function
2420       // will be called with "undefined" as first argument
2421-      if(!doc) return;
2422+      if(!doc || doc.type !== "Artist") return;
2423 
2424hunk ./contrib/musicplayer/src/controllers/default_columns.js 60
2425-      if(doc.type === "Artist")
2426-        emit(doc.id, {
2427-          title: doc.title
2428-        });
2429+      emit(doc.id, {
2430+        title: doc.title
2431+      });
2432     }
2433   },
2434   
2435hunk ./contrib/musicplayer/src/controllers/default_columns.js 71
2436   },
2437   
2438   compareFunction: function (a, b) {
2439-    a = a && a.value.title ? a.value.title.split(the_regex).slice(-1) : a;
2440-    b = b && b.value.title ? b.value.title.split(the_regex).slice(-1) : b;
2441+    a = a && a.value.title ? a.value.title.split(THE_REGEX).slice(-1) : a;
2442+    b = b && b.value.title ? b.value.title.split(THE_REGEX).slice(-1) : b;
2443     
2444     if(a < b) return -1;
2445     if(a > b) return 1;
2446hunk ./contrib/musicplayer/src/controllers/default_columns.js 90
2447   Extends: NavigationColumn,
2448 
2449   options: {
2450-    rowHeight: 72/3,
2451-    iconSize: 1,
2452-    renderImmediately: false
2453+    rowHeight: 50
2454   },
2455   
2456hunk ./contrib/musicplayer/src/controllers/default_columns.js 93
2457-  // We can't reuse "Album" view because of #_passesFilter().
2458   view: {
2459     id: "albums_column",
2460     
2461hunk ./contrib/musicplayer/src/controllers/default_columns.js 97
2462     map: function (doc, emit) {
2463-      if(!doc || !this._passesFilter(doc)) return;
2464+      if(!doc || doc.type !== "Album" || !this._passesFilter(doc)) return;
2465 
2466hunk ./contrib/musicplayer/src/controllers/default_columns.js 99
2467-      if(doc.type === "Album")
2468-        emit(doc.id, {
2469-          title: doc.title,
2470-          icons: doc.album_cover_urls || []
2471-        });
2472+      emit(doc.id, {
2473+        title: doc.title,
2474+        icon: doc.album_cover_urls ? doc.album_cover_urls[1] : null
2475+      });
2476     }
2477   },
2478   
2479hunk ./contrib/musicplayer/src/controllers/default_columns.js 106
2480-  initialize: function (options) {
2481-    this.parent(options);
2482-   
2483-    // TODO: select icon size depending on the column's width
2484-    //       also, adjust margins between the elements accordingly
2485-    this.options.iconSize = this.options.totalCount <= (this.getVisibleIndexes()[1] + 1) ? 2 : 1;
2486-    this._row_dim = this.options.iconSize === 1 ? 64 : 174;
2487+  renderItem: function (index) {
2488+    var item = this.getItem(index);
2489+    if(!item.value.icon) {
2490+      item.value.icon = "resources/images/album_cover_1.png";
2491+      fetchAlbumCover(Album.findById(item.id), function (urls) {
2492+        item.value.icon = urls[1];
2493+      });
2494+    }
2495     
2496hunk ./contrib/musicplayer/src/controllers/default_columns.js 115
2497-    this._el.addEvent("resize", function () {
2498-      var width = this._el.getWidth();
2499-      // 4 + 4 being padding on the element
2500-      this.options.rowHeight = width / (4 + this._row_dim + 4);
2501-    }.bind(this));
2502-    this._el.fireEvent("resize");
2503     
2504hunk ./contrib/musicplayer/src/controllers/default_columns.js 116
2505-    this.render();
2506+    return this.parent(index);
2507   },
2508   
2509   createFilter: function (item) {
2510hunk ./contrib/musicplayer/src/controllers/default_columns.js 121
2511     return {album_id: item.id};
2512+  }
2513+}));
2514+
2515+/**
2516+ *  class da.controller.Navigation.columns.Genres < da.ui.NavigationColumn
2517+ *  filters: [[da.controller.Navigation.columns.Songs]]
2518+ * 
2519+ *  Displays song genres.
2520+ **/
2521+var GENRES = da.util.GENRES;
2522+Navigation.registerColumn("Genres", ["Songs"], new Class({
2523+  Extends: NavigationColumn,
2524
2525+  view: {
2526+    id: "genres_column",
2527+    map: function (doc, emit) {
2528+      // If there are no documents in the DB this function
2529+      // will be called with "undefined" as first argument
2530+      if(!doc || doc.type !== "Song") return;
2531+
2532+      emit(doc.genre || -1, 1);
2533+    },
2534+    reduce: function (key, values, rereduce) {
2535+      //console.log("reduce", key, values);
2536+     
2537+      if(key !== null) {
2538+        var key_n = isNaN(+key) ? key : + key;
2539+       
2540+        return {
2541+          title:    typeof key_n === "number" ? GENRES[key_n] || "Unknown" : key_n,
2542+          subtitle: values.length,
2543+          genre:    key_n
2544+        }
2545+      } else {
2546+        var n = values.length, count = 0;
2547+        while(n--)
2548+          count += values[n].subtitle;
2549+       
2550+        return {
2551+          title:    values[0].title,
2552+          subtitle: count,
2553+          genre:    values[0].genre
2554+        }
2555+      }
2556+    }
2557   },
2558   
2559hunk ./contrib/musicplayer/src/controllers/default_columns.js 168
2560-  renderItem: function (index) {
2561-    var item = this.getItem(index),
2562-        data = item.value,
2563-        el = new Element("a", {
2564-          id:     this.options.id + "_column_item_" + item.id,
2565-          href:   "#",
2566-          title:  data.title
2567-        }),
2568-        cover = data.icons[this.options.iconSize];
2569+  mapReduceFinished: function (view) {
2570+    this._addIdsToReducedView(view);
2571+    this.parent(view);
2572+  },
2573
2574+  mapReduceUpdated: function (view) {
2575+    this._addIdsToReducedView(view);
2576+    this.parent(view);
2577+  },
2578
2579+  _addIdsToReducedView: function (view) {
2580+    var n = view.rows.length;
2581+    while(n--)
2582+      view.rows[n].id = view.rows[n].value.genre;
2583+    return view;
2584+  },
2585
2586+  createFilter: function (item) {
2587+    return {genre: item.value.genre};
2588+  },
2589
2590+  compareFunction: function (a, b) {
2591+    a = a && a.value.title ? a.value.title.split(THE_REGEX).slice(-1) : a;
2592+    b = b && b.value.title ? b.value.title.split(THE_REGEX).slice(-1) : b;
2593     
2594hunk ./contrib/musicplayer/src/controllers/default_columns.js 193
2595-    el.style.width = this._row_dim + "px";
2596-    el.style.height = this._row_dim + "px";
2597-    if(!cover || !cover.length) {
2598-      cover = "resources/images/album_cover_" + this.options.iconSize + ".png";
2599-      fetchAlbumCover(Album.findById(item.id));
2600+    if(a < b) return -1;
2601+    if(a > b) return 1;
2602+    return 0;
2603+  }
2604+}));
2605+
2606+
2607+/**
2608+ *  class da.controller.Navigation.columns.Playlists < da.ui.NavigationColumn
2609+ *  filters: [[da.controller.Navigation.columns.Songs]]
2610+ * 
2611+ *  Displays songs.
2612+ **/
2613+Navigation.registerColumn("Playlists", ["_PlaylistSongs"], new Class({
2614+  Extends: NavigationColumn,
2615
2616+  view: {
2617+    id: "playlists_column",
2618+    map: function (doc, emit) {
2619+      if(!doc || doc.type !== "Playlist" || !this._passesFilter(doc)) return;
2620+
2621+      emit(doc.id, {
2622+        title:    doc.title,
2623+        song_ids: doc.song_ids
2624+      });
2625     }
2626hunk ./contrib/musicplayer/src/controllers/default_columns.js 219
2627+  },
2628
2629+  initialize: function (options) {
2630+    this.parent(options);
2631     
2632hunk ./contrib/musicplayer/src/controllers/default_columns.js 224
2633-    el.grab(new Element("img",  {src: cover}));   
2634-    return el;
2635+    this._el.addEvent("click:relay(.action)", function (event, el) {
2636+      var item = this.getItem(el.parentNode.retrieve("column_index"));
2637+      da.controller.Playlist.edit(Playlist.findById(item.id));
2638+    }.bind(this));
2639   },
2640   
2641hunk ./contrib/musicplayer/src/controllers/default_columns.js 230
2642-  getBoxCoords: function(index) {
2643-    return [0, (this.options.rowHeight * index)/3];
2644+  mapReduceUpdated: function (view) {
2645+    this.parent(view);
2646+    if(this._active_el)
2647+      this._el.fireEvent("click:relay(.column_item)", [null, this._active_el], 1);
2648+  },
2649
2650+  createFilter: function (item) {
2651+    var   id = item.id,
2652+          songs = item.value.song_ids;
2653+   
2654+    return function (song) {
2655+      return song.type === "Song" ? songs.contains(song.id) : song.id === id;
2656+    }
2657+  },
2658
2659+  renderItem: function (index) {
2660+    var item = this.getItem(index),
2661+         data = item.value;
2662+   
2663+    return (new Element("a", {
2664+      id:       this.options.id + "_column_item_" + item.id,
2665+      href:     "#",
2666+      "class":  index % 2 ? "even" : "odd"
2667+    })).adopt([
2668+      new Element("a", {
2669+        href:     "#",
2670+        html:     "Edit",
2671+        title:    "Edit the playlist",
2672+        "class":  "action"
2673+      }),
2674+      new Element("span", {
2675+        html:     data.title,
2676+        title:    data.title,
2677+        "class":  "title"
2678+      })
2679+    ]);
2680   }
2681 }));
2682 
2683hunk ./contrib/musicplayer/src/controllers/default_columns.js 269
2684+
2685 /**
2686  *  class da.controller.Navigation.columns.Songs < da.ui.NavigationColumn
2687  *  filters: none
2688hunk ./contrib/musicplayer/src/controllers/default_columns.js 283
2689     this.parent(options);
2690     
2691     this.addEvent("click", function (item, event, el) {
2692-      da.controller.Player.setPlaylist(this._playlist);
2693-      da.controller.Player.play(Song.findById(item.id));
2694-    }.bind(this), true);
2695+      el.removeClass("active_column_item");
2696+    }, true);
2697+   
2698+    this.addEvent("click", function (item, event, el) {
2699+      Player.play(Song.findById(item.id));
2700+      Player.setPlaylist(this._playlist);
2701+    }.bind(this));
2702+   
2703+    this._onSongChange = this._updateSelectedItem.bind(this);
2704+    da.controller.Player.addEvent("play", this._onSongChange);
2705   },
2706   
2707   view: {
2708hunk ./contrib/musicplayer/src/controllers/default_columns.js 298
2709     id: "songs_column",
2710     map: function (doc, emit) {
2711-      if(!doc || !this._passesFilter(doc)) return;
2712+      if(!doc || doc.type !== "Song" || !this._passesFilter(doc))
2713+        return;
2714 
2715hunk ./contrib/musicplayer/src/controllers/default_columns.js 301
2716-      if(doc.type === "Song" && doc.title)
2717+      if(doc.title && doc.title.length)
2718         emit(doc.title, {
2719           title: doc.title,
2720           track: doc.track
2721hunk ./contrib/musicplayer/src/controllers/default_columns.js 312
2722   mapReduceFinished: function (values) {
2723     this.parent(values);
2724     this.createPlaylist();
2725+    this._updateSelectedItem(da.controller.Player.nowPlaying());
2726   },
2727   
2728hunk ./contrib/musicplayer/src/controllers/default_columns.js 315
2729-  mapReduceUpdated: function (values) {
2730-    this.parent(values);
2731+  mapReduceUpdated: function (values, rerender) {
2732+    this.parent(values, rerender);
2733     this.createPlaylist();
2734   },
2735   
2736hunk ./contrib/musicplayer/src/controllers/default_columns.js 328
2737       playlist[n] = this._rows[n].id;
2738     
2739     this._playlist = playlist;
2740-    delete playlist;
2741+    playlist = null;
2742   },
2743   
2744   compareFunction: function (a, b) {
2745hunk ./contrib/musicplayer/src/controllers/default_columns.js 337
2746     
2747     if(a < b) return -1;
2748     if(a > b) return 1;
2749-    return 0;
2750+              return 0;
2751+  },
2752
2753+  _updateSelectedItem: function (song) {
2754+    if(!song)
2755+      return false;
2756+   
2757+    var new_active_el = $(this.options.id + "_column_item_" + song.id);
2758+    if(!new_active_el)
2759+      return false;
2760+   
2761+    if(this._active_el)
2762+      this._active_el.removeClass("active_column_item");
2763+   
2764+    this._active_el = new_active_el;
2765+    this._active_el.addClass("active_column_item");
2766+    new_active_el = null;
2767+  },
2768
2769+  destroy: function () {
2770+    da.controller.Player.removeEvent("play", this._onSongChange);
2771+    this.parent()
2772+  }
2773+}));
2774+
2775+
2776+/**
2777+ *  class da.controller.Navigation.columns.PlaylistSongs < da.controller.Navigation.columns.Songs
2778+ *  filters: none
2779+ * 
2780+ *  Displays songs from a playlist - adds drag&drop functionality.
2781+ **/
2782+Navigation.registerColumn("_PlaylistSongs", "Songs", [], new Class({
2783+  Extends: Navigation.columns.Songs,
2784
2785+  view: {
2786+    id: "playlist_songs_column",
2787+    map: function (doc, emit) {
2788+      var type = doc.type;
2789+      if(!doc || (type !== "Song" && type !== "Playlist") || !this._passesFilter(doc))
2790+        return;
2791+     
2792+      if(type === "Song")
2793+        emit(doc.title, {
2794+          title:  doc.title
2795+        });
2796+      else
2797+        emit("_playlist", {
2798+          id:       doc.id,
2799+          title:    doc.title + " (playlist)",
2800+          song_ids: doc.song_ids
2801+        });
2802+    }
2803+  },
2804
2805+  mapReduceFinished: function (view) {
2806+    var playlist_pos = view.findRow("_playlist");
2807+    this.addPositions(view.rows[playlist_pos], view.rows);
2808+    return this.parent(view);
2809+  },
2810
2811+  mapReduceUpdated: function (view) {
2812+    var full_view = da.db.DEFAULT.views[this.view.id].view,
2813+        new_rows = $A(full_view.rows);
2814+    // this is why we can't use this.parent(view, true),
2815+    // we need to add positions to the all elements, before
2816+    // the sorting occurs (remember that `view` contains only the playlist)
2817+    this.addPositions(new_rows[full_view.findRow("_playlist")], new_rows);
2818+    new_rows.sort(this.compareFunction);
2819+   
2820+    this.options.totalCount = new_rows.length;
2821+    this._rows = new_rows;
2822+   
2823+    var active = this.getActiveItem();
2824+    this.rerender();
2825+   
2826+    if(active) {
2827+      console.log("has_active_item");
2828+      this._active_el = $(this.options.id + "_column_item_" + active.id);
2829+      this._active_el.addClass("active_column_item");
2830+    }
2831+   
2832+    full_view = null;
2833+    new_rows = null;
2834+    active = null;
2835+  },
2836
2837+  compareFunction: function (a, b) {
2838+    a = a && a.value ? a.value.playlist_pos : 0;
2839+    b = b && b.value ? b.value.playlist_pos : 0;
2840+   
2841+    if(a < b) return -1;
2842+    if(a > b) return 1;
2843+              return 0;
2844+  },
2845
2846+  addPositions: function (playlist, rows) {
2847+    if(playlist) {
2848+      rows.erase(playlist);
2849+      this._playlist = playlist.value.song_ids;
2850+    }
2851+    var n = rows.length,
2852+        playlist = this._playlist;
2853+   
2854+    while(n--)
2855+      rows[n].value.playlist_pos = playlist.indexOf(rows[n].id);
2856+  },
2857
2858+  createPlaylist: function () {}
2859
2860+  /*
2861+  mapReduceUpdated: function (view) {
2862+    this._rows = $A(da.db.DEFAULT.views[this.view.id].view.rows);
2863+    this._rows.sort(this.compareFunction);
2864+    this.options.totalCount = this._rows.length;
2865+    this.rerender();
2866+   
2867+    var active = this.getActiveItem();
2868+    if(active) {
2869+      this._active_el = $(this.options.id + "_column_item_" + active.id);
2870+      this._active_el.addClass("active_column_item");
2871+    }
2872   }
2873hunk ./contrib/musicplayer/src/controllers/default_columns.js 460
2874+  */
2875 }));
2876 
2877hunk ./contrib/musicplayer/src/controllers/default_columns.js 463
2878+
2879 })();
2880hunk ./contrib/musicplayer/src/controllers/default_contexts.js 315
2881       },
2882       onHide: function () {
2883         this.video.src = "about:blank";
2884-        setTimeout(function () {
2885-          Player.play();
2886-        }, 1000);
2887       }.bind(this)
2888     });
2889     
2890hunk ./contrib/musicplayer/src/doctemplates/Artist.js 10
2891  *  - title (String): name of the artist
2892  *
2893  **/
2894+
2895 (function () {
2896 var DocumentTemplate = da.db.DocumentTemplate;
2897 
2898addfile ./contrib/musicplayer/src/doctemplates/Playlist.js
2899hunk ./contrib/musicplayer/src/doctemplates/Playlist.js 1
2900+//#require "libs/db/DocumentTemplate.js"
2901+
2902+/**
2903+ *  class da.db.DocumentTemplate.Playlist < da.db.DocumentTemplate
2904+ *
2905+ *  Class representing playlists
2906+ *
2907+ *  #### Standard properties
2908+ *  - `title` (String): name of the playlist.
2909+ *  - `description` (String): a few words about the playlist.
2910+ *  - `song_ids` (Array): list of ID's of songs belonging to the playlist.
2911+ **/
2912+
2913+(function () {
2914+var DocumentTemplate = da.db.DocumentTemplate;
2915+
2916+DocumentTemplate.registerType("Playlist", new Class({
2917+  Extends: DocumentTemplate
2918+}));
2919+
2920+/*
2921+DocumentTemplate.Playlist.findOrCreate({
2922+  properties: {id: "offline", },
2923+  onSuccess: function (offline_playlist, was_created) {
2924+    if(was_created)
2925+      offline_playlist.update({
2926+        title:        "Offline",
2927+        description:  "Songs on this playlist will be available even after you go offline."
2928+      });
2929+  }
2930+});
2931+*/
2932+
2933+})();
2934hunk ./contrib/musicplayer/src/doctemplates/Setting.js 17
2935  *        id:           "volume",
2936  *        group_id:     "general",
2937  *        representAs:  "Number",
2938- *
2939+ *       
2940  *        title:        "Volume",
2941  *        help:         "Configure the volume",
2942  *        value:        64
2943hunk ./contrib/musicplayer/src/doctemplates/Setting.js 107
2944   value:        ""
2945 });
2946 
2947+/*
2948 Setting.register({
2949   id:           "lastfm_enabled",
2950   group_id:     "lastfm",
2951hunk ./contrib/musicplayer/src/doctemplates/Setting.js 137
2952   value:        "",
2953   position:     2
2954 });
2955+*/
2956 
2957 })();
2958hunk ./contrib/musicplayer/src/doctemplates/Song.js 2
2959 //#require "libs/db/DocumentTemplate.js"
2960+//#require "libs/util/genres.js"
2961 
2962 (function () {
2963hunk ./contrib/musicplayer/src/doctemplates/Song.js 5
2964-var DocumentTemplate = da.db.DocumentTemplate;
2965-
2966+var DocumentTemplate = da.db.DocumentTemplate,
2967+    GENRES = da.util.GENRES;
2968 /**
2969  *  class da.db.DocumentTemplate.Song < da.db.DocumentTemplate
2970  *  belongsTo: [[da.db.DocumentTemplate.Artist]], [[da.db.DocumentTemplate.Album]]
2971hunk ./contrib/musicplayer/src/doctemplates/Song.js 12
2972  * 
2973  *  #### Standard properties
2974- *  * `id` - Read-only cap of the file
2975- *  * `title` - name of the song
2976- *  * `track` - track number
2977- *  * `year` - year in which track was published
2978- *  * `lyrics` - lyrics of the song
2979- *  * `artist_id` - id of an [[da.db.DocumentTemplate.Artist]]
2980- *  * `album_id` - id of an [[da.db.DocumentTemplate.Album]]
2981+ *  - `id` ([[String]]): Read-only cap of the file.
2982+ *  - `title` ([[String]]): name of the song.
2983+ *  - `track` ([[Numner]]): track number.
2984+ *  - `year` ([[Number]]): year in which the track was published, `0` if the year
2985+ *    is unkown.
2986+ *  - `duration` ([[Number]]): length of the song in milliseconds.
2987+ *  - `artist_id` ([[String]]): id of an [[da.db.DocumentTemplate.Artist]]
2988+ *  - `album_id` ([[String]]): id of an [[da.db.DocumentTemplate.Album]]
2989+ *  - `plays` ([[Number]]): number of full plays
2990+ *  - `genre` ([[String]] | [[Number]]): id of the genre or name of the genre
2991+ *    itself. If it's a number, it's a index of an [[da.util.GENRES]].
2992+ *    `-1` if the genre isn't specified.
2993+ *  - `mbid` ([[String]]): Musicbrainz ID
2994+ *  - `lastfm_id` ([[String]]): Last.fm ID
2995  *
2996  **/
2997 
2998hunk ./contrib/musicplayer/src/doctemplates/Song.js 29
2999-// Defined by ID3 specs:
3000-// http://www.id3.org/id3v2.3.0#head-129376727ebe5309c1de1888987d070288d7c7e7
3001-var GENRES = [
3002-  "Blues","Classic Rock","Country","Dance","Disco","Funk","Grunge","Hip-Hop","Jazz",
3003-  "Metal","New Age","Oldies","Other","Pop","R&B","Rap","Reggae","Rock","Techno",
3004-  "Industrial","Alternative","Ska","Death Metal","Pranks","Soundtrack","Euro-Techno",
3005-  "Ambient","Trip-Hop","Vocal","Jazz+Funk","Fusion","Trance","Classical","Instrumental",
3006-  "Acid","House","Game","Sound Clip","Gospel","Noise","AlternRock","Bass","Soul","Punk",
3007-  "Space","Meditative","Instrumental Pop","Instrumental Rock","Ethnic","Gothic",
3008-  "Darkwave","Techno-Industrial","Electronic","Pop-Folk","Eurodance","Dream",
3009-  "Southern Rock","Comedy","Cult","Gangsta","Top 40","Christian Rap","Pop/Funk",
3010-  "Jungle","Native American","Cabaret","New Wave","Psychadelic","Rave","Showtunes",
3011-  "Trailer","Lo-Fi","Tribal","Acid Punk","Acid Jazz","Polka","Retro","Musical",
3012-  "Rock & Roll","Hard Rock","Folk","Folk-Rock","National Folk","Swing","Fast Fusion",
3013-  "Bebob","Latin","Revival","Celtic","Bluegrass","Avantgarde","Gothic Rock",
3014-  "Progressive Rock","Psychedelic Rock","Symphonic Rock","Slow Rock","Big Band",
3015-  "Chorus","Easy Listening","Acoustic","Humour","Speech","Chanson","Opera","Chamber Music",
3016-  "Sonata","Symphony","Booty Bass","Primus","Porn Groove","Satire","Slow Jam","Club","Tango",
3017-  "Samba","Folklore","Ballad","Power Ballad","Rhythmic Soul","Freestyle","Duet","Punk Rock",
3018-  "Drum Solo","A capella","Euro-House","Dance Hall"
3019-];
3020-
3021 DocumentTemplate.registerType("Song", new Class({
3022   Extends: DocumentTemplate,
3023   
3024hunk ./contrib/musicplayer/src/doctemplates/doctemplates.js 12
3025 //#require "doctemplates/Artist.js"
3026 //#require "doctemplates/Album.js"
3027 //#require "doctemplates/Song.js"
3028-
3029+//#require "doctemplates/Playlist.js"
3030hunk ./contrib/musicplayer/src/index.html 3
3031 <!DOCTYPE html>
3032 <html>
3033+<!-- html manifest="cache.manifest" -->
3034   <head>
3035     <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
3036hunk ./contrib/musicplayer/src/index.html 6
3037+   
3038     <title>Music Player for Tahoe-LAFS</title>
3039     <link rel="stylesheet" href="resources/css/reset.css" type="text/css" media="screen" charset="utf-8"/>
3040hunk ./contrib/musicplayer/src/index.html 9
3041-    <link rel="stylesheet" href="resources/css/text.css" type="text/css" media="screen" charset="utf-8"/>
3042-    <link rel="stylesheet" href="resources/css/app.css" type="text/css" media="screen" charset="utf-8"/>
3043+    <link rel="stylesheet" href="resources/css/text.css"  type="text/css" media="screen" charset="utf-8"/>
3044+    <link rel="stylesheet" href="resources/css/app.css"   type="text/css" media="screen" charset="utf-8"/>
3045     
3046     <script src="js/app.js" type="text/javascript" charset="utf-8"></script>
3047   </head>
3048hunk ./contrib/musicplayer/src/index_devel.html 43
3049     <script src="libs/util/ID3.js" type="text/javascript" charset="utf-8"></script>
3050     <script src="libs/util/ID3v2.js" type="text/javascript" charset="utf-8"></script>
3051     <script src="libs/util/ID3v1.js" type="text/javascript" charset="utf-8"></script>
3052-
3053-    <script src="Application.js" type="text/javascript" charset="utf-8"></script>
3054-
3055-    <script src="doctemplates/Setting.js" type="text/javascript" charset="utf-8"></script>
3056-    <script src="doctemplates/Artist.js" type="text/javascript" charset="utf-8"></script>
3057-    <script src="doctemplates/Album.js" type="text/javascript" charset="utf-8"></script>
3058-    <script src="doctemplates/Song.js" type="text/javascript" charset="utf-8"></script>
3059-    <script src="doctemplates/Playlist.js" type="text/javascript" charset="utf-8"></script>
3060+    <script src="libs/util/ID3v1.js" type="text/javascript" charset="utf-8"></script>
3061+    <script src="libs/util/PlaylistExporters.js" type="text/javascript" charset="utf-8"></script>
3062 
3063     <script src="libs/ui/ui.js" type="text/javascript" charset="utf-8"></script>
3064     <script src="libs/ui/Column.js" type="text/javascript" charset="utf-8"></script>
3065hunk ./contrib/musicplayer/src/index_devel.html 55
3066     <script src="libs/ui/SegmentedProgressBar.js" type="text/javascript" charset="utf-8"></script>
3067     <script src="libs/vendor/Roar.js" type="text/javascript" charset="utf-8"></script>
3068     
3069+    <script src="Application.js" type="text/javascript" charset="utf-8"></script>
3070+
3071+    <script src="doctemplates/Setting.js" type="text/javascript" charset="utf-8"></script>
3072+    <script src="doctemplates/Artist.js" type="text/javascript" charset="utf-8"></script>
3073+    <script src="doctemplates/Album.js" type="text/javascript" charset="utf-8"></script>
3074+    <script src="doctemplates/Song.js" type="text/javascript" charset="utf-8"></script>
3075+    <script src="doctemplates/Playlist.js" type="text/javascript" charset="utf-8"></script>
3076+
3077     <script src="services/services.js" type="text/javascript" charset="utf-8"></script>
3078     <script src="libs/vendor/LastFM.js" type="text/javascript" charset="utf-8"></script>
3079     <script src="services/lastfm.js" type="text/javascript" charset="utf-8"></script>
3080hunk ./contrib/musicplayer/src/index_devel.html 78
3081     <script src="controllers/Settings.js" type="text/javascript" charset="utf-8"></script>
3082     <script src="controllers/CollectionScanner.js" type="text/javascript" charset="utf-8"></script>
3083     <script src="controllers/SongContext.js" type="text/javascript" charset="utf-8"></script>
3084+    <script src="controllers/Search.js" type="text/javascript" charset="utf-8"></script>
3085     <script src="controllers/default_contexts.js" type="text/javascript" charset="utf-8"></script>
3086hunk ./contrib/musicplayer/src/index_devel.html 80
3087+    <script src="controllers/Playlist.js" type="text/javascript" charset="utf-8"></script>
3088   </head>
3089   <body>
3090     <div id="loader">Loading...</div>
3091hunk ./contrib/musicplayer/src/libs/TahoeObject.js 75
3092     }
3093     this._fetched = true;
3094 
3095-    new Request.JSON({
3096+    var req = new Request.JSON({
3097       url: "/uri/" + encodeURIComponent(this.uri),
3098       
3099       onSuccess: function (data) {
3100hunk ./contrib/musicplayer/src/libs/TahoeObject.js 81
3101         this.applyMeta(data);
3102         (success||$empty)(this);
3103+       
3104+        delete req;
3105       }.bind(this),
3106       
3107       onFailure: failure || $empty
3108hunk ./contrib/musicplayer/src/libs/TahoeObject.js 86
3109-    }).get({t: "json"});
3110+    });
3111+    req.get({t: "json"});
3112     
3113     return this;
3114   },
3115hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 258
3116       
3117       for(var view_name in views)
3118         this.view(views[view_name].options, dict);
3119-      }.bind(this), true);
3120+    }.bind(this), true);
3121   },
3122   
3123   /**
3124hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 309
3125   put: function (doc, cb) {
3126     if ($type(doc) === "array") {
3127       this.dict.setDocs(doc);
3128-      //var n = doc.length, _doc;
3129-      //while(n--) {
3130-      //  _doc = doc[n];
3131-      //  this.dict.set(_doc.id, _doc);
3132-      //}
3133     } else
3134       this.dict.set(doc.id, doc);
3135 
3136hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 397
3137         var full_view = this.views[id].view.rows.concat(view.rows),
3138             rereduce = {},
3139             reduce = options.reduce,
3140-            n = full_view.length;
3141+            n = full_view.length,
3142+            row, key;
3143         
3144         while(n--) {
3145hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 401
3146-          var row = full_view[n],
3147-              key = row.key;
3148+          row = full_view[n];
3149+          key = row.key;
3150+         
3151           if(!rereduce[key])
3152             rereduce[key] = [row.value];
3153           else
3154hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 430
3155    *  - id (String): name of the view.
3156    **/
3157   killView: function (id) {
3158-    delete this.views[id].view;
3159+    this.removeEvents("updated." + id);
3160     delete this.views[id];
3161     return this;
3162   }
3163hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 438
3164 
3165 // Maximum number of items to process before giving the UI a chance
3166 // to breathe.
3167-var DEFAULT_CHUNK_SIZE = 1000;
3168-
3169+var DEFAULT_CHUNK_SIZE = 1000,
3170 // If no progress callback is given, we'll automatically give the
3171 // UI a chance to breathe for this many milliseconds before continuing
3172 // processing.
3173hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 442
3174-var DEFAULT_UI_BREATHE_TIME = 50;
3175+  DEFAULT_UI_BREATHE_TIME = 50;
3176 
3177 function defaultProgress(phase, percent, resume) {
3178   window.setTimeout(resume, DEFAULT_UI_BREATHE_TIME);
3179hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 513
3180 
3181 function findRowInReducedView (key, rows) {
3182   if(rows.length > 1) {
3183-    var midpoint = Math.floor(rows.length / 2);
3184-    var row = rows[midpoint];
3185-    if(key < row.key)
3186+    var midpoint = Math.floor(rows.length / 2),
3187+        row = rows[midpoint],
3188+        row_key = row.key;
3189+   
3190+    if(key < row_key)
3191       return findRowInReducedView(key, rows.slice(0, midpoint));
3192hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 519
3193-    if(key > row.key)
3194-      return midpoint + findRowInReducedView(key, rows.slice(midpoint));
3195-    return row.key === key ? midpoint : -1;
3196-  }
3197
3198-  return rows[0].key === key ? 0 : -1;
3199+    if(key > row_key) {
3200+      var p = findRowInReducedView(key, rows.slice(midpoint))
3201+      return p === -1 ? -1 : midpoint + p;
3202+    }
3203+    return midpoint;
3204+  } else
3205+    return rows[0] && rows[0].key === key ? 0 : -1;
3206 }
3207 
3208 /**
3209hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 544
3210   this.rows = [];
3211   var keyRows = [];
3212   
3213-  this._include = function (mapResult) {
3214+  this._include = function (mapResult) {   
3215     var mapKeys = mapResult.keys,
3216         mapDict = mapResult.dict;
3217     
3218hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 570
3219           value: item.values[j]
3220         };
3221       
3222-      if(has_key)
3223+      if(has_key && this.rows[ki])
3224         newRows.shift();
3225       this.rows = this.rows.concat(newRows);
3226     }
3227hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 575
3228     
3229-    this.rows.sort(idSort);
3230+    this.rows.sort(keySort);
3231     
3232     var keys = [];
3233     keyRows = [];
3234hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 587
3235           pos: keys.push(key) - 1
3236         });
3237     }
3238-   
3239-    //delete keys;
3240   };
3241   
3242   /**
3243hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 616
3244   return this;
3245 }
3246 
3247-function idSort (a, b) {
3248-  a = a.id;
3249-  b = b.id;
3250
3251-  if(a < b) return -1;
3252-  if(a > b) return  1;
3253-            return  0;
3254-}
3255-
3256 function findRowInMappedView (key, keyRows) {
3257hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 617
3258-  if (keyRows.length > 1) {
3259+  if(keyRows.length > 1) {
3260     var midpoint = Math.floor(keyRows.length / 2);
3261     var keyRow = keyRows[midpoint];
3262hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 620
3263-    if (key < keyRow.key)
3264+    if(key < keyRow.key)
3265       return findRowInMappedView(key, keyRows.slice(0, midpoint));
3266hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 622
3267-    if (key > keyRow.key)
3268+    if(key > keyRow.key)
3269       return findRowInMappedView(key, keyRows.slice(midpoint));
3270     return keyRow ? keyRow.pos : -1;
3271   } else
3272hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 646
3273  *  - worker (Object): reference to Web worker implementation. Defaults to `window.Worker`.
3274  **/
3275 function WebWorkerMapReducer(numWorkers, Worker) {
3276-  if (!Worker)
3277+  if(!Worker)
3278     Worker = window.Worker;
3279 
3280   var pool = [];
3281hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 666
3282     };
3283   }
3284 
3285-  for (var i = 0; i < numWorkers; i++)
3286+  for(var i = 0; i < numWorkers; i++)
3287     pool.push(new MapWorker(i));
3288 
3289   this.map = function WWMR_map(map, dict, progress, chunkSize, finished) {
3290hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 676
3291         mapDict = {};
3292 
3293     function getNextChunk() {
3294-      if (keys.length) {
3295+      if(keys.length) {
3296         var chunkKeys = keys.slice(0, chunkSize),
3297             chunk = {},
3298             n = chunkKeys.length;
3299hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 697
3300 
3301     function nextJob(mapWorker) {
3302       var chunk = getNextChunk();
3303-      if (chunk) {
3304-        mapWorker.map(
3305-          map,
3306-          chunk,
3307-          function jobDone(aMapDict) {
3308-            for (var name in aMapDict)
3309-              if (name in mapDict) {
3310-                var item = mapDict[name];
3311-                item.keys = item.keys.concat(aMapDict[name].keys);
3312-                item.values = item.values.concat(aMapDict[name].values);
3313-              } else
3314-                mapDict[name] = aMapDict[name];
3315+      if(chunk) {
3316+        mapWorker.map(map, chunk, function jobDone(aMapDict) {
3317+          for(var name in aMapDict)
3318+            if(name in mapDict) {
3319+              var item = mapDict[name];
3320+              item.keys = item.keys.concat(aMapDict[name].keys);
3321+              item.values = item.values.concat(aMapDict[name].values);
3322+            } else
3323+              mapDict[name] = aMapDict[name];
3324 
3325hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 707
3326-            if (keys.length)
3327-              progress("map",
3328-                       (size - keys.length) / size,
3329-                       function() { nextJob(mapWorker); });
3330-            else
3331-              workerDone();
3332-          });
3333+          if(keys.length)
3334+            progress("map",
3335+              (size - keys.length) / size,
3336+              function() { nextJob(mapWorker); }
3337+            );
3338+          else
3339+            workerDone();
3340+        });
3341       } else
3342         workerDone();
3343     }
3344hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 727
3345 
3346     function allWorkersDone() {
3347       var mapKeys = [];
3348-      for (var name in mapDict)
3349+      for(var name in mapDict)
3350         mapKeys.push(name);
3351       mapKeys.sort();
3352       finished({dict: mapDict, keys: mapKeys});
3353hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 733
3354     }
3355 
3356-    for (var i = 0; i < numWorkers; i++)
3357+    for(var i = 0; i < numWorkers; i++)
3358       nextJob(pool[i]);
3359   };
3360 
3361hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 823
3362 
3363       do {
3364         var key   = mapKeys[i],
3365-            item  = mapDict[key]
3366+            item  = mapDict[key];
3367         
3368         rows.push({
3369           key: key,
3370hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 844
3371   }
3372 };
3373 
3374-da.db.BrowserCouch = BrowserCouch;
3375-da.db.BrowserCouch.Dictionary = Dictionary;
3376-da.db.SingleThreadedMapReducer = SingleThreadedMapReducer;
3377-da.db.WebWorkerMapReducer = WebWorkerMapReducer;
3378+da.db.BrowserCouch              = BrowserCouch;
3379+da.db.BrowserCouch.Dictionary   = Dictionary;
3380+da.db.SingleThreadedMapReducer  = SingleThreadedMapReducer;
3381+da.db.WebWorkerMapReducer       = WebWorkerMapReducer;
3382 
3383 })();
3384hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 137
3385         this[cache_key] = owner;
3386       
3387       callback(owner);
3388-     
3389-      /*
3390-      DocumentTemplate.find({
3391-        properties: {
3392-          id:   this.doc[key + "_id"],
3393-          type: this.belongsTo[key]
3394-        },
3395-       
3396-        onSuccess: function (doc) {
3397-          this[cache_key] = doc;
3398-          callback(doc);
3399-        }.bind(this),
3400-       
3401-        onFailure: callback
3402-      }, this.constructor.db());
3403-      */
3404     } else if(key in this.hasMany) {
3405       var relation = this.hasMany[key],
3406hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 139
3407-          props = {type: relation[0]};
3408+          props    = {type: relation[0]};
3409       
3410       props[relation[1]] = this.id;
3411       
3412hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 159
3413    *  - properties (Object): updated properties.
3414    *  fires propertyChange
3415    **/
3416-  set: function (properties) {
3417-    if(arguments.length == 2) {
3418+  set: function (properties, value) {
3419+    if(typeof value !== "undefined") {
3420       var key = properties;
3421       properties = {};
3422hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 163
3423-      properties[key] = arguments[1];
3424+      properties[key] = value;
3425     }
3426     
3427     $extend(this.doc, properties);
3428hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 178
3429    *  fires propertyRemove
3430    **/
3431   remove: function (property) {
3432-    if(property !== "_id")
3433+    if(property !== "id")
3434       delete this.doc[property];
3435     
3436     this.fireEvent("propertyRemove", [property, this]);
3437hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 217
3438    *  da.db.DocumentTemplate#destroy([callback]) -> this
3439    *  - callback (Function): function called after `destroy` event.
3440    * 
3441-   *  Removes all document's properties except for `id` and adds `_deleted` property.
3442+   *  Destroys the document.
3443+   *
3444+   *  #### Notes
3445+   *  The document won't be completely destroyed from the db, all of its properties
3446+   *  will be deleted, but it will get a `_deleted` property set to `true`.
3447    **/
3448   destroy: function (callback) {
3449hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 224
3450-    for(var property in this.doc)
3451-      delete this.doc[property];
3452+    for(var prop in this.doc)
3453+      delete this.doc[prop];
3454     
3455     this.doc.id = this.id;
3456     this.doc._deleted = true;
3457hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 234
3458       this.fireEvent("destroy", [this]);
3459       if(callback)
3460         callback(this);
3461-    });
3462+    }.bind(this));
3463     
3464     return this;
3465   }
3466hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 352
3467     
3468     template.find = function (options) {
3469       options.properties.type = type;
3470-      if(options.id)
3471+      if(options.properties.id)
3472         template.findById(options.id, function (doc) {
3473           if(doc)
3474             options.onSuccess([doc]);
3475hunk ./contrib/musicplayer/src/libs/ui/Column.js 42
3476    **/
3477   initialize: function (options) {
3478     this.setOptions(options);
3479-    if(!this.options.id)
3480+    if(!this.options.id || !this.options.id.length)
3481       this.options.id = "duC_" + (IDS++);
3482     
3483     this._populated = false;
3484hunk ./contrib/musicplayer/src/libs/ui/Column.js 51
3485     this._rendered = [];
3486     
3487     this._el = new Element("div", {
3488-      id: options.id + "_column",
3489+      id: this.options.id + "_column",
3490       "class": "column",
3491       styles: {
3492         overflowX: "hidden",
3493hunk ./contrib/musicplayer/src/libs/ui/Column.js 91
3494     // ask for it in every #render() - which can be quite expensive.
3495     this._el.addEvent("resize", function () {
3496       this._el_height = this._el.getHeight();
3497+      this.render();
3498     }.bind(this));
3499   },
3500   
3501hunk ./contrib/musicplayer/src/libs/ui/Column.js 111
3502   render: function () {
3503     if(!this._populated)
3504       this.populate();
3505-    if(this._rendered.length === this.options.totalCount + 1)
3506+    if(this._rendered.length === this.options.totalCount)
3507       return false;
3508     
3509     // We're pre-fetching previous 5 and next 10 items
3510hunk ./contrib/musicplayer/src/libs/ui/Column.js 119
3511     var total_count = this.options.totalCount,
3512         ids = this.getVisibleIndexes(),
3513         n = Math.max(0, ids[0] - 6),
3514-        m = Math.max(Math.min(ids[1] + 10, total_count), total_count),
3515+        m = Math.min(ids[1] + 10, total_count),
3516         first_rendered = -1,
3517         box;
3518 
3519hunk ./contrib/musicplayer/src/libs/ui/Column.js 144
3520     
3521     if(first_rendered !== -1) {
3522       var coords = this.getBoxCoords(first_rendered);
3523+      console.log("rendering box at", this.options.id, [first_rendered, m], coords);
3524       box.setStyles({
3525         position: "absolute",       
3526         top:      coords[1],
3527hunk ./contrib/musicplayer/src/libs/ui/Column.js 164
3528   populate: function () {
3529     var o = this.options;
3530     this._populated = true;
3531-    this._weight.setStyle("top", o.rowHeight * o.totalCount /*+ o.rowHeight*/);
3532+    this._weight.setStyle("top", o.rowHeight * o.totalCount);
3533     this._el.fireEvent("resize");
3534     
3535     return this;
3536hunk ./contrib/musicplayer/src/libs/ui/Column.js 171
3537   },
3538   
3539   /**
3540-   *  da.ui.Column#rerender() -> this
3541+   *  da.ui.Column#rerender() -> this | false
3542    **/
3543   rerender: function () {
3544     if(!this._el)
3545hunk ./contrib/musicplayer/src/libs/ui/Column.js 177
3546       return false;
3547     
3548-    console.log("rerender", this.options.id, this._deleted);
3549     var weight = this._weight;
3550     this._el.empty();
3551     this._el.grab(weight);
3552hunk ./contrib/musicplayer/src/libs/ui/Column.js 219
3553   },
3554 
3555   /**
3556-   *  da.ui.Column#getVisibleIndexes() -> Array
3557+   *  da.ui.Column#getVisibleIndexes() -> [first_visible_index, last_visible_index]
3558    * 
3559    *  Returns an array with indexes of first and last item in visible portion of list.
3560    **/
3561hunk ./contrib/musicplayer/src/libs/ui/Column.js 226
3562   getVisibleIndexes: function () {
3563     // Math.round() and Math.ceil() are used in such combination
3564     // to include items which could be only partially in viewport
3565-    var rh = this.options.rowHeight,
3566-        per_viewport = Math.round(this._el_height / rh),
3567-        first = Math.ceil(this._el.getScroll().y / rh);
3568+    var rh           = this.options.rowHeight,
3569+        first         = Math.ceil(this._el.getScroll().y / rh),
3570+        per_viewport  = Math.round(this._el_height / rh);
3571     if(first > 0) first--;
3572     
3573     return [first, first + per_viewport];
3574hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 12
3575   Implements: [Events, Options],
3576 
3577   options: {
3578-    title: null,
3579+    title:              null,
3580+    closeButton:        false,
3581+    show:               false,
3582+    draggable:          false,
3583     hideOnOutsideClick: true,
3584hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 17
3585-    show: false
3586+    destroyOnHide:      false
3587   },
3588 
3589   /**
3590hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 26
3591    *  - options.hideOnOutsideClick (Boolean): if `true`, the dialog will be
3592    *    hidden when the click outside the dialog element occurs (ie. on the dimmed
3593    *    portion of screen)
3594+   *  - options.closeButton (Boolean): toggle the close button. If `true`, the button
3595+   *    will be injected at the top of `options.html`, before the title (if any).
3596    *  - options.show (Boolean): if `true` the dialog will be shown immediately as it's created.
3597    *    Defaults to `false`.
3598hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 30
3599+   *  - options.draggable (Boolean): when set to `true`, the dialog will be draggable.
3600+   *    There won't be a dialog wrapper, ie. the users will be able to interact with
3601+   *    the content around the dialog. Defaults to `false`.
3602+   *  - options.destroyOnHide (Boolean): destroy the dialog after the dialog has been hidden
3603+   *    for the first time.
3604    *  - options.html (Element): contents of the.
3605    *
3606    *  To the `options.html` element `dialog` CSS class name will be added and
3607hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 38
3608-   *  the element will be wrapped into a `div` with `dialog_wrapper` CSS class name.
3609+   *  the element will be wrapped into a `div` with `dialog_wrapper` (or `draggable_dialog_wrapper`) CSS class name.
3610    *
3611    *  If `options.title` is provided, the title element will be injected at the top of
3612    *  `options.html` and will be given `dialog_title` CSS class name.
3613hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 44
3614    *
3615    *  #### Notes
3616-   *  All dialogs are hidden by default, use [[Dialog.show]] to show them immediately
3617-   *  after they are created method.
3618+   *  * All dialogs are hidden by default, use [[Dialog.show]] to show them immediately
3619+   *    after they are created.
3620+   *  * When the close button is clicked, before `hide` event is fired, a `dismiss`
3621+   *    event will be fired. To cancel hiding of the dialog just throw an error from
3622+   *    an listener.
3623+   *  * If the dialog will be draggable, you're expected to privide a `options.title`,
3624+   *    as that will be the handle.
3625    *
3626    *  #### Example
3627hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 53
3628-   *      new da.ui.Dialog({
3629-   *        title: "What's your name?"
3630+   *      var hai = new da.ui.Dialog({
3631+   *        title: "Bonjur tout le monde!"
3632    *        html: new Element("div", {
3633hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 56
3634-   *          html: "Hello!"
3635+   *          html: "Hai World!"
3636    *        }),
3637hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 58
3638-   *        show: true
3639+   *        show: true,
3640+   *
3641+   *        onHide: function () {
3642+   *          hai.destroy();
3643+   *          delete hai;
3644+   *        }
3645    *      });
3646    *
3647    **/
3648hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 71
3649     this.setOptions(options);
3650     if(!this.options.html)
3651      throw "options.html must be provided when creating an Dialog";
3652-
3653+   
3654     this._el = new Element("div", {
3655hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 73
3656-      "class": "dialog_wrapper"
3657+      "class": this.options.draggable ? "draggable_dialog_wrapper" : "dialog_wrapper"
3658     });
3659     if(!this.options.show)
3660       this._el.style.display = "none";
3661hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 78
3662     
3663-    if(this.options.title)
3664+    if(this.options.title) {
3665+      var title;
3666+     
3667       if(typeof this.options.title === "string")
3668hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 82
3669-        (new Element("h2", {
3670+        title = new Element("h2", {
3671           html: this.options.title,
3672hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 84
3673+          href: "#",
3674           "class": "dialog_title no_selection"
3675hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 86
3676-        })).inject(this.options.html, "top");
3677+        });
3678       else if($type(this.options.title) === "element")
3679hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 88
3680-        this.options.title.inject(this.options.html, "top");
3681+        title = this.options.title;
3682+     
3683+      title.inject(this.options.html, "top");
3684+      delete title;
3685+    }
3686+   
3687+    if(this.options.closeButton)
3688+      (new Element("a", {
3689+        "class": "dialog_close no_selection",
3690+        html: "Close",
3691+        title: "Close",
3692+        events: {
3693+          click: function () {
3694+            this.fireEvent("dismiss");
3695+            this.hide();
3696+          }.bind(this)
3697+        }
3698+      })).inject(this.options.html, "top");
3699     
3700     if(this.options.hideOnOutsideClick)
3701       this._el.addEvent("click", this.hide.bind(this));
3702hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 112
3703     
3704     this._el.grab(options.html.addClass("dialog"));
3705     document.body.grab(this._el);
3706+   
3707+    if(this.options.draggable)
3708+      this._el.makeDraggable({
3709+        handle: this.options.html.getElement(".dialog_title")
3710+      });
3711   },
3712 
3713   /**
3714hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 143
3715     
3716     this._el.hide();
3717     this.fireEvent("hide", [this]);
3718+   
3719+    if(this.options.destroyOnHide)
3720+      this.destroy();
3721+   
3722     return this;
3723   },
3724 
3725hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 159
3726 
3727   /**
3728    *  da.ui.Dialog#destroy() -> this
3729-   *  fires hide
3730    **/
3731   destroy: function () {
3732hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 161
3733-    this.hide();
3734-   
3735     this._el.destroy();
3736     delete this._el;
3737     delete this.options;
3738hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 168
3739     return this;
3740   }
3741 });
3742-
3743hunk ./contrib/musicplayer/src/libs/ui/Menu.js 90
3744     this._el = (new Element("ul")).addClass("menu").addClass("no_selection");
3745     this._el.style.display = "none";
3746     this._el.addEvent("click:relay(.menu_item a)", this.click.bind(this));
3747+    this._el.addEvent("dragend:relay(.menu_item a)", this.click.bind(this));
3748     this._id = "_menu_" + (ID++) + "_";
3749     
3750     this.render();
3751hunk ./contrib/musicplayer/src/libs/ui/Menu.js 245
3752     if(event)
3753       event.stop();
3754     
3755-    if(event && event.target)
3756+    if(event && event.target) {
3757       this._el.position($extend({
3758         relativeTo: event.target
3759       }, this.options.position));
3760hunk ./contrib/musicplayer/src/libs/ui/Menu.js 249
3761+    }
3762     
3763     this._el.style.zIndex = 5;
3764     this._el.style.display = "block";
3765hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 25
3766       if(!this._passesFilter(doc))
3767         return false;
3768 
3769-      emit(doc.id, {
3770+      emit(doc.title, {
3771         title: doc.title || doc.id
3772       });
3773     },
3774hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 73
3775         this.view.finished = this.view.finished.bind(this);
3776       
3777       if(this.view.reduce)
3778-        this.view.reduce = this.view.reduced.bind(this);
3779+        this.view.reduce = this.view.reduce.bind(this);
3780       if(!this.view.updated && !this.view.temporary)
3781         this.view.updated = this.mapReduceUpdated;
3782       if(this.view.updated)
3783hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 103
3784     this.injectBottom(this.options.parentElement || document.body);
3785     if(this.options.renderImmediately !== false)
3786       this.render();
3787+   
3788     return this;
3789   },
3790   
3791hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 108
3792   /**
3793-   *  da.ui.NavigationColumn#mapReduceUpdated(values) -> this
3794+   *  da.ui.NavigationColumn#mapReduceUpdated(values[, forceRerender = false]) -> this
3795    *  - values (Object): rows returned by map/reduce process.
3796    * 
3797    *  Note that this will have to re-render the whole column, as it's possible
3798hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 115
3799    *  that one of the new documents should be rendered in the middle of already
3800    *  rendered ones (due to sorting).
3801    **/
3802-  mapReduceUpdated: function (values) {
3803-    var new_rows = $A(da.db.DEFAULT.views[this.view.id].view.rows);
3804+  mapReduceUpdated: function (values, rerender) {
3805+    var new_rows = $A(da.db.DEFAULT.views[this.view.id].view.rows),
3806+        active = this.getActiveItem();
3807     new_rows.sort(this.compareFunction);
3808     
3809     // Noting new was added, so we can simply re-render those elements
3810hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 121
3811-    if(this.options.totalCount === new_rows.length) {
3812+    if(!rerender && this.options.totalCount === new_rows.length) {
3813       values = values.rows;
3814       var n = values.length,
3815           id_prefix = this.options.id + "_column_item_",
3816hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 132
3817         el = $(id_prefix + item.id);
3818         if(el) {
3819           index = el.retrieve("column_index");
3820+          console.log("Rerendering item", id_prefix, index);
3821           
3822           this.renderItem(index)
3823             .addClass("column_item")
3824hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 143
3825       
3826       this._rows = new_rows;
3827     } else {
3828+      console.log("total count was changed, rerendering whole column", this.options.id);
3829       this.options.totalCount = new_rows.length;
3830       this._rows = new_rows;
3831hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 146
3832-      return this.rerender();
3833+      this.rerender();
3834+    }
3835+   
3836+    if(active) {
3837+      this._active_el = $(this.options.id + "_column_item_" + active.id);
3838+      this._active_el.addClass("active_column_item");
3839     }
3840   },
3841   
3842hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 164
3843   },
3844   
3845   /**
3846+   *  da.ui.NavigationColumn#getActiveItem() -> Object | undefined
3847+   **/
3848+  getActiveItem: function () {
3849+    if(!this._active_el)
3850+      return;
3851+   
3852+    return this.getItem(this._active_el.retrieve("column_index"));
3853+  },
3854
3855+  /**
3856    *  da.ui.NavigationColumn#renderItem(index) -> Element
3857    *  - index (Number): position of the item that needs to be rendered.
3858    * 
3859hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 227
3860    *  #### Examples
3861    * 
3862    *      function createFilter (item) {
3863-   *        return {artist_id: item.id};
3864+   *        return {artist_id: item.id}
3865+   *      }
3866+   *
3867+   *      function createFilter(item) {
3868+   *        var id = item.id;
3869+   *        return function (doc) {
3870+   *          return doc.chocolates.contains(id)
3871+   *        }
3872    *      }
3873    * 
3874    **/
3875hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 239
3876   createFilter: function (item) {
3877-   return {};
3878+    return {};
3879   },
3880   
3881   click: function (event, el) {
3882hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 248
3883       this._active_el.removeClass("active_column_item");
3884 
3885     this._active_el = el.addClass("active_column_item");
3886-    this.fireEvent("click", [item, event, el]);
3887+    this.fireEvent("click", [item, event, el], 1);
3888     
3889     return item;
3890   },
3891hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 263
3892    *  [See meanings of return values](https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/sort#Description).
3893    **/
3894   compareFunction: function (a, b) {
3895-    a = a.value.title;
3896-    b = b.value.title;
3897+    a = a && a.value ? a.value.title : -1;
3898+    b = b && b.value ? b.value.title : -1;
3899     
3900     if(a < b) return -1;
3901     if(a > b) return 1;
3902hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 274
3903   destroy: function () {
3904     this.parent();
3905     delete this._rows;
3906+    delete this._active_el;
3907     
3908hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 276
3909-    if(this.view)
3910+    if(this.view && !this.view.temporary)
3911       if(this.options.killView)
3912         (this.options.db || da.db.DEFAULT).killView(this.view.id);
3913       else
3914hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 33
3915    *
3916    *      progress_bar.toElement().setStyle("width", 100);
3917    *
3918+   *  If you want your progress bar as a lovely gradient, just put a `LinearGradient`
3919+   *  object to `options.foreground`.
3920+   *
3921+   *      var pb = new da.ui.ProgressBar({width: 100, height: 5, foreground: "#ffa"});
3922+   *      var gradient = pb.ctx.createLinearGradient(0, 0, 0, 5);
3923+   *      gradient.addColorStop(0, "#ffa");
3924+   *      gradient.addColorStop(1, "#ffe");
3925+   *      pb.options.foregound = gradient;
3926+   *      gradient = null;
3927+   *
3928    **/
3929   initialize: function (canvas, options) {
3930     this.setOptions(options);
3931hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 125
3932     this.ctx.fillRect(0, 0, this.progress * opts.width, opts.height);
3933     
3934     delete opts;
3935-    return this;
3936+    return this;
3937   },
3938   
3939   /**
3940hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 14
3941 var SegmentedProgressBar = new Class({
3942   
3943   /**
3944-   *  new da.ui.SegmentedProgressBar(width, height, segments)
3945+   *  new da.ui.SegmentedProgressBar(width, height, segments[, ticks = 0])
3946+   *  - width (Number): width of the progressbar in pixels.
3947+   *  - height (Number): height of the progressbar in pixels.
3948+   *  - segments (Object): names of individual progress bars and their forground
3949+   *    color, see example below.
3950+   *  - ticks (Number): number of 1px marks along the progress bar.
3951    *
3952    *  #### Example
3953    *      var mb = new da.ui.SegmentedProgressBar(100, 15, {
3954hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 32
3955    *
3956    *  The first define progress bar will be in foreground, while
3957    *  the last defined will be in background;
3958+   *
3959+   **/
3960+  /**
3961+   *  da.ui.SegmentedProgressBar.segments -> {segment1: da.ui.ProgressBar, ...}
3962    **/
3963hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 37
3964-  initialize: function (width, height, segments) {
3965+  initialize: function (width, height, segments, ticks) {
3966     this._index = [];
3967     this.segments = {};
3968hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 40
3969+    this.ticks = ticks;
3970     
3971     this._el = new Element("canvas");
3972     this._el.width = width;
3973hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 85
3974     while(n--)
3975       this.segments[idx[n]].rerender();
3976     
3977+    if(this.ticks) {
3978+      var inc = Math.round(this._el.width/this.ticks),
3979+          h = this._el.height;
3980+     
3981+      this.ctx.fillStyle = "rgba(255, 255, 255, 0.3)";
3982+      //this.ctx.fillStyle = "#ddd";
3983+      for(var n = 0, m = this._el.width; n < m; n += inc) {
3984+        if(n > 5)
3985+          this.ctx.fillRect(n, 0, 1, h);
3986+      }
3987+      this.ctx.fillStyle = "rgba(0, 0, 0, 1)";
3988+    }
3989+   
3990     return this;
3991   },
3992   
3993hunk ./contrib/musicplayer/src/libs/util/ID3.js 54
3994   
3995   _getFile: function (parser) {
3996     if(!parser)
3997-      return this.options.onFailure("fromID3");
3998+      return this.options.onFailure("noParserFound");
3999     
4000     this.request = new Request.Binary({
4001       url:        this.options.url,
4002hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 22
4003  *  * TYER
4004  *  * TIME
4005  *  * TCON
4006- *  * USLT
4007  *  * WOAR
4008  *  * WXXX
4009hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 24
4010+ *  * USLT - not parsed by default but frame decoder is present
4011  * 
4012  *  As well as their equivalents in ID3 v2.2 specification.
4013  * 
4014hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 46
4015  **/
4016 var BinaryFile    = da.util.BinaryFile,
4017     CACHE         = [],
4018-    GENRE_REGEXP  = /^\(\d+\)/,
4019-    BE_BOM        = "\xFE\xFF",
4020-    LE_BOM        = "\xFF\xFE",
4021+    GENRE_REGEXP  = /^\(?(\d+)\)?|(.+)/,
4022+    //BE_BOM        = "\xFE\xFF",
4023+    //LE_BOM        = "\xFF\xFE",
4024     UNSYNC_PAIR   = /(\uF7FF\0)/g,
4025hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 50
4026-FFLAGS = {
4027-  ALTER_TAG_23:   0x8000,
4028-  ALTER_FILE_23:  0x4000,
4029-  READONLY_23:    0x2000,
4030-  COMPRESS_23:    0x0080,
4031-  ENCRYPT_23:     0x0040,
4032-  GROUP_23:       0x0020,
4033+    FFLAGS = {
4034+      ALTER_TAG_23:   0x8000,
4035+      ALTER_FILE_23:  0x4000,
4036+      READONLY_23:    0x2000,
4037+      COMPRESS_23:    0x0080,
4038+      ENCRYPT_23:     0x0040,
4039+      GROUP_23:       0x0020,
4040 
4041hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 58
4042-  ALTER_TAG_24:   0x4000,
4043-  ALTER_FILE_24:  0x2000,
4044-  READONLY_24:    0x1000,
4045-  GROUPID_24:     0x0040,
4046-  COMPRESS_24:    0x0008,
4047-  ENCRYPT_24:     0x0004,
4048-  UNSYNC_24:      0x0002,
4049-  DATALEN_24:     0x0001
4050-},
4051+      ALTER_TAG_24:   0x4000,
4052+      ALTER_FILE_24:  0x2000,
4053+      READONLY_24:    0x1000,
4054+      GROUPID_24:     0x0040,
4055+      COMPRESS_24:    0x0008,
4056+      ENCRYPT_24:     0x0004,
4057+      UNSYNC_24:      0x0002,
4058+      DATALEN_24:     0x0001
4059+    },
4060 
4061 FrameType = {
4062   /**
4063hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 78
4064     if(d.getByteAt(offset) === 1) {
4065       // Unicode is being used, and we're trying to detect Unicode BOM.
4066       // (we don't actually care if it's little or big endian)
4067-      var test_string = d.getStringAt(offset, 5),
4068-          bom_pos = test_string.indexOf(LE_BOM);
4069-      if(bom_pos === -1)
4070-        bom_pos = test_string.indexOf(BE_BOM);
4071-      if(bom_pos === -1)
4072-        window._ts = test_string;
4073+      //var test_string = d.getStringAt(offset, 5),
4074+      //    bom_pos = test_string.indexOf(LE_BOM);
4075+      //if(bom_pos === -1)
4076+      //  bom_pos = test_string.indexOf(BE_BOM);
4077         
4078hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 83
4079-      offset += bom_pos + 1;
4080-      size -= bom_pos + 1;
4081-     
4082-      console.log("Unicode BOM detected", [bom_pos, d.getStringAt(offset, size)]);
4083+      //offset += bom_pos + 1;
4084+      //size -= bom_pos + 1;
4085       
4086hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 86
4087-      //if(d.getByteAt(offset + 1) + d.getByteAt(offset + 2) === 255 + 254) {
4088-      //  offset += 2;
4089-      //  size -= 2;
4090-      //}
4091+      if(d.getByteAt(offset + 1) + d.getByteAt(offset + 2) === 255 + 254) {
4092+        console.log("Unicode BOM detected");
4093+        offset += 2;
4094+        size -= 2;
4095+      }
4096     }
4097     
4098     return d.getStringAt(offset + 1, size - 1).strip();
4099hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 150
4100   //TIME: $empty,
4101   TCON: function (offset, size) {
4102     // Genre, can be either "(123)Genre", "(123)" or "Genre".
4103-    var data = FrameType.text.call(this, offset, size);
4104-    return +((data.match(GENRE_REGEXP) || " ")[0].slice(1, -1));
4105+    var data = FrameType.text.call(this, offset, size),
4106+        match = data.match(GENRE_REGEXP);
4107+   
4108+    if(!match)
4109+      return -1;
4110+    if(match[1])
4111+      return +match[1];
4112+    if(match[2])
4113+      return match[2].strip();
4114+    return -1;
4115   },
4116hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 161
4117-  USLT: FrameType.unsyncedLyrics,
4118+  //USLT: FrameType.unsyncedLyrics,
4119   WOAR: FrameType.link,
4120   WXXX: FrameType.userLink
4121 };
4122hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 415
4123    *
4124    **/
4125   unsync: function (n, m) {
4126-    console.log("unsyncing file", this.options.url);
4127-   
4128     if(arguments.length) {
4129       var data = this.data.data,
4130           part = data
4131hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 451
4132       artist: f.TPE2 || f.TPE1 || f.TP2 || f.TP1 || "Unknown",
4133       track:  f.TRCK || f.TRK || 0,
4134       year:   f.TYER || f.TYE || 0,
4135-      genre:  f.TCON || f.TCO || 0,
4136-      lyrics: f.USLT || f.ULT || "",
4137+      genre:  f.TCON || f.TCO || -1,
4138       links: {
4139         official: f.WOAR || f.WXXX || f.WAR || f.WXX || ""
4140       }
4141hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 495
4142    *  Use this method to add your own ID3v2 frame parsers. You can access this as `da.util.ID3v2Parser.addFrameParser`.
4143    * 
4144    *  `fn` will be called with following arguments:
4145-   *  * offset - position at frame appears in data
4146-   *  * size - size of the frame, including header
4147-   * 
4148+   *  * offset: position at frame appears in data
4149+   *  * size: size of the frame, including header
4150    * 
4151hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 498
4152-   *  `this` keyword inside `fn` will refer to instance of ID3v2.
4153+   *  `this` keyword inside `fn` will refer to an instance of [[da.util.ID3v2Parser]].
4154    **/
4155   addFrameParser: function (name, fn) {
4156     FRAMES[name] = fn;
4157addfile ./contrib/musicplayer/src/libs/util/PlaylistExporters.js
4158hunk ./contrib/musicplayer/src/libs/util/PlaylistExporters.js 1
4159+//#require <libs/util/util.js>
4160+//#require <doctemplates/Song.js>
4161+
4162+(function () {
4163+var Song              = da.db.DocumentTemplate.Song,
4164+    SERVER            = location.protocol + "//" + location.host,
4165+    XML_HEADER        = '<?xml version="1.0" encoding="UTF-8"?>\n',
4166+    TRACKLIST_REGEXP  = /tracklist/gi,
4167+    TRACKNUM_REGEXP   = /tracknum/gi;
4168+
4169+/**
4170+ *  da.util.playlistExporter.XSPF(playlist) -> String
4171+ *  - playlist (da.util.Playlist
4172+ * 
4173+ *  #### External resources
4174+ *  * [XSPF v1 specification](http://xspf.org/xspf-v1.html)
4175+ *  * [XSPF Quickstart](http://xspf.org/quickstart/)
4176+ *  * [XSPF Validator](http://validator.xspf.org/) - we're generating valid XSPF!
4177+ * 
4178+ **/
4179+function XSPFExporter (playlist) {
4180+  var ids = playlist.get("song_ids"),
4181+      file = new Element("root"),
4182+      track_list = new Array(ids.length),
4183+      //track_list = new Element("trackList"),
4184+      song, artist, album, track, duration;
4185
4186+  for(var n = 0, m = ids.length; n < m; n++) {
4187+    song = Song.findById(ids[n]);
4188+    // getting a 'belongs to' relationship is always synchronous
4189+    song.get("artist",  function (_a) { artist = _a });
4190+    song.get("album",   function (_a) { album = _a  });
4191+    // XSPF specification requires positive intergers,
4192+    // whereas we're using negative ones indicate that the value isn't present.
4193+    track = song.get("track");
4194+    duration = song.get("duration");
4195+   
4196+    track_list[n] = tag("track", [
4197+      tag("location", makeURL(song)),
4198+      tag("title",    song.get("title").stripTags()),
4199+      tag("creator",  artist.get("title").stripTags()),
4200+      tag("album",    album.get("title").stripTags()),
4201+      track     < 1 ? "" : tag("trackNum", track),
4202+      duration  < 1 ? "" : tag("duration", duration)
4203+    ].join(""));
4204+  }
4205
4206+  file.grab(new Element("playlist", {
4207+    version: 1,
4208+    xmlns: "http://xspf.org/ns/0/",
4209+   
4210+    html: [
4211+      tag("title",      playlist.get("title").stripTags()),
4212+      tag("annotation", playlist.get("description").stripTags()),
4213+      tag("trackList",  track_list.join(""))
4214+    ].join("")
4215+  }));
4216
4217+  // As per some specification `document.createElement(tagName)`, lowercases
4218+  // tagName if the `document` is an (X)HTML document.
4219+  var output = file.innerHTML
4220+    .replace(TRACKLIST_REGEXP, "trackList")
4221+    .replace(TRACKNUM_REGEXP,  "trackNum");
4222
4223+  openDownloadWindow(makeDataURI("application/xspf+xml", XML_HEADER + output));
4224+}
4225+
4226+/**
4227+ *  da.util.playlistExporter.M3U(playlist) -> undefined
4228+ *  - playlist (da.db.DocumentTemplate.Playlist): playlist which will be exported
4229+ *
4230+ *  #### Resources
4231+ *  * [Wikipedia article on M3U](http://en.wikipedia.org/wiki/M3U)
4232+ *  * [M3U specification](http://schworak.com/programming/music/playlist_m3u.asp)
4233+ *
4234+ **/
4235+function M3UExporter (playlist) {
4236+  var ids    = playlist.get("song_ids"),
4237+      file = ["#EXTM3U"],
4238+      song;
4239
4240+  for(var n = 0, m = ids.length; n < m; n++) {
4241+    song = Song.findById(ids[n]);
4242+    song.get("artist", function (artist) {
4243+      file.push(
4244+        "#EXTINFO:-1,{0} - {1}".interpolate([artist.get("title"), song.get("title")]),
4245+        makeURL(song)
4246+      );
4247+    });
4248+  }
4249
4250+  openDownloadWindow(makeDataURI("audio/x-mpegurl", file.join("\n")));
4251+}
4252+
4253+/**
4254+ *  da.util.playlistExporter.PLS(playlist) -> String
4255+ *
4256+ *  #### Resources
4257+ *  * [PLS article on Wikipedia](http://en.wikipedia.org/wiki/PLS_(file_format))
4258+ **/
4259+function PLSExporter(playlist) {
4260+  var ids = playlist.get("song_ids"),
4261+      file = ["[playlist]", "NumberOfEntries=" + ids.length],
4262+      song;
4263
4264+  for(var n = 0, m = ids.length; n < m; n++) {
4265+    song = Song.findById(ids[n]);
4266+    file.push(
4267+      "File"   + (n + 1) + "=" + makeURL(song),
4268+      "Title"  + (n + 1) + "=" + song.get("title"),
4269+      "Length" + (n + 1) + "=" + song.get("duration")
4270+    )
4271+  }
4272
4273+  file.push("Version=2");
4274+  openDownloadWindow(makeDataURI("audio/x-scpls", file.join("\n")));
4275+}
4276+
4277+function makeURL(song) {
4278+  var named = "?@@named=" + encodeURIComponent(song.get("title")) + ".mp3";
4279+  return [SERVER, "uri", encodeURIComponent(song.id)].join("/") + named;
4280+}
4281+
4282+function makeDataURI(mime_type, data) {
4283+  var x = "data:" + mime_type + ";charset=utf-8," + encodeURIComponent(data);
4284+  return x;
4285+}
4286+
4287+function openDownloadWindow(dataURI) {
4288+  var download_window = window.open(dataURI, "_blank", "width=400,height=200");
4289+  window.wdx = download_window;
4290
4291+  // This allows Firefox to open the download dialog,
4292+  // while Chrome will show the blank page.
4293+  setTimeout(function () {
4294+    download_window.location = "playlist_download.html";
4295+    download_window.onload = function () {
4296+      var dl = download_window.document.getElementById("download_link");
4297+      dl.href = dataURI;
4298+    };
4299+  }, 2*1000);
4300+}
4301+
4302+function tag(tagName, text) {
4303+  return "<" + tagName + ">" + text + "</" + tagName + ">";
4304+}
4305+
4306+
4307+/**
4308+ * da.util.playlistExporter
4309+ * Methods for exporting playlists to other formats.
4310+ *
4311+ * #### External resources
4312+ * * [A survey of playlist formats](http://gonze.com/playlists/playlist-format-survey.html)
4313+ **/
4314+da.util.playlistExporter = {
4315+  XSPF:   XSPFExporter,
4316+  M3U:    M3UExporter,
4317+  PLS:    PLSExporter
4318+};
4319+
4320+})();
4321addfile ./contrib/musicplayer/src/libs/util/genres.js
4322hunk ./contrib/musicplayer/src/libs/util/genres.js 1
4323+//#require <libs/util/util.js>
4324+
4325+(function () {
4326+/**
4327+ *  da.util.GENRES -> [String, ...]
4328+ *  List of genres defined by ID3 spec.
4329+ *
4330+ *  #### Links
4331+ *  * [List of genres](http://www.id3.org/id3v2.3.0#head-129376727ebe5309c1de1888987d070288d7c7e7)
4332+ **/
4333+da.util.GENRES = [
4334+  "Blues","Classic Rock","Country","Dance","Disco","Funk","Grunge","Hip-Hop","Jazz",
4335+  "Metal","New Age","Oldies","Other","Pop","R&B","Rap","Reggae","Rock","Techno",
4336+  "Industrial","Alternative","Ska","Death Metal","Pranks","Soundtrack","Euro-Techno",
4337+  "Ambient","Trip-Hop","Vocal","Jazz+Funk","Fusion","Trance","Classical","Instrumental",
4338+  "Acid","House","Game","Sound Clip","Gospel","Noise","AlternRock","Bass","Soul","Punk",
4339+  "Space","Meditative","Instrumental Pop","Instrumental Rock","Ethnic","Gothic",
4340+  "Darkwave","Techno-Industrial","Electronic","Pop-Folk","Eurodance","Dream",
4341+  "Southern Rock","Comedy","Cult","Gangsta","Top 40","Christian Rap","Pop/Funk",
4342+  "Jungle","Native American","Cabaret","New Wave","Psychadelic","Rave","Showtunes",
4343+  "Trailer","Lo-Fi","Tribal","Acid Punk","Acid Jazz","Polka","Retro","Musical",
4344+  "Rock & Roll","Hard Rock","Folk","Folk-Rock","National Folk","Swing","Fast Fusion",
4345+  "Bebob","Latin","Revival","Celtic","Bluegrass","Avantgarde","Gothic Rock",
4346+  "Progressive Rock","Psychedelic Rock","Symphonic Rock","Slow Rock","Big Band",
4347+  "Chorus","Easy Listening","Acoustic","Humour","Speech","Chanson","Opera","Chamber Music",
4348+  "Sonata","Symphony","Booty Bass","Primus","Porn Groove","Satire","Slow Jam","Club","Tango",
4349+  "Samba","Folklore","Ballad","Power Ballad","Rhythmic Soul","Freestyle","Duet","Punk Rock",
4350+  "Drum Solo","A capella","Euro-House","Dance Hall"
4351+];
4352+da.util.GENRES[-1] = "Unknown";
4353+
4354+})();
4355addfile ./contrib/musicplayer/src/playlist_download.html
4356hunk ./contrib/musicplayer/src/playlist_download.html 1
4357-
4358+<!DOCTYPE html>
4359+<html>
4360+  <head>
4361+    <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
4362+   
4363+    <title>Playlist download</title>
4364+    <link rel="stylesheet" href="resources/css/reset.css" type="text/css" media="screen" charset="utf-8"/>
4365+    <link rel="stylesheet" href="resources/css/text.css"  type="text/css" media="screen" charset="utf-8"/>
4366+   
4367+    <style type="text/css">
4368+      body {
4369+        text-align: center;
4370+        width: 300px;
4371+        margin: auto;
4372+        background: #f3f3f3;
4373+      }
4374+     
4375+      #download_link {
4376+        padding: 10px;
4377+        background: #339D4C;
4378+        color: #fff;
4379+        font-size: 1.2em;
4380+        display: block;
4381+        margin: 10px auto;
4382+        text-decoration: none;
4383+        width: 100px;
4384+        font-weight: bold;
4385+        text-shadow: #326732 0 1px 0;
4386+       
4387+        -webkit-border-radius: 4px;
4388+        -moz-border-radius: 4px;
4389+        border-radius: 4px;
4390+       
4391+        -webkit-box-shadow: #ffa 0 0 10px;
4392+        -moz-box-shadow: #ffa 0 0 10px;
4393+        box-shadow: #ffa 0 0 10px;
4394+      }
4395+     
4396+      #download_link:active { 
4397+        background: #326732;
4398+        padding: 11px 10px 9px 10px;
4399+        text-shadow: #000 0 -1px 0;
4400+      }
4401+    </style>
4402+  </head>
4403+  <body>
4404+    <a id="download_link" href="#download">Download</a>
4405+    <small>
4406+      Right-click on the button above and select <em>Save Link As...</em>
4407+      from the context menu.
4408+    </small>
4409+  </body>
4410+</html>
4411hunk ./contrib/musicplayer/src/resources/css/app.css 2
4412 /*** Global styles ***/
4413-@font-face {
4414-  font-family: Junction;
4415-  font-style: normal;
4416-  font-weight: normal;
4417-  src: local('Junction'), url('resources/fonts/Junction.ttf') format('truetype');
4418-}
4419-
4420 body {
4421   font-family: 'Droid Sans', 'Lucida Grande', 'Lucida Sans', 'Bitstream Vera', sans-serif;
4422   overflow: hidden;
4423hunk ./contrib/musicplayer/src/resources/css/app.css 5
4424-  background: #c0c0c0;
4425+  background: #c0c0c0 url(../images/radio_pattern.png) 0 0 repeat;
4426 }
4427 
4428 a {
4429hunk ./contrib/musicplayer/src/resources/css/app.css 13
4430   color: inherit;
4431 }
4432 
4433-input[type="text"], input[type="password"] {
4434+input[type="text"], input[type="password"], textarea {
4435   border: 1px solid #ddd;
4436   border-top: 1px solid #c0c0c0;
4437   background: #fff;
4438hunk ./contrib/musicplayer/src/resources/css/app.css 20
4439   padding: 2px;
4440 }
4441 
4442-input:focus, input:active {
4443+input:focus, input:active, textarea:focus, input[type="button"]:focus, input[type="submit"]:focus, button:focus {
4444   border-color: #33519d;
4445   -webkit-box-shadow: #33519d 0 0 5px;
4446   -moz-box-shadow: #33519d 0 0 5px;
4447hunk ./contrib/musicplayer/src/resources/css/app.css 24
4448-  -o-box-shadow: #33519d 0 0 5px;
4449   box-shadow: #33519d 0 0 5px;
4450 }
4451 
4452hunk ./contrib/musicplayer/src/resources/css/app.css 28
4453 input[type="button"], input[type="submit"], button {
4454-  background: #ddd;
4455-  border: 1px transparent;
4456+  background: #ddd url(../images/selection_background.png) 0 50% repeat-x;
4457+  border: 0;
4458   border-bottom: 1px solid #c0c0c0;
4459hunk ./contrib/musicplayer/src/resources/css/app.css 31
4460+  border-top: 1px solid rgba(0,0,0,0);
4461   padding: 2px 7px;
4462   color: #000;
4463   text-shadow: #fff 0 1px 0;
4464hunk ./contrib/musicplayer/src/resources/css/app.css 35
4465+  outline: 0;
4466   
4467   -webkit-border-radius: 4px;
4468   -moz-border-radius: 4px;
4469hunk ./contrib/musicplayer/src/resources/css/app.css 39
4470-  -o-border-radius: 4px;
4471   border-radius: 4px;
4472 }
4473 
4474hunk ./contrib/musicplayer/src/resources/css/app.css 42
4475-input[type="button"]:active, input[type="submit"]:active, button:active {
4476-  border-top: 1px solid #1e2128;
4477-  border-bottom: 0;
4478-  background: #33519d !important;
4479+input[type="button"]:active, input[type="submit"]:active, button:active, button.active {
4480+  border-top: 1px solid #1e2128 !important;
4481+  border-bottom: 1px solid rgba(0,0,0,0) !important;
4482+  background: #33519d url(../images/selection_background_inverted.png) 0 100% repeat-x !important;
4483   color: #fff;
4484   text-shadow: #000 0 1px 1px;
4485 }
4486hunk ./contrib/musicplayer/src/resources/css/app.css 50
4487 
4488+input[type="submit"] {
4489+  font-weight: bold;
4490+  font-size: 1.1em;
4491+}
4492+
4493+.button_group button {
4494+  margin: 0;
4495+  border-left: 1px solid #c0c0c0;
4496+  border-top: 1px solid #ddd;
4497
4498+  -webkit-border-radius: 0;
4499+  -moz-border-radius: 0;
4500+  -o-border-radius: 0;
4501+  border-radius: 0;
4502+}
4503+
4504+.button_group button:first-child {
4505+  border-top-left-radius: 4px;
4506+  border-bottom-left-radius: 4px;
4507+}
4508+
4509+.button_group button:last-child {
4510+  border-right: 1px solid #ddd;
4511+  border-top-right-radius: 4px;
4512+  border-bottom-right-radius: 4px;
4513+}
4514+
4515+.button_group button:active:first-child, .button_group button.active:first-child {
4516+  border-left-color: #33519D;
4517+}
4518+
4519+.button_group button:active:last-child, .button_group button.active:last-child {
4520+  border-right-color: #33519D;
4521+}
4522+
4523 .no_selection {
4524   -webkit-user-select: none;
4525   -moz-user-select: none;
4526hunk ./contrib/musicplayer/src/resources/css/app.css 160
4527   z-index: 2;
4528 }
4529 
4530+.draggable_dialog_wrapper {
4531+  position: fixed;
4532+  top: 50px;
4533+  left: 100px;
4534+  z-index: 3;
4535+}
4536+
4537 .dialog {
4538   display: block;
4539   margin: 50px auto 0 auto;
4540hunk ./contrib/musicplayer/src/resources/css/app.css 170
4541-  background: #fff;
4542-  border: 1px solid #ddd;
4543+  border: 5px solid rgba(255, 255, 255, 0.3);
4544
4545+  -webkit-border-radius: 4px;
4546+  -moz-border-radius: 4px;
4547+  border-radius: 4px;
4548   
4549   -webkit-box-shadow: rgba(0, 0, 0, 0.4) 0 10px 40px;
4550   -moz-box-shadow: rgba(0, 0, 0, 0.4) 0 10px 40px;
4551hunk ./contrib/musicplayer/src/resources/css/app.css 178
4552-  -o-box-shadow: rgba(0, 0, 0, 0.4) 0 10px 40px;
4553   box-shadow: rgba(0, 0, 0, 0.4) 0 10px 40px;
4554 }
4555 
4556hunk ./contrib/musicplayer/src/resources/css/app.css 181
4557+.draggable_dialog_wrapper .dialog {
4558+  margin: 0;
4559+}
4560+
4561+iframe.dialog {
4562+  background: #000;
4563+  border: 0;
4564+}
4565+
4566 .dialog_title {
4567   margin: 0;
4568   padding: 5px;
4569hunk ./contrib/musicplayer/src/resources/css/app.css 201
4570   text-shadow: #1e2128 0 1px 0;
4571 }
4572 
4573+.draggable_dialog_wrapper .dialog_title {
4574+  cursor: move;
4575+}
4576+
4577+.dialog_close {
4578+  display: block;
4579+  float: right;
4580+  width: 16px;
4581+  height: 16px;
4582+  background: transparent url(../images/close.png) 0 100% no-repeat;
4583+  cursor: default;
4584+  margin: 10px;
4585+  text-indent: -4444px;
4586+  opacity: 0.3;
4587+}
4588+
4589+.dialog_close:active {
4590+  opacity: 0.1;
4591+}
4592+
4593 #loader {
4594   font-size: 2em;
4595   width: 100%;
4596hunk ./contrib/musicplayer/src/resources/css/app.css 308
4597        background: url(bubble.png) bottom right;
4598 }
4599 
4600+/** Drag&drop **/
4601+.drag_clone {
4602+  background: transparent !important;
4603+  z-index: 10 !important;
4604+}
4605+
4606 /*** Navigation columns ***/
4607 .column_container {
4608   float: left;
4609hunk ./contrib/musicplayer/src/resources/css/app.css 344
4610   width: 100%;
4611 }
4612 
4613-.column_container .column_header:active, .column_container .column_header:focus, .column_header.active {
4614+.column_container .column_header:active, .column_container .column_header:focus, .column_header.active_menu {
4615   background-color: #1e2128;
4616   padding: 3px 0 1px 0;
4617   outline: 0;
4618hunk ./contrib/musicplayer/src/resources/css/app.css 348
4619+  -webkit-box-shadow: #000 0 3px 7px inset;
4620 }
4621 
4622 .column_header.active {
4623hunk ./contrib/musicplayer/src/resources/css/app.css 363
4624 .navigation_column {
4625   width: 100%;
4626   background: #fff url(../images/column_background.png) 0 0 repeat;
4627-/*  background-attachment: fixed; */
4628+  background-attachment: scroll;
4629   z-index: 1;
4630 }
4631 
4632hunk ./contrib/musicplayer/src/resources/css/app.css 388
4633   background-color: #fff;
4634 }
4635 
4636+.column_item .action {
4637+  display: block;
4638+  float: right;
4639+  margin-top: -1px;
4640+  cursor: default;
4641+}
4642+
4643 .navigation_column a.column_item {
4644   display: block;
4645   cursor: default;
4646hunk ./contrib/musicplayer/src/resources/css/app.css 426
4647 .navigation_column .active_column_item, .menu_item:hover, #next_song:hover, #prev_song:hover,
4648 #video_search_results a:active, #video_search_results a:focus {
4649   background-color: #33519d !important;
4650+  background-image: url(../images/selection_background.png);
4651+  background-position: bottom left;
4652+  background-repeat: repeat-x;
4653   text-shadow: #000 0 1px 0;
4654   color: #fff !important;
4655   outline: 0 !important;
4656hunk ./contrib/musicplayer/src/resources/css/app.css 441
4657 
4658 /** Albums column **/
4659 #Albums_column {
4660-  background: #fff;
4661+  background-image: url(../images/column_background_tall.png);
4662 }
4663 
4664 #Albums_column .column_item {
4665hunk ./contrib/musicplayer/src/resources/css/app.css 445
4666+  height: 40px;
4667+}
4668+
4669+#Albums_column .column_item img {
4670+  display: block;
4671+  float: left;
4672+  margin: -12px 0 0 0;
4673   width: 64px;
4674   height: 64px;
4675hunk ./contrib/musicplayer/src/resources/css/app.css 454
4676-  padding: 4px;
4677-  text-indent: 0;
4678-  border-radius: 2px;
4679-  display: inline-block;
4680-  margin: 0;
4681
4682+  -webkit-box-shadow: rgba(0, 0, 0, 0.5) 1px 0 5px;
4683+  -moz-box-shadow: rgba(0, 0, 0, 0.5) 1px 0 5px;
4684+  box-shadow: rgba(0, 0, 0, 0.5) 1px 0 5px;
4685 }
4686 
4687hunk ./contrib/musicplayer/src/resources/css/app.css 460
4688-#Albums_column .column_item.even, #Albums_column .column_item.odd {
4689-  background: transparent;
4690+#Albums_column .column_item span {
4691+  vertical-align: top;
4692 }
4693 
4694hunk ./contrib/musicplayer/src/resources/css/app.css 464
4695-#Albums_column .column_item img {
4696-  display: block;
4697
4698-  -webkit-box-shadow: rgba(0, 0, 0, 0.5) 0 1px 3px;
4699-  -moz-box-shadow: rgba(0, 0, 0, 0.5) 0 1px 3px;
4700-  -o-box-shadow: rgba(0, 0, 0, 0.5) 0 1px 3px;
4701-  box-shadow: rgba(0, 0, 0, 0.5) 0 1px 3px;
4702+/** Songs column **/
4703+
4704+/** Genres column **/
4705+#Genres_column .column_item span {
4706+  display: inline;
4707+}
4708+
4709+#Genres_column .subtitle {
4710+  float: right;
4711+  margin-right: 5px;
4712+}
4713+
4714+/** Playlists column **/
4715+#Playlists_column .action {
4716+  display: none;
4717+  width: 16px;
4718+  height: 16px;
4719+  background: url(../images/cog.png) 50% 50% no-repeat;
4720+  text-indent: -4444px;
4721+  margin: 1px 4px 0 0;
4722+  opacity: 0.4;
4723+  cursor: default;
4724+  outline: 0;
4725+}
4726+
4727+#Playlists_column .action:active {
4728+  opacity: 0.3 !important;
4729+}
4730+
4731+#Playlists_column .column_item:hover .action, #Playlists_column .active_column_item .action {
4732+  display: block !important;
4733+}
4734+
4735+#Playlists_column .active_column_item .action {
4736+  background-image: url(../images/cog_inverted.png);
4737+  opacity: 1;
4738 }
4739 
4740 /*** Menus ***/
4741hunk ./contrib/musicplayer/src/resources/css/app.css 507
4742   display: block;
4743   text-indent: 0;
4744   margin: 0 0 0 -1px;
4745-  padding: 3px 0;
4746+  padding: 4px 0;
4747   position: fixed;
4748   background: #fff;
4749   color: #000;
4750hunk ./contrib/musicplayer/src/resources/css/app.css 511
4751-  min-width: 100px;
4752+  min-width: 170px;
4753   overflow: hidden;
4754   text-overflow: ellipsis;
4755   white-space: nowrap;
4756hunk ./contrib/musicplayer/src/resources/css/app.css 518
4757   list-style: none;
4758   cursor: default;
4759   z-index: 5;
4760-  border: 1px solid #ddd;
4761   
4762   -webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px;
4763   -moz-box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px;
4764hunk ./contrib/musicplayer/src/resources/css/app.css 524
4765   -o-box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px;
4766   box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px;
4767   
4768-  -webkit-border-radius: 3px;
4769-  -moz-border-radius: 3px;
4770-  -o-border-radius: 3px;
4771-  border-radius: 3px;
4772+  -webkit-border-radius: 4px;
4773+  -moz-border-radius: 4px;
4774+  -o-border-radius: 4px;
4775+  border-radius: 4px;
4776 }
4777 
4778 .menu_item {
4779hunk ./contrib/musicplayer/src/resources/css/app.css 541
4780   color: inherit;
4781   text-decoration: none;
4782   cursor: default;
4783+  margin: 0 20px 0 0;
4784 }
4785 
4786 .menu_item .menu_separator {
4787hunk ./contrib/musicplayer/src/resources/css/app.css 563
4788   content: " ✔ ";
4789 }
4790 
4791+.menu_item a.title {
4792+  background: #fff !important;
4793+  font-weight: bold;
4794+}
4795+
4796+.menu_item:hover a.title {
4797+  color: #000 !important;
4798+  text-shadow: none !important;
4799+}
4800+
4801+.menu_item.disabled {
4802+  color: #c0c0c0;
4803+  background: #fff !important;
4804+}
4805+
4806 .navigation_menu {
4807   border-top: 0;
4808   -webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px;
4809hunk ./contrib/musicplayer/src/resources/css/app.css 586
4810   box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px;
4811 }
4812 
4813+.menu .button_group button {
4814+  background-color: #fff;
4815+}
4816+
4817+#main_menu {
4818+  position: absolute;
4819+  top: 5px;
4820+  right: 5px;
4821+  color: #000;
4822+  cursor: default;
4823+  text-shadow: #fff 0 1px 0;
4824+  display: block;
4825+  padding: 3px;
4826+  z-index: 7;
4827+  width: 20px;
4828+  height: 20px;
4829+  text-indent: -4444px;
4830+  background: url(../images/cog.png) 50% 50% no-repeat;
4831
4832+  -webkit-border-radius: 4px;
4833+  -moz-border-radius: 4px;
4834+  -o-border-radius: 4px;
4835+  border-radius: 4px;
4836+}
4837+
4838+#main_menu:hover {
4839+  background-color: #ddd;
4840
4841+  -webkit-box-shadow: #fff 0 0 4px;
4842+  -moz-box-shadow: #fff 0 0 4px;
4843+  -o-box-shadow: #fff 0 0 4px;
4844+  box-shadow: #fff 0 0 4px;
4845+}
4846+
4847+#main_menu:active, #main_menu.active_menu {
4848+  background-color: #fff;
4849+  -webkit-box-shadow: #fff 0 0 6px;
4850+}
4851+
4852+#main_menu.active_menu {
4853+  border-bottom-left-radius: 0;
4854+  border-bottom-right-radius: 0;
4855+}
4856+
4857 /*** Settings ***/
4858 #settings {
4859   width: 600px;
4860hunk ./contrib/musicplayer/src/resources/css/app.css 710
4861 #revert_settings {
4862   float: left;
4863   background: transparent;
4864-  border-bottom: 1px transparent;
4865+  border-bottom: 1px solid rgba(0, 0, 0, 0);
4866 }
4867 
4868 /** Navigation columns **/
4869hunk ./contrib/musicplayer/src/resources/css/app.css 735
4870   position: fixed;
4871   top: 0;
4872   right: 0;
4873+  background: #f3f3f3 url(../images/song_details_background.png) 0 0 repeat;
4874 }
4875 
4876 #song_info_block {
4877hunk ./contrib/musicplayer/src/resources/css/app.css 740
4878   width: 100%;
4879-  background: #f3f3f3;
4880+/*  background: #f3f3f3 url(../images/radio_pattern.png) 0 0 repeat;
4881+  -webkit-box-shadow: rgba(0, 0, 0, 0.6) 0 1px 15px inset; */
4882 }
4883 
4884 #song_details {
4885hunk ./contrib/musicplayer/src/resources/css/app.css 748
4886   font-size: 0.9em;
4887   color: #585858;
4888   float: left;
4889-  width: 300px;
4890-  padding: 10px;
4891-  margin-top: 20px;
4892+  width: 315px;
4893+  padding: 0 0 0 10px;
4894+  margin: 27px 0 0 0;
4895   cursor: default;
4896hunk ./contrib/musicplayer/src/resources/css/app.css 752
4897+  text-shadow: #fff 0 1px 0;
4898 }
4899 
4900 #song_details h2, #song_details span {
4901hunk ./contrib/musicplayer/src/resources/css/app.css 775
4902   width: 160px;
4903   max-width: 160px;
4904   overflow: hidden;
4905-  padding: 10px;
4906+  padding: 0;
4907   float: left;
4908   display: block;
4909hunk ./contrib/musicplayer/src/resources/css/app.css 778
4910+  margin: 12px 0 10px 5px;
4911 }
4912 
4913 #song_album_cover {
4914hunk ./contrib/musicplayer/src/resources/css/app.css 782
4915-  /*float: left;
4916-  display: block;
4917-  margin: 10px;*/
4918-  max-width: 150px;
4919-  border: 1px solid #fff;
4920+  max-width: 140px;
4921   
4922hunk ./contrib/musicplayer/src/resources/css/app.css 784
4923-  -webkit-box-shadow: #c0c0c0 0 3px 5px;
4924-  -moz-box-shadow: #c0c0c0 0 3px 5px;
4925-  -o-box-shadow: #c0c0c0 0 3px 5px;
4926-  box-shadow: #c0c0c0 0 3px 5px;
4927+  -webkit-box-shadow: #000 0 1px 3px;
4928+  -moz-box-shadow: #000 0 1px 3px;
4929+  -o-box-shadow: #000 0 1px 3px;
4930+  box-shadow: #000 0 1px 3px;
4931   
4932hunk ./contrib/musicplayer/src/resources/css/app.css 789
4933-  -webkit-border-radius: 3px;
4934-  -moz-border-radius: 3px;
4935-  -o-border-radius: 3px;
4936-  border-radius: 3px;
4937+  -webkit-border-radius: 4px;
4938+  -moz-border-radius: 4px;
4939+  -o-border-radius: 4px;
4940+  border-radius: 4px;
4941 }
4942 
4943 #song_title {
4944hunk ./contrib/musicplayer/src/resources/css/app.css 816
4945 
4946 #play_button_wrapper {
4947   display: inline-block;
4948-  width: 40px;
4949-  height: 40px;
4950+  width: 41px;
4951+  height: 41px;
4952   vertical-align: middle;
4953 }
4954 
4955hunk ./contrib/musicplayer/src/resources/css/app.css 833
4956 }
4957 
4958 #play_button:active, #play_button:focus, #play_button.active {
4959-  background-position: 0 -40px;
4960+  background-position: 0 -41px;
4961 }
4962 
4963 #play_button.active:active, #play_button.active:focus {
4964hunk ./contrib/musicplayer/src/resources/css/app.css 837
4965-  background-position: 0 -80px;
4966+  background-position: 0 -82px;
4967 }
4968 
4969 #next_song, #prev_song {
4970hunk ./contrib/musicplayer/src/resources/css/app.css 888
4971 
4972 #next_song:hover {
4973   background-image: url(../images/next_active.png);
4974+  background-position: 0% 50%;
4975+  background-repeat: no-repeat;
4976 }
4977 
4978 #prev_song {
4979hunk ./contrib/musicplayer/src/resources/css/app.css 902
4980 
4981 #prev_song:hover {
4982   background-image: url(../images/previous_active.png);
4983+  background-position: 100% 50%;
4984+  background-repeat: no-repeat;
4985+}
4986+
4987+#song_position {
4988+  font-size: 0.8em;
4989+  text-align: center;
4990+  width: 100px;
4991+  position: relative;
4992+  top: 10px;
4993+  left: -100px;
4994 }
4995 
4996 /*** Song Context ***/
4997hunk ./contrib/musicplayer/src/resources/css/app.css 917
4998 #context_tabs {
4999-  background: #ddd;
5000+/*
5001+  background: #ddd url(../images/song_details_background.png) 0 0 repeat;
5002   border: 1px solid #c0c0c0;
5003   border-left: 0;
5004   border-right: 0;
5005hunk ./contrib/musicplayer/src/resources/css/app.css 922
5006+  -webkit-box-shadow: #c0c0c0 0px -1px 7px inset;
5007+*/
5008+  background: #fff;
5009+  border-top: 1px solid #c0c0c0;
5010   width: 100%;
5011   text-align: center;
5012   padding: 0;
5013hunk ./contrib/musicplayer/src/resources/css/app.css 930
5014   text-shadow: #fff 0 1px 0;
5015
5016-  -webkit-box-shadow: #c0c0c0 0px -1px 7px inset;
5017+  margin-top: 5px;
5018 }
5019 
5020hunk ./contrib/musicplayer/src/resources/css/app.css 933
5021-#context_tabs a.tab {
5022-  text-decoration: none;
5023-  display: inline-block;
5024-  cursor: default;
5025-  padding: 2px 7px;
5026-  margin: 0 0 -1px 0;
5027-  outline: 0;
5028-  border-left: 1px solid #c0c0c0;
5029-}
5030 
5031hunk ./contrib/musicplayer/src/resources/css/app.css 934
5032-#context_tabs a.tab:last-child {
5033-  border-right: 1px solid #c0c0c0;
5034+#context_tabs button {
5035+  position: relative;
5036+  top: -10px;
5037+  z-index: 2;
5038+  border-top: 1px solid #c0c0c0;
5039+  background-color: #f3f3f3;
5040 }
5041 
5042hunk ./contrib/musicplayer/src/resources/css/app.css 942
5043-#context_tabs a.tab:active, #context_tabs a.tab:focus, #context_tabs a.tab.active {
5044-  -webkit-box-shadow: #c0c0c0 0px 1px 5px inset;
5045-  -moz-box-shadow: #c0c0c0 0px 1px 5px inset;
5046-  -o-box-shadow: #c0c0c0 0px 1px 5px inset;
5047-  box-shadow: #c0c0c0 0px 1px 5px inset;
5048+#context_tabs button:first-child {
5049+  border-left-color: #c0c0c0;
5050 }
5051 
5052hunk ./contrib/musicplayer/src/resources/css/app.css 946
5053-#context_tabs a.tab.active {
5054-  background: #fff;
5055+#context_tabs button:last-child {
5056+  border-right-color: #c0c0c0;
5057 }
5058 
5059 #song_context {
5060hunk ./contrib/musicplayer/src/resources/css/app.css 952
5061   overflow: auto;
5062+  background: #fff;
5063 }
5064 
5065 #song_context_loading {
5066hunk ./contrib/musicplayer/src/resources/css/app.css 961
5067   text-align: center;
5068   padding-top: 20px;
5069   font-size: 1.5em;
5070+  z-index: 1 !important;
5071 }
5072 
5073 /** Artist context **/
5074hunk ./contrib/musicplayer/src/resources/css/app.css 1101
5075   margin: auto;
5076 }
5077 
5078-#recommended_songs li:nth-child(2n+1), #video_search_results li:nth-child(2n+1) {
5079+#recommended_songs li:nth-child(odd), #video_search_results li:nth-child(odd) {
5080   background: #f3f3f3;
5081 }
5082 
5083hunk ./contrib/musicplayer/src/resources/css/app.css 1152
5084   float: left;
5085   width: 360px;
5086 }
5087+
5088+#youtube_music_video {
5089+  /* fun fact: Chromium won't play <video>s if there's a border-radius set */
5090+  -webkit-border-radius: 0;
5091+  -moz-border-radius: 0;
5092+  border-radius: 0;
5093+}
5094+
5095+
5096+/*** Search ***/
5097+#search_dialog_wrapper {
5098+  width: 500px;
5099+}
5100+
5101+#search_dialog {
5102+  background: #fff;
5103+}
5104+
5105+#search_header {
5106+  padding: 5px;
5107+  text-indent: 0;
5108+}
5109+
5110+#search_field {
5111+  margin: 0;
5112+}
5113+
5114+#search_by_filters {
5115+  display: inline;
5116+}
5117+
5118+#search_by_filters.button_group button:first-child {
5119+  border-left: 0;
5120+  -webkit-border-radius: 0;
5121+  -moz-border-radius: 0;
5122+  border-radius: 0;
5123+}
5124+
5125+#search_results_column {
5126+  height: 200px;
5127+  max-height: 350px;
5128+  background-image: url(../images/column_background_tall.png);
5129+}
5130+
5131+
5132+#search_results_column .column_item {
5133+  height: 40px;
5134+}
5135+
5136+#search_results_column .column_item u {
5137+  font-weight: bold;
5138+}
5139+
5140+#search_results_column .column_item .result_number {
5141+  display: block;
5142+  float: left;
5143+  width: 35px;
5144+  height: 50px;
5145+  margin: -4px 0 0 0;
5146+  padding: 0 5px 0 0;
5147+  border-right: 1px solid #ddd;
5148+  background: rgba(255, 255, 255, 0.1);
5149+  text-align: right;
5150+  vertical-align: middle;
5151+}
5152+
5153+/** Playlist editor **/
5154+.playlist_editor_wrapper {
5155+  width: 500px;
5156+}
5157+
5158+.playlist_editor {
5159+  background: #fff;
5160+}
5161+
5162+#playlist_details {
5163+  width: 200px;
5164+  padding: 5px 0 0 5px;
5165+}
5166+
5167+#playlist_details input, #playlist_details textarea {
5168+  width: 190px;
5169+}
5170+
5171+#playlist_details textarea {
5172+  height: 120px;
5173+}
5174+
5175+#playlist_details label {
5176+  font-size: 0.9em;
5177+  margin-top: 5px;
5178+  display: block;
5179+  color: #585858;
5180+}
5181+
5182+#playlist_songs {
5183+  width: 294px;
5184+  height: 300px;
5185+  margin: 0;
5186+  border-left: 1px solid #ddd;
5187+  overflow-y: auto;
5188+  overflow-x: hidden;
5189+}
5190+
5191+#playlists_songs li:nth-child(odd) {
5192+  background-color: #f0f6fd !important;
5193+}
5194+
5195+#playlist_songs .column_item {
5196+  margin: 0;
5197+/*
5198+  text-indent: 16px;
5199+  background: #fff url(../images/move_vertical.png) 5px 50% no-repeat;
5200+*/
5201+  cursor: move;
5202+}
5203+
5204+#playlist_songs .column_item .action {
5205+  width: 16px;
5206+  height: 16px;
5207+  margin: 1px 5px 0 0;
5208+  opacity: 0.2;
5209+  outline: 0;
5210+  background: url(../images/close.png) 0 0 no-repeat;
5211+  display: none;
5212+}
5213+
5214+#playlist_songs .column_item .action:active {
5215+  opacity: 0.3;
5216+}
5217+
5218+#playlist_songs .column_item:hover .action {
5219+  display: block;
5220+}
5221+
5222+#playlist_details, #playlist_songs {
5223+  display: inline-block;
5224+  vertical-align: top;
5225+}
5226+
5227+.dialog .footer {
5228+  text-align: right;
5229+  border-top: 1px solid #ddd;
5230+  background: #f3f3f3;
5231+  padding: 5px;
5232+}
5233+
5234+.dialog .footer button {
5235+  background: transparent;
5236+  border-bottom: 1px transparent;
5237+  float: left;
5238+}
5239+
5240+#playlist_delete:active {
5241+  background-color: #f00 !important;
5242+}
5243+
5244+/** 'Add to playlist' dialog **/
5245+.add_to_pl_wrapper {
5246+  width: 300px;
5247+}
5248+
5249+.add_to_pl_wrapper .dialog_title {
5250+  font-size: 0.9em;
5251+  text-indent: 0;
5252+}
5253+
5254+.add_to_pl_wrapper .dialog_title span {
5255+  display: inline-block;
5256+  max-width: 100px;
5257+  overflow: hidden;
5258+  text-overflow: ellipsis;
5259+  white-space: nowrap;
5260+}
5261+
5262+.add_to_pl_wrapper .dialog_title .title {
5263+  display: block;
5264+  font-size: 1.3em;
5265+  font-weight: bold;
5266+}
5267+
5268+.add_to_pl_wrapper .dialog_title img {
5269+  float: left;
5270+  margin-right: 10px;
5271
5272+  -webkit-border-radius: 4px;
5273+  -moz-border-radius: 4px;
5274+  border-radius: 4px;
5275+}
5276+
5277+#add_to_pl_dialog {
5278+  background: #fff;
5279+}
5280+
5281+#add_to_pl_playlists {
5282+  padding: 10px 5px;
5283+}
5284+
5285+#add_to_pl_playlists label {
5286+  display: block;
5287+  margin-top: 5px;
5288+}
5289+
5290+#add_to_new_pl {
5291+  display: none;
5292+  margin: 10px 0 0 0;
5293+}
5294+
5295+#add_to_pl_dialog input[type="text"], #add_to_pl_dialog textarea, #add_to_pl_dialog select {
5296+  width: 98%;
5297+}
5298addfile ./contrib/musicplayer/src/resources/images/close.png
5299binary ./contrib/musicplayer/src/resources/images/close.png
5300oldhex
5301*
5302newhex
5303*89504e470d0a1a0a0000000d49484452000000100000002008060000001b89f8cc000000047342
5304*4954080808087c0864880000000970485973000006ec000006ec011e7538350000001974455874
5305*536f667477617265007777772e696e6b73636170652e6f72679bee3c1a0000012a494441544889
5306*ad94c16d853010449fa3d4c201714b13d4902ae8e0534d1a4094910672e284ec06b84e0e597f99
5307*0dfce8c7ac3497d1ccc86baf3748a2ac104203bc036f06804fc387a4af9d411216128001d8009d
5308*60334db8fb0af3fcc0e831e7901c303c61ce1872fb8d3bf602f4402cb868dce2da69004697dc5b
5309*720b24436b5cefb43780c991b1307440570446a79d00d683fe52361641e940b7be704155b73052
5310*798975cf583d48978c72cd670ab5dff915573ef0cfca479114240d92369dd7669afd3e30627e60
5311*f435e7901c303c61ce757fc646fb632f927a49b1e0a2714bc16d921a248d2eb9b753b59292a135
5312*ae77da1b922647c6c2d019726074da0949eb417fa930e6a074a0bb601f5cd142f525563f63f520
5313*5d32cafffe4cbff6013f3bf2741f00bb7df00d1714f4b762e44b340000000049454e44ae426082
5314*
5315addfile ./contrib/musicplayer/src/resources/images/cog.png
5316binary ./contrib/musicplayer/src/resources/images/cog.png
5317oldhex
5318*
5319newhex
5320*89504e470d0a1a0a0000000d49484452000000100000001008060000001ff3ff61000000047342
5321*4954080808087c0864880000000970485973000006540000065401c4f08d9d0000001974455874
5322*536f667477617265007777772e696e6b73636170652e6f72679bee3c1a0000011749444154388d
5323*95d3312f844110c6f1df5ea320e4140a112d22e10b50899e4629514aa85444f0159412a528a8f0
5324*157444145a8942e322512824b8ace64ee6deec917b934d76669ef9efecec3b29e7acf4a594fab0
5325*8f8c839cf3575198732e2eacb6923356823f75e842601a8f38c602ee03e0068b38c113264b808b
5326*90f0df3a2f01967b002c9500836806d129e6308fb3e06f6220e419c20e9e83e8bad0d4db107fc6
5327*1eeab0542871ad00582f5da586577c545e77aaf0e255df3b5edaf43ab603b981b170fa38de427c
5328*0b83d5268e569af88d4b5c55fccd0e7800ecf6f08c07edbc5ab8d370d83fe033d85fb80b76ffef
5329*2e5450c326665af66138f128fcee1bc23cfc354c130130db4d97ba8d33a494465a5536ba697e00
5330*6077360dd40f77970000000049454e44ae426082
5331addfile ./contrib/musicplayer/src/resources/images/cog_inverted.png
5332binary ./contrib/musicplayer/src/resources/images/cog_inverted.png
5333oldhex
5334*
5335newhex
5336*89504e470d0a1a0a0000000d49484452000000100000001008060000001ff3ff61000000017352
5337*474200aece1ce900000006624b474400ff00ff00ffa0bda7930000000970485973000006540000
5338*065401c4f08d9d0000000774494d4507da080b131e05224b8f500000011a4944415438cb9593b1
5339*4a03611084e7aeb1500cb1b010b1d520c417d04aec53590a9642ac5229a27985948285855a68a5
5340*be829d2216b682459a04c1c242301e9fcd05c7632fc485e3fedd7ffe5966d84d0095c484a42349
5341*486a4b1a8428a0ecdbe23736ad9e38ce1f2c032fc009b00e3c19c13db0019c01afc0524470cdf8
5342*71357c979a9a538d1fe79107d340665d2e8055600db8b47a064cb9840ab00f740d741798fa60f7
5343*5de010a80a68041ab703829d00d74825bd49fa2c68ac05ba8bb50f49bd217b15d833e63e306fdd
5344*178077bb6fe59efd3171ae60e2377003dc16ea99933bc1c13fe6a01dcdc18c9d9f257d593e90f4
5345*68f964340729b00bd4f3bc631d8f6ddc9bbe0fa39669d10856ca70c9887596a4d9fcdf2f03fc00
5346*e3bb727f8d61312a0000000049454e44ae426082
5347binary ./contrib/musicplayer/src/resources/images/column_background.png
5348oldhex
5349*89504e470d0a1a0a0000000d49484452000000010000003c0802000000289347ad0000033b6943
5350*43504943432050726f66696c650000780185944b68d4501486ff8c2982b482a8b51694e0428bb4
5351*253ed08a50db69b5d6917118fbd022c83473671a4d333199191f884841dcf95a8a1b1f888b2ae2
5352*42ba5070a50b9142eb6b510471a52288423752c6ff26ed4c2a562f24f972ce7fcfeb8600550f53
5353*8e63453460d8cebbc9aea876e8f080b6780255a84135b85286e7b42712fb7da6563ee7afe9b750
5354*a465b249c68af51d98f8b46df5fd4b8f62efeb9ef6fa9ef9fa796f352e13028a46eb8a6cc05b25
5355*0f06bc57f2c9bc93a7e68864632895263be446b727d941be415e9a0df16088d3c23380aa366a72
5356*86e3324e6405b9a5686465cc51b26ea74d9b3c25ed69cf18a686fd467ec859d0c6958f01ad6b80
5357*452f2bb6010f18bd0bac5a5fb135d4012bfb81b12d15dbcfa43f1fa576dccb6cd9ec8753aaa3ac
5358*e943a9f4733db0f81a3073b554fa75ab549ab9cd1cace3996514dca2af6561ca2b20a837d8cdf8
5359*c9395a88839e7c550b709373ec5f02c42e00d73f021b1e00cb1f03891aa0670722e7d96e70e5c5
5360*29ce05e8c839a75d333b94d736ebfa76ad9d472bb46edb686ed45296a5f92e4f738527dca24837
5361*63d82a70cefe5ac67bb5b07b0ff2c9fe22e784b77b96959174aa93b34433dbfa92169dbbc98de4
5362*7b19734f37b981d754c6ddd31bb0b2d1cc77f7041ce9b3ad38cf456a2275f660fc0099f115d5c9
5363*47e53c248f78c58332a66f3f96da9720d7d39e3c9e8b494d2df7b69d19eae1194956ac33431df1
5364*597eed169232ef3a6aee3896ffcdb3b6c8731c8605011336ef363424d185289ae0c0450e197a4c
5365*2a4c5aa55fd06ac2c3f1bf2a2d24cabb2c2abaf0997b3efb7b4ea0c0dd327e1fa2718c34962368
5366*fa3bfd9bfe46bfa9dfd1bf5ea92f34543c23ee51d318bffc9d716566598d8c1bd428e3cb9a82f8
5367*06ab6da7d74296d6615e414f5e59df14ae2e635fa92f7b3499435c8c4f87ba14a14c4d18643cd9
5368*b5ecbe48b6f826fc7c73d9169a1eb52fce3ea9abe47aa38e1d99ac7e71365c0d6bffb3ab60d2b2
5369*abf0e48d902e3c6ba1ae5537a9dd6a8bba039aba4b6d535bd54ebeed54f79777f47256264eb26e
5370*97d5a7d8838dd3f4564eba325b04ff167e31fc2f75095bb8a6a1c97f68c2cd654c4bf88ee0f61f
5371*7748f92ffc0d0185150d7c4b3b3b000000097048597300000b1300000b1301009a9c180000001a
5372*49444154081d63f8ffff3f13030303ddf1dbafffe86e27d09f0052e10654d7b720ec0000000049
5373*454e44ae426082
5374newhex
5375*89504e470d0a1a0a0000000d49484452000000010000003c0802000000289347ad000000017352
5376*474200aece1ce900000006624b474400ff00ff00ffa0bda793000000097048597300000b130000
5377*0b1301009a9c180000000774494d4507da08070e031b790051b4000000184944415408d763f8ff
5378*ff3f13030303ddf1c7efff06c25e0055a9065a60440f6e0000000049454e44ae426082
5379addfile ./contrib/musicplayer/src/resources/images/column_background_tall.png
5380binary ./contrib/musicplayer/src/resources/images/column_background_tall.png
5381oldhex
5382*
5383newhex
5384*89504e470d0a1a0a0000000d4948445200000001000000640802000000c84ecd37000000017352
5385*474200aece1ce9000000097048597300000b1300000b1301009a9c180000000774494d4507da08
5386*070e041b3641c773000000194944415418d363f8ffff3f130303c390c71fbfff1b0efe00007fd6
5387*06aad7b111140000000049454e44ae426082
5388addfile ./contrib/musicplayer/src/resources/images/menu_selection_bg.png
5389binary ./contrib/musicplayer/src/resources/images/menu_selection_bg.png
5390oldhex
5391*
5392newhex
5393*89504e470d0a1a0a0000000d49484452000000010000001e0806000000ed9574c7000000017352
5394*474200aece1ce900000006624b4744000000000000f943bb7f000000097048597300000b130000
5395*0b1301009a9c180000000774494d4507da08050b3a0bc6f1c60a0000001974455874436f6d6d65
5396*6e74004372656174656420776974682047494d5057810e17000000364944415408d78d8dc10900
5397*2100c342177317f77434cd3d44455ff70981b694529b018876a2121cb75d4077f909ce4c22cb40
5398*e6c74f7c55bd325b7366d9130000000049454e44ae426082
5399binary ./contrib/musicplayer/src/resources/images/play.png
5400oldhex
5401*89504e470d0a1a0a0000000d49484452000000280000007808060000008070b09a000000047342
5402*4954080808087c086488000000097048597300000dd700000dd70142289b780000001974455874
5403*536f667477617265007777772e696e6b73636170652e6f72679bee3c1a000013cd49444154789c
5404*ad5b7b9015d599ff9dc7edee3b330e0c030e8f0b988cbb8802098b24881544a694adac60b4023e
5405*8810145722082aec4682c435d172b792182ba58b0fd06cb1a861acb5c4ec26d9dd02190bb59652
5406*b22b6aca9555c60b0c0cc3639899dbddb7fb9cfd63fab4e79edb7def9d49beaa53ddd3ddf79c5f
5407*ffbe73bef33d7ac8f9f3e7311c3977eedcc50016039805603c21640221643c0048298f49298f02
5408*3806e05d00bb478c18f1c970c621430178f6ecd9b100d612426ee49c4fe59c8331064208082125
5409*cf4a2921a544188608820041107c24a5fc17004f8e1c39b2eb4f0af0cc99331700d8c818db68db
5410*761de77cf0c71aa8248026d82008e0fbfe4018863f05f0d3a6a6a6aa83570578faf4e92594d2ad
5411*b66d37673299188879d4cf4d703a4805d4f3bc1e21c4f7468d1ad53e2c803d3d3d04c08f6ddbde
5412*6c5956ac46bd99004dd1c19920a594f03c0fbeef3f0a604b7373b34cea2311e0a953a7ea09212f
5413*398eb388730e4a6922b0a4b9970630ad158b45789ef7ba94f296d1a347f79b7d50f34277773721
5414*84bcec38ce22b500cadeaa02d83496d35e88730ec7711611425eeeeeee2e1bac0c2080476ddbbe
5415*4eb156099cd94ca6d31836af11426059d675001ea908f0c489134b2ccbda446912eee48e4d7069
5416*204db0665f94526432991f9c3871624922c0aeaeae0b38e74f33c6e29bfa6a343bafa4dea4395b
5417*8b30c6c0397fbaababeb8232808490ef673299512630536a9977d5d84a123526e77c1421e4fb25
5418*008f1d3b369631767f357069809398d3ef992fa68f93643329a5f71f3b766c6c0c90527a2f632c
5419*6b9a854a800f1e3c8842a15006b29a7d4c33e2fa35c6589610b25e0778a3092cad3325fbf6edc3
5420*0d37de88679f7bae0c4412b86afd9af69110722300d07c3e7f31a5f4cf86dae185175e88f3e7cf
5421*63c78e1d58b478313a3a3aaab25e4bffea0529a57f1e61fb82bda174aa240802747777e3079b37
5422*63c58a15f8fcf3cf13770cf5fb5ac78940de4029a5b3cd9b95e649d220524af8be8f3f7cfc316e
5423*bbed366cd9b2059ee7559dcb9558040042c8e514c0f8b41f24814beb0c00a410182814b077ef5e
5424*2cbefe7abcf8d24b15f7e14a20a3f3f114404bda83953aafa4aa200cd1dbdb8be79e7d164b6fba
5425*09efbdf75eea0b579906632980b1d5549ad659352906014e9c3c8907366dc2baf5ebd1dddd5d91
5426*c5040d8ee3e6e095409ae7b588140242084ccce54029ad49e5fa185c4ad925a56cd5ec4f458042
5427*889267aac955f3e661fdfaf5686e6e2e5938d534145d3fc6a594c700b49ae875104208a4793869
5428*d2fae52fe3fe0d1bf0951933502814e0791e841035b1a7b5e31cc0d16af32109641a838d8d8df8
5429*deead558bc78310a85020606065241a501d6fa3fcaa594ef4a296faec59428f5266d658c312cf9
5430*f6b771c71d77803186bebebeb2fe2a81d3ff1642a8dfbccb01bc26a5fc896268386a9d33670eee
5431*bbf75eb4b4b4c0755d846158115c2de62b3abec6274c98f0bf478f1efd08c054730525a9553128
5432*a5c4a4499370effaf5b8fcf2cbd1dfdf8f81818112e0b52c840a203fcce5729ff0a8af57a59453
5433*15bd3a285dadba2f376fde3cdc7aebad705d174991611ab85ad41db5578128eccce7f36309219f
5434*1042ea6b0980d47d7dc527c95019d480f603b83897cb755100c8e5725d001e371f4e6aea7e1886
5435*f1df959e359fa9654503f85984e98bc03d9fcf5f00e03021644c2d716ed23189bd4aea5553c8b8
5436*d60da03597cb9d07b4a029bab026eded2ab152893dd37c549a7b91ac51e04a004620db013c6a02
5437*4bfa3be97a2590d5541dc9a311865838ca650b806952caeb4d9529f3a29b1a5dcc88cd5477da8a
5438*8ee4b568ecd23e934c443e9faf07b013c0f5fac0fa792d71af390f938e1ab865b95cae7af20800
5439*a2076f00f0a8eaacd2aaabb6e22ba814d118372481036a4860e6f3f925009e0230a6ecc743c818
5440*244837061744c50466d5cd36eaa015c08f0094bc658d5b9629fd515fadd5c001434ca2e7f3f9b1
5441*00d66250fd97d6fcc341f910c0ab009e5446b8161912405df2f97c4919426bc060f941b57701ec
5442*cee572c32a432499999aa4b3b3b35e08318610d22ca51c01a04e08c100104a699d94720421c48b
5443*76a6fa5c2e37ac7186c4e05b6fbd75919472939472a194727252360bf8626118fbeb1142c8ef28
5444*a58fcd9d3bf7b33f29c077de79a74908f18810e27642884329856a00121ddb249324844018862e
5445*63ec05dff71fbcfaeaab4fffd100f7efdf7f9794f21f28a52328a5608c95a47b4d1695a4eddbca
5446*0b12429c638c3d3077eedca78705309fcf93cececead52cabb1863508d520abd3491c4a2527118
5447*8689e0c2308c1b21e4d9c99327afcee572b5d7490e1e3cd85828147603b84ad5e3a2fc71095013
5448*a42eba8a7540aa76a79f03e820842cbef2ca2bcf99fd94ade27c3e4f0a85c26e42c8559cf31854
5449*26932901a900aaa4bbae629d41210438e708820042080441004a697c24842008827952caddf97c
5450*7ebec96419c0cecece7f54e032990cf4a302c739af9aee9552c6ccea20f5978bd80300044130ef
5451*c891234fe772b9bbf47e4a54fcf6db6faf96526eb52c2b06a637a5de6a796813a83a4a3958fa52
5452*2a2e168b28168bf07d3fbece395f3d67ce9c67ca0046a6e4ff38e7232dcb826559c86432b02c2b
5453*56b15e7daae66e5572b5822088c1e9207ddf471004e73299cc97e6cc9973062855f18f1963234d
5454*d638e750d54e05a6160675a3ad3bba524aa87ab3fe029a511f21847804c09a98c103070e4c0ec3
5455*f00f9c73c7719c9839d5aa9519861a76aaf3a8c00ddff7e1799ecea4cb189b7ac515577c460120
5456*0cc30728a58ebe52d3c029b372f4e85184615866b44df3631ef54592a4ada83952ca077415ffa5
5457*5a00fac349c1bb62ecf5d75fc7ecaf7d0d19ce316bd6ac54f6f4bf5553aa9652c2b2ac446fbd58
5458*2c2e0400d6d6d63603c0265da5aa99ace8ecfce217bfc0cf1f7f1c4d4d4dc85816c230c4a851a3
5459*4a58ae56f9d45b42983bb2ababeb554a29bd39896653a5a69a0821f07d1f4f3df514d6ad5b87ff
5460*3a7000fbf7efc7f9f3e7535fccec47357d87d2370321c4cd9c1032d3bc99a4dea4bf95e9e93975
5461*0a3fd8b409b366cdc2da7bee8163dbf8fad7bf0ec658995a4d75ab7b994c2636e40a4710043339
5462*21a4459b9c65fbac0e4e37338c31f008208b9e3f74e810d6ae5983c58b17833286fafa7acc983e
5463*3d11a4298a49b547178b4510425ab8cac528d529f6aacd1fc61832ea59eda528a5f8b7dffc061d
5464*6fbe89952b57a2bfbf1f93274dc2f8f1e313d9532d93c9c4ce836251083186534a47eb5b581a38
5465*9d45f5324ac571d380168b456cdbb60d13274ec4aa55ab70e4f3cf3163fa74d4d7d59530a7b36a
5466*9a2400a3b9c94a926ad36c21d700324a13c19e3c79128f3df61866cf9e8d712d2d68f8d2974a80
5467*2970a633ac1aa7949e2284e4d218ab04b08c41a3314a315028005262de37be81d6d6d68a73d124
5468*87527a8a13426280fa22a8a662ce79ea1c54aed4a953a7b06ad52aac5dbb16d96cb66cbb4b1263
5469*43e8e194d293690f993b88ba0700445331d3d42aa5c4a9ee6ecc9f3f1f0f3ffc30264c98909630
5470*2a11fd9e06f004c760607dad7eb3961d80192a2684e0dcd9b3b8f0c20bf1e28b2fe28a2bae484b
5471*b3a50234934f8490f72821e465750128cfeaa7aa5cdbb7dd4201bde7cee1c1071f44474707e6ce
5472*9d5b114c922465c084102fd2a953a7fe0f21a4d3445f4d2821088200c78f1dc3b7bef52d1c3870
5473*00cb972f8fdd7c7d5ad4225a38aaa2c1cee9d3a7bfcf23f4bf1542fcb51e1aea5f2025c9c48913
5474*d1dbdb8bf65dbbd0dada3a6440bae8c0d4f88490df0291c37ae8d0a149b66d7f9ccd666dc771a0
5475*5aa5b9d8d7d787c6c6c632372c6dbfadd43ccf83ebbaf1b15028789ee74d993973e6110a00d3a6
5476*4deb0cc370bb0a5cd4d1147df0868686b23c6092e75c4d92e26500cfcf9c39f308a025303399cc
5477*e620087a4d90692074d0464c91c8629228b75f6fbeeff78661b8593d13036c6d6d3d5b2c1637a8
5478*1050b54a9da73197042ce99e8a47f43109217f337dfaf433650001e0d24b2fdd562c16b7eaa1a0
5479*ebbaa94970d30baef68c2e66d8592c161186e1d353a74e7d567fae2ca9327efcf8359ee775441f
5480*c0c611975e034e63aad24250f7c33084e779ea035b3d92eb983061c2dd269e32808d8d8df2dcb9
5481*738b060606de705d376651753894d569828b3ea82d01e8ba2e5cd7ddc7185bd4d8d858369f52d3
5482*6fbdbdbde4f8f1e34f5a967577369b8d43513389544d947d538b409f3e11d0adb95c6e4d12b88a
5483*00957cf4d147776432999f398e33c20cac92fc477dcb542a552ebc3a46e07a8510f75f72c925db
5484*2b8d5f530af8f0e1c3238bc5e2df5996b5dab66ddb4c24e9ae98ce9cbe332980aeeb7a41103c63
5485*59d643adadad67ab8d3da424fac71f7f3c514af9b794d2459cf3c92ab164eebffaca56f6340882
5486*cf8410bf0e82e027d3a64debac75cc61d7493ef8e083698cb1258490bfa0948e2384b400181ddd
5487*3e25a53c1104c17100ef0921da2fbbecb243c31967d87592f7df7fdff17dbf290882116118d609
5488*213261181200608c6528a5758cb1119cf326cbb29ccb2ebb6c58e30c89c15ffdea57adaeeb3eec
5489*baee0221c4383302d4455773f425c971c771f6388ef3d04d37dd74f84f0a70d7ae5dcdaeebfe6c
5490*6060e0164288a5672292c0e9119b5ac92a3f2da5f4ebeaea5e721c67c3d2a54b7bfe68803b77ee
5491*bcafafafef47849006ddb4e82646813217090881d4cc8d021901ed6b6868f8e1b265cb7e3e2c80
5492*bb76eda2aeebfed3c0c0c07794fda39482710e9a14b7500ac5a3b98be8056e95ed5706bbaeaeee
5493*9f1dc759b174e9d244373e11e0ae5dbb46f6f5f5fd7b1004b3952931eb2395f28709b145ecbaa9
5494*e28ee65e81737ea0a1a1e1daa54b9796d9c53280edededb4afafef9d2008669730c71818e7e009
5495*60930cb519a5e98ea902ac8e9ee7299073962c5952c264999929140abfd4c1e9b511fd3ca9e294
5496*0630a9c24408898f00e079de6cd7757f0960792a833b77eebcafbfbfff71bd4ea280e9451dc526
5497*ab854129213470dace525633f17d1f0d0d0df7dd72cb2d4f94016c6f6f6f3e7bf6ec678cb10633
5498*b9adb2feba27a3a7e92a89623108020461883002a67bd186a7d33772e4c88b962c59d253a262cf
5499*f37e4a296dd0d5a897239252c4b5489c48620c2c0810180b4b673c62bdc1f3bc9f00b81d881cd6
5500*f6f6f656d7756f8dd3af3a7b5ac549011d4e0c4c0829eb471df5f9cd1883ebbacb5e79e595d698
5501*41cff31e228458716252abd3595aa7ba3a3dcf43c6b250152a21f133849012f3a433a7e62ce71c
5502*524acbf3bc1f02584101c0f7fd365d157a09563735ba3a3ef8f043504ae1ba6ee53c4e048c68aa
5503*658c95943b32d151b7b74110b401006b6d6d9de579de0635e76cdb866559254773ce1142f0daee
5504*ddd8be7d3b265f7411c68d1b8762b19858bed081c68c2638b9a1e689034010048d9f7efae9afb9
5505*10e2b69839cd94e87344b1664eec8181013cf1c41398386912d6dd730f9aa3428efa4d25b55342
5506*60dbf620b8304426caf033c6e2125bb158fc0e0dc3f0ab3ab5ba19d11744996ba5cd9dce2347b0
5507*71e3463cb76d1b42213030305031534b0989992c2922191884105fa552cad1f1feaa81d3cd49da
5508*9e1b0641497bb3a303b7af5c89bd7bf7220843f4f5f7c740cadc322921012491a3762829e5182a
5509*84685613576f8abdc47430210885403108ca9ae7fb78fe8517b06ad52a7c72f8709cb1aab48074
5510*428c15decca5944d31f5caa0320692b243c481b810081332604ace9c3e8d87b66cc1942953b061
5511*e346148300f5757565c56c0550f733d50b08219a68d9fcd04a08496a55f755a6a05a3b74e81056
5512*7ef7bb78e1f9e7d1d3d35336964a279b5502f50c678c9da1948ed5e75a9203a027278141731044
5513*f99a4a22c210239b9a306dda348c1d3b367e495d28ca1761c4e669ce18eb01305677dd9326b632
5514*333ac04a2a1642c0b22c2c5fbe1ccb962d83e33825e18129a626010c02e49c7787061304286330
5515*0940b1584cbc4729455b5b1bd6ac5983969696448fa7acef84ad8f317692673299834110cc57ea
5516*d319531dcb0874124813d8942953b061c306cc983123be6eb29fd48ff9516e1886c866b3bfe7d9
5517*6c7647a150b84f75a43f18771a4568fa80261ba3468dc2dd77df8d6f7ef39b3565bdca006ae180
5518*aad55896b5832e5cb8f02063eca859a730d59e26b66d63c58a15686f6fc775d75d17aff0a14868
5519*808bb6bca3d75e7bed410e008ee3fca7ebba2bf49841455f69aa696969c1fcf9f3b16edd3ae472
5520*b9125043f117f5149d8a5b8410b06dfb3f80c81fcc66b30f7b9e77731004b6f24a941db32c2bb1
5521*e365cb96a1bebebe0454b5b9962409796a1042bc6c36fb2320f2a8afb9e69a4febebeb7724c407
5522*830bc118941052024ebf3e14d103f820622f0ae6775c73cd359fc60001c0b6edef33c67af56046
5523*25d1875fe4aa0cbca404119d534a7b6ddb2efd1f7700686b6b3b5d5f5fbf390c43785afd42b55a
5524*07ade539424859ffbeef430881fafafacd6d6d6df1c7b7251674e1c2854fd6d5d56d2f4609ee28
5525*031f7ff895387805a0959833fbf77d1fd96c76fbc2850b9fd49f2dcb2cd8b67da79472aaeffb73
5526*cdad4e4809cb88eac820925490bae8a508139ce3386fd9b67d67d94b26258ff6ecd933c2f7fd7f
5527*f57dff4a15a7388e3318e4d836ac2834188aa805a0d74794662ccbda6f59d65f2d58b0a0ec23db
5528*d4f4dbdebd7b491004cf140a853b1963b06d1bb66d7f112b1bb16c92e8b64d65b2f4624eb49d3d
5529*c739bfebeaabaf1e5e9d64cf9e3ddf735df7efa5948d2a5c8cbf698df23366a63529b3aa5b86c8
5530*d6f53a8ef3c082050bb6561abfa614f01b6fbc312a0cc3477cdf5f298470f4cf96f50c17b4bddb
5531*dc369511a694ba9665bdc0187b70fefcf97ffca7f2baecdbb7ef2221c4a62008160a2126ab4ddd
5532*4ca49b15d0e8058e70ce7f47297decaaabaefaacd631875d27e9e8e8f88a10e26629e55785102d
5533*52ca3142886600a094f61042ba29a5270821bfa794be3c6fdebcff1ece38ff0f8edc041b2aa64d
5534*f20000000049454e44ae426082
5535newhex
5536*89504e470d0a1a0a0000000d49484452000000290000007b0806000000e926a90a000000047342
5537*4954080808087c086488000000097048597300000dd700000dd70142289b780000001974455874
5538*536f667477617265007777772e696e6b73636170652e6f72679bee3c1a0000170349444154789c
5539*bd9c7b701cc59dc7bfd3f3d8d9d5eaad95654bd8c6369643b031c482606c12f3281e95405dce29
5540*1f2624e67825962dc7492ea0e4ca7155eace9c814b0a88cf4e418ebb54487c475d7c45151020e1
5541*2e9cb5367e617c211658b22d6c6957ab95f635bbf3eefb43dbe3d9d1ec6a65c3fdaaba667734d3
5542*fd995f77ffba7fbfdfac384a292e458e1e3dba9852bac2b6ed4edbb617504a5b398e1b25840c12
5543*42fa398e3b7cedb5d77e78296d70170379f8f0e1af98a6f928cff3cb64599e25cb322749120281
5544*004451846118d0340dbaae435555aaaa6adcb2acf70541d8b362c58afff85421df7df7dd1f504a
5545*b7d6d7d7b734353521100880e3b80b95153f7beba49442d3348c8f8f239d4e8f711cf793ebaebb
5546*eeef3f51c843870eadb72ceb1f9a9a9a3a5a5b5bc1719c03e486f413563f3bdab68d442281f1f1
5547*f1733ccf3fd6d5d5f5d225431e3870606f4d4dcdda3973e6109ee74be0aa01a5949680b2625916
5548*4646466c45515efefce73fbfeea2200f1f3e1cd2753ddadadabaaca9a9c981721737a01fa817ce
5549*0b4a29453299442291785f92a41b56ac5891af1ab208f8e165975dd61e0a85c0711c0821be8033
5550*812c571445c1b973e7ce4b92b4d80f94f891ebba7ea0a3a3a33d180c9605f0d36c256d7bcfb925
5551*180ca2a3a3a35dd7f5a81fcf14c86834ba3712892c6580170b5709caef9c2ccb68696959168d46
5552*f756843c70e0c0fa5028b4b6aeaeae2ca0df77379077589403f603adabab4330185c7be0c081f5
5553*65216ddb7e3212891060aaad2b075c0da09f94ab3f128910dbb677fa4246a3d11f363636ce99ce
5554*ee95032c07ebbedefbd90dcb8e1cc7a1a1a1a13d1a8dfe600a2480adac9bbd3756035dad89aa54
5555*27fb5b5d5d1d28a5df29818c46a35f0987c3cd5e7b56ee6999dcf7b5afe1c4891353402b01f89d
5556*f3b3a3e170b8391a8d7ec581b46d7b636d6d6dd91bca418f8c8ca067cb16747777637c7cbce47a
5557*3fb84a50decfe1701896657dcb81e4797e2921c4b7c2e93e6b9a8663c78fe32fbffa55ecdcb913
5558*96654d81bd989d1621043ccf2f0500128d46178ba218a9a652bf270600dbb2a0e472d8b76f1fee
5559*fad297f0ca2baf80520adbb6a7f4c44c14218a626b341a5d4c005c274992ef40f2ebee4a93cab4
5560*2ca42626b073e74edc7befbde8efef9fb20496abd7afce22571701b084e7795f6d953be737f6dc
5561*d7ea8681a1a1216ceceec6f7beff7de472b992fb2ad5e7ae97e779504a9710dbb617088250f1e6
5562*72a592d8944255551c3a7408ebd6adc3ee3d7bca6ad54f199452067939a194460821659fa81270
5563*35625b16b2b91cfefde597d1dbdb3b6d7deeef3ccfc3b6ed5681e3b8846559601bda4aa0c0e4ce
5564*9a3d54b53267ce1c6cd9b2052b6fb8018542c19950d3f590699a20848c0a1cc70d98a6095114cb
5565*c279c16cdb9ed66d0026b7600f3cf000fe6add3a98a65911d0db2ea593bb778ee306058ee3fa0d
5566*c3802ccba0943adaf46a7026b68ee338dc75e79dd8b871236a6a6aa0aaeab4e3dbaf1886018ee3
5567*fa0542c821c33028a594ab648499f6d806a21cf4d2a54bf1ddef7c070b162c80aaaad034ad443b
5568*ee325db71b8641011c12aebffefafebebebe38a5b4ad12a45bfcbabbb5b5159b376dc29a356b50
5569*28145028147c2703fb3e1d20a514a669c657ae5cf9a1506cf48465596d1cc7958cbf4a93843528
5570*4912be7efffd58bf7e3d2ccb42369b9d725db526cd5d2ccb82655927004000009ee7f7288a729b
5571*77abe6ed666fb9f5d65bf1e8238f201c0ea35028540c0e4c07cb34cb8ef97c1e3ccfef065cdee2
5572*fefdfb139148a4a51a1780c1d4d6d642511467c295d3f64c34c92013894462d5aa55ad806bd3cb
5573*71dc4f5983b66dfb8e19769efd2d93c99468c05bfc34540da0a228e038eea70e9bbb6bde79e79d
5574*8f5b5b5b3baa7553dd47f767bfee9e6e7cba1f2291487cbc7af5eab9ecfe92592108426f3a9db6
5575*2b3d69250d797ba09276fdce177bc71604e1076e2e6f048344a3d19783c1e05fb80303338d5c94
5576*d3a29f56dd455555140a85dfde70c30d6b01d8e5200140eeebeb3b5a5757f71941102a02560339
5577*1d20fb6e9a263299cc9f57ae5c792d00b544733e75aa9d9d9d77643299b8699abe26c2afeb2e66
5578*78b88c3632994cbcb3b3f30e2f60394d0200d2e9f4c293274fbe2ecbf2a2402050a2b59968d25d
5579*bf57a300a0691a54553db564c9923beaebeb07fcea982e3ed972ecd8b1976ddb5e5d535353a2f5
5580*7221173fb872e71445b10921ef5c73cd356b018c9583a826d21b1e1818f86132997c281008b430
5581*ad5e8a689a064dd3c69a9b9b9f5fb870e1df01c855ba7e2631f3f91f7ef8e113e974fa4e4992ea
5582*2449aa6ad230a19442d775e8ba9ea9afaf7f6df1e2c58f033853cdbd33cd3e70003e3b3434b421
5583*954a7dd9308c568ee3ea0541e0789e9fb231b62c0ba669524a695a14c5d186868657e6ce9dfb22
5584*803f01a8bae18b4a9114450470c5f8f8f81773b9dc325dd7e7198631db34cd5a4110b2a2288e48
5585*9274361c0ebfdfd4d4f45f003e02605c4c43c2c51232485114570702812b09216da228d699a619
5586*e0795e130461b6288a734451ac07701e935d7b519033d224a59407b02e91486ccd6432cb73b91c
5587*d5344db02c8bb32c0b8661c0344d0882005114511c02549665331c0e73757575ef4522919f00d8
5588*cb719cf589435a9675c7c8c8c82f46474723baae0b8aa2387b4d3616bd1b0cb7c1268420140a21
5589*100898adadad89d9b367ff35cff3af7f2290a669ce4f2412ff198bc53e5328144445514008714a
5590*b9905fa56d5e4d4d0d42a190316bd6ac3fb7b6b6de2308c2998b86340ce3c63367cebc36363616
5591*cee5721ccff38e7fee5dd7bda07e8096653947cbb2505b5b4b2391486efefcf9778aa2b87fc690
5592*8aa23c323030f04c269309689a0606e82eacab9946bde20e03b27d80699a0eac699a906519f5f5
5593*f5dac2850b7b6a6a6a7e5e3564369b7da8bfbf7f97a228a26559100401822080e779e7c8e058b0
5594*cbebb0793559b4990e1cfbce265a381c363a3b3b37d6d6d63e3f2d64a150b8bebfbfffed743a1d
5595*b46d1ba2283a90ac10429ce374216837a4179059836238058d8d8d6a6767e71783c1e0c1b290f9
5596*7cbee3cc9933c793c96413d3a0288a4e619a148449f35aedb2c8da606391011a860166ba0cc380
5597*288a686969199f3f7ffed5a150e81cbbbfc498c7e3f1df4c4c4c34fa01b2e23537d3c1527a2174
5598*e31d2a841018c605fbaeeb3ac6c7c71b83c1e0af2fbffcf2d5ecbc339072b9dcaa4422f1394dd3
5599*389ee7218a22d8db00ecc8bad77df47ef69a27bfa3f7c1dde3bd502870a3a3a32b72b9dcaa124d
5600*66b359323c3cbc5bd775d9ab414110c0763c33f5719806bd47008e09f30e09dbb6a1699a7cfefc
5601*f9dd73e6cc59565b5b6bb314dd2d1313138b0cc3f0ed666fb6cbab9d996a9215775b922439df0d
5602*c3c0f8f8f8224ae92d4e77c7e3f16f99a619709b1a76a37b0c7961f7f7f5211e8ffb463c987972
5603*1fbd85e7796738797bcf34cd403c1e9fcce34c4c4c04b2d9ec6ad3344b66afd7c4f835d2dbdb8b
5604*23478fa2afaf0fbaae97187abfebbd806e8d7a4b31f875d3c4c44440a0942e5755b5c60dc89ec6
5605*db4deef1c8711c060706f0e8238fe08b6bd660d3a64d902409d75f779def582be7ceb2f1e95edb
5606*99a952553544295d2e643299db2dcb92bd4fc29eb452b8855dd3d7d78723870fe36bf7df0f0068
5607*6a6cc492254ba685741706ca56a0a2d1973399cced423e9f5fca711cc7bac0bdf44d1713627693
5608*69f9a5975ec2ef7ef73b74777723393e8ecf2c5982e6e6e6123836c3fd2019204b34504ab97c3e
5609*bf54300ca38301ba35e937112a41b292cb66f1e4934fa2b3b313dfdab81135c120bababa2049d2
5610*946e7603b39d159b1b3ccfb3d5a843304d33c22e700f683f30f76e87e338082e48de037be6f469
5611*f43efe38d6ac59034110100804b07cf9f2299a748b77420180699a2d826559212f84775697eb6e
5612*81e7c1171fce77061382befdfb71f4c811ac5dbb163945c1ea55ab504ebcb6b4a8cd1a4208c9fa
5613*19eae90059778b8230597cd679a158345dc773cf3d87ff3d71a2ec03f999b922784620848c5896
5614*b578baf1e73746850bce566977bb7cf08989092c5bba142ffdea5758b468916ff8c5ad45aff922
5615*84c4049ee707755dff827b304f07e7d66439c39dcd64100a85f0dcb3cfe2b6db6e2b0be716773b
5616*0c92e7f9d34492a4c3966551f71fa6836315fa75b3699a488e8da1bbbb1bd16814b7df7efb140d
5617*95139f30239524e990400879cbb66d85521a761b5c2f2403731fdddd6ddb36e2b118eeb9e71e6c
5618*dbb60dcdcdcd5569cf0fd205ab1042de123299cc69dbb613966585d985b66d3bbe4b25613ba4e4
5619*d818162f5e8c7f79f1455c75d5554e83ee6335803ec1d9442693394dbababa0ccbb27eab288ab3
5620*665a5675c1857c3e8fbca2e0e9a79fc6abafbeea005e8c301f88adddf97c1e94d2df7675751904
5621*00d2e9f49e42a1907743dab63d5dbd78e8c10771f0e041dc7df7dd25e72fa69bdd6d173717f954
5622*2ab50728ee27d3e9f447aaaabe9e4aa54a9ca5e964f3e6cd9065b9a4b14abb9d72c2da64de6336
5623*9b85a669afa7d3e98f1cc80d1b36d0c1c1c12dd96c36cd5c4c56aad142b952cd35b66dc3300ce8
5624*baee28475194f4e0e0e0960d1b36500712007a7a7acea5d3e917e2f13875fbc4acdb2b814c77be
5625*92b85d5bd33431363646d3e9f40b3d3d3d8e4b5b12763872e4c8b6898989436363632c740c4dd3
5626*ca4ea44a1aaaf4105e40a694542a85743a7df8c89123dbdced4c89607cf39bdf6cb9ebaebb8eb5
5627*b7b77734343438eeacdba52d673ffd1ec2ab51d6c56e25e8ba8e4c2683e1e1e1f3afbdf6daf2dd
5628*bb77976422a6249b76efde3db67ffffedb46464652a954caa948d334678c56a3c1721ab52ccb01
5629*d3751d86612093c920168ba5fafafa6ef502fa6a92494f4fcfe29b6fbef9cdd6d6d6b9b366cd72
5630*fc6f77c865269a64ae81699a0e9caeeb4824124824121fbffdf6dbb73ef3cc33bebf91a8189f5c
5631*bd7a75c3e6cd9b5f696969b9b1a3a38373bb9c6e8fb2dceae436d0de0095aeeb181e1ea6c96472
5632*ffb3cf3efbe577de7927558e63da48efd5575f1de8eeeefe9bd9b3677fbbb1b1b1b9adadad242c
5633*c276f55e6dba97393673d9311e8f23954a8d0f0f0fff74d7ae5d3b8f1f3fae5562a83a663e7ffe
5634*fcfaeddbb73f118944ee0b87c3b5914804b22c4fd9cd33405698360b850292c9241445c9261289
5635*97b66fdffed8993367d2d5b43da3ec4353531307a07ec78e1df73737377f3d140a2d9265b93e10
5636*0870a228429665274ca2aa2a9b1cb45028a40b85c2a96432f9cbdedede7f05901e1f1ffff4934d
5637*822070006ab66ddb764d7b7bfb75c16070b12008f308218db66d4f98a679b650287c188bc5defd
5638*d18f7e740c80629ae64535762919b11080db52a9d4bd8542e1b3a669ce324db3ceb66d8910a20b
5639*82901104211e0c06ffd4d0d0f06b006f02f0fd01c6270a498bc9a6783cfedd6c36bb2c954a6162
5640*62822f140a9ca228c8e572c8e7f30885420887c3a8a9a9413018a48d8d8d56434303c2e1f0f1b6
5641*b6b67fc4a7996c3a7ffefc8bb158ac259148f067cf9e85aeeb533c4cd70395cc704992306fde3c
5642*442211abadad6dacbdbd7dc3279a6c8ac562afc462b1251f7ffcb170f6ecd92991b3723e8c1794
5643*6dc9e6cf9f8f8e8e0eb3adaded645b5bdb972f39d9343838f8fae0e0604d7f7f3fc7eca35f9403
5644*28f5816cdb9ecc15d3d2f72edca991cece4eba60c10265c18205775c54b22993c93c3a3030f0cc
5645*471f7d24c56231b801fd124deed020abd3ef6512b7460dc3405b5b1baeb8e20a7de1c2853d7575
5646*757bfc587c53c9e974faa193274f3ef7c1071f088aa238115fa641f772e8e7ee4e2ad03f6d6759
5647*56c9c38d8e8e229fcf4bbaae3fb764c912abbebebeba64d3071f7cf0dfefbfff7e405114277256
5648*2e2be68d637a21bd3945d33461d936ace232c9ce0583412c5bb64cbbf2ca2bbfe04d369568329f
5649*cf770c0c0cbc76ead4290790e77908c5980f8ba5bb3717c41305738b6ddba09e6c18cff39363d2
5650*b31f2d140a3875ea544092a4d7162e5cb8ac6cb269787878efd0d050c3e8e8a803e8972ee10501
5651*bc2be85a8d262ddb76e28fcc74b1eb59f0349148606868a821180cee5db468d18d532073b9dcaa
5652*783cbe62606080f3039424c9d128fb7ba500424960ab18bd2dbeed5c32d1dc228a224e9f3ecd35
5653*3434ac686b6b5b150e87ffc781cc66b3646868e8e7c3c3c3923b24cd82fc522000c9a3d199ca74
5654*b695699d1082919111a9a1a1e1e773e7cebdaa24d994482416c662319062654c7ba224412a7e66
5655*0921afd032c54fd89061bb7cf767a69c582c864422b190ba934de7ce9ddb3c3e3e2e1142207862
5656*e7a2abd2921f7200304c73f2156d4aa714effa5312212e86a76559be308c8a0157069a4aa5a473
5657*e7ce6d022e249b6e8cc7e393dde1e9ea293e0d9b91009e7efa69a89a06c3309c489cb73bbdddea
5658*1ea7ece12551747a8c0d8bb1b131e472b9551313130142295d9ecbe542ccc88a82e03c99288ace
5659*6fb639171c6bb8ffe449f4f4f4e0df5e7e19e0381454758ad6fc629c7069d4e96e4100efea41d3
5660*3491cbe54294d2e524994cde95cbe502ee258f75391b2b6e7be6a79d37df78030f3ffc308e1e3b
5661*06701c54559d0257a2ddc9ca2ec4dd99792b1ed922a1284a20994cde49144559aaeb3ac771dca4
5662*fd734d1ab6b2f80112424aa2609aa6e19f76edc2b7b76e452c168356745bcbc6df8ba06e4be2b5
5663*bbbaae738aa22c239aae77a8aaead07bd3775e406775e13878835ba66962647818df7fec313cf5
5664*d45328a82ab2d96cd9300d03f583e4791edae478ef20b665353b1956529afee57d326340d1e9a7
5665*c5d767ca9423870f63c337be817dfbf6b1d761fdbbbf6896bcbb2c420873839b89655935c59f94
5666*9454400899342565623f5c5193c634e5377bf762fd7df7e1bdf7de43626ccc7fe67353f3e31cc7
5667*b1685b0de1382ec3fec141c953fa9812782aafa44977c96632f8f18f7f8cdfbff596ffecc78594
5668*9d7bfb278a220064048e90782010b842d7f552503f9780d20b76b208395dd8dab66d74757561cb
5669*962d98376f1e339625c69ee3381057569849719b1813784206054158e5de99f89919eab2914ca6
5670*8bad5f76d965e8e9e9c10d2b57965d819cefaebad9b1b8880c0ab22c1f9224e9fe7c3eef6c80a9
5671*ab227725704317278e1792e338d4d4d4e0e1871fc6dab56bcbbee8e4fdee17a6164591cab27c58
5672*9065f98d402050a094869c8b8a0d4f99d11ef1466e4551c4dd77df8d471f7d140d0d0da5d7b207
5673*75ddebb70bf2b8c1f94020f08690c9644e1342121cc7cdf33a4cd50a2104d75e7b2db66edd8a45
5674*8b16f95fe41acf7e0feb7d6db158ef98936cb26d7b5f30182cb9a89aa413cff3686f6fc7134f3c
5675*819ffdec67e501a7116f9bb66da3c8b3afababcb10004055d55db22c3f9c4aa5426ebf986debcb
5676*49777737d6ae5d0b49921c8d0028abb172e26e8fb51f0e87f3aaaaee025cc926d334df64613b76
5677*03fb5c4ed6af5fef004eb2b9d6658f4c31672effc69d81606fff99a6f9e69464d3f9f3e737c9b2
5678*9c310c037a319ecd6ef293298dce50d8ddac0dd65ef1edd4ccf9f3e737f9269b1445f9452010b0
5679*cde24deeccc34cbbb09a076275bb33119224d98aa2fca26cb2e9c489137f6b59d631b6f5621568
5680*9a06d347a3d568d3f79ae2baecae5fd37536718f9d3871e26f4b2ef74b36dd7cf3cdc7755d9f23
5681*491282c12064599ef4470201045c63d02d53ec6899b109c001535595fde40abaae4392a4e13ffc
5682*e10f5757956c3a78f0e02d9224a58bffdec9299aaa42d5345f1beabba9f548f1ddc8923a596c5d
5683*92a4f4c183076f9951b269e3c68d8b6fbae9a6dfdbb6ddc1bc3b56045174fc703f27df0f8e4d10
5684*d6cdee2c1b21e4dc1ffff8c75b76edda7571c9a6871e7ae8d54020703d00c29c32e6fbf86df939
5685*8ef30db1989e595cfcb5b2ad69dac1e79f7ffeae4b4e363df8e0838fcf9a35ab8752dac4f679ee
5686*c015e7dada954016f7016ebb6b18c6e46f2608198fc762cfbcf0c20b4f7ca2c9a6dededea71a1b
5687*1bd701a805a6864edc5deff73e6451b21313137b77ecd8f1bd4f35d9b47dfbf6079a9b9bef9724
5688*e97200f55cd1ce300d7a764f14405ad7f5d3c964f297dbb76fff67fc7f279b7a7b7b3f377bf6ec
5689*eb6559be4214c579849066dbb69386619c5555f5a3919191833b76ec38824b4836fd1f68f9825c
5690*509cb2730000000049454e44ae426082
5691hunk ./contrib/musicplayer/src/resources/images/play.svg 13
5692    xmlns:xlink="http://www.w3.org/1999/xlink"
5693    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
5694    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
5695-   width="40"
5696-   height="120"
5697+   width="41"
5698+   height="123"
5699    id="svg2"
5700    version="1.1"
5701    inkscape:version="0.47 r22583"
5702hunk ./contrib/musicplayer/src/resources/images/play.svg 19
5703    sodipodi:docname="play.svg"
5704-   inkscape:export-filename="/home/josip/bin/tahoe-lafs/contrib/musicplayer/src/resources/images/play.png"
5705+   inkscape:export-filename="/home/josip/bin/tahoe/contrib/musicplayer/src/resources/images/play.png"
5706    inkscape:export-xdpi="90"
5707    inkscape:export-ydpi="90">
5708   <defs
5709hunk ./contrib/musicplayer/src/resources/images/play.svg 25
5710      id="defs4">
5711     <linearGradient
5712+       id="linearGradient8130">
5713+      <stop
5714+         style="stop-color:#ffffff;stop-opacity:1;"
5715+         offset="0"
5716+         id="stop8132" />
5717+      <stop
5718+         style="stop-color:#000000;stop-opacity:0.14503817;"
5719+         offset="1"
5720+         id="stop8136" />
5721+    </linearGradient>
5722+    <linearGradient
5723+       id="linearGradient8103">
5724+      <stop
5725+         id="stop8105"
5726+         offset="0"
5727+         style="stop-color:#ffffff;stop-opacity:1;" />
5728+      <stop
5729+         style="stop-color:#e2e2e2;stop-opacity:0.49803922;"
5730+         offset="0.60655737"
5731+         id="stop8109" />
5732+      <stop
5733+         id="stop8107"
5734+         offset="1"
5735+         style="stop-color:#000000;stop-opacity:0.14503817;" />
5736+    </linearGradient>
5737+    <linearGradient
5738        id="linearGradient8237">
5739       <stop
5740          id="stop8239"
5741hunk ./contrib/musicplayer/src/resources/images/play.svg 166
5742        y1="339.4957"
5743        x2="198.26703"
5744        y2="309.34891" />
5745+    <radialGradient
5746+       inkscape:collect="always"
5747+       xlink:href="#linearGradient7951"
5748+       id="radialGradient8043"
5749+       gradientUnits="userSpaceOnUse"
5750+       gradientTransform="matrix(1.1891384,0,0,1.1891384,-29.316449,-56.053463)"
5751+       cx="155"
5752+       cy="289.22653"
5753+       fx="155"
5754+       fy="289.22653"
5755+       r="43" />
5756+    <inkscape:perspective
5757+       id="perspective8191"
5758+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
5759+       inkscape:vp_z="1 : 0.5 : 1"
5760+       inkscape:vp_y="0 : 1000 : 0"
5761+       inkscape:vp_x="0 : 0.5 : 1"
5762+       sodipodi:type="inkscape:persp3d" />
5763+    <radialGradient
5764+       inkscape:collect="always"
5765+       xlink:href="#linearGradient8045"
5766+       id="radialGradient8227"
5767+       gradientUnits="userSpaceOnUse"
5768+       gradientTransform="matrix(1.1891384,0,0,1.1891384,-29.316449,-56.053463)"
5769+       cx="155"
5770+       cy="289.22653"
5771+       fx="155"
5772+       fy="289.22653"
5773+       r="43" />
5774+    <filter
5775+       inkscape:collect="always"
5776+       id="filter8233"
5777+       color-interpolation-filters="sRGB">
5778+      <feGaussianBlur
5779+         inkscape:collect="always"
5780+         stdDeviation="2.15"
5781+         id="feGaussianBlur8235" />
5782+    </filter>
5783+    <radialGradient
5784+       inkscape:collect="always"
5785+       xlink:href="#linearGradient8045"
5786+       id="radialGradient7253"
5787+       gradientUnits="userSpaceOnUse"
5788+       gradientTransform="matrix(1.1891384,0,0,1.1891384,-29.316449,-56.053463)"
5789+       cx="155"
5790+       cy="289.22653"
5791+       fx="155"
5792+       fy="289.22653"
5793+       r="43" />
5794+    <radialGradient
5795+       inkscape:collect="always"
5796+       xlink:href="#linearGradient7951"
5797+       id="radialGradient7261"
5798+       gradientUnits="userSpaceOnUse"
5799+       gradientTransform="matrix(1.1891384,0,0,1.1891384,-29.316449,-56.053463)"
5800+       cx="155"
5801+       cy="289.22653"
5802+       fx="155"
5803+       fy="289.22653"
5804+       r="43" />
5805+    <radialGradient
5806+       inkscape:collect="always"
5807+       xlink:href="#linearGradient7951"
5808+       id="radialGradient7269"
5809+       gradientUnits="userSpaceOnUse"
5810+       gradientTransform="matrix(1.1891384,0,0,1.1891384,-29.316449,-56.053463)"
5811+       cx="155"
5812+       cy="289.22653"
5813+       fx="155"
5814+       fy="289.22653"
5815+       r="43" />
5816+    <radialGradient
5817+       inkscape:collect="always"
5818+       xlink:href="#linearGradient7951"
5819+       id="radialGradient8077"
5820+       gradientUnits="userSpaceOnUse"
5821+       gradientTransform="matrix(1.1891384,0,0,1.1891384,-29.316449,-56.053463)"
5822+       cx="155"
5823+       cy="289.22653"
5824+       fx="155"
5825+       fy="289.22653"
5826+       r="43" />
5827+    <linearGradient
5828+       inkscape:collect="always"
5829+       xlink:href="#linearGradient8103"
5830+       id="linearGradient8087"
5831+       x1="198.5"
5832+       y1="207.78288"
5833+       x2="198.5"
5834+       y2="308.74759"
5835+       gradientUnits="userSpaceOnUse" />
5836     <linearGradient
5837        inkscape:collect="always"
5838        xlink:href="#linearGradient7977"
5839hunk ./contrib/musicplayer/src/resources/images/play.svg 260
5840-       id="linearGradient8027"
5841+       id="linearGradient8089"
5842        gradientUnits="userSpaceOnUse"
5843        x1="198.26703"
5844        y1="339.4957"
5845hunk ./contrib/musicplayer/src/resources/images/play.svg 266
5846        x2="198.26703"
5847        y2="309.34891" />
5848+    <radialGradient
5849+       inkscape:collect="always"
5850+       xlink:href="#linearGradient7951"
5851+       id="radialGradient8091"
5852+       gradientUnits="userSpaceOnUse"
5853+       gradientTransform="matrix(1.1891384,0,0,1.1891384,-29.316449,-56.053463)"
5854+       cx="155"
5855+       cy="289.22653"
5856+       fx="155"
5857+       fy="289.22653"
5858+       r="43" />
5859+    <linearGradient
5860+       inkscape:collect="always"
5861+       xlink:href="#linearGradient8103"
5862+       id="linearGradient8101"
5863+       x1="111.5"
5864+       y1="296.36218"
5865+       x2="198.5"
5866+       y2="296.36218"
5867+       gradientUnits="userSpaceOnUse" />
5868     <linearGradient
5869        inkscape:collect="always"
5870        xlink:href="#linearGradient7977"
5871hunk ./contrib/musicplayer/src/resources/images/play.svg 289
5872-       id="linearGradient8041"
5873+       id="linearGradient8111"
5874        gradientUnits="userSpaceOnUse"
5875        x1="160.67363"
5876        y1="287.52835"
5877hunk ./contrib/musicplayer/src/resources/images/play.svg 298
5878     <radialGradient
5879        inkscape:collect="always"
5880        xlink:href="#linearGradient7951"
5881-       id="radialGradient8043"
5882+       id="radialGradient8113"
5883        gradientUnits="userSpaceOnUse"
5884        gradientTransform="matrix(1.1891384,0,0,1.1891384,-29.316449,-56.053463)"
5885        cx="155"
5886hunk ./contrib/musicplayer/src/resources/images/play.svg 309
5887     <linearGradient
5888        inkscape:collect="always"
5889        xlink:href="#linearGradient8045"
5890-       id="linearGradient8051"
5891+       id="linearGradient8115"
5892+       gradientUnits="userSpaceOnUse"
5893        x1="143.63651"
5894        y1="299.68353"
5895        x2="160.13068"
5896hunk ./contrib/musicplayer/src/resources/images/play.svg 314
5897-       y2="306.98428"
5898-       gradientUnits="userSpaceOnUse" />
5899+       y2="306.98428" />
5900     <radialGradient
5901        inkscape:collect="always"
5902        xlink:href="#linearGradient8129"
5903hunk ./contrib/musicplayer/src/resources/images/play.svg 318
5904-       id="radialGradient8135"
5905+       id="radialGradient8117"
5906+       gradientUnits="userSpaceOnUse"
5907+       gradientTransform="matrix(1.4885599,0,0,1.4885599,-75.726792,-144.79069)"
5908        cx="155"
5909        cy="296.36218"
5910        fx="153.96471"
5911hunk ./contrib/musicplayer/src/resources/images/play.svg 325
5912        fy="292.49844"
5913-       r="43"
5914-       gradientUnits="userSpaceOnUse"
5915-       gradientTransform="matrix(1.4885599,0,0,1.4885599,-75.726792,-144.79069)" />
5916-    <inkscape:perspective
5917-       id="perspective8191"
5918-       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
5919-       inkscape:vp_z="1 : 0.5 : 1"
5920-       inkscape:vp_y="0 : 1000 : 0"
5921-       inkscape:vp_x="0 : 0.5 : 1"
5922-       sodipodi:type="inkscape:persp3d" />
5923+       r="43" />
5924+    <linearGradient
5925+       inkscape:collect="always"
5926+       xlink:href="#linearGradient8130"
5927+       id="linearGradient8127"
5928+       x1="96.712578"
5929+       y1="296.36218"
5930+       x2="174.13466"
5931+       y2="296.36218"
5932+       gradientUnits="userSpaceOnUse" />
5933     <linearGradient
5934        inkscape:collect="always"
5935        xlink:href="#linearGradient7977"
5936hunk ./contrib/musicplayer/src/resources/images/play.svg 338
5937-       id="linearGradient8225"
5938+       id="linearGradient8138"
5939        gradientUnits="userSpaceOnUse"
5940        x1="160.67363"
5941        y1="287.52835"
5942hunk ./contrib/musicplayer/src/resources/images/play.svg 347
5943     <radialGradient
5944        inkscape:collect="always"
5945        xlink:href="#linearGradient8045"
5946-       id="radialGradient8227"
5947+       id="radialGradient8140"
5948        gradientUnits="userSpaceOnUse"
5949        gradientTransform="matrix(1.1891384,0,0,1.1891384,-29.316449,-56.053463)"
5950        cx="155"
5951hunk ./contrib/musicplayer/src/resources/images/play.svg 358
5952     <linearGradient
5953        inkscape:collect="always"
5954        xlink:href="#linearGradient8045"
5955-       id="linearGradient8229"
5956+       id="linearGradient8142"
5957        gradientUnits="userSpaceOnUse"
5958        x1="146.0701"
5959        y1="306.98428"
5960hunk ./contrib/musicplayer/src/resources/images/play.svg 367
5961     <radialGradient
5962        inkscape:collect="always"
5963        xlink:href="#linearGradient8237"
5964-       id="radialGradient8231"
5965+       id="radialGradient8144"
5966        gradientUnits="userSpaceOnUse"
5967        gradientTransform="matrix(1.4398708,0,0,1.4398707,-68.179991,-130.36105)"
5968        cx="155"
5969hunk ./contrib/musicplayer/src/resources/images/play.svg 375
5970        fx="155"
5971        fy="289.86111"
5972        r="43" />
5973-    <filter
5974+    <inkscape:perspective
5975+       id="perspective8154"
5976+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
5977+       inkscape:vp_z="1 : 0.5 : 1"
5978+       inkscape:vp_y="0 : 1000 : 0"
5979+       inkscape:vp_x="0 : 0.5 : 1"
5980+       sodipodi:type="inkscape:persp3d" />
5981+    <linearGradient
5982        inkscape:collect="always"
5983hunk ./contrib/musicplayer/src/resources/images/play.svg 384
5984-       id="filter8233"
5985-       color-interpolation-filters="sRGB">
5986-      <feGaussianBlur
5987-         inkscape:collect="always"
5988-         stdDeviation="2.15"
5989-         id="feGaussianBlur8235" />
5990-    </filter>
5991+       xlink:href="#linearGradient8103-0"
5992+       id="linearGradient8101-0"
5993+       x1="111.5"
5994+       y1="296.36218"
5995+       x2="198.5"
5996+       y2="296.36218"
5997+       gradientUnits="userSpaceOnUse" />
5998+    <linearGradient
5999+       id="linearGradient8103-0">
6000+      <stop
6001+         id="stop8105-5"
6002+         offset="0"
6003+         style="stop-color:#ffffff;stop-opacity:1;" />
6004+      <stop
6005+         style="stop-color:#e2e2e2;stop-opacity:0.49803922;"
6006+         offset="0.60655737"
6007+         id="stop8109-8" />
6008+      <stop
6009+         id="stop8107-5"
6010+         offset="1"
6011+         style="stop-color:#000000;stop-opacity:0.14503817;" />
6012+    </linearGradient>
6013+    <linearGradient
6014+       y2="296.36218"
6015+       x2="198.5"
6016+       y1="296.36218"
6017+       x1="111.5"
6018+       gradientUnits="userSpaceOnUse"
6019+       id="linearGradient8164"
6020+       xlink:href="#linearGradient8103-0"
6021+       inkscape:collect="always" />
6022+    <linearGradient
6023+       inkscape:collect="always"
6024+       xlink:href="#linearGradient8103"
6025+       id="linearGradient8195"
6026+       gradientUnits="userSpaceOnUse"
6027+       x1="198.5"
6028+       y1="207.78288"
6029+       x2="198.5"
6030+       y2="308.74759"
6031+       gradientTransform="matrix(1,0,0,-1,-1.5578274,603.34646)" />
6032+    <linearGradient
6033+       inkscape:collect="always"
6034+       xlink:href="#linearGradient7977"
6035+       id="linearGradient8197"
6036+       gradientUnits="userSpaceOnUse"
6037+       x1="198.26703"
6038+       y1="339.4957"
6039+       x2="198.26703"
6040+       y2="309.34891"
6041+       gradientTransform="matrix(0.46511627,0,0,-0.46511627,-51.593022,158.34287)" />
6042+    <linearGradient
6043+       inkscape:collect="always"
6044+       xlink:href="#linearGradient7951"
6045+       id="linearGradient8200"
6046+       gradientUnits="userSpaceOnUse"
6047+       x1="198.5"
6048+       y1="356.6759"
6049+       x2="198.5"
6050+       y2="319.47015" />
6051+    <inkscape:perspective
6052+       id="perspective8210"
6053+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
6054+       inkscape:vp_z="1 : 0.5 : 1"
6055+       inkscape:vp_y="0 : 1000 : 0"
6056+       inkscape:vp_x="0 : 0.5 : 1"
6057+       sodipodi:type="inkscape:persp3d" />
6058+    <inkscape:perspective
6059+       id="perspective8210-4"
6060+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
6061+       inkscape:vp_z="1 : 0.5 : 1"
6062+       inkscape:vp_y="0 : 1000 : 0"
6063+       inkscape:vp_x="0 : 0.5 : 1"
6064+       sodipodi:type="inkscape:persp3d" />
6065   </defs>
6066   <sodipodi:namedview
6067      id="base"
6068hunk ./contrib/musicplayer/src/resources/images/play.svg 461
6069-     pagecolor="#f3f3f3"
6070+     pagecolor="#ffffff"
6071      bordercolor="#d7d7d7"
6072      borderopacity="1"
6073      inkscape:pageopacity="0"
6074hunk ./contrib/musicplayer/src/resources/images/play.svg 466
6075      inkscape:pageshadow="2"
6076-     inkscape:zoom="1"
6077-     inkscape:cx="11.334097"
6078-     inkscape:cy="82.921351"
6079+     inkscape:zoom="8"
6080+     inkscape:cx="5.2312101"
6081+     inkscape:cy="73.776456"
6082      inkscape:document-units="px"
6083hunk ./contrib/musicplayer/src/resources/images/play.svg 470
6084-     inkscape:current-layer="layer1"
6085+     inkscape:current-layer="g8008"
6086      showgrid="false"
6087      inkscape:snap-global="false"
6088      inkscape:showpageshadow="false"
6089hunk ./contrib/musicplayer/src/resources/images/play.svg 476
6090      showborder="true"
6091      borderlayer="false"
6092-     inkscape:window-width="1280"
6093-     inkscape:window-height="741"
6094+     inkscape:window-width="1278"
6095+     inkscape:window-height="798"
6096      inkscape:window-x="0"
6097hunk ./contrib/musicplayer/src/resources/images/play.svg 479
6098-     inkscape:window-y="26"
6099-     inkscape:window-maximized="1" />
6100+     inkscape:window-y="0"
6101+     inkscape:window-maximized="0" />
6102   <metadata
6103      id="metadata7">
6104     <rdf:RDF>
6105hunk ./contrib/musicplayer/src/resources/images/play.svg 497
6106      inkscape:label="Layer 1"
6107      inkscape:groupmode="layer"
6108      id="layer1"
6109-     transform="translate(-331.68859,-262.98427)">
6110+     transform="translate(-331.68859,-256.98427)">
6111     <g
6112        id="g8211"
6113hunk ./contrib/musicplayer/src/resources/images/play.svg 500
6114-       transform="matrix(0.46511627,0,0,0.46511627,280.32014,220.20089)">
6115+       transform="matrix(0.46511627,0,0,0.46511627,280.82014,216.70089)">
6116       <path
6117          transform="matrix(1,0,0,-1,-1.5578274,603.34646)"
6118hunk ./contrib/musicplayer/src/resources/images/play.svg 503
6119-         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"
6120+         d="m 198,296.36218 a 43,43 0 1 1 -86,0 43,43 0 1 1 86,0 z"
6121          sodipodi:ry="43"
6122          sodipodi:rx="43"
6123          sodipodi:cy="296.36218"
6124hunk ./contrib/musicplayer/src/resources/images/play.svg 509
6125          sodipodi:cx="155"
6126          id="path8213"
6127-         style="fill:url(#linearGradient8225);fill-opacity:1;stroke:none"
6128+         style="fill:url(#linearGradient8138);fill-opacity:1;stroke:none"
6129          sodipodi:type="arc" />
6130       <path
6131          sodipodi:type="arc"
6132hunk ./contrib/musicplayer/src/resources/images/play.svg 513
6133-         style="opacity:0.1072797;fill:url(#radialGradient8227);fill-opacity:1;stroke:none;filter:url(#filter8233)"
6134+         style="opacity:0.1072797;fill:url(#radialGradient8140);fill-opacity:1;stroke:none;filter:url(#filter8233)"
6135          id="path8215"
6136          sodipodi:cx="155"
6137          sodipodi:cy="296.36218"
6138hunk ./contrib/musicplayer/src/resources/images/play.svg 519
6139          sodipodi:rx="43"
6140          sodipodi:ry="43"
6141-         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"
6142+         d="m 198,296.36218 a 43,43 0 1 1 -86,0 43,43 0 1 1 86,0 z"
6143          transform="matrix(0.99142262,0,0,-0.99142262,-0.79182653,607.2467)"
6144          clip-path="url(#clipPath8021)" />
6145       <path
6146hunk ./contrib/musicplayer/src/resources/images/play.svg 552
6147          sodipodi:cx="153.44217"
6148          sodipodi:sides="3"
6149          id="path8219"
6150-         style="opacity:0.60536397;fill:url(#linearGradient8229);fill-opacity:1;stroke:none"
6151+         style="opacity:0.60536397;fill:url(#linearGradient8142);fill-opacity:1;stroke:none"
6152          sodipodi:type="star" />
6153       <path
6154          id="path8221"
6155hunk ./contrib/musicplayer/src/resources/images/play.svg 560
6156          style="fill:#e0e1e1;fill-opacity:1;stroke:none" />
6157       <path
6158          sodipodi:type="arc"
6159-         style="fill:url(#radialGradient8231);fill-opacity:1;stroke:none"
6160+         style="fill:url(#radialGradient8144);fill-opacity:1;stroke:none"
6161          id="path8223"
6162          sodipodi:cx="155"
6163          sodipodi:cy="296.36218"
6164hunk ./contrib/musicplayer/src/resources/images/play.svg 566
6165          sodipodi:rx="43"
6166          sodipodi:ry="43"
6167-         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"
6168-         transform="matrix(1,0,0,-1,-1.5578274,603.34646)" />
6169+         d="m 198,296.36218 a 43,43 0 1 1 -86,0 43,43 0 1 1 86,0 z"
6170+         transform="matrix(1,0,0,-1,-1.5578266,603.34646)" />
6171+      <path
6172+         transform="matrix(0,1,1,0,-142.92001,151.98428)"
6173+         d="m 198,296.36218 a 43,43 0 1 1 -86,0 43,43 0 1 1 86,0 z"
6174+         sodipodi:ry="43"
6175+         sodipodi:rx="43"
6176+         sodipodi:cy="296.36218"
6177+         sodipodi:cx="155"
6178+         id="path8093-7"
6179+         style="fill:none;stroke:url(#linearGradient8164);stroke-width:2.1500001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
6180+         sodipodi:type="arc" />
6181+      <path
6182+         style="fill:#b7b7b7;fill-opacity:1;stroke:none"
6183+         d="m 153.44217,263.98428 c -23.74824,0 -43,19.25176 -43,43 0,23.74824 19.25176,43 43,43 23.74825,0 43,-19.25176 43,-43 0,-23.74824 -19.25175,-43 -43,-43 z m 0,1.075 c 23.16727,0 41.925,18.75773 41.925,41.925 0,23.16727 -18.75773,41.925 -41.925,41.925 -23.16727,0 -41.925,-18.75773 -41.925,-41.925 0,-23.16727 18.75773,-41.925 41.925,-41.925 z"
6184+         id="path8187-9" />
6185     </g>
6186     <g
6187hunk ./contrib/musicplayer/src/resources/images/play.svg 584
6188-       transform="matrix(0.46511627,0,0,0.46511627,280.32014,180.20089)"
6189+       transform="matrix(0.46511627,0,0,0.46511627,280.82014,175.70089)"
6190        id="g8029">
6191       <path
6192          sodipodi:type="arc"
6193hunk ./contrib/musicplayer/src/resources/images/play.svg 588
6194-         style="fill:url(#linearGradient8041);fill-opacity:1;stroke:none"
6195+         style="fill:url(#linearGradient8111);fill-opacity:1;stroke:none"
6196          id="path8031"
6197          sodipodi:cx="155"
6198          sodipodi:cy="296.36218"
6199hunk ./contrib/musicplayer/src/resources/images/play.svg 594
6200          sodipodi:rx="43"
6201          sodipodi:ry="43"
6202-         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"
6203+         d="m 198,296.36218 a 43,43 0 1 1 -86,0 43,43 0 1 1 86,0 z"
6204          transform="matrix(1,0,0,-1,-1.5578274,603.34646)" />
6205       <path
6206          clip-path="url(#clipPath8021)"
6207hunk ./contrib/musicplayer/src/resources/images/play.svg 599
6208          transform="matrix(0.99142262,0,0,-0.99142262,-0.79182653,607.2467)"
6209-         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"
6210+         d="m 198,296.36218 a 43,43 0 1 1 -86,0 43,43 0 1 1 86,0 z"
6211          sodipodi:ry="43"
6212          sodipodi:rx="43"
6213          sodipodi:cy="296.36218"
6214hunk ./contrib/musicplayer/src/resources/images/play.svg 605
6215          sodipodi:cx="155"
6216          id="path8033"
6217-         style="opacity:0.9425287;fill:url(#radialGradient8043);fill-opacity:1;stroke:none;filter:url(#filter7971)"
6218+         style="opacity:0.9425287;fill:url(#radialGradient8113);fill-opacity:1;stroke:none;filter:url(#filter7971)"
6219          sodipodi:type="arc" />
6220       <path
6221          transform="matrix(0,-1.2147699,1.2147699,0,-217.35179,495.14899)"
6222hunk ./contrib/musicplayer/src/resources/images/play.svg 625
6223          sodipodi:type="star" />
6224       <path
6225          sodipodi:type="star"
6226-         style="fill:url(#linearGradient8051);fill-opacity:1;stroke:none"
6227+         style="fill:url(#linearGradient8115);fill-opacity:1;stroke:none"
6228          id="path8037"
6229          sodipodi:sides="3"
6230          sodipodi:cx="153.44217"
6231hunk ./contrib/musicplayer/src/resources/images/play.svg 645
6232          id="path8039" />
6233       <path
6234          transform="matrix(1,0,0,-1,-1.5578274,603.34646)"
6235-         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"
6236+         d="m 198,296.36218 a 43,43 0 1 1 -86,0 43,43 0 1 1 86,0 z"
6237          sodipodi:ry="43"
6238          sodipodi:rx="43"
6239          sodipodi:cy="296.36218"
6240hunk ./contrib/musicplayer/src/resources/images/play.svg 651
6241          sodipodi:cx="155"
6242          id="path8127"
6243-         style="opacity:0.83524906;fill:url(#radialGradient8135);fill-opacity:1;stroke:none"
6244+         style="opacity:0.83524906;fill:url(#radialGradient8117);fill-opacity:1;stroke:none"
6245+         sodipodi:type="arc" />
6246+      <path
6247+         transform="matrix(0,1,1,0,-142.92001,151.98428)"
6248+         d="m 198,296.36218 a 43,43 0 1 1 -86,0 43,43 0 1 1 86,0 z"
6249+         sodipodi:ry="43"
6250+         sodipodi:rx="43"
6251+         sodipodi:cy="296.36218"
6252+         sodipodi:cx="155"
6253+         id="path8093"
6254+         style="fill:none;stroke:url(#linearGradient8101);stroke-width:2.1500001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
6255          sodipodi:type="arc" />
6256hunk ./contrib/musicplayer/src/resources/images/play.svg 663
6257+      <path
6258+         style="fill:#b7b7b7;fill-opacity:1;stroke:none"
6259+         d="m 153.44217,263.98428 c -23.74824,0 -43,19.25176 -43,43 0,23.74825 19.25176,43 43,43 23.74825,0 43,-19.25175 43,-43 0,-23.74824 -19.25175,-43 -43,-43 z m 0,1.075 c 23.16727,0 41.925,18.75773 41.925,41.925 0,23.16727 -18.75773,41.925 -41.925,41.925 -23.16727,0 -41.925,-18.75773 -41.925,-41.925 0,-23.16727 18.75773,-41.925 41.925,-41.925 z"
6260+         id="path8187-5" />
6261     </g>
6262     <g
6263        id="g8008"
6264hunk ./contrib/musicplayer/src/resources/images/play.svg 670
6265-       transform="matrix(0.46511627,0,0,0.46511627,280.32014,140.20089)">
6266+       transform="matrix(0.46511627,0,0,0.46511627,280.82014,134.70089)">
6267       <path
6268          transform="matrix(1,0,0,-1,-1.5578274,603.34646)"
6269hunk ./contrib/musicplayer/src/resources/images/play.svg 673
6270-         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"
6271+         d="m 198,296.36218 a 43,43 0 1 1 -86,0 43,43 0 1 1 86,0 z"
6272          sodipodi:ry="43"
6273          sodipodi:rx="43"
6274          sodipodi:cy="296.36218"
6275hunk ./contrib/musicplayer/src/resources/images/play.svg 679
6276          sodipodi:cx="155"
6277          id="path7167"
6278-         style="fill:url(#linearGradient8027);fill-opacity:1;stroke:none"
6279+         style="fill:url(#linearGradient8089);fill-opacity:1;stroke:none"
6280          sodipodi:type="arc" />
6281       <path
6282          sodipodi:type="arc"
6283hunk ./contrib/musicplayer/src/resources/images/play.svg 683
6284-         style="fill:url(#radialGradient8017);fill-opacity:1;stroke:none;filter:url(#filter7971)"
6285+         style="fill:url(#radialGradient8091);fill-opacity:1;stroke:none;filter:url(#filter7971)"
6286          id="path7941"
6287          sodipodi:cx="155"
6288          sodipodi:cy="296.36218"
6289hunk ./contrib/musicplayer/src/resources/images/play.svg 689
6290          sodipodi:rx="43"
6291          sodipodi:ry="43"
6292-         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"
6293+         d="m 198,296.36218 a 43,43 0 1 1 -86,0 43,43 0 1 1 86,0 z"
6294          transform="matrix(0.99142262,0,0,0.99142262,-0.79182653,7.0289736)"
6295          clip-path="url(#clipPath8021)" />
6296       <path
6297hunk ./contrib/musicplayer/src/resources/images/play.svg 728
6298          id="path8003"
6299          d="m 141.71875,291 0,1.75 26.15625,15.125 1.53125,-0.90625 L 141.71875,291 z"
6300          style="fill:#3a3b3b;fill-opacity:1;stroke:none" />
6301+      <path
6302+         sodipodi:type="arc"
6303+         style="fill:none;stroke:url(#linearGradient8200);stroke-width:2.15000010000000019;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
6304+         id="path8079"
6305+         sodipodi:cx="155"
6306+         sodipodi:cy="296.36218"
6307+         sodipodi:rx="43"
6308+         sodipodi:ry="43"
6309+         d="m 198,296.36218 a 43,43 0 1 1 -86,0 43,43 0 1 1 86,0 z"
6310+         transform="translate(-1.5578274,10.622095)" />
6311+      <path
6312+         style="fill:#b7b7b7;fill-opacity:1;stroke:none"
6313+         d="M 20.5 0.5 C 9.4543052 0.5 0.5 9.4543052 0.5 20.5 C 0.5 31.545695 9.4543052 40.5 20.5 40.5 C 31.545695 40.5 40.5 31.545695 40.5 20.5 C 40.5 9.4543052 31.545695 0.5 20.5 0.5 z M 20.5 1 C 31.275474 1 40 9.7245258 40 20.5 C 40 31.275474 31.275474 40 20.5 40 C 9.7245258 40 1 31.275474 1 20.5 C 1 9.7245258 9.7245258 1 20.5 1 z "
6314+         transform="matrix(2.15,0,0,2.15,109.36717,262.90928)"
6315+         id="path8187" />
6316     </g>
6317     <path
6318        sodipodi:type="star"
6319addfile ./contrib/musicplayer/src/resources/images/radio_pattern.png
6320binary ./contrib/musicplayer/src/resources/images/radio_pattern.png
6321oldhex
6322*
6323newhex
6324*89504e470d0a1a0a0000000d494844520000001200000014080600000080976d4a000000017352
6325*474200aece1ce900000006624b474400ff00ff00ffa0bda793000000097048597300000b130000
6326*0b1301009a9c180000000774494d4507da080600102d6577cfd00000001974455874436f6d6d65
6327*6e74004372656174656420776974682047494d5057810e17000002ee4944415438cb7594ddab54
6328*6514c67fefd77e67f6d139e746828ee82124eac0d14e7591f779a3f8b798fd1b452444d0859fa8
6329*7840bc90e8c62282149210ba313ae1a839158274e17166f67e3fbbd8337b66985a579b772d9efd
6330*ac673d6b89c7fda7b91a793aa56129b202110170ce5114054a29628c6d495dd7586b91009dd230
6331*1e8f9770b41138e7482951140500a361d5e68510586b011a20806eb7db2601aa51680192570dc1
6332*24b09d8679f490736e41e53c03a36d9bec949a941200e36a8cd61a219b9c520a35512284d000cd
6333*a33adfd00e6ef6561405fb573b7897dab7697b524ab4d6440f5208410c69a12d31e129a5a4aaaa
6334*099b19d0b4bd29636526ad292d79b537135b69b1505857016316a73adc6b588d871e63cc4ca35e
6335*6fdf42615dd7043fd1444355550bf94e69d05ad35d3178efd1648990906223bd5686e1e8553b56
6336*10b82af2f3839fb873e73b767777b1d6b2b1b1c189131f72fcf80793a98944ce0964842c09d163
6337*adc5684bf40dd4975f7dc1471f9f6530f893fe933ebffef69067cf069c397b86cfcf9d436b8d9e
6338*37572641d220033ed428033f7cff2337767628cb158e6d6df3f7f3e740e6e8d636bf3fdae5c6ce
6339*0eefbdfb3ea2ffe8498e3192a2a0b06ac9dd9f7ef219c1670e1e3c44b9b282924d4d8c91d168c8
6340*60f007ba1048630cde450abbb843294844368c4615f7eedfe5d6ed9b186db872fd2257ae5fc418
6341*c3addb37b977ff2ede256435ae29573aad635bcbebc4b81a71e0c06b28a55b8fcd871002a534eb
6342*afaf231ef79fb6369eee4f08a1bd06df7cfd2dde47a494fc57a494e8f5f62dee9a3280683c32f5
6343*cd5b6f1fe1f2b50b5cba7a1e804b57cf2f7c5fbe7681378e1c66e93753c1cbb204e0d8f616a74e
6344*9ee4ffe2f4e9536c6ebed9b426a522a5d8582067040a2120e5885292d5b5557e79f090c1e02f5e
6345*bcf807a5246b6bab1c3abcced1773679f972af0152d2e09c6b776c3e6280fdbd2e9d6e6749e89c
6346*33c3bd8ada8d1b43c6e45b90e9499d2d30781f08614851580adbdca918137555e39c03e05f159a
6347*546925140d0a0000000049454e44ae426082
6348addfile ./contrib/musicplayer/src/resources/images/selection_background.png
6349binary ./contrib/musicplayer/src/resources/images/selection_background.png
6350oldhex
6351*
6352newhex
6353*89504e470d0a1a0a0000000d49484452000000010000001e0806000000ed9574c7000000017352
6354*474200aece1ce900000006624b474400330051009dfc02d257000000097048597300000b130000
6355*0b1301009a9c180000000774494d4507da0801163b2206c4c8530000001974455874436f6d6d65
6356*6e74004372656174656420776974682047494d5057810e170000001c4944415408d76360c0008c
6357*4c308209170b8d604665612358486201001d48007c8267aa550000000049454e44ae426082
6358addfile ./contrib/musicplayer/src/resources/images/selection_background_inverted.png
6359binary ./contrib/musicplayer/src/resources/images/selection_background_inverted.png
6360oldhex
6361*
6362newhex
6363*89504e470d0a1a0a0000000d49484452000000010000001e0806000000ed9574c7000000047342
6364*4954080808087c0864880000002a49444154089995c6b70d002010c0c0d3ff70ecbf0fa9448406
6365*17d64109b440fdd66b7dd7b1b19490ee26f64b183b722d44f40000000049454e44ae426082
6366addfile ./contrib/musicplayer/src/resources/images/song_details_background.png
6367binary ./contrib/musicplayer/src/resources/images/song_details_background.png
6368oldhex
6369*
6370newhex
6371*89504e470d0a1a0a0000000d494844520000006400000064080600000070e29554000000017352
6372*474200aece1ce900000006624b474400ff00ff00ffa0bda793000000097048597300000b130000
6373*0b1301009a9c180000000774494d4507da08060c390bfa262ca20000001974455874436f6d6d65
6374*6e74004372656174656420776974682047494d5057810e17000020004944415478da957d5b97e3
6375*bc72ddc695a4d4339f9d64e5ffffaa64c549ec75bce2e778a65b22896b1e8a550048f61c472f33
6376*dd2d512450d75dbb0aea5ffff67f6ace1900506b85520a356b2853504a81d61aff91572d40ce15
6377*d629a8e2b0871dd63acc0f8310c21f3e680095875fcdf38c6ddb0000ce4e886987d61aa594cbc7
6378*534a98a609c68cdf134280f77e78afb51629a5cb35c29ea00dfdfdbb578a155a032569d8a9a224
6379*858a0a6301e71c5eaf37bc77c367e2066897518b86750ac618e49c917386d61a4a29940c6803c4
6380*bdc24d0a3a1dcfa0a087cde00de29731e67293db1a916281520a4a03d629ba9114e1670d6db32c
6381*528ef79b116340c96abceeb6c1598fd7e78e987694529043130cbe2fa514acb5c83923ec11de4d
6382*b2f0aada8bc0f49b9142fbd95afbc7cd000063016d14ec747cb729f43b65106394cd5068eb643d
6383*6d02af0b0bbe3146d6531f6f7713bd475b6b506b85368a16d6946113d657944588a13dd0beef98
6384*1707400d1b473772f340ee66635586f306da54595896c698029e3f6881b5d6c8a57d77ce1961cb
6385*c3f72a0d84b88bd6b849e3ebf7766c864645914de47be44dd8c33adc2bdf87734de2f973acc1b5
6386*56acef805273a745055a1d1b5b1c94aea8a509523e1ec17b7fd15e7ee95c03945278bf36315980
6387*92857193420801a5142c8f593e384dc762294d6687d57fad8369c9693433fb461b7c96e094128c
6388*31a4ca9a3ed35fc7cf7af8eee5393e106f0edf57ce191f3fe763b39ae9adb522ee551638078565
6389*594833df518426e78c1847b54e01883162db36586bf1d73f7c8c82e8347225a128d8f1f96b85d2
6390*65b8ceb66d085b1693ccaff54d96446fdb06ad35acd3dd8355fa52a7e09c93dd6415dfd6382e44
6391*e703fca206bf632cfddf683f485d55493edf9b8b524a53e7ee3aa5142858596c9662ad35f6b599
6392*d87ddfa114998a5a34726a120f00259379a805d8de19c6372d9b1f4d23b4ba9a68eb016b1ce679
6393*46290521046ceb7eef1a6bc58fbf16acaf08e71c099c056dbe1e7da63106f3ec1142809ee71971
6394*af17e73dcf245dbcbb71a7dd4d29c13a8594127d89a387e49b2849c9c287bdd9cc9477e49c5191
6395*65734b21ff9352924d2f590d1b3e68d7de1e5ede5f8ad868a514b4b230da89dd377634937eb2f4
6396*b3aad0363713d8050429256c5bfbd9ea594c8ef316dbb689df9b97e9729f3955f9cee5e950b386
6397*b516a514bcdf6fba0f378d2638eef0de4393833aec713183daf3838710001d31cfb33840a3ddc5
6398*31bdbf02b4adb2917e324dba15f92896666bed2004efaf43654d3d7c1310c328497e32e4c7ea18
6399*5119ab0e5fa1506a42ca6110a6b369cc398bd96181a1c570f2f3f2f02d322b1bb67587e9ac6c8e
6400*f5306355dec72689ef87ad02fb65adb59847f677bc21b51c4e5d54546b40679452f0f5f91a4c85
6401*f71e8fc7033194ce89921ddeb72837c34e789096d8ecbbd61ad3a2c53ff537643d2dc4beb64d98
6402*667775b4de8a895cd715d65a7cfe5ee59ece9161c9a481252be49cb1bed2f1e06ef02b64f723f9
6403*b8ece47ed94cfff8f93108895f94445239676c2b0981b78b38ef5c02e25ec5e1e7747c4fbaa612
6404*f3e2db86f4365c6b4d0f7d98085ea018239c1f2fa46030cd641f6b1d9d39eff81e76b2dbce619a
6405*26b971a08a04b3a6e59c312d661006fe9e73d8edacc7b22cc839e3c7cfe5d87c35444335b7084e
6406*1bdaace5793cab2eb711d4f3f1016553f367ca0fdaa67485d5f3b0f131643c9e730b753b4d7a7e
6407*2ce2f0a10e4db1c7bf4744364f0fd1783259d50f8e2fa782b0657c3c7fc23a4d923d4d97f0366c
6408*69d4a4ce2eb687ac50ba625b23f67dc7f298a0e1619d1607cfefddfbebe5e6d42b326a3683d989
6409*4702c5f764cd04e3c6fb5326237556cbbbb933c50eafcffd22ada98c4efafd5eaf66af6c17e9e6
6410*050deb185586b889b089e085235f8b09db1a10d3e13f9481fa977ffe5b4d11b08e24b12293e33e
6411*274ab5edf0399a504a89fdbe4b20478747499ad215f3e2112399898a7c24741e4091bc055501aa
6412*25829ce572b2a73469066f063f784e80d60a4ad7dbfb58df9b84f15a6be49c074db16642083bb4
6413*c11f110be71c628c887b81367ad08ef37bfab0fdf3f70b7eb2c3b5534ad0291658d716d7180305
6414*3bdc1c65dfd7f06edf02c2ceeadd220bb6950a5622303189a692bfb04a6e725e3ce6793e32fe6e
6415*33487d25abafb5629a261116a539e9acf2409c88d53c220d691f9f476b8bf5d5b4ac64851cb564
6416*da2926d94c369defd74602024804c9cfe0268d69becff6638cc386eefb0e3fd9411824d051bab6
6417*45538524cc768eeeb8993e6b6d4ed7c35805efbdc4d8f479fa5d056144ce39d960fe97af573265
6418*fa31464a0cbf51b07ddfe4e1f81a2c403d56c521f0f2e1a04df3650c79b4244e4972592b6152c6
6419*1509cba1b368240bc0e3398b807004291b5c3d5eaf178c3197a4afe55166482b185beb9f41538e
6420*102e713e5f2447f5471364ad15fbb9aeab5c9cb37b5ec45aabe059d6da8b43978db2f719f89069
6421*478aec78c1ce593d7f678cf15b93658c414d2d9828b9caf5f99adbfbb0fbd55c42e773705354c0
6422*344dc839639e67c9ad7a8824e50014dadcafdf2bad5bd528a9e55eba97d6c7fc17e2762c4e2518
6423*c34d94f5a613606bcd3d1653921922b6d1f9cd87eaef9d796bea9a62452e4942c57e439c371291
6424*d062a5c1467318dd2f166f58298534a56a0941c51c568ae08ca51097fda1520af3e3786ed5f9ad
6425*daa2528e92580bd777681a7908eafbb50d42b0ef478e9402522073ad6dcbbd742f9db96ea8a802
6426*87b0ead54a76bf94229bf7ebd7af2147f8fabd0185c24c8233126ab283c47ebd7e5dec2627926c
6427*464a29c3a25aeb60d4240fca00272b9235e4308db617f0d2da232baf9a34e5084a18268a29484e
6428*b3bd5b3e659da660055abe873550c0c3ee3ad047aef2e3034ad7c181cb7bd8cc1f79d8b22cf073
6429*cb733842d5fdeea550e1bd1587c5197b2945d058feb2657e0e2afbf173268c46511dc5cf06da65
6430*09379d73b07a12b0ef4e83583396a7ebcc6646ae7b73b08812169319a012003bf6b38fba5b14d6
6431*7076ac35eb8b8f2c8910e2699ac8fc1c2173451a6a2efde2afdb4bd68aef45f2b12d8edf710847
6432*8a94bf8548351f3d66cb0a552571584a29c48d1e3ea524174cb140db82b01549967aa9e405a9b5
6433*625fb3c4e23d04efbd4729b47935eba126704a77b0bfbb1ce9f069facfd1f5203c8c36f4b916fb
6434*cd1008ee31561f3e1322f50c563ae7e4190648a618c1fcf8991926ea8153a514fcd4fc66cd4d33
6435*ac233036a7a320c81f94a24f75430ee2e6f1e1dfaf4d248e21f15ed2534a483b04999d1673c969
6436*d657a48550045ff435181284e63bde5f01d3a3452eda34b439ee65f409ddbde4440bc442d4071c
6437*fdfd78efc991178af25868b4d648a1228420f7d287ce314640e76193073379688536574d2a358b
6438*2f645368ec910bfdcf7ffadfb5bfc118e3457df77d1f00475af80ce72c6aad9744d218837d0bb0
6439*ce485279c699241bef369bab95db3be3f9a349742dd7042f868ce531094ea54d85511e552549f2
6440*ce255bfe396c057ed62899f2270e9559ea6b51d02ea36425e6f5efbd723acac9a75ce42ea9dcb6
6441*4d346b5f47b84833fc2d95b62e67e00dead5b294827ddfc195c6b3c47dfeda10f60ceb8c2469b9
6442*c4cb669c6dbbb5b6958e91b0ef3b396b350b7ada0396ce1bb96f6d9a7fe933ee949268511f8189
6443*669b567616a76d2b942573a574b99497098e9f644d9a8403d36c25291eb0c193805be325ffe837
6444*63001773546d616b83cdd917f082c61d783e9ff270ec5c590a7efc35c35848d67d578fe78728a5
6445*605dd761b1b4d6581e948d3be790eb4691575703bfcb2db4d69234d66c10f64c8b731483ce35fd
6446*f57dcdb94ad252d194fa8aa964d64a5bd8f77b95b0dbdb6580daa1284acca937d161acb53b2d41
6447*d2597bd4bffcf3df6a332115351b185f6ef1aaa136e13d7efffa12d898133feb9598843f259382
6448*dd2833d4a5a92219e4ba6c0e1823ea41ba9e9051b286eeea0e29253c1e8f216bbe63aef0ef7204
6449*2a46f3c58c973fbdc29e0893caf781460c19da70714b4b3533ee6508f9070de1c294360ac69701
6450*593dc3d3fcffd7e73e2c5adc2bac57e2c8f9bdbdc31d1cff21ed3937ed636ca9bf2e87bea5940b
6451*54d20703da9421ebd75a238420f079832fdcad8d370ea32f01f07a7fde6a550801decd94893326
6452*651a7b856bf229252c0fa2283d3f6659bb1823b4ad62de9ced50f2bffdcbbfd55213c256e16775
6453*811fee30ac5e527bed091b85c3b556383b2397302c600a94d0e554290c6429a91a7b58ef2b9537
6454*2f551caa8eb778d177a86c7fbd9412553c2de509825fdd3d676ce17a0a04bdf73e35e78c1c013f
6455*9b8b06dfdd530c5950875af42568d0a5b2a3531787e49c6b5ca2a34ead95432d9a40b4350cf41d
6456*63bb9ab6a21b63f3f47e6d92f53e9e3352a8709316bbcb9bc1e121e33ce7326c8c115547c96386
6457*8751eedb852da588bfb3d64269a2f1f0664c7e41adb89866de0ce71cac07a6699685d78ab4812d
6458*c3d95fbe3e8940c21ac310d01d894384ed7ffdd3dfea19c327381a97e8a624756bf7946adc2cc6
6459*fe8df6c8e52ae1decd43923592d99ac470cd9b31a1ef784c7f4a0ccfc1445f3739dfb7b516db1a
6460*61b5034c923202c328b55638e7b0efbb00886781605fd76b159bcdaa8ed4a0ab2b6def84f96187
6461*fb1a9dfa8123e5a8246b36da4269aae6d55a244258df816a18dfc4e93967a8eaa06db9e4033d5a
6462*dbab3517c8fea32f058d94f2352c4e550a5edf39f45a0811383be2699a06d4fbb65877545599e2
6463*d417f09cf552cdec37fcbb97f71edb3b239515cbf4839c7a4a89c2c6408be7670d050bad1c6dc6
6464*4a26e5e7cf9f72a1e5e1aff6af03de50b5d48e7ffddfb78486295eedbcd61a356b81c07b93790e
6465*95fbe72bf5ba196c3acfdad1b32e29f4a4da8b564e8a6c7d09226e7fd88c9c87cdc899303c542d
6466*9bd123d577848f9e7ea46d2668a7eeb4218cfb4cb3159b475c5652d569d180224c2ac731d161c6
6467*1d2d50470eb0047dc70d122ce4128417d5db6a6727285386a8c9bb452843bdbd35468bf9524a49
6468*22db87b77d11eb4cd8639fc4b078a9512225f9db57809bafecca0b9675544d65d35481c674d1e2
6469*de1ff5f7658c8186eb723a056d0db14154f5979ac700cd1f4ee9c75ff37043778e89434eef3d61
6470*615a0bb675c760e7587f3029b50891ad77f4a5944108628cd8d7d15ff4fe86cd0623d82c4c3dc5
6471*484cd341887b7cf8c1a10f9adf9108fbb05dcc2f760aeb23a30e45105fa61fc99a4620e60d3967
6472*783753e922e5889c2a0af68124dd5fa457bfcfcfcf01509be6c6143cdbc6d7d75b248103871e0d
6473*1d12ac1004662056c97e213df3427331e7fd3a1cbf33ad247ce42bfd8610da7a007e2bfdfd9c1f
6474*117473f0bd5ea37963cdbf08d25ef17812aa3098a7620758883590e11d01728fdc699a26b23e39
6475*1ffaa4ca602b733a41eaa621c0b37f5226dc3d502ef1827a9e6996dbb6113eb557a9c8f51bc361
6476*ecd7d7d72d46d6274fac31cc09ebb9c51700b3d33ae71ca685b2655eb0f33d50f168fad601f3fa
6477*f4fcaa7349b7969ea3d118270ac486e95b2b9a8f2b8709eb896a471e622c5d5470a60e081224f5
6478*78a0dece7f97c8394b52344d1394a1c4a8d62a3c2c2efe4cd384699a1a21fb58dc6d8d88a9957d
6479*cff5f8de6c728de642c3d9abdc1f9b9c7314144220bfa180143372c2c0a4e4cf0b8c130fd421d6
6480*01daefe85fa8aa23a68308248c64f4dfcd24446df5847d0b073b5b89cd541ac2433d079bec80b8
6481*24c98c75ee62b26afc5c8a7990b21823158d264a1c432400f19c04325acc68eed984ddc132cb63
6482*1a84a4210e56804796e8d7e7de88e24791ac2ad238ef2794448bd76bffe0ab6643f89d6b648b21
6483*bc0f791062aeade43c82ae2cef25039aba7c1c504ce31879731bb15005cca02483799e06dc88b3
6484*dd9f3f7f22d591ed5755123689774bcb0b7263c1f335faeb7158dd7fa6e717f7765aebe3fe5591
6485*baf5beef12a86ce145cf6272e3ebfeb5481ec2111e9bc65293483a718449faf7354b085d4ac1b4
6486*b4168992b42cae316658c75a2b96f9096b26c9b5acf1087b6ec18339fa439c75d0b6817c7d62c4
6487*f0bbc00f88c865976c9b39bd35139d68dbb64b31ab94422c8b5811e22a0e9959817d971117bc46
6488*a51c258d4c5393daed9d514a16cde18a22d15fbbc8e844e761a9564a895ff87afdbe35bbdae000
6489*0bbd709fcf9aa36d813184fcf67f9be719fb468c961ebd08711bfcdfeb93f39042b83cd7abcfbe
6490*80fb3e8c31d0cac1f9d64ac00d3f1ca170a973207f9d58e67dd8a9b5c6e3390d0cc7ef0042f25d
6491*4acc4549fa2087ab5330e104b1ae2a481797f31ac80d9f63123713e57a7f760e0ef8e7dfbf5e22
6492*40f3f4400a4a8423e702e83464ff5ca799662b75fc3eaae412722d1ad372387566522c4f877d23
6493*82d70073ab866195da88038ce99cc3586deb6dd4c58e8bffd677669d218c21e13c11ee9a93a4ea
6494*e50061706de6682d2356a596eb959a510bd9eb8ab126fefadc0154c4902fcfc4a6cc7a489b5a2e
6495*11d657417a39091cc8145c6638297d9f2a28a5e03c4152fa5c6398662b6c913f017a1ca6713da5
6496*4f2a25d1cb1832e85a2be25605057e7dadb7ce1a001e8fe512e69e0b65d4056ba14fbb99539586
6497*200e4cb4d287c988029b5cc3740fa59b0f6d707d2673bc8696001eac7967e7c1ac09a5aaab80f6
6498*91590feff49421e2252b6836151cf2a64899652e71305dd3340d344fe735bc5b089f51043cc6ad
6499*5eec2e87985a51869a0f0dab45cb83b3638f31ca02f62023fba43b01a107cb835631a1a22f114f
6500*0b099af3e4a0fb8d65d6a3b10aefaf5d4a0a9c1b3184e2ec249fe3d035a60d31e42bb3f38481dd
6501*dd7b0fba6edb06a52bb4b18a1aef6de33b4dd384c9cfd22fc88e7e0fdb6046425c1bec3e69b8f9
6502*9a81a7588fcc7b1307a7940254c6e7af4d6e8c1be97b4664ff60ac8d0249d42a9d4efcfac7fff4
6503*53decb0bd737037168ab0d4ed93c04fc7c7c4ca8590f390e43e3c6d5db760be70dfcac45f35300
6504*4a34f2fce7a10603427cbc18b8d5e7a84a6b4db0456dd5be3313be5745ebafd0778b7ea84194cb
6505*9a2900dbb6cbf57efc350ff17aadb54925cc688399b2ef74b3f1260e6dd78c16ef5b922081af53
6506*0bf5e09f2121d60c063f0120d720da66b4bbb454e444426bd4446b52a954cd655beb81545758eb
6507*601d55252f8cf8620748463aa838c17b7fed43ca5fb391d6e59ceaa5a185cc90119eeaf9e1140c
6508*e6c7a8b6d6371c8ad91d29a5a18a26bd14df340739e7e03c6157afcf5dc2dab817fcfaf72f31a7
6509*da50f2ca918d75d405a674152ad3194beb4bcddcf419e236f81bab27a4bc53a1aaeec7bd5668d3
6510*900dc1f25e5f30daa3d43408f5e417e1035fc2eb9c33522c787c4c707692ea580f345ad7d81223
6511*e8c6b67b1b6ce6be25546471da7d32c73e61794c8353a396005cdadccee62bc608ef0871fe2fff
6512*f51f24397493c6f224a6226b2f67f8e75a87d61ad64cb7a8f6344d54f78f8d38d7d35253d92f11
6513*93a004cb42c3058ee79da609b9509b020badb3f3657204af6b2905daf9363420955d40bf12f550
6514*782aa50a591a008c9a04d286aa170418a0864792683d98bc7ddf6f09093ddb84cc963ed51e624b
6515*a8d0496435434dbebfcf4110fc3244687d145792a120e060be187724c295a07d16941c157efef5
6516*448e04859c05c77a5c9a63df9f3cef45a3d43898c05aabe44c5a6b6856df0be3ceb5669d5213ac
6517*35434324412a5aa20beed7e68d39b77cf5d8148a13bbfefedabf05272bca0968540412eed74cfe
6518*f5d9d748f2c5af586b2f92d9b335532253b98796bf50d269badcc1c3b87a0c4c207fd723060c87
6519*f07373bf88f115fb16615c91a0a24709b4697e5473df5e6fb3432098a327049c0b4b216e98262b
6520*a6ed3c5cc64f06612b50d50d52b9ef3ba029a456bae21ffff3cfdb0de99986fb16254b77931aea
6521*0b3c1cc7762332b8ab3527e29a09f5c7984b74d3f7d82ba5b03c3cb635de4645eff59322a86e2d
6522*b83d42f86687cf72ce1d656ea23909f5e7f025033d2a25cc8b3fdab7d97ea5c6f125ea64954829
6523*27dc50843c6228b724ba96b1976162c15d4ef17abde4737dacde377e9eab7b83dd7e3aa9c4b582
6524*108599fc0c6798beafc3f7511c2fd63c4ff2fb5adabab07f113c2d0438330bec4f1dc45492185a
6525*c9d5689e6badf25e2e60718426357537e9b1203469bc3e37cc0f7b60fe7948908c6d9d4667bc87
6526*9d9eb576b0a771bb6244d6dadb16b64b08bd6db7704c2fa93d1c5f72f3031c304829784f62f6ac
6527*b5a8b9b510bc5f3b2da0b292649e933ca5144aa2c6566620fa598b49e292c4b6eef7d5d12d0f1a
6528*1d4220ee819ba16b053e3e3e10b7eb6c90e70f2e1c392c4f2f6130432331ed70d6b71a71aa883b
6529*0df6daba261b81f5e7f6306747c8b34ea8ebe860dd2f5e1a86e6791eea07712f627aac9950b21a
6530*e1f8834f2bd08969dc32a83234d0e4ba0959faf19c6423fe48411a209c56474a29d1d4396b65d3
6531*c5af8600ad2ca6a57d37698583714788ad14452bbd03bda0aeaa5e1c21df544cada1c558053711
6532*023b3fcc2da3502b7b4bb67b7c789963a57423dcf50d43ccad8d2910eb91dbc2f23e84b8d2edeb
6533*d425dfd0b61ee390a8cd6e5fd3d1709a2f0bcef0096bbc449547c0d12ff6beb5395a15059fbf36
6534*4c8bc5be26c9d4bdf747806405e7abd9a020ca33ea100235b2cf57ae542965301b2996a1c3aa77
6535*4c4387503fc0ece87be78d39130638f28a21c34d94d1e6541b06d6d95e9e2722e17131b7a39ccc
6536*f72e4784e9f3f35330ae6ddda14cc6bea64b7e104268d0fc645a2e63c640619a1db1e093c2be05
6537*ccd34c7d2c93968dedef81212a0e3a64ddbdf79782520f67f4619f751a5545d8e3894b6ad31858
6538*bb38576076c9184dc44be6ad34f37aabd4188c55787dc64b52796101eadcda894f84e633242409
6539*68bc36853219c33a7bf16fe73e126749b350ec21e927987e32589605ca264295b51eda27ce5ac8
6540*9bbd6f9136e89c309dede248bfd403e96179fa8b9973de4081ccd6790109ceb8da616aacf4c368
6541*0ce3f2c12f6ebd84bce8fd351823e2096fbd10f5b895444a478dfc4c58e3b0bc9fd1f5f5f585e5
6542*31218470cc4ea1c5a5fec27448fa899d980f98a46a548c83d1cebd9483c97e3cb1ef47c5f02e61
6543*ba756427ea284f47bbd04271708473231533a4c2b692216f2ee18678c02f479bc1b606aa316b2d
6544*2c79693b336de243afdde788665e28e0102d3b4ab85c25643204f1a11c4aae036ec56686478794
6545*5286b668a965c431fcd7b6c8b34b53695f08fbdaae44c4420e5f5fcc41b1171b7a0931cb7dabda
6546*59dd8d39c80f931f20158ac88a90cd942ed719bba611dffa3ec126f1f9428a70ce51f3651c49d5
6547*0293f0e03336433a52987bf81d6daea5e7699a6e43d77e1d183cecb963e237e1c574f358a6e7c7
6548*48436250775db7b621620b75ba44193d95b2667dc1a0d65718baaece0f7056d59cb320c15ce8b9
6549*f4dae96ef0cda1212c915c7e15ded6464522aaeb9c0605a8eb3c494e34e7799630f76e1c2000ec
6550*ef32e4493d6b91cd320b13a7033d182964baa086819b6c46f77da7a946c5c07b471b4259ed15ea
6551*f69ef8beebfafea31dfceb1f3f84e450f275600cff2b44b3aec429e05e199db7b15447f9fabd4b
6552*afc7c6e3f90c41e4fd004de78df46e9c43d43bb37a215d4c8ddca0b596cf975a2e7388adf1d2ae
6553*71ae9570e593c912fc9c3cdbad6432b325934f5ee62745943a53c590ed1c7f697f93afd70bcf1f
6554*13a6f95a7eec87aaf4f6b18fca7891532cd293774738e3b14e670d0b7bc2c7cf493a9094a9c3f7
6555*701bc32551531ed6fc79523575ca9646d339c63fa1128acbd12213becfb513065597c774816758
6556*832f15ce4398deefb7b464974af42009a7cf95307e30ad8d740c9deb1900f07c7edc6e48ce59b2
6557*ee3ebafafdf9efb7f6f702fc755ac2a627ec71c8b663a0b956544ff1b0c68b9336c6608fefdbe4
6558*b30f021e8f07ec21857d354f69328d3ffe9a874a26a3bd3114e20a1c265746fbed6d0e71bf3631
6559*26d1aa36c0c65c82a369a2de77dd473cc22fd55a8803fb9a063c8a8758bedfaf8b6de60c99e9fc
6560*bd83ef29fcfb460d4261ad43c4f51d34e1a756b40a2190f41f9dc3b96e4899d0024e1c852715af
6561*9bbfbe776997a84862f6183a194882de9cc2f3c621e071b32d9fe866076b25f51a6678a61c3a46
6562*660b38041b2b85386e0c310833b194566f2806d659aa1747b279f3e2878adf25f2b8997cd03fa4
6563*520ade3b842db751ab4e5d78b1bdd6f6b981f7fef063cdd1b366734dfcf131094c720e18982dd9
6564*6baab5563a86732416e3bea5639eae162d20fa50ed40492d21f4344da269b944a120dd6162bcbe
6565*4af78063b99a2cbe79b1f59a608fc773a6ee1fdd2a7e325baa2b8552e858873a3b8fee1bb3f37a
6566*19b77d479da1fb5170beb139da1fa26064ce6be4542fb895524a9cf377a16bfffb9c331e1f5ec6
6567*641863e0a7562dd4fa98501df541666824eb7ddf895103334cd3ebe949c44dce433d87b5c45a0b
6568*0573dd10630c4a4dc3054308c43d4a0db5143ed1fec2dd35d6751552c2ed428086bc50567baad5
6569*e7e60c59c2d67595613767fc8cf0263524891c9971a3ce9f9a2fcfa35d5930788a4f5fff0821a0
6570*a820802337f7f073f2bc494e07c8f1f763d1750756261168eb2910b86c0857e0cea1ea343b3923
6571*a38f9858d564164a8c836d159b1beb1065396f84caa9f4318a1c6d74f979e898730e316f17e662
6572*bf809c0b717b443f8fb11f91c4f3b3c40c2e24f135530d3fa524cccdbe5460ad3df2320763e9d9
6573*97a7450c4504884bc2463b3c9ecbd8777fc2affc6491eb489ad03da400d0a49c9aed70c3db76f4
6574*6f70d4716034db1ac581f793dc6433f7d4ceeb30d46fde9b15eefa65a6cbbabd2e7ad4a3b9bd49
6575*bb3b848573a1deec863d1d499787338bd4d93f7fbf87e7eba7110d09abad825ef0bd33e2c008b9
6576*f35ae61d135414e5e0010e665af7551593c6d16b1fa16ba33da032d2ae8e93623cfcac866ea379
6577*9e81d2fa1db4d6887bb9b41a9f435ab695564f429090f33d627b302606708da2a73958fb3d3cd3
6578*4fee69e5d77990483fb5fa4be368d1314bfd493dece7b8c7b19426f5cc2d384f9bb8f381ce4e04
6579*ffab02ad88691f4346cd16cefac3d23081e244122f0aeabfffb7ff51ad9e8481b86d1b2dca31b8
6580*58e922a3ebbe7b3109ba9f8df2fe0a43f8ab6029cc044534dfb51c706f78cd5a7a5608ef217646
6581*4906dae6c3c459941aa1aa6da4be9b090e67c883b3ecffdfd777d7664a533fb5883770df7758e3
6582*90721ccce8483874d255a0adb5031d9435823021929ccb08899cc793123a73e51c512f79087d93
6583*f524ff72cfc41942e719590cd1f4122c1535db256b07585855b3ffcc11eec3cb7e81783318609c
6584*e7f9209873899722a110c285e2d313da869a919d1b88599c68279b382e57f0738af92b0b844e00
6585*000cff49444154e256b7b87648b11d9be6991d28635c1517a3cc7063ac6e8c96f6918f36e467de
6586*af8dc63675730865e0a59dc53fccf32c91dbdd6cae3ec9ebf95fbda028530499e5a38b782cf85d
6587*55d3fb09391efec369c909b42d449ef67e98f0334a9792c0645b23720952d2c53118e7eec5dd68
6588*3d5a4e2dda559263cdf02fbf79dbb6014a28351e0d3bb4008cfce6484e99d1d2bbf8fef19ce126
6589*c26918b413c7a98e6a5fd512bdf4713bcf5dd9d70c83191f3f17843d23e6edd25054f2d14cca14
6590*cec54a1ea56bbb3f393446d3d07c9ecfd812b6e6578603634e66aa2fbbce8b1b4a007d72db17c8
6591*b8844033c42aac71425d9a1727c74b9186d4366796618edede71df9fd65a6ae26e1a9dd2b91db9
6592*7f3066be1be5874d65c2ddbed2cc299ef6f0fc38e6fc5a4b7380d58e75a503b696656921e4f17e
6593*e7a98ac760214ba0b516b1bc65d3187feaff3ece5dafe86750f69b780e2238e1ac651c6b35709c
6594*0f412453a907a665ca716030f268749d7386750a935f2ed1c3794a742d4a9a1765184c5150d509
6595*618d1785ede773f9ebc09c76c4bc0d9b2ad538771c798124f3d3b99f639a1d3e7fad973af8e097
6596*8ed1191c860ab5bfefa032b80543cfe63ac53a686c8f5e4f33f9488596702a5d259a94331b8f0d
6597*e28a6529057713a9620a17129fe6acf9ebf55b2a6e433952eb76d08a2ad2f2d6df6455f198aca6
6598*608c95711139027b7c1de4856757e432d7cc59d561d025fb85142b1e1ffe1656093b6ddcfbb562
6599*9aad84a53d9079ae78864063a5726e2c132edff6b85adbf4d620ca8841dfdde5bd6f4391f58c9c
6600*b84b6b9cec3a3db4e45e21c40b6c2348304063eb9c73d4435dc7f9baa514c9179880c0f56c8612
6601*1a9655652a27254e46e69748334cce727e159738253bf757e9b58eb2fd9e94cdf7f5fc988903b6
6602*4c8296f277f4f7c5dacaf9c6baae40d5e26bb87c9ba3be86b9c7ccc68a3c9c49c5fe8aeb4264ff
6603*8bf40e9ec90f52fdf41adebb36c514e34c7dcdaa1243c11ede2221fbbe0f0d28b4215554ed4c30
6604*383bf4f76b1fec636f0639a2f1dea3aa885af4c089921338d7eea852d7f208d6dcf3bc94fe251d
6605*beb188c670ddc55adb583549cbdf990a2a519159844ccdd11167fbec3f19e2f9fdfbf705ade601
6606*fb97b9f25921ec418ef91bd068ce489dd763376ed764c263f6420c434de46c76640655a1e84b46
6607*60a4041ec98162506b1be327dcacc51e874176cc48d3d5a6652ef002ad2c4d6948e3b1a7e7bec2
6608*7d279e6ecf389793d20e2848db727b000b00c4bc62f2cb60c6bf4b2a9d99c70108858ec653c575
6609*674152114d9b4ae8792526236359fb9aa19d7317204fa0f5ae65d77b2fa8e5bcf8cb0823862938
6610*049ce77970964c848326cc27843098273e7242e9b6a0fd201a5e906ddb28e13b0e426943edf56d
6611*757018a31e8adce3e4974b6bb4736e8816e9b49cf1fcc2de6a30b45232f55a3aafc56a284dc9e7
6612*16bfa46ce02625633fb635a26087515e4e929b96e350b0bb71b0fdfcc37e1824dfe8bec56e2250
6613*6ab13f801235aa8e97f83dc57a759a999080f3096b3250f2c86699e1311edce591729061c63db4
6614*c1838dcff7d60bc077a581efe0121ec1e12773cc9c6f1dbd2519289380e22e75781ec85cf3717c
6615*5f1714c49dd00736c15a2b7b7b63fd878c55975981fd0c14aed0891d74656c0465bbdd9d1ad023
6616*b48c79f5052896ca655928d2dbf78133d683857eb248a19e720abadeb4d8e1dc5c09679dfb360c
6617*1e42f253716d5e5c37a5a8a3843ac2b3b4cdd8b7337412a8dedf9f7125beb10cad717a685e897f
6618*a6e02fcb22095b45be8cffeb99793cfd99ff4e1255065e548c112557c195fa835cfa30b5242db3
6619*b6f873673620e3713995ae657bc4dab82e22d49cd35caeb306dd9104d77740ca74fe61df8739f4
6620*bdc3a25632993c9249e97aa126f1ba3095376c197a3c9e475d42b5811d12eb80737df77a3e9f17
6621*360787979cfd1a45bc606d1a70783eeb8f43cb54b6a1438adb08ee90d769a6795cc39cc38e1f96
6622*4217a4ec092511399a23ba3b66bf1c4ecc27d8659afca35d414e040d59bd8cb5f7038da0091813
6623*cd067377189d6e018fcee4437a3bb96fc46ee7c52b4923954d54fd3c83b78fc2be83d4bfab659c
6624*cfbab5c68f737bbf3bccf2384f248480e7f329ed6835eb4badfeeeec93b39f9ae7f9223cfda0ff
6625*c13775ddc3dcf3c1daeadd8298b6fb690f8736863ddd121f18aa92117f7cd0b09fecd05fa1ed58
6626*eeec7bf1fa1a86ba5683259aba7b31efb78f5864a3ab1ac0b9730b353f503f9d8e26005dc37181
6627*ba236478739b6d42e1298f98621f1643c2b66fc8510d7e8dbabff4e5da0a06251a941a1b3bfe44
6628*3ffa7bc7f8899feb6de0348f87b984b5625bc3304ae87c08714f30ebeb0f7cfed285442d27a199
6629*5359d4b5664c3e6a95e9a985bed39cfacffbb36d7b33c35199524a48042543ecb9754a18f9c638
6630*584d63c373dda1aa85f3163ffffa3830b636bf378602e7fc85a5530b60fa632e0e08aa3f7e494c
6631*671d05b1afb95092082792d83b9d7ddfa10c716c738912febebe36416fa7691a90ce9ede72cea0
6632*fb4ea9334ff68ebb356ee2519728fb70b03d0f10d8d6787b400be17047cfc8dc1137b622e7e056
6633*24396c789a2654453d7f31b60102fdacb0aac2407ca0402182e76c85ad1c48407b0e630fbff72e
6634*43a24ad76c74d65213f990bb834eb894da8fdb3ee701bd6d65ba0c9de76abf9d752b9276aa27ef
6635*3b4d1dd55acb6cc5f13d8a82f14442a231a160bf14b194a211e74c22e85b20f85986c357bac38f
6636*7913a9faa7859d32f4cf1fcfccc74ee49ce9704adbb4206ce5203e10cacb8adcfba5bbf51493c5
6637*adc36cc3fb632604573ac6610c5cd7a379d2688a98f86c4399aef34d3436d451f63464d5d2ebc1
6638*b456ae9d143d503619ac2ca5482f494c61609e9c0fe872761a32f494f2d0d02a76dcb4f2c2d001
6639*053a30e0f5b977a3cfdb66a4700c25f3ad29d4cdcd175e4fa35317f3ab998bd56fc220b96b53a9
6640*2163e5f362b74d16877bc9bf7e531e22f0b6b2c383f5da3898b9ea6ff2814489a41e8309a52b1d
6641*ade4bd0cc1792c4f281861cf702d823f5791a55bcc7b7f61b4084c83be83b84b0c4144ebe78f49
6642*06fe33119cf29e7a5b4b0a7b1aa6de71599ccd2f4fb6064ec75578375d262f9cd5bd472c6bede0
6643*e66f425e0ee73854ed19163d99ae27d5fd3de648c3a54e75956ce5ec0f8edcee30aebbb0980f3d
6644*1e5a26b447ca3b6d90fafbc7e7f1a4d5be1e73cbd229807334e6e986cadb6ef8d7ef7fbf2d45f2
6645*b04b8e4c88c8505bc3fcd11331460fe3e2cad1a9c766f0d10dbd7ffaae86dde71f0cee397f6d2a
6646*ad8aecb9946887b9bef71b63f58cb027397db4e5284536e37ca6096b4d7f4d42b889dbdb23cffc
6647*de7e7d8cd5b2193d8c64ad25f89d1760599601e1ed1728a5243569b6ebdb3b0e784d2daa3bdaa7
6648*7529ddf9928a225cae9e06cadfcf93e53867d8378ae1efa66dd388db233f51f9b656c23c5b8eb2
6649*d86f1a4725d873ed874fde84ba1e1e10133d6b7fb0006fcee339137f9769ac954e3ee8bbc77a93
6650*cdc971ce19db3bd286e49cdbc29e46eaf51341bf7eafe2b44b29981f6ee8542593740c99d495ce
6651*e170d71179e7f037a70a65b24cba6667aab516b8e13c80a6379384168f848473e5b3477cfdaca5
6652*44c0b44e867b7a02c4d9a73285c91823bee87c7203d393b4d6509580cd75dd64d801afeb59488d
6653*a1890eea9fffe9dfaa7659ecef772c7155dd30583ee78cc9cf420253d08d9b74810d3c620c2d2f
6654*a8d4eadc9f69658cc1ebf592c3e17b42f4dd10837e332f006905e68793c61c058398f6dba0e53b
6655*9497dbb2ef8480a59cc63fd501ce3f1f99340432c5b49e1578140499455f4093e7f4f420f5aafa
6656*fe785456b57e3304a83b9d9a708ea0d8316aa5856be5bc1569d5b6a2942a6405f253f9327bc4f9
6657*c62b3e9ffa7c3ef2c83a053f53dd7cdb36021a8ff3a88cb117f3334d93008b7ddb41ef1fcecc10
6658*c1ced0866d328963dff701516eea6586363b4e4695c9787e2c82b7e9fecbe5e89dd22627303b9d
6659*0ff9e2f7f5147aef3db45158df57803197801077a9366edb262c4422cfb5c35600c8d09ac77391
6660*9637417f0f48bc1f44d6dbf0fedc11a3fde51cda52f2c5a7fdfefd1bd36250f298a47162bbaeeb
6661*d094d30ba4316d20f31ede12ac50486d0722df99f42073eadf7138b54edf252abd6dcea98a0364
6662*08854eb55162535f072384e78e5cb46cab32677d9e674990fa0598e7e5a2817e516d86cabed318
6663*23e5871333fb737599e41642b82c006b6b2ff13ce785cb00fdc8595e833e88782c1fb229259fea
6664*31476bddf2f417b6cbd9f4c5d01893f3e338512846acebda36246e47866e3cc61a8991dff745fc
6665*3ef3e461f88384753ecb4ded149efeb4cdfeb56d6b032f73a3d9f487c91b473375a552f99c81aa
6666*2e4d351495a58bf3643496f9c16c1de4041c3de26e177f55626b49e8486e1cba1a8be184a01ef6
6667*efd7d4793b508e1428805896055a556224f201bbfb96442a69845fab4f334de67cf2f2f990b052
6668*ca0049ecfb0835d442a6902588bb9bf635d15921aa0838c7b51686d6874d5c690a02cfc0e2eb11
6669*726d07de179fa39e7386f56a001a870438e0b605a31fea793685d3dc4656f148f59ebf0600cfc7
6670*c7f03df3c3b4b3460ef0b19482ff0732ec26a210639c300000000049454e44ae426082
6671hunk ./contrib/musicplayer/src/services/albumCover.js 8
6672     Artists = da.db.DocumentTemplate.Artist.view();
6673 
6674 function fetchAlbumCover(search_params, album, callback) {
6675+  console.log("Fetching album covers", album.doc.title);
6676   lastfm.album.getInfo(search_params, {
6677     success: function (data) {
6678       var urls = data.album.image ? data.album.image : null,
6679hunk ./contrib/musicplayer/src/services/albumCover.js 42
6680       if(!callback)
6681         return;
6682       
6683-      callback([
6684+      var urls = [
6685         "resources/images/album_cover_0.png",
6686         "resources/images/album_cover_1.png",
6687         "resources/images/album_cover_2.png",
6688hunk ./contrib/musicplayer/src/services/albumCover.js 47
6689         "resources/images/album_cover_3.png"
6690-      ]);
6691+      ];
6692       
6693hunk ./contrib/musicplayer/src/services/albumCover.js 49
6694+      album.update({album_cover_urls: urls});
6695+      if(callback)
6696+        callback(urls);
6697       delete callback;
6698     }
6699   });
6700hunk ./contrib/musicplayer/src/services/albumCover.js 58
6701 }
6702 
6703 /**
6704- *  da.service.albumCover(song[, callback]) -> undefined
6705- *  - song (da.db.DocumentTemplate): song whose album art needs to be fetched
6706+ *  da.service.albumCover(album[, callback]) -> undefined
6707+ *  - album (da.db.DocumentTemplate.Album): album whose album art needs to be fetched
6708  *  - callback (Function): called once album cover is fetched, with first
6709  *    argument being an array of four URLs.
6710  * 
6711hunk ./contrib/musicplayer/src/workers/indexer.js 59
6712 onmessage = function (event) { 
6713   queue.push(event.data);
6714   
6715-  if(queue.length < 3)
6716+  if(queue.length === 1)
6717     getTags(event.data);
6718 };
6719 
6720hunk ./contrib/musicplayer/src/workers/indexer.js 84
6721     },
6722     
6723     onFailure: function (calledBy) {
6724-      console.log("Failed to parse tags for", cap);
6725+      console.log("Failed to parse ID3 tags for", [cap, calledBy]);
6726       parser.destroy();
6727       delete parser;
6728       
6729hunk ./contrib/musicplayer/src/workers/scanner.js 13
6730     document = {},
6731     queue = [];
6732 
6733+var console = {
6734+  log: function (msg, obj) {
6735+    postMessage({debug: true, msg: msg, obj: obj});
6736+  }
6737+};
6738+
6739 this.da = {};
6740 
6741 //#require "libs/vendor/mootools-1.2.4-core-server.js"
6742hunk ./contrib/musicplayer/src/workers/scanner.js 36
6743  * 
6744  **/
6745 function scan (obj) {
6746-  obj.get(function () {
6747-    queue.erase(obj.uri);
6748-   
6749+  console.log("Inspecting cap", obj.uri);
6750
6751+  obj.get(function () { 
6752     if(obj.type === "filenode") {
6753       postMessage(obj.uri);
6754hunk ./contrib/musicplayer/src/workers/scanner.js 41
6755+      queue.erase(obj.uri);
6756       checkQueue();
6757       return;
6758     }
6759hunk ./contrib/musicplayer/src/workers/scanner.js 46
6760     
6761-    var n = obj.children.length;
6762+    var n = obj.children.length,
6763+        child;
6764     while(n--) {
6765hunk ./contrib/musicplayer/src/workers/scanner.js 49
6766-      var child = obj.children[n];
6767+      child = obj.children[n];
6768       
6769       if(child.type === "filenode")
6770         postMessage(child.ro_uri);
6771hunk ./contrib/musicplayer/src/workers/scanner.js 54
6772       else
6773-        scan(child);
6774+        queue.push(child.ro_uri);
6775     }
6776     
6777hunk ./contrib/musicplayer/src/workers/scanner.js 57
6778+    queue.erase(obj.uri);
6779     checkQueue();
6780   });
6781 }
6782hunk ./contrib/musicplayer/src/workers/scanner.js 78
6783 onmessage = function (event) {
6784   queue.push(event.data);
6785   
6786-  if(queue.length < 2)
6787+  if(queue.length === 1)
6788     scan(new TahoeObject(event.data));
6789 };
6790hunk ./contrib/musicplayer/tests/data/songs.js 11
6791       album: "Found Songs",
6792       track: 7,
6793       year:  2009,
6794-      genre: 0,
6795-      lyrics: "",
6796+      genre: -1,
6797       links: {
6798         official: ""
6799       }
6800hunk ./contrib/musicplayer/tests/data/songs.js 35
6801       album: "Unknown",
6802       track: 0,
6803       year:  0,
6804-      genre: 0,
6805-      lyrics: "",
6806+      genre: "Rock",
6807       links: {
6808         official: ""
6809       }
6810hunk ./contrib/musicplayer/tests/data/songs.js 43
6811     frames: {
6812       TIT2: "Death Will Never Conquer",
6813       TPE1: "Coldplay",
6814-      TCON: 0
6815+      TCON: "Rock"
6816     }
6817   },
6818   
6819hunk ./contrib/musicplayer/tests/data/songs.js 57
6820       track: 6,
6821       year: 2010,
6822       genre: 52, // Electornic,
6823-      lyrics: "",
6824       links: {
6825         official: "http://delphic.cc"
6826       }
6827hunk ./contrib/musicplayer/tests/initialize.js 29
6828   'test_NavigationController',
6829   'test_SettingsController',
6830   'test_PlayerController',
6831-  'test_SongContextController'
6832+  'test_SongContextController',
6833+  'test_SearchController',
6834+  'test_PlaylistController'
6835 ]);
6836hunk ./contrib/musicplayer/tests/shared.js 65
6837   }
6838 };
6839 
6840+// verifies that elements from `a` are in `b`, as well as on same positions
6841+// jum.assertEqualArrays([1, 2, 3], [1, 2, 3]): pass
6842+// jum.assertEqualArrays([1, 2, 3], [3, 1, 2]): fail
6843 jum.assertEqualArrays = function (a, b) {
6844   jum.assertEquals("arrays should have same length", a.length, b.length);
6845   
6846hunk ./contrib/musicplayer/tests/shared.js 86
6847   return true;
6848 };
6849 
6850+// verifies that elements from `a` are in `b`, without comparing their positions
6851+// or number of occurance.
6852+// jum.assertEqualArrays([1, 2, 3], [1, 2, 3]): pass
6853+// jum.assertEqualArrays([1, 2, 3], [3, 1, 2]): pass
6854 jum.assertSameArrays = function (a, b) {
6855   jum.assertEquals("arrays should have same length", a.length, b.length);
6856   
6857hunk ./contrib/musicplayer/tests/test_PlayerController.js 85
6858     );
6859   };
6860   
6861-  this.test_queue = function () {
6862-    jum.assertTrue("there shouldn't be anything left in the playlist",
6863-      !Player.getNext()
6864+  this.test_turnShuffleOn = function () {
6865+    this.tbs_playlist = [self.songs[0].id, 2, 3, 4, 5];
6866+    Player.play(self.songs[0]);
6867+    Player.setPlaylist(this.tbs_playlist);
6868+    Player.setPlayMode("shuffle");
6869+   
6870+    jum.assertNotEquals("Playlist should be shuffled",
6871+      Player.getPlaylist(), this.tbs_playlist
6872+    );
6873+   
6874+    jum.assertTrue("Next item is being chosen from the right playlist",
6875+      this.tbs_playlist.slice(1).contains(Player.getNext())
6876     );
6877hunk ./contrib/musicplayer/tests/test_PlayerController.js 98
6878+  };
6879
6880+  this.test_turnShuffleOff = function () {
6881+    Player.setPlayMode("normal");
6882     
6883hunk ./contrib/musicplayer/tests/test_PlayerController.js 103
6884+    jum.assertEquals("Non-shuffled playlist should be restored",
6885+      this.tbs_playlist, Player.getPlaylist()
6886+    );
6887+  };
6888
6889+  this.test_queue = function () {
6890     Player.queue(self.songs[0].id);
6891     
6892     jum.assertEquals("next song should be from queue",
6893hunk ./contrib/musicplayer/tests/test_PlayerController.js 116
6894       Player.getNext()
6895     );
6896     
6897-    Player.setPlaylist([self.songs[2].id]);
6898-    jum.assertEquals("next song should be from queue, ignoring the playlist",
6899+    Player.setPlaylist([]);
6900+    jum.assertEquals("next song should be from queue, ignoring the fact that playlist is empty",
6901       self.songs[0].id,
6902       Player.getNext()
6903     );
6904addfile ./contrib/musicplayer/tests/test_PlaylistController.js
6905hunk ./contrib/musicplayer/tests/test_PlaylistController.js 1
6906+var test_PlaylistController = new function () {
6907+  var PlaylistController  = da.controller.Playlist,
6908+      Playlist            = da.db.DocumentTemplate.Playlist,
6909+      Song                = da.db.DocumentTemplate.Song,
6910+      self                = this;
6911+     
6912+  this.setup = function () {
6913+    this.songs = $A(Song.view().rows).map(function (x) { return x.id });
6914+   
6915+    Playlist.create({
6916+      id:           "PLA",
6917+      title:        "Playlist A",
6918+      description:  "DA",
6919+      song_ids:     [this.songs[0], this.songs[1], this.songs[2]]
6920+    }, function (pl) { self.A = pl });
6921+   
6922+    Playlist.create({
6923+      id:           "PLB",
6924+      title:        "Playlist B",
6925+      description:  "DB",
6926+      song_ids:     [this.songs[2], this.songs[0]]
6927+    }, function (pl) { self.B = pl });
6928+   
6929+    Playlist.create({
6930+      id:           "PLC",
6931+      title:        "Playlist C",
6932+      description:  "DC",
6933+      song_ids:     [this.songs[1]]
6934+    }, function (pl) { self.pl_a = pl });
6935+  };
6936
6937+  this.test_addToExistingPlaylist = function () {
6938+    PlaylistController.addSong(Song.findById(this.songs[2]));
6939+   
6940+    jum.assertTrue("playlist chooser dialog should be visible",
6941+      $("add_to_pl_dialog").style.display !== "none"
6942+    );
6943+   
6944+    var playlist_ids = $("playlist_selector").getElements("option").map(function (x) {
6945+      return x.value;
6946+    });
6947+    jum.assertSameArrays(["PLA", "PLB", "PLC", "_new_playlist"], playlist_ids);
6948+   
6949+    $("playlist_selector").value = "PLC";
6950+   
6951+    $("add_to_pl_dialog").fireEvent("submit");
6952+    jum.assertFalse("dialog shouldn't be visible anymore",
6953+      !!$("add_to_pl_dialog")
6954+    );
6955+   
6956+    var plc = Playlist.findById("PLC");
6957+    jum.assertEqualArrays(plc.get("song_ids"), [this.songs[1], this.songs[2]]);
6958+  };
6959
6960+  this.test_addToNewPlaylist = function () {
6961+    PlaylistController.addSong(Song.findById(this.songs[1]));
6962+   
6963+    jum.assertTrue("playlist chooser dialog should be visible",
6964+      $("add_to_pl_dialog").style.display !== "none"
6965+    );
6966+   
6967+    $("playlist_selector").value = "_new_playlist";
6968+   
6969+    jum.assertTrue("form for creating new playlists should be visible",
6970+      $("add_to_new_pl").style.display !== "none"
6971+    );
6972+   
6973+    $("add_to_new_pl_title").value = "Playlist D";
6974+    $("add_to_new_pl_description").value = "DD";
6975+   
6976+    $("add_to_pl_dialog").fireEvent("submit");
6977+   
6978+    jum.assertFalse("dialog shouldn't be visible anymore",
6979+      !!$("add_to_pl_dialog")
6980+    );
6981+   
6982+    Playlist.find({
6983+      properties: {
6984+        title:       "Playlist D"
6985+      },
6986+      onSuccess: function (docs) {
6987+        self.D = docs[0];
6988+      }
6989+    });
6990+  };
6991
6992+  this.test_waitForNewPlaylist = {
6993+    method: "waits.forJS",
6994+    params: {js: function () { return Playlist.view().rows.length === 4 }}
6995+  };
6996
6997+  this.test_verfiyNewPlaylist = function () {
6998+    jum.assertEqualArrays(self.D.get("song_ids"), [self.songs[1]]);
6999+  };
7000
7001+  this.test_playlistEditor = function () {
7002+    PlaylistController.edit(self.B);
7003+   
7004+    jum.assertEquals("playlist editor should be visible",
7005+      1, $$(".playlist_editor").length
7006+    );
7007+   
7008+    var rendered_ids = $$("#playlist_songs li").map(function (x) {
7009+      return x.id.split("playlist_song_")[1]
7010+    });
7011+   
7012+    jum.assertEqualArrays(self.B.get("song_ids"), rendered_ids);
7013+    jum.assertEquals("playlist's title should be correct",
7014+      self.B.get("title"), $("playlist_title").value
7015+    );
7016+    jum.assertEquals("playlist's description should be correct",
7017+      self.B.get("description"), $("playlist_description").value
7018+    );
7019+  };
7020
7021+  this.test_playlistEditorSearchDialog = function () {
7022+    $("playlist_add_more_songs").fireEvent("click");
7023+    $("search_field").value = Song.findById(self.songs[1]).get("title");
7024+    $("search_field").fireEvent("keyup");
7025+  };
7026
7027+  this.test_waitForSearchResults = {
7028+    method: "waits.forJS",
7029+    params: {
7030+      js: function () { return !!$$("#search_results_column .column_item") }
7031+    }
7032+  };
7033
7034+  this.test_clickResultItem = function () {
7035+    // clicking the first (and only) search result
7036+    $("search_results_column").fireEvent("click:relay(.column_item)", [
7037+      {id: self.songs[1]},
7038+      $$("#search_results_column .column_item")[0]
7039+    ]);
7040+  };
7041
7042+  this.test_waitForSongToBeAddedToThePlaylist = {
7043+    method: "waits.forJS",
7044+    params: {
7045+      js: function () { return $$("#playlist_songs li").length === 3 }
7046+    }
7047+  };
7048
7049+  this.test_verifySearchDialogClick = function () {
7050+    var rendered_ids = $$("#playlist_songs li").map(function (x) {
7051+      return x.id.split("playlist_song_")[1]
7052+    });
7053+   
7054+    jum.assertEqualArrays([self.songs[2], self.songs[0], self.songs[1]], rendered_ids);
7055+  };
7056
7057+  this.test_removingAnSongFromPlaylist = function () {
7058+    var delete_icon = $("playlist_song_" + self.songs[0]).getElement(".action");
7059+    $("playlist_songs").fireEvent("click:relay(.action)", [null, delete_icon]);
7060+  };
7061
7062+  this.test_verifyRemovingAnSongFromPlaylist = {
7063+    method: "waits.forJS",
7064+    params: {
7065+      js: function () { return !$("playlist_song_" + self.songs[0]) }
7066+    }
7067+  };
7068
7069+  this.test_playlistEditorSave = function () {
7070+    $("playlist_save").fireEvent("click");
7071+  };
7072
7073+  this.test_waitForPlaylistToBeSaved = {
7074+    method: "waits.forJS",
7075+    params: {
7076+      js: function () { return Playlist.findById(self.B.id).get("song_ids").contains(self.songs[1]) }
7077+    }
7078+  };
7079+
7080+  this.test_verifySave = function () { 
7081+    jum.assertEqualArrays([self.songs[2], self.songs[1]], Playlist.findById(self.B.id).get("song_ids"));
7082+  };
7083+};
7084addfile ./contrib/musicplayer/tests/test_SearchController.js
7085hunk ./contrib/musicplayer/tests/test_SearchController.js 1
7086+var test_SearchController = new function () {
7087+  var Search = da.controller.Search,
7088+      self = this;
7089
7090+  this.setup = function () {
7091+    self._searches = 0;
7092+    self.results = {};
7093+   
7094+    Search.search("bang", ["Song"], {onComplete: function (results) {
7095+      self._searches++;
7096+      self.results.bang_song = results;
7097+    }});
7098+    Search.search("sunshine", ["Album"], {onComplete: function (results) {
7099+      self._searches++;
7100+      self.results.sunshine_album = results;
7101+    }});
7102+    Search.search("super", ["Artist"], {onComplete: function (results) {
7103+      self._searches++;
7104+      self.results.super_artist = results;
7105+    }});
7106+    Search.search("marina", ["Artist"], {onComplete: function (results) {
7107+      self._searches++;
7108+      self.results.marina_artist = results;
7109+    }});
7110+   
7111+    Search.search("super", ["Song", "Artist"], {onComplete: function (results) {
7112+      self._searches++;
7113+      self.results.super_song_artist = results;
7114+    }});
7115+    Search.search("sunshine", ["Artist", "Album"], {onComplete: function (results) {
7116+      self._searches++;
7117+      self.results.sunshine_artist_album = results;
7118+    }});
7119+    Search.search("bang", ["Album", "Artist"], {onComplete: function (results) {
7120+      self._searches++;
7121+      self.results.bang_album_artist = results;
7122+    }});
7123+    Search.search("super", ["Artist", "Song"], {onComplete: function (results) {
7124+      self._searches++;
7125+      self.results.super_artist_song = results;
7126+    }});
7127+   
7128+    Search.search("maps", ["Song", "Album", "Artist"], {onComplete: function (results) {
7129+      self._searches++;
7130+      self.results.maps_song_album_artist = results;
7131+    }});
7132+    Search.search("keane", ["Album", "Artist", "Song"], {onComplete: function (results) {
7133+      self._searches++;
7134+      self.results.keane_album_artist_song = results;
7135+    }});
7136+    Search.search("symmetry", ["Artist", "Song", "Album"], {onComplete: function (results) {
7137+      self._searches++;
7138+      self.results.symmetry_artist_song_album = results;
7139+    }});
7140+  };
7141
7142+  this.test_waitForResuls = {
7143+    method: "waits.forJS",
7144+    params: {js: function () { return self._searches === 11 }}
7145+  };
7146+
7147+  this.test_oneFilter = function () {
7148+    var r = self.results.bang_song;
7149+    jum.assertEquals("Search for 'bang' should result with only one song",
7150+      1, r.length
7151+    );
7152+    jum.assertEquals("Found song should have been 'Hey Big Bang'",
7153+      "Hey Big Bang", r[0].key
7154+    );
7155+   
7156+    r = self.results.sunshine_album;
7157+    jum.assertEquals("Search for 'sunshine' should result with only one song",
7158+      1, r.length
7159+    );
7160+    jum.assertEquals("Found song should have been 'Maps'",
7161+      "Maps", r[0].key
7162+    );
7163+   
7164+    r = self.results.super_artist;
7165+    jum.assertEquals("Search for 'super' should result with two songs",
7166+      2, r.length
7167+    );
7168+    // search results are sorted by song's title
7169+    jum.assertEquals("First song should be 'Persona'",
7170+      "Persona", r[0].key
7171+    );
7172+    jum.assertEquals("Second song shuold be 'Hey Big Bang'",
7173+      "Hey Big Bang", r[1].key
7174+    );
7175+   
7176+    r = self.results.marina_artist;
7177+    jum.assertEquals("There should be no results for non-existing artist",
7178+      0, r.length
7179+    );
7180+  };
7181
7182+  this.test_twoFilters = function () {
7183+    var r = self.results.sunshine_artist_album;
7184+    jum.assertEquals("Search for 'sunshine' with 'Artist' and 'Album' filters should give one result",
7185+      1, r.length
7186+    );
7187+    jum.assertEquals("The result should be 'Maps'",
7188+      "Maps", r[0].key
7189+    );
7190+   
7191+    r = self.results.bang_album_artist;
7192+    jum.assertEquals("Search for 'bang' with 'Artist' and 'Album' filters should give no results",
7193+      0, r.length
7194+    );
7195+  };
7196
7197+  this.test_reversedFilterNames = function () {
7198+    var r = self.results.super_song_artist;
7199+    jum.assertEquals("Search for 'super' with 'Song' and 'Artist' filters should give two songs",
7200+      2, r.length
7201+    );
7202+    jum.assertEquals("The first song should be 'Persona'",
7203+      "Persona", r[0].key
7204+    );
7205+    jum.assertEquals("The scond song should be 'Hey Big Bang'",
7206+      "Hey Big Bang", r[1].key
7207+    );
7208+   
7209+    r = self.results.super_artist_song;
7210+    jum.assertEquals("Search for 'super' with 'Song' and 'Artist' filters should give two songs",
7211+      2, r.length
7212+    );
7213+    jum.assertEquals("The first song should be 'Persona'",
7214+      "Persona", r[0].key
7215+    );
7216+    jum.assertEquals("The scond song should be 'Hey Big Bang'",
7217+      "Hey Big Bang", r[1].key
7218+    );
7219+  };
7220
7221+  this.test_threeFilters = function () {
7222+    var r = self.results.maps_song_album_artist;
7223+    jum.assertEquals("Search for 'maps' with 'Song', 'Album' and 'Artist' filters should give one result",
7224+      1, r.length
7225+    );
7226+    jum.assertEquals("The first song should be 'Maps'",
7227+      "Maps", r[0].key
7228+    );
7229+   
7230+    r = self.results.keane_album_artist_song;
7231+    jum.assertEquals("Search for 'keane' with 'Album', 'Artist' and 'Song' filters should give one result",
7232+      1, r.length
7233+    );
7234+    jum.assertEquals("The first song should be 'Maps'",
7235+      "Maps", r[0].key
7236+    );
7237+   
7238+    r = self.results.symmetry_artist_song_album;
7239+    jum.assertEquals("Search for 'symmetry' with 'Artist', 'Song' and 'Album' filters shoudl give no results",
7240+      0, r.length
7241+    );
7242+  };
7243
7244+  this.teardown = function () {
7245+    $("search_dialog").parentNode.parentNode.hide();
7246+  }
7247+};
7248hunk ./setup.py 141
7249     # Mock - Mocking and Testing Library
7250     # http://www.voidspace.org.uk/python/mock/
7251     "mock",
7252+    # Windmill - Testing framework for web apps, see #1001
7253+    # http://getwindmill.com/, http://github.com/windmill/
7254+    "windmill",
7255     ]
7256 
7257 class ShowSupportLib(Command):
7258hunk ./src/allmydata/test/test_musicplayer.py 69
7259   def _set_up_tree(self):
7260     self.settings['JAVASCRIPT_TEST_DIR'] = '../contrib/musicplayer/tests'
7261     self.settings['SCRIPT_APPEND_ONLY'] = True
7262-   
7263     self.browser_debugging = True
7264hunk ./src/allmydata/test/test_musicplayer.py 70
7265+   
7266     self.test_url = 'static/musicplayer/index_devel.html'
7267     
7268     shutil.copytree('../contrib/musicplayer/src', self.public_html_path + '/musicplayer')
7269hunk ./src/allmydata/test/test_musicplayer.py 108
7270 
7271     return d
7272 
7273-#class ChromeTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Chrome):
7274-#  pass
7275-
7276-class FirefoxTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Firefox):
7277+class ChromeTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Chrome):
7278   pass
7279 
7280hunk ./src/allmydata/test/test_musicplayer.py 111
7281+#class FirefoxTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Firefox):
7282+#  pass
7283+
7284hunk ./src/allmydata/test/tilting.py 34
7285     return d
7286   
7287   def _set_up_windmill(self):
7288-    self.browser_debugging = False
7289+    self.browser_debugging = False
7290     self.browser_name = 'firefox'
7291     self.test_url = '/'
7292     self._js_test_details = []
7293}
7294
7295Context:
7296
7297[added-songcontext-and-services
7298josip.lisec@gmail.com**20100731231316
7299 Ignore-this: e8cd467b7d5681d1b9fbf9decb8b1f4b
7300 * Added da.controller.SongContext with default contexts:
7301   * 'Artist' - shows basic information about the artist
7302     the currently playing song,
7303   * 'Recommendations' - shows similar artists and songs,
7304   * 'Music Videos' - presents search results from YouTube
7305     of currently playing song.
7306 * Added da.service.* APIs which are used by contexts above
7307   * da.service.lastFm - interface to the Last.fm services
7308   * da.service.artistInfo
7309   * da.service.recommendations
7310   * da.service.musicVideo
7311 * UTF-8 related fixes to ID3v2 parser
7312] 
7313[updated-docs
7314josip.lisec@gmail.com**20100731231211
7315 Ignore-this: cbfb98d6ebeed2f2826250eb43d912ff
7316 * Added NOTES file with instructions for running the tests.
7317 * Fixed typos in INSTALL
7318] 
7319[add-player-controls
7320josiplisec@gmail.com**20100728131919
7321 Ignore-this: 56f8d094357df285a679a04bf536c582
7322 * Added player controls (previos/play/next) with tests.
7323 * Made other, smaller, improvements to da.ui.NavigationColumn (smarter re-rendering)
7324 * Limited both scanner and indexer workers to allow only one request to Tahoe-LAFS,
7325   thus making network I/O almoast synchronous, mainly due to the fact that
7326   Tahoe never completes requests under high load.
7327 
7328] 
7329[add-controller-tests
7330josip.lisec@gmail.com**20100725094652
7331 Ignore-this: 506444f1ed082b7fd7caca82c1a2129f
7332 Added tests for:
7333   * da.ui.Dialog
7334   * da.controller.CollectionScanner
7335   * da.controller.Settings
7336] 
7337[player-interface
7338josip.lisec@gmail.com**20100719121357
7339 Ignore-this: b7287f386bcbfeb6e59b8686da0ec65c
7340  * Fixed bugs in (Segmented)ProgressBar
7341  * Added basic player controls
7342  * Added album cover fetcing via Last.fm
7343  * Reorganised the way external libraries are being imported
7344  * Fixed tests
7345] 
7346[add-progress-bars
7347josip.lisec@gmail.com**20100713181106
7348 Ignore-this: b96a74ab38924967a55383f75f888439
7349 Added da.ui.ProgressBar and da.ui.SegmentedProgressBar classes,
7350 the latter one will be used mainly for visualizing progress of
7351 currently playing song and load percentage.
7352] 
7353[add-navigation-and-settings-tests
7354josip.lisec@gmail.com**20100712142621
7355 Ignore-this: 559424eabbd88c496d69d076f2fcd5de
7356 Added tests for da.controller.Navigation, da.controller.Settings and
7357 da.controller.CollectionScanner.
7358] 
7359[add-music-players-full-deps
7360josip.lisec@gmail.com**20100710190714
7361 Ignore-this: 59d7b3890a1f9349bc8ac511af05b2b8
7362] 
7363[add-music-players-core-deps
7364josip.lisec@gmail.com**20100710185908
7365 Ignore-this: 58f5546ff75501f77d6b346ae99eebec
7366] 
7367[add-music-player
7368josip.lisec@gmail.com**20100710185704
7369 Ignore-this: c29dc0709640abd2e33cfd119b2681f
7370] 
7371[misc/build_helpers/run-with-pythonpath.py: fix stale comment, and remove 'trial' example that is not the right way to run trial.
7372david-sarah@jacaranda.org**20100726225729
7373 Ignore-this: a61f55557ad69a1633bfb2b8172cce97
7374] 
7375[docs/specifications/dirnodes.txt: 'mesh'->'grid'.
7376david-sarah@jacaranda.org**20100723061616
7377 Ignore-this: 887bcf921ef00afba8e05e9239035bca
7378] 
7379[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'.
7380david-sarah@jacaranda.org**20100723054703
7381 Ignore-this: f3b98183e7d0a0f391225b8b93ac6c37
7382] 
7383[docs: use current cap to Zooko's wiki page in example text
7384zooko@zooko.com**20100721010543
7385 Ignore-this: 4f36f36758f9fdbaf9eb73eac23b6652
7386 fixes #1134
7387] 
7388[__init__.py: silence DeprecationWarning about BaseException.message globally. fixes #1129
7389david-sarah@jacaranda.org**20100720011939
7390 Ignore-this: 38808986ba79cb2786b010504a22f89
7391] 
7392[test_runner: test that 'tahoe --version' outputs no noise (e.g. DeprecationWarnings).
7393david-sarah@jacaranda.org**20100720011345
7394 Ignore-this: dd358b7b2e5d57282cbe133e8069702e
7395] 
7396[TAG allmydata-tahoe-1.7.1
7397zooko@zooko.com**20100719131352
7398 Ignore-this: 6942056548433dc653a746703819ad8c
7399] 
7400Patch bundle hash:
7401e420382ab010a68698dc21731d531d378fa42622