1 patch for repository /home/josip/bin/tahoe-tmp: Sat Aug 14 19:29:19 CEST 2010 josip.lisec@gmail.com * updates14082010 * Implemented search interface * Implemented playlist manager * Implemented playlist exporters (XSPF, PLS, M3U) * Separated 'main menu' into two menus * Fixed possible issues with da.ui.Column's performance * More UTF-8 related fixes to da.util.ID3v2Parser New patches: [updates14082010 josip.lisec@gmail.com**20100814172919 Ignore-this: 4a70a398132d7b306c0b29d5ed36fe54 * Implemented search interface * Implemented playlist manager * Implemented playlist exporters (XSPF, PLS, M3U) * Separated 'main menu' into two menus * Fixed possible issues with da.ui.Column's performance * More UTF-8 related fixes to da.util.ID3v2Parser ] { hunk ./contrib/musicplayer/INSTALL 1 -=== Installing Music Player for Tahoe (codename 'Daaw') === += Installing Music Player for Tahoe (codename 'Daaw') = == Maths and Systems Theory quiz == If you already have a 'build' directory, feel free to skip this step. hunk ./contrib/musicplayer/INSTALL 14 and computing related courses. Just in case you haven't, you can type in next line into your shell: - $ python manage.py build - running build + $ python manage.py roll + running roll Calculating dependencies... Compressing ... ... hunk ./contrib/musicplayer/INSTALL 19 - Done! + You're ready to rock 'n' roll! Bravo, you're done! (just make sure you have a 'build' directory) hunk ./contrib/musicplayer/INSTALL 29 == Battle for the Configuration File == Player's configuration file is a real beast on its own, and in order to edit it we must prepare ourselves really good, -otherwise, we're doomed (actually, only you are )! +otherwise, we're doomed (actually, only you are)! Read next few steps carefully, the beast is just around the corner! hunk ./contrib/musicplayer/INSTALL 33 -1. Create two dirnodes on your Tahoe-LAFS server, one will be used for storing +1. Create two dirnodes on your Tahoe-LAFS server, one which will be used for storing all your music files and the other one for syncing settings between multiple computers. hunk ./contrib/musicplayer/INSTALL 45 (make sure Tahoe-LAFS is running on your computer before issuing those commands) + Pro tip: create a (S)FTP account with your music directory as it's home directory + and upload your music files in batches without a sweat. 2. Take a big breath, as we're about to open example configuration file! hunk ./contrib/musicplayer/INSTALL 54 Now quickly, we have to replace her evil genes with a good ones, find following line in her DNA sequence: - "music_cap": "", + "music_cap": "", "settings_cap": "" and quickly replace with as well as hunk ./contrib/musicplayer/INSTALL 66 knows how to re-sequence DNA of living beings, and we don't want others to find out about that and use it in evil purposes, don't we?) - Now save the new genes under name of 'config.json'. + Now save the new genes under the name of 'config.json'. == The Critical Step == After we've conquered the beast of configuration file we're ready to hunk ./contrib/musicplayer/INSTALL 70 -upload the player to the Tahoe! +upload the player to the Tahoe-LAFS! To do that, just copy the 'build' directory to 'public_html' directory of your Tahoe storage node (usually ~/.tahoe). hunk ./contrib/musicplayer/INSTALL 74 -Note that 'public_html' directory is probably missing, -so you'll have to create it on your own. +Note that 'public_html' directory is probably missing, so you'll have to create it on your own. hunk ./contrib/musicplayer/INSTALL 76 +If you are on a UNIX-like operating system, you can do it with following command: $ mkdir -p ~/.tahoe/public_html/musicplayer hunk ./contrib/musicplayer/INSTALL 78 - $ cp -r build/ ~/.tahoe/public_html/musicplayer + $ cp -r build/ ~/.tahoe/public_html/musicplayer/ + + Pro tip: instead of copying whole 'build' directory, make a symbolic link + so that 'installing' a new version will be a breeze. + +or if you're using a Windows machine with Command prompt + $ mkdir %HOMEDRIVE%%HOMEPATH%\.tahoe\public_html\musicplayer + $ xcopy /S build\ %HOMEDRIVE%%HOMEPATH%\.tahoe\public_html\musicplayer\ + +(drag and drop also works) WARNING: If you don't perform next step exactly as you're instructed, the whole process could fail and you'll hunk ./contrib/musicplayer/INSTALL 99 == Fin == You can now upload your music to the dirnode and -launch music player by typing this URL into your web browser: +launch music player by typing this URI into your web browser: http://localhost:3456/static/musicplayer If it appears that something isn't working, it probably means hunk ./contrib/musicplayer/INSTALL 103 -that you haven't read 'The Critical Step' carefully enough. +that you haven't read 'The Critical Step' carefully enough, but +if you're sure you did it as instructed, please report all bugs you encounter +on the following address: + http://tahoe-lafs.org/trac/tahoe-lafs/ticket/1023 +or ask around for josipl on tahoe-lafs IRC channel (irc.freenode.net). We hope you're going to enjoy your music even more with Music Player for Tahoe-LAFS! hunk ./contrib/musicplayer/INSTALL 110 + +Note: During the initial collection scan (or any other), it's suggested to +turn off Firebug or Web Inspector's XMLHttpRequest logging feature, for sake of performance. hunk ./contrib/musicplayer/NOTES 53 following into Web Inspector's or Firebugs' console: `windmill.jsTests.testFailures` -* === Documentation === Player's code is fully annotated with PDoc[2] syntax, which can then generate hunk ./contrib/musicplayer/manage.py 6 import os, shutil, sys, subprocess, re from time import sleep +import tempfile from tempfile import mkstemp from setuptools import setup from setuptools import Command hunk ./contrib/musicplayer/manage.py 70 """ requires_re = re.compile('^//#require ["|\<](.+)["|\>]$', re.M) - def __init__(self, root_directory): + def __init__(self, root_directory, syntax_check = False): self.files = {} self.included = [] hunk ./contrib/musicplayer/manage.py 73 - self.root = root_directory + self.root = root_directory + self.syntax_check = syntax_check self.scan() hunk ./contrib/musicplayer/manage.py 99 self.files[path] = reqs - def parse(self, path, syntax_check = False): + def parse(self, path): if path in self.included: return '' if not path.endswith('.js'): hunk ./contrib/musicplayer/manage.py 106 # TODO: If path points to a directory, require all the files within that directory. return '' - if syntax_check: + if self.syntax_check: compiler = ClosureCompiler([path], None) if not compiler.syntax_check(): raise Exception('There seems to be a syntax problem. Fix it.') hunk ./contrib/musicplayer/manage.py 117 if not os.path.isfile(req_path): raise Exception('%s requires non existing file: %s' % (path, req_path)) - return self.parse(req_path, syntax_check) + return self.parse(req_path) script_file = open(path, 'r') script = script_file.read() hunk ./contrib/musicplayer/manage.py 144 description = 'builds whole application into build directory' user_options = [ ('compilation-level=', 'c', 'compilation level for Google\'s Closure compiler.'), + ('syntax-check', 's', 'run syntax check on every individal file') ] def initialize_options(self): hunk ./contrib/musicplayer/manage.py 149 self.compilation_level = 'SIMPLE_OPTIMIZATIONS' + self.syntax_check = False def finalize_options(self): compilation_levels = [ hunk ./contrib/musicplayer/manage.py 167 shutil.copytree('src/resources', 'build/resources') shutil.copy('src/config.example.json', 'build/') shutil.copy('src/index.html', 'build/') + shutil.copy('src/playlist_download.html', 'build/') + shutil.copy('src/about.html', 'build/') + shutil.copy('src/cache.manifest', 'build/') shutil.copytree('src/libs/vendor/soundmanager/swf', 'build/resources/flash') shutil.copy('src/libs/vendor/persist-js/persist.swf', 'build/resources/flash') hunk ./contrib/musicplayer/manage.py 174 - os.makedirs('build/js/libs') os.makedirs('build/js/workers') shutil.copy('src/libs/vendor/browser-couch/js/worker-map-reducer.js', 'build/js/workers/map-reducer.js') hunk ./contrib/musicplayer/manage.py 178 print 'Calculating dependencies...' - self.deps = JSDepsBuilder('src/') + self.deps = JSDepsBuilder('src/', syntax_check = self.syntax_check) self._make_js('Application.js', 'build/js/app.js') hunk ./contrib/musicplayer/manage.py 186 if worker.endswith('.js'): self._make_js('workers/' + worker, 'build/js/workers/' + worker) - print 'Done!' + print 'You\'re ready to rock \'n\' roll!' def _make_js(self, root, output): hunk ./contrib/musicplayer/manage.py 189 - tmp_file = mkstemp()[1] + fd = mkstemp() + os.close(fd[0]) + tmp_file = fd[1] self.deps.write_to_file(tmp_file, root_file = root) compiler = ClosureCompiler([tmp_file], output) compiler.compile(self.compilation_level) hunk ./contrib/musicplayer/manage.py 303 setup( name = 'tahoe-music-player', cmdclass = { - 'build': Build, + 'roll': Build, 'install': Install, 'watch': Watch, 'tests': VerifyTests, hunk ./contrib/musicplayer/src/Application.js 17 //#require "libs/db/PersistStorage.js" //#require "libs/db/DocumentTemplate.js" //#require "libs/util/Goal.js" +//#require "libs/ui/Menu.js" +//#require "libs/ui/Dialog.js" (function () { var BrowserCouch = da.db.BrowserCouch, hunk ./contrib/musicplayer/src/Application.js 23 PersistStorage = da.db.PersistStorage, - Goal = da.util.Goal; + Goal = da.util.Goal, + Menu = da.ui.Menu, + Dialog = da.ui.Dialog; /** section: Controllers hunk ./contrib/musicplayer/src/Application.js 28 - * class da.app + * App * hunk ./contrib/musicplayer/src/Application.js 30 - * The main controller. All methods are public. + * Private interface of the [[da.app]]. **/ hunk ./contrib/musicplayer/src/Application.js 32 -da.app = { - /** - * da.app.caps -> Object - * Object with `music` and `settings` properties, ie. the contents of `config.json` file. - **/ - caps: {}, - +var App = { initialize: function () { this.startup = new Goal({ hunk ./contrib/musicplayer/src/Application.js 35 - checkpoints: ["domready", "settings_db", "caps", "data_db", "soundmanager"], + checkpoints: ["domready", "settings_db", "caps", "data_db"], onFinish: this.ready.bind(this) }); hunk ./contrib/musicplayer/src/Application.js 42 BrowserCouch.get("settings", function (db) { da.db.SETTINGS = db; if(!db.getLength()) - this.loadInitialSettings(); + App.loadInitialSettings(); else { hunk ./contrib/musicplayer/src/Application.js 44 - this.startup.checkpoint("settings_db"); - this.getCaps(); + App.startup.checkpoint("settings_db"); + App.getCaps(); } hunk ./contrib/musicplayer/src/Application.js 47 - }.bind(this), new PersistStorage("tahoemp_settings")); + }, new PersistStorage("tahoemp_settings")); BrowserCouch.get("data", function (db) { da.db.DEFAULT = db; hunk ./contrib/musicplayer/src/Application.js 51 - this.startup.checkpoint("data_db"); - }.bind(this), new PersistStorage("tahoemp_data")); + App.startup.checkpoint("data_db"); + }, new PersistStorage("tahoemp_data")); hunk ./contrib/musicplayer/src/Application.js 54 - this.addEvent("ready.controller.CollectionScanner", function () { + da.app.addEvent("ready.controller.CollectionScanner", function () { if(!da.db.DEFAULT.getLength()) da.controller.CollectionScanner.scan(); }); hunk ./contrib/musicplayer/src/Application.js 61 }, loadInitialSettings: function () { - new Request.JSON({ + var req = new Request.JSON({ url: "config.json", noCache: true, hunk ./contrib/musicplayer/src/Application.js 70 {id: "music_cap", type: "Setting", group_id: "caps", value: data.music_cap}, {id: "settings_cap", type: "Setting", group_id: "caps", value: data.settings_cap} ], function () { - this.startup.checkpoint("settings_db"); + App.startup.checkpoint("settings_db"); hunk ./contrib/musicplayer/src/Application.js 72 - this.caps.music = data.music_cap; - this.caps.settings = data.settings_cap; + da.app.caps.music = data.music_cap; + da.app.caps.settings = data.settings_cap; hunk ./contrib/musicplayer/src/Application.js 75 - this.startup.checkpoint("caps"); + App.startup.checkpoint("caps"); if(!da.db.DEFAULT.getLength()) hunk ./contrib/musicplayer/src/Application.js 78 - da.controller.CollectionScanner.scan(); - }.bind(this)); - }.bind(this), + App.callController("CollectionScanner", "scan"); + }); + }, onFailure: function () { hunk ./contrib/musicplayer/src/Application.js 83 + delete req; alert("You're missing a config.json file! See docs on how to set it up."); hunk ./contrib/musicplayer/src/Application.js 85 - var showSettings = function () { - da.controller.Settings.showGroup("caps"); - }; hunk ./contrib/musicplayer/src/Application.js 86 - if(da.controller.Settings) - showSettings(); - else - da.app.addEvent("ready.controller.Settings", showSettings); + App.callController("Settings", "showGroup", ["caps"]); } hunk ./contrib/musicplayer/src/Application.js 88 - }).get() + }); + + req.get(); }, getCaps: function () { hunk ./contrib/musicplayer/src/Application.js 106 finished: function (result) { if(!result.rows.length) - return this.loadInitialSettings(); + return App.loadInitialSettings(); + + da.app.caps = { + music: result.getRow("music_cap"), + settings: result.getRow("settings_cap") + }; hunk ./contrib/musicplayer/src/Application.js 113 - this.caps.music = result.getRow("music_cap"); - this.caps.settings = result.getRow("settings_cap"); - if(!this.caps.music.length || !this.caps.music.length) - this.loadInitialSettings(); + if(!da.app.caps.settings.length || !da.app.caps.music.length) + App.loadInitialSettings(); else hunk ./contrib/musicplayer/src/Application.js 116 - this.startup.checkpoint("caps"); - }.bind(this), + App.startup.checkpoint("caps"); + }, updated: function (result) { var music = result.getRow("music_cap"), hunk ./contrib/musicplayer/src/Application.js 124 settings = result.getRow("settings_cap"); if(music) - da.controller.CollectionScanner.scan(this.caps.music = music); + da.controller.CollectionScanner.scan(da.app.caps.music = music); if(settings) hunk ./contrib/musicplayer/src/Application.js 127 - this.caps.settings = settings; + da.app.caps.settings = settings; hunk ./contrib/musicplayer/src/Application.js 129 - this.startup.checkpoint("caps"); - }.bind(this) + App.startup.checkpoint("caps"); + } }); }, hunk ./contrib/musicplayer/src/Application.js 134 + callController: function(controller, method, args) { + function callControllerMethod() { + var c = da.controller[controller]; + c[method].apply(c, args); + c = null; + } + + if(da.controller[controller]) + callControllerMethod(); + else + da.app.addEvent("ready.controller." + controller, callControllerMethod); + }, + /** * da.app.ready() -> undefined * fires ready hunk ./contrib/musicplayer/src/Application.js 155 **/ ready: function () { $("loader").destroy(); - $("panes").setStyle("display", "block"); + $("panes").style.display = "block"; + + this.setupMainMenu(); + + da.app.fireEvent("ready"); + + var about_iframe = new Element("iframe", { + src: "about:blank", + width: 400, + height: 500 + }); + about_iframe.style.background = "#fff"; + this.about_dialog = new Dialog({ + html: about_iframe, + onShow: function () { + about_iframe.src = "about.html"; + }, + onHide: function () { + about_iframe.src = "about:blank"; + } + }); + }, + + setupMainMenu: function () { + var main_menu_button = new Element("a", { + id: "main_menu", + // triangle + html: "▼", + href: "#", + events: { + mousedown: function (event) { + da.app.mainMenu.show(event); + }, + click: function (event) { + Event.stop(event); + } + } + }); hunk ./contrib/musicplayer/src/Application.js 194 - this.fireEvent("ready"); + da.app.mainMenu = new Menu({ + items: { + toggleShuffle: {html: "Turn shuffle on", id: "player_toggle_shuffle_menu_item", href: "#"}, + mute: {html: "Mute", id: "player_mute_menu_item", href: "#"}, + _sep0: Menu.separator, + + addToPl: {html: "Add to playlist…", href: "#"}, + share: {html: "Share…", href: "#"}, + + _sep1: Menu.separator, + + search: {html: "Search…", href: "#"}, + upload: {html: "Import…", href: "#"}, + rescan: {html: "Rescan collection", href: "#"}, + settings: {html: "Settings…", href: "#"}, + + _sep2: Menu.separator, + + help: {html: "Help", href: "#"}, + about: {html: "About", href:"#"} + }, + + position: { + position: "bottomRight", + edge: "topRight", + offset: { y: -3 } + }, + + onShow: function () { + main_menu_button.addClass("active_menu"); + }, + onHide: function () { + main_menu_button.removeClass("active_menu"); + }, + onClick: function (item) { + var fn = da.app.mainMenuActions[item]; + if(fn) + fn(); + fn = null; + } + }); + document.body.grab(main_menu_button); + } +}; + +/** + * class da.app + * + * The main controller. + **/ +da.app = { + /** + * da.app.caps -> Object + * Object with `music` and `settings` properties, ie. the contents of `config.json` file. + **/ + caps: {}, + + /** + * da.app.mainMenu -> da.ui.Menu + **/ + mainMenu: null, + + /** + * da.app.mainMenuActions -> Object + * Object's keys match [[da.app.mainMenu]] item keys. + **/ + mainMenuActions: { + toggleShuffle: function () { da.controller.Player._toggleShuffle() }, + mute: function () { da.controller.Player.toggleMute() }, + addToPl: function () { da.controller.Playlist.addSong() }, + search: function () { da.controller.Search.show() }, + rescan: function () { da.controller.CollectionScanner.scan(da.app.caps.music) }, + settings: function () { da.controller.Settings.show() }, + about: function () { App.about_dialog.show() } } }; $extend(da.app, new Events()); hunk ./contrib/musicplayer/src/Application.js 272 -da.app.initialize(); +App.initialize(); window.addEvent("domready", function () { hunk ./contrib/musicplayer/src/Application.js 275 - da.app.startup.checkpoint("domready"); + App.startup.checkpoint("domready"); }); })(); addfile ./contrib/musicplayer/src/about.html hunk ./contrib/musicplayer/src/about.html 1 + + + + + + + About Daaw + + + + + + +
+

Daaw0.1

+
+ +

+ Daaw has been developed as part of Google Summer of Code 2010 by Josip Lisec and + mentored by David-Sarah Hopwood under Tahoe-LAFS project. +

+

+ Project is licenced under GNU General Public License version 2, or any later version. +

+ +

Notices

+

+ + This application makes use of services provided by Last.fm. +

+

+ Some of the icons are derived from the Iconic Icon Set, while + others are used without any modifications. +

+

+ + + + All generated XSPF playlists are valid XSPF documents. +

+ +

Links

+ + + hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 5 //#require "doctemplates/Song.js" //#require "doctemplates/Artist.js" //#require "doctemplates/Album.js" +//#require "doctemplates/Setting.js" (function () { var DocumentTemplate = da.db.DocumentTemplate, hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 12 Song = DocumentTemplate.Song, Artist = DocumentTemplate.Artist, Album = DocumentTemplate.Album, - Goal = da.util.Goal; + Setting = DocumentTemplate.Setting, + Goal = da.util.Goal, + GENRES = da.util.GENRES; /** section: Controllers * class CollectionScanner hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 32 * Starts a new scan using [[Application.caps.music]] as root directory. **/ initialize: function (root) { + root = root || da.app.caps.music; + if(!root) { + this.finished = true; + return false; + } + console.log("collection scanner started"); this.indexer = new Worker("js/workers/indexer.js"); this.indexer.onmessage = this.onIndexerMessage.bind(this); hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 45 this.scanner = new Worker("js/workers/scanner.js"); this.scanner.onmessage = this.onScannerMessage.bind(this); - this.scanner.postMessage(root || da.app.caps.music); + this.scanner.postMessage(root); this.finished = false; hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 63 this._found_files ? "Your patience has paid off." : "Make sure your files have proper ID3 tags." ]) ); + + Setting.findById("last_scan").update({value: new Date()}); }.bind(this) }); hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 70 da.ui.ROAR.alert( "Collection scanner started", - "We're scanning your musical collection. Soon new artists and songs\ - should start popping up. Patience." + "Your musical collection is being scanned. You should see new artists showing \ + up in the area above. Patience." ); }, hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 86 this._goal.checkpoint("scanner"); return; } + if(cap.debug) { console.log("SCANNER", cap.msg, cap.obj); return; hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 127 title: tags.title, track: tags.track, year: tags.year, - lyrics: tags.lyrics, - genre: tags.genere, + genre: fixGenre(tags.genre), artist_id: artist_id, hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 129 - album_id: album_id + album_id: album_id, + plays: 0 }); delete links; hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 183 } }); -var SCANNER; +function fixGenre (genre) { + return typeof genre === "number" ? genre : (GENRES.contains(genre) ? GENRES.indexOf(genre) : genre); +} + +Setting.register({ + id: "last_scan", + group_id: "CollectionScanner", + representAs: "text", + title: "Last scan", + help: "The date your collection was scanned.", + value: new Date(0) +}); + +da.app.addEvent("ready", function () { + var last_scan = Setting.findById("last_scan"), + five_days_ago = (new Date()) - 5*24*60*60*1000; + if((new Date(last_scan.get("value"))) < five_days_ago) + da.controller.CollectionScanner.scan(); +}); + +var CS; /** * da.controller.CollectionScanner * Public interface of [[CollectionScanner]]. hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 214 * Starts scanning music directory **/ scan: function (cap) { - if(!SCANNER || (SCANNER && SCANNER.finished)) - SCANNER = new CollectionScanner(cap); + if(!CS || (CS && CS.finished)) + CS = new CollectionScanner(cap); + else if(cap && cap.length) + CS.scanner.postMessage(cap); }, /** hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 224 * da.controller.CollectionScanner.isScanning() -> true | false **/ isScanning: function () { - return SCANNER && !SCANNER.finished; + return CS ? !CS.finished : false; } }; hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 229 da.app.fireEvent("ready.controller.CollectionScanner", [], 1); + })(); hunk ./contrib/musicplayer/src/controllers/Navigation.js 69 this.createHeader(); this.column = new Navigation.columns[this.column_name]({ - id: this.column_name, - filter: options.filter, - parentElement: this._el + id: this.column_name, + filter: options.filter, + parentElement: this._el, + parentColumn: this.parent_column ? this.parent_column.column : null }); Navigation.adjustColumnSize(this.column); hunk ./contrib/musicplayer/src/controllers/Navigation.js 108 **/ createHeader: function () { this.header = new Element("a", { - "class": "column_header", - href: "#" + "class": "column_header", + href: "#" }); this.header.addEvent("click", function (event) { hunk ./contrib/musicplayer/src/controllers/Navigation.js 119 }); this._el.grab(this.header.grab(new Element("span", { - html: this.column_name, - "class": "column_title" + html: Navigation.columns[this.column_name].title, + "class": "column_title" }))); return this; hunk ./contrib/musicplayer/src/controllers/Navigation.js 134 **/ createMenu: function () { var filters = this.column.constructor.filters, - items = {}; + items = {}, + column; if(!filters || !filters.length) return false; hunk ./contrib/musicplayer/src/controllers/Navigation.js 141 items[this.column_name] = {html: this.column.constructor.title, "class": "checked", href: "#"}; - for(var n = 0, m = filters.length; n < m; n++) - items[filters[n]] = {html: filters[n], href: "#"}; + for(var n = 0, m = filters.length; n < m; n++) { + column = Navigation.columns[filters[n]]; + if(!column.hidden) + items[filters[n]] = {html: column.title, href: "#"}; + } this.menu = new Menu({ items: items hunk ./contrib/musicplayer/src/controllers/Navigation.js 155 this.menu.addEvent("show", function () { var header = this.header; - header.addClass("active"); + header.addClass("active_menu"); // adjusting menu's width to the width of the header header.retrieve("menu").toElement().style.width = header.getWidth() + "px"; }.bind(this)); hunk ./contrib/musicplayer/src/controllers/Navigation.js 161 this.menu.addEvent("hide", function () { - this.header.removeClass("active"); + this.header.removeClass("active_menu"); }.bind(this)); if(filters && filters.length) hunk ./contrib/musicplayer/src/controllers/Navigation.js 209 if(element) element.addClass("checked"); - header.getElement(".column_title").set("text", filter_name); + header.getElement(".column_title").set("text", Navigation.columns[filter_name].title); }, /** hunk ./contrib/musicplayer/src/controllers/Navigation.js 292 initialize: function () { var root_column = new NavigationColumnContainer({columnName: "Root"}); root_column.menu.removeItem("Root"); - root_column.menu.addItems({ - separator_1: Menu.separator, - search: {html: "Search", href:"#"}, - settings: {html: "Settings", href:"#", events: {click: da.controller.Settings.show}}, - help: {html: "Help", href:"#"} - }); var artists_column = new NavigationColumnContainer({ columnName: "Artists", hunk ./contrib/musicplayer/src/controllers/Navigation.js 302 root_column.header = artists_column.header; this._header_height = artists_column.header.getHeight(); + this._player_pane_width = $("player_pane").getWidth(); window.addEvent("resize", function () { var columns = Navigation.activeColumns, n = columns.length, hunk ./contrib/musicplayer/src/controllers/Navigation.js 312 while(n--) columns[n].column._el.setStyles({ height: height, - width: width - }); + width: width + }).fireEvent("resize"); hunk ./contrib/musicplayer/src/controllers/Navigation.js 315 - //$("navigation_pane").style.width = width*3 + "px"; $("navigation_pane").style.height = window.getHeight() + "px"; }.bind(this)); hunk ./contrib/musicplayer/src/controllers/Navigation.js 328 * Adjusts column's height to window. **/ adjustColumnSize: function (column) { - column._el.setStyles({ - height: window.getHeight() - this._header_height, + var el = column.toElement(); + el.style.height = (window.getHeight() - this._header_height) + "px"; // -1 for te right border hunk ./contrib/musicplayer/src/controllers/Navigation.js 331 - width: (window.getWidth() - $("player_pane").getWidth())/3 - 1 - }); + el.style.width = ((window.getWidth() - this._player_pane_width)/3 - 1) + "px"; + el.fireEvent("resize"); + el = null; }, /** hunk ./contrib/musicplayer/src/controllers/Navigation.js 337 - * da.controller.Navigation.registerColumn(name, filters, column) -> undefined - * - name (String): name of the column. - * - filters (Array): names of the columns which can accept filter created (with [[da.ui.NavigationColumn#createFilter]]) by this one. + * da.controller.Navigation.registerColumn(id[, title], filters, column) -> undefined + * - id (String): id of the column. + * - title (String): name of the column. Defaults to the value `id`, if not provided. + * - filters (Array): names of the columns which can accept filter created + * (with [[da.ui.NavigationColumn#createFilter]]) by this one. * - column (da.ui.NavigationColumn): column class. * hunk ./contrib/musicplayer/src/controllers/Navigation.js 344 - * `name` (renamed to `title`) and `filters` will be added to `column` as static methods. + * #### Notes + * `title` and `filters` will be added to `column` as class properties. + * If the `id` begins with an underscore, the column will be considered private + * and it won't be visible in the menus. **/ hunk ./contrib/musicplayer/src/controllers/Navigation.js 349 - registerColumn: function (name, filters, col) { + registerColumn: function (id, title, filters, col) { + if(arguments.length === 3) { + col = filters; + filters = title; + title = id; + } col.extend({ hunk ./contrib/musicplayer/src/controllers/Navigation.js 356 - title: name, - filters: filters || [] + title: title, + filters: filters || [], + hidden: id[0] === "_" }); hunk ./contrib/musicplayer/src/controllers/Navigation.js 361 - this.columns[name] = col; - if(name !== "Root") - this.columns.Root.filters.push(name); + this.columns[id] = col; + if(id !== "Root") + this.columns.Root.filters.push(id); // TODO: If Navigation is initialized // then Root's menu has to be updated. hunk ./contrib/musicplayer/src/controllers/Player.js 55 }, _loading: [], + play_mode: null, + /** * new Player() * Sets up soundManager2 and initializes player's interface. hunk ./contrib/musicplayer/src/controllers/Player.js 62 **/ initialize: function () { - soundManager.onready(function () { - da.app.startup.checkpoint("soundmanager"); - }); + //soundManager.onready(function () { + // da.app.startup.checkpoint("soundmanager"); + //}); da.app.addEvent("ready", this.initializeUI.bind(this)); hunk ./contrib/musicplayer/src/controllers/Player.js 79 window.fireEvent("resize"); this.progress_bar = new SegmentedProgressBar(150, 5, { - track: "#33519d", + track: "#339D4C", load: "#C1C6D4" }); hunk ./contrib/musicplayer/src/controllers/Player.js 82 + var load_grad = this.progress_bar.ctx.createLinearGradient(0, 0, 0, 5); + load_grad.addColorStop(0, "#6f7074"); + load_grad.addColorStop(1, "#cfccd7"); + this.progress_bar.segments.load.options.foreground = load_grad; + load_grad = null; + + var track_grad = this.progress_bar.ctx.createLinearGradient(0, 0, 0, 5); + track_grad.addColorStop(0, "#339D4C"); + track_grad.addColorStop(1, "#326732"); + this.progress_bar.segments.track.options.foreground = track_grad; + track_grad = null; this.progress_bar.toElement().id = "track_progress"; this.progress_bar.toElement().addEvents({ hunk ./contrib/musicplayer/src/controllers/Player.js 96 - // Has some issues in Firefox - the object's width also gets scaled - /* - mouseenter: function () { - this.tween("height", 15); - }, - mouseleave: function () { - this.tween("height", 5); - }, - */ mouseup: function (e) { var sound = Player.nowPlaying.sound; if(!sound) hunk ./contrib/musicplayer/src/controllers/Player.js 103 var p = e.event.offsetX / this.getWidth(); sound.setPosition(sound.durationEstimate * p); + }, + mouseover: function () { + Player.elements.position.fade("in"); } }); hunk ./contrib/musicplayer/src/controllers/Player.js 114 wrapper: new Element("div", {id: "song_details"}), cover_wrapper: new Element("div", {id: "song_album_cover_wrapper"}), album_cover: new Element("img", {id: "song_album_cover"}), - song_title: new Element("h2", {id: "song_title"}), - album_title: new Element("span", {id: "song_album_title"}), - artist_name: new Element("span", {id: "song_artist_name"}), - controls: new Element("div", {id: "player_controls", "class": "no_selection"}), - play: new Element("a", {id: "play_button", href: "#"}), - next: new Element("a", {id: "next_song", href: "#"}), - prev: new Element("a", {id: "prev_song", href: "#"}) + song_title: new Element("h2", {id: "song_title", html: "Unknown"}), + album_title: new Element("span", {id: "song_album_title", html: "Unknown"}), + artist_name: new Element("span", {id: "song_artist_name", html: "Unknown"}), + controls: new Element("div", {id: "player_controls", "class": "no_selection"}), + play: new Element("a", {id: "play_button", href: "#"}), + next: new Element("a", {id: "next_song", href: "#"}), + prev: new Element("a", {id: "prev_song", href: "#"}), + position: new Element("span", {id: "song_position", href: "#"}) }; var play_wrapper = new Element("div", {id: "play_button_wrapper"}); hunk ./contrib/musicplayer/src/controllers/Player.js 126 play_wrapper.grab(els.play); - els.controls.adopt(els.prev, play_wrapper, els.next, this.progress_bar.toElement()); + els.controls.adopt( + els.prev, play_wrapper, els.next, + this.progress_bar.toElement(), els.position + ); els.wrapper.grab(els.song_title); els.wrapper.appendText("from "); hunk ./contrib/musicplayer/src/controllers/Player.js 143 this._el.adopt(els.info_block); + els.position.set("tween", {duration: "short", link: "cancel"}); + this._el.style.visibility = "hidden"; this._visible = false; hunk ./contrib/musicplayer/src/controllers/Player.js 148 - var next = els.next, prev = els.prev; + // We're using them in mouseover event, + // to avoid creating another closure. + var next = els.next, + prev = els.prev; els.play.addEvents({ click: function () { Player.playPause(); hunk ./contrib/musicplayer/src/controllers/Player.js 165 } }); - var hideNextPrev = function () { - next.fade("out"); - prev.fade("out"); - }; + next.addEvent("click", function () { Player.playNext() }); + next.set("tween", {duration: "short", link: "cancel"}); hunk ./contrib/musicplayer/src/controllers/Player.js 168 - els.next.addEvents({ - click: function () { Player.playNext() } - }); - els.next.set("tween", {duration: "short", link: "cancel"}); + prev.addEvent("click", function () { Player.playPrev() }); + prev.set("tween", {duration: "short", link: "cancel"}); hunk ./contrib/musicplayer/src/controllers/Player.js 171 - els.prev.addEvents({ - click: function () { Player.playPrev() } + els.controls.addEvent("mouseleave", function () { + next.fade("out"); + prev.fade("out"); + Player.elements.position.fade("out"); }); hunk ./contrib/musicplayer/src/controllers/Player.js 176 - els.prev.set("tween", {duration: "short", link: "cancel"}); hunk ./contrib/musicplayer/src/controllers/Player.js 177 - els.controls.addEvent("mouseleave", hideNextPrev); + this.play_mode = "normal"; this.elements = els; delete els; hunk ./contrib/musicplayer/src/controllers/Player.js 182 delete play_wrapper; + delete play_modes; }, /** hunk ./contrib/musicplayer/src/controllers/Player.js 214 np.sound.stop(); this._loading.push(song.id); + var _np_update_buffer = +new Date(); this.sounds[song.id] = soundManager.createSound({ id: song.id, url: "/uri/" + encodeURIComponent(song.id), hunk ./contrib/musicplayer/src/controllers/Player.js 223 stream: true, onload: function () { - this._loading.remove(song.id); - if(!song.get("duration")) song.update({duration: this.duration}); hunk ./contrib/musicplayer/src/controllers/Player.js 225 + + if(!Player.progress_bar.ticks) + Player.progress_bar.ticks = Math.round(this.duration / (60 * 1000)); + + Player._loading.remove(song.id); }, onplay: function () { hunk ./contrib/musicplayer/src/controllers/Player.js 237 }, whileloading: function () { + // It will usually take less time to load the song than to complete the + // playback so we're not buffering the updates here. if(Player.nowPlaying.sound === this) Player.progress_bar.setProgress("load", this.bytesLoaded/this.bytesTotal); }, hunk ./contrib/musicplayer/src/controllers/Player.js 244 whileplaying: function () { - if(Player.nowPlaying.sound === this) { - Player.progress_bar.setProgress("track", this.position / this.durationEstimate); - Player.progress_bar.toElement().title = this.position + "/" + this.durationEstimate; + var d = +new Date(); + if(d - _np_update_buffer > 1000 && Player.nowPlaying.sound === this) { + var pb = Player.progress_bar; + pb.setProgress("track", this.position / this.durationEstimate); + Player.elements.position.set("text", + (new Date(this.position)).format("%M:%S") + " of " + (new Date(this.durationEstimate)).format("%M:%S") + ); + pb = null; + _np_update_buffer = d; } }, hunk ./contrib/musicplayer/src/controllers/Player.js 257 onfinish: function () { + song.update({plays: song.get("plays") + 1}); + if(Player.nowPlaying.sound === this) Player.playbackFinished(); } hunk ./contrib/musicplayer/src/controllers/Player.js 270 if(!this._visible) { this._visible = true; this._el.style.visibility = "visible"; + this.elements.position.position({ + relativeTo: $("track_progress"), + position: "centerBottom", + edge: "centerTop", + offset: {y: 2} + }); } }, hunk ./contrib/musicplayer/src/controllers/Player.js 327 delete els; }); + if(song.get("duration")) + this.progress_bar.ticks = Math.round(song.get("duration") / (60*1000)); + da.controller.Player.fireEvent("play", [song]); song = null; hunk ./contrib/musicplayer/src/controllers/Player.js 479 }, /** + * Player.switchPlayMode(mode) -> undefined + * - mode (String): `normal` or `shuffle`. + **/ + switchPlayMode: function (mode) { + var old = this.play_mode; + this.play_mode = mode; + + if(old === "shuffle" || mode === "shuffle") { + var np = this.nowPlaying.song; + if(old === "shuffle") { + this.playlist = this._normalised_playlist || []; + if(np) + this._playlist_pos = this.playlist.indexOf(np.id); + else + this._playlist_pos = 0; + + delete this._normalised_playlist; + } else if(mode === "shuffle") { + this._normalised_playlist = this.playlist; + // .shuffle() modifies the array itself + this.playlist = $A(this.playlist).shuffle(); + + // moving now playing song to the beginning of the playlist + if(np) + this.playlist.erase(np.id).unshift(np.id); + + this._playlist_pos = 0; + } + + delete np; + } + + + $("player_toggle_shuffle_menu_item").set("text", + mode === "shuffle" ? "Turn shuffle off" : "Turn shuffle on" + ); + + this.updateNextPrev(); + }, + + /** * Player#free() -> undefined * * Frees memory taken by loaded songs. This method is ran about every hunk ./contrib/musicplayer/src/controllers/Player.js 526 * eight minutes and it destroys all SMSound objects which were played * over eight minutes ago, ie. we're caching only about two songs in memory. * - * #### Links + * #### External resources * * (The Universality of Song Length?)[http://a-candle-in-the-dark.blogspot.com/2010/02/song-length.html] * **/ hunk ./contrib/musicplayer/src/controllers/Player.js 537 for(var id in this.sounds) { sound = this.sounds[id]; if(this.sounds.hasOwnProperty(id) - && this.nowPlaying.song.id !== id - && (sound._last_played >= eight_mins_ago || !sound.loaded)) - { - console.log("Freed sound ", id, sound._last_played); + && (this.nowPlaying.song.id !== id) + && (sound._last_played >= eight_mins_ago || !sound.loaded)) { + console.log("Freed sound", id, sound._last_played); sound.destruct(); delete this.sounds[id]; } hunk ./contrib/musicplayer/src/controllers/Player.js 545 } - delete sound; + sound = null; } }; hunk ./contrib/musicplayer/src/controllers/Player.js 551 Player.initialize(); -// Check is performed every four minutes -setTimeout(function () { +setInterval(function () { Player.free(); }, 8*60*1000); hunk ./contrib/musicplayer/src/controllers/Player.js 649 if(!playlist || $type(playlist) !== "array") return false; + if(Player.play_mode === "shuffle") { + Player._normalised_playlist = $A(playlist); + playlist.shuffle(); + } + Player.playlist = playlist; hunk ./contrib/musicplayer/src/controllers/Player.js 655 - Player._playlist_pos = 0; + if(Player.nowPlaying.song) + Player._playlist_pos = playlist.indexOf(Player.nowPlaying.song.id) + else + Player._playlist_pos = 0; + + Player.updateNextPrev(); + }, + + /** + * da.controller.Player.getPlaylist() -> [String, ...] + * Returns an array with ids of the songs belonging to the playlist. + **/ + getPlaylist: function () { + return Player.playlist; }, /** hunk ./contrib/musicplayer/src/controllers/Player.js 676 **/ nowPlaying: function () { return Player.nowPlaying.song; + }, + + /** + * da.controller.Player#setPlayMode(mode) -> undefined + * - mode (String): `normal`, `shuffle` or `repeat`. (all lowercase) + **/ + setPlayMode: function (mode) { + var old = Player.play_mode; + if(!mode || mode === old) + return false; + + Player.switchPlayMode(mode); + }, + + /** + * da.controller.Player#toggleMute() -> Boolean + * Returns `true` if the sound volume will be set to 0, `false` otherwise. + **/ + toggleMute: function () { + var muted = Player.nowPlaying.sound.muted; + da.vendor.soundManager[muted ? "unmute" : "mute"](); + $("player_mute_menu_item").set("text", muted ? "Mute" : "Unmute"); + + return !muted; + }, + + _toggleShuffle: function () { + if(Player.play_mode === "shuffle") + this.setPlayMode("normal"); + else + this.setPlayMode("shuffle"); } }; $extend(da.controller.Player, new Events()); addfile ./contrib/musicplayer/src/controllers/Playlist.js hunk ./contrib/musicplayer/src/controllers/Playlist.js 1 +//#require "libs/ui/Dialog.js" +//#require "libs/ui/Menu.js" +//#require "libs/util/PlaylistExporters.js" +//#require "doctemplates/Playlist.js" +//#require "doctemplates/Song.js" + +(function () { +var Playlist = da.db.DocumentTemplate.Playlist, + Song = da.db.DocumentTemplate.Song, + Dialog = da.ui.Dialog, + Menu = da.ui.Menu, + playlistExport = da.util.playlistExporter, + SONG_PREFIX = "playlist_song_"; + +/** section: Controllers + * class PlaylistEditor + **/ +var PlaylistEditor = new Class({ + /** + * new PlaylistEditor(playlist) + * - playlist (da.db.DocumentTemplate.Playlist): playlist wich will be edited. + **/ + initialize: function (playlist) { + this.playlist = playlist; + + var playlist_details = (new Element("div", { + id: "playlist_details" + })).adopt( + new Element("label", {html: "Name of the playlist:", "for": "playlist_title"}), + new Element("input", {type: "text", value: playlist.get("title"), id: "playlist_title"}), + + new Element("label", {html: "Description:", "for": "playlist_description"}), + (new Element("textarea", {id: "playlist_description", value: playlist.get("description")})) + ), + songs = (new Element("ol", { + id: "playlist_songs", + "class": "navigation_column" + })), + footer = (new Element("div", { + "class": "footer" + })).adopt( + new Element("button", { + id: "playlist_delete", + html: "Delete this playlist", + events: { + click: this.destroyPlaylist.bind(this) + } + }), + new Element("button", { + id: "playlist_add_more_songs", + html: "Add more songs", + events: { + click: this.showSearchDialog.bind(this) + } + }), + new Element("button", { + id: "playlist_export", + html: "Export ▼", + events: { + mousedown: function (event) { + this.export_menu.show(event); + }.bind(this) + } + }), + new Element("input", { + type: "submit", + id: "playlist_save", + value: "Save", + events: { + click: this.save.bind(this) + } + }) + ), + song_ids = $A(playlist.get("song_ids")), + n = song_ids.length; + + while(n--) + song_ids[n] = this.renderItem(Song.findById(song_ids[n])); + + songs.adopt(song_ids); + songs.addEvent("click:relay(.action)", this.removeSong.bind(this)); + + this._sortable = new Sortables(songs, { + opacity: 0, + revert: false, + clone: true, + constrain: true + }); + + this._el = (new Element("div", { + id: "playlist_editor_" + playlist.id, + "class": "playlist_editor" + })).adopt(playlist_details, songs, footer); + + this.dialog = new Dialog({ + title: "Edit playlist", + html: (new Element("div", { + "class": "playlist_editor_wrapper no_selection" + })).grab(this._el), + show: true, + closeButton: true, + hideOnOutsideClick: false, + destroyOnHide: true, + onHide: this.destroy.bind(this) + }); + + var export_formats = {}; + for(var format in playlistExport) + export_formats[format] = {html: "." + format, href: "#"}; + + this.export_menu = new Menu({ + items: export_formats, + position: { + position: "center", + edge: "center", + relativeTo: "playlist_export" + }, + onClick: this.exportPlaylist.bind(this) + }); + }, + + /** + * PlaylistEditor#removeSong(event, element) -> undefined + **/ + removeSong: function (event, element) { + var el = element.parentNode; + this._sortable.removeItems(el); + el.set("slide", {duration: 360, mode: "horizontal"}); + el.slide("out").fade("out").get("slide").chain(function () { + el.destroy(); + delete el; + }.bind(this)); + }, + + /** + * PlaylistEditor#destroyPlaylist(event, element) -> undefined + **/ + destroyPlaylist: function (event, element) { + if(!confirm("Are you sure?")) + return; + + this.playlist.destroy(); + this.destroy(); + }, + + /** + * PlaylistEditor#save() -> undefined + **/ + save: function () { + var ids = this._sortable.serialize(), + _pref_l = SONG_PREFIX.length, + n = ids.length; + + while(n--) + if(ids[n]) + ids[n] = ids[n].slice(_pref_l); + + this.playlist.update({ + title: $("playlist_title").value, + description: $("playlist_description").value, + song_ids: ids.clean() + }); + }, + + exportPlaylist: function (format) { + var exporter = playlistExport[format]; + if(!exporter) + return false; + + exporter(this.playlist); + }, + + renderItem: function (song) { + return new Element("li", { + id: SONG_PREFIX + song.id, + "class": "column_item" + }).adopt( + new Element("a", { + title: "Remove this song from the playlist", + href: "#", + "class": "action" + }), + new Element("span", {html: song.get("title")}) + ); + }, + + showSearchDialog: function () { + var addSearchResult = this.addSearchResult.bind(this); + + da.controller.Search.search("/.*?/", ["Song"], { + onComplete: function (results, column) { + console.log("Hacking search results"); + + column.removeEvents("click"); + column.addEvent("click", addSearchResult); + + }.bind(this) + }); + }, + + addSearchResult: function (item) { + if($("playlist_song_" + item.id)) + return false; + + item = this.renderItem(Song.findById(item.id)); + $("playlist_songs").grab(item); + this._sortable.addItems(item); + }, + + destroy: function () { + this.export_menu.destroy(); + this.playlist = null; + delete this.dialog; + delete this.export_menu; + delete this._el; + } +}); + +/** section: Controllers + * class AddToPlaylistDialog + **/ +var AddToPlaylistDialog = new Class({ + /** + * new AddToPlaylistDialog(song) + * - song (da.db.DocumentTemplate.Song): song which will be added to selected playlist. + **/ + /** + * AddToPlaylistDialog#song -> da.db.DocumentTemplate.Song + **/ + initialize: function (song) { + this.song = song; + + var playlist_selector = new Element("select", {id: "playlist_selector"}), + playlists = Playlist.view().rows, + n = playlists.length; + + + while(n--) + playlist_selector.grab(new Element("option", { + value: playlists[n].id, + html: playlists[n].value.title + })); + + playlist_selector.grab(new Element("option", { + value: "_new_playlist", + html: "New playlist" + })); + + playlist_selector.addEvent("change", this.selectionChange.bind(this)); + + this._new_playlist_form = (new Element("div", {id: "add_to_new_pl"})).adopt( + new Element("label", {html: "Title:", "for": "add_to_new_pl_title"}), + new Element("input", {id: "add_to_new_pl_title", type: "text"}), + new Element("label", {html: "Description:", "for": "add_to_new_pl_description"}), + new Element("textarea", {id: "add_to_new_pl_description"}) + ); + + this._el = (new Element("form", { + id: "add_to_pl_dialog" + }).adopt( + new Element("div", {id: "add_to_pl_playlists"}).adopt( + new Element("label", {html: "Choose an playlist:", "for": "playlist_selector"}), + playlist_selector, + this._new_playlist_form + ), + (new Element("div", {"class": "footer"})).grab( + new Element("input", { + type: "submit", + value: "Okay", + events: { + click: this.save.bind(this) + } + }) + ) + )); + + this._el.addEvent("submit", this.save.bind(this)); + + var title = (new Element("div", {"class": "dialog_title no_selection"})).adopt( + new Element("img", { + src: "resources/images/album_cover_1.png", + id: "add_to_pl_album_cover" + }), + new Element("span", {html: song.get("title"), "class": "title"}) + ); + + this.dialog = new Dialog({ + title: title, + html: (new Element("div", {"class": "add_to_pl_wrapper"})).grab(this._el), + closeButton: true, + show: true, + destroyOnHide: true + }); + + this.song.get("album", function (album) { + title.appendText("from ").grab( + new Element("span", {html: album.get("title")}) + ); + + var album_covers = album.get("album_cover_urls"); + if(album_covers && album_covers[1]) + $("add_to_pl_album_cover").src = album_covers[1]; + }); + + this.song.get("artist", function (artist) { + title.appendText(" by ").adopt( + new Element("span", {html: artist.get("title")}), + new Element("div", {"class": "clear"}) + ); + }); + + this._playlist_selector = playlist_selector; + playlist_selector = null; + }, + + /** + * AddToPlaylistDialog#save([event]) -> undefined + **/ + save: function (event) { + if(event) + Event.stop(event); + + var playlist_id = this._playlist_selector.value; + if(playlist_id === "_new_playlist") { + var title = $("add_to_new_pl_title"); + if(!title.value.length) + return title.focus(); + + Playlist.create({ + title: title.value, + description: $("add_to_new_pl_description").value, + song_ids: [this.song.id] + }); + } else { + var playlist = Playlist.findById(playlist_id); + playlist.get("song_ids").include(this.song.id); + playlist.save(); + playlist = null; + } + + this.destroy(); + }, + + /** + * AddToPlaylistDialog#selectionChange() -> undefined + * Called on `change` event by playlist selector. + **/ + selectionChange: function () { + if(this._playlist_selector.value === "_new_playlist") + this._new_playlist_form.show(); + else + this._new_playlist_form.hide(); + }, + + /** + * AddToPlaylistDialog#destroy() -> undefined + **/ + destroy: function () { + this.song = null; + this.dialog.destroy(); + delete this.dialog; + delete this._el; + delete this._playlist_selector; + delete this._new_playlist_form; + } +}); + +/** + * da.controller.Playlist + **/ +da.controller.Playlist = { + /** + * da.controller.Playlist.edit(playlist) -> undefined + * - playlist (da.db.DocumentTemplate.Playlist): playlist which will be edited. + **/ + edit: function (playlist) { + new PlaylistEditor(playlist); + }, + + /** + * da.controller.Playlist.addSong([song]) -> undefined + * - song (da.db.DocumentTemplate.Song): song which will be added to an playlist. + * If not provided, [[da.controller.Player.nowPlaying]] will be used. + **/ + addSong: function (song) { + if(!song) + song = da.controller.Player.nowPlaying(); + if(!song) + return false; + + new AddToPlaylistDialog(song); + } +}; + +da.app.fireEvent("ready.controller.Playlist", [], 1); +})(); addfile ./contrib/musicplayer/src/controllers/Search.js hunk ./contrib/musicplayer/src/controllers/Search.js 1 +//#require "libs/ui/Dialog.js" +//#require "controllers/default_columns.js" + +(function () { +var Dialog = da.ui.Dialog, + SongsColumn = da.controller.Navigation.columns.Songs, + Playlist = da.db.DocumentTemplate.Search, + ACTIVE = null; + +/** section: Controllers + * class SearchResults < da.controller.Navigation.columns.Songs + **/ +var SearchResults = new Class({ + Extends: SongsColumn, + + options: { + id: "search_results", + rowHeight: 50 + }, + + view: { + id: null, + temporary: true, + + map: function (doc, emit) { + var type = doc.type, + filters = this.options.filters, + query = this.options.query; + + // we have to emit every document because the filters + // represent OR operation, ie. if user selected ["Title", "Album"] + // it means that only one of those filters have to be satisfied + // in order fot the song to show up in the search results. + + emit(type, type === "Song" ? { + id: doc.id, + title: doc.title, + artist_id: doc.artist_id, + album_id: doc.album_id, + track: doc.track, + match: query.test(doc.title) + } : { + id: doc.id, + title: doc.title, + match: query.test(doc.title) + }); + }, + + reduce: function (key, values, rereduce) { + var query = this.options.query; + + if(key !== "Song") { + var _values = {}, + n = values.length, + val; + + while(n--) { + val = values[n]; + _values[val.id] = val; + } + + return _values; + } else { + var n = values.length, + val; + + while(n--) { + val = values[n]; + values[n] = { + id: val.id, + key: val.title, + value: val + }; + } + + return values; + } + } + }, + + mapReduceFinished: function (view) { + var songs = view.getRow("Song"); + if(!songs) + return this.parent({rows: []}); + + var n = songs.length, + filters = this.options.filters, + query = this.options.query, + matches; + + this._albums = view.getRow("Album"); + this._artists = view.getRow("Artist"); + + + while(n--) { + song = songs[n].value; + matches = []; + if(filters.contains("Song")) + matches.push(song.match); + if(filters.contains("Album")) + matches.push(this._albums[song.album_id].match); + if(filters.contains("Artist")) + matches.push(this._artists[song.artist_id].match); + + var m = matches.length, false_count = 0; + while(m--) + if(!matches[m]) + false_count++; + + if(false_count === matches.length) + delete songs[n]; + } + + songs = songs.clean(); + this.parent({ + rows: songs + }); + + this._finished = true; + Search.search_field.disabled = false; + + this.fireEvent("complete", [songs, this], 1); + }, + + renderItem: function (index) { + var item = this.getItem(index), + data = item.value, + query = this.options.query, + artist = this._artists[data.artist_id].title, + album = this._albums[data.album_id].title; + + return (new Element("a", { + id: "search_results_column_item_" + item.id, + href: "#", + title: "{0} by {1}".interpolate([data.title, artist]), + "class": index % 2 ? "even" : "odd" + }).adopt([ + new Element("span", {html: index + 1, "class": "result_number"}), + new Element("span", { + html: data.title.replace(query, underline), + "class": "title" + }), + new Element("span", { + html: "{0}from {1} by {2}".interpolate([ + data.track ? "track no." + data.track + " " : "", + album.replace(query, underline), artist.replace(query, underline) + ]), + "class": "subtitle" + }) + ])); + }, + + compareFunction: function (a, b) { + a = a.key + a.id; + b = b.key + b.id; + + if(a < b) return -1; + if(a > b) return 1; + return 0; + } +}); + +function underline (str) { + return "" + str + ""; +} + + +/** section: Controllers + * class Search + **/ +var Search = ({ + /** + * Search.query -> String + * Current search query + **/ + query: "", + + /** + * Search.active_filters -> [String, ] + * List of active filters, possible values are: + * * `Song`, + * * `Album` or + * * `Artist` + * + **/ + active_filters: ["Song"], + + /** + * Search.results_column -> SearchResults + **/ + results_column: null, + + initialize: function () { + this._el = new Element("div", {id: "search_dialog"}); + this.search_field = new Element("input", { + type: "text", + id: "search_field", + placeholder: "Search..." + }); + var header = (new Element("form", { + id: "search_header", + action: "#", + "class": "dialog_title" + })).adopt([ + this.search_field, + (new Element("div", { + id: "search_by_filters", + "class": "button_group" + })).adopt([ + new Element("button", {id: "search_filter_Song", html: "Song title", "class": "active"}), + new Element("button", {id: "search_filter_Album", html: "Album"}), + new Element("button", {id: "search_filter_Artist", html: "Artist"}) + ]) + ]); + + function searchFromField (event) { + if(event) + Event.stop(event); + + Search.search(Search.search_field.value, Search.active_filters); + } + + header.addEvent("submit", searchFromField); + + var _search_buffer; + this.search_field.addEvent("keyup", function (event) { + clearTimeout(_search_buffer); + _search_buffer = setTimeout(searchFromField, 360); + }); + this.search_field.addEvent("mousedown", function (event) { + // since the title is draggable, the text field would never + // get focus. + event.stopPropagation(); + }); + + this._el.grab(header); + var _sf_l = "search_filter_".length; + header.addEvent("click:relay(button)", function (event, button) { + var filter = button.id.slice(_sf_l); + if(Search.active_filters.contains(filter)) + Search.deactivateFilter(filter); + else + Search.activateFilter(filter); + + Search.query = null; + Search.search_field.focus(); + Search.search(Search.search_field.value); + }); + + this.dialog = new Dialog({ + title: header, + html: (new Element("div", {id: "search_dialog_wrapper"})).grab(this._el), + closeButton: true, + draggable: true, + hideOnOutsideClick: false, + + onShow: function () { + Search.search_field.focus(); + }, + + onHide: function () { + if(this.results_column) + this.results_column.destroy(); + + delete this.results_column; + } + }); + + this.initialized = true; + delete header; + }, + + /** + * Search.show() -> undefined + **/ + show: function () { + this.dialog.show(); + }, + + /** + * Search.search(query[, filters, options]) -> undefined | false + * - query (String | RegExp): search query. + * - filter (String): one of the filters. See [[Search.active_filter]] for + * possible values, this also the default value. + * - options (Function): passed to the [[SearchResults]] class. + * + * `false` will be returned in cases when search won't be started: + * * if the last query was the same as the new one, + * * if the last search hasn't finished, + * * there are no active filters. + * + * #### Notes + * If the `query` is a [[String]], modifications to it will be applied + * in order to get semi-fuzzy search. + * + * #### See also + * * [Autocomplete fuzzy matching](http://www.dustindiaz.com/autocomplete-fuzzy-matching/) + * + **/ + search: function (query, filters, options) { + if(this.query === query || query.length < 3) + return false; + if(!filters || !filters.length) + filters = this.active_filters; + if(!filters.length) + return false; + + this.query = query; + if(this.results_column) { + if(!this.results_column._finished) + return false; + + this.results_column.destroy(); + delete this.results_column; + } + + if(!(query instanceof RegExp)) + if(query[0] === "/" && query.slice(-1) === "/") + query = new RegExp(query.slice(1,-1), "ig") + else if(query.contains(" ")) + query = new RegExp("(" + query.escapeRegExp() + ")", "ig"); + else + query = new RegExp(query.replace(/\W/g, "").split("").join("\\w*"), "ig"); + + console.log("searching for", query, filters); + this.search_field.disabled = true; + + // This is a small hack which allows playlist editor + // add drag&drop controls, as the options persist between + // calls. + if(options) + this.column_options = options; + else + options = this.column_options; + + if(!options.parentElement) + options.parentElement = this._el; + this.results_column = new SearchResults($extend(options, { + query: query, + filters: filters + })); + }, + + /** + * Search.saveAsPlaylist() -> undefined + * Saves search results as a new playlist and opens [[PlaylistEditor#edit]] dialog. + **/ + saveAsPlaylist: function () { + if(!this.results_column || !this.results_column.finished) + return; + + var songs = this.results_column._rows, + n = songs.length, + song_ids = new Array(n); + + while(n--) + song_ids[n] = songs[n].id; + + Playlist.create({ + title: "Search results", + song_ids: song_ids + }, function (playlist) { + da.controller.Playlist.edit(playlist); + }); + }, + + /** + * Search.activateFilter(filter) -> false | undefined + * - filter (String): filter to activate. See [[Search.active_filter]] for + * possible values. + **/ + activateFilter: function (filter) { + if(this.active_filters.contains(filter)) + return false; + + $("search_filter_" + filter).addClass("active"); + this.active_filters.push(filter); + }, + + /** + * Search.deactivateFilter(filter) -> false | undefined + * - filter (String): filter to deactivate. + **/ + deactivateFilter: function (filter) { + if(!this.active_filters.contains(filter)) + return false; + + $("search_filter_" + filter).removeClass("active"); + this.active_filters.erase(filter); + }, + + /** + * Search.destroy() -> undefined + **/ + destroy: function () { + this.dialog.destroy(); + delete this.dialog; + + if(this.results_column) + this.results_column.destroy(); + delete this.results_column; + delete this._el; + delete this.search_field; + } +}); + +/** + * da.controller.Search + **/ +da.controller.Search = { + /** + * da.controller.Search.show() -> undefined + * + * Shows search overlay. + * + **/ + show: function () { + if(!Search.initialized) + Search.initialize(); + + Search.column_options = {}; + Search.show(); + }, + /** + * da.controller.Search.search(searchTerm[, filters][, options]) -> undefined + * - searchTerm (String): the query. + * - filters (Array): filters to use, [[Search.active_filters]]. + * - options (Object): options passed to [[SearchResults]] class. + * - options.onComplete (Function): function called with search results as first + * argument and instance of the class as the second argument. + **/ + search: function (term, filters, options) { + this.show(); + Search.search_field.value = term; + Search.search(term, filters, options || {}); + } +}; + +})(); hunk ./contrib/musicplayer/src/controllers/Settings.js 14 * This is private class. * Public interface is accessible via [[da.controller.Settings]]. **/ - -var Dialog = da.ui.Dialog, - Setting = da.db.DocumentTemplate.Setting; + +var Dialog = da.ui.Dialog, + NavigationColumn = da.ui.NavigationColumn, + Setting = da.db.DocumentTemplate.Setting; var GROUPS = [{ hunk ./contrib/musicplayer/src/controllers/Settings.js 20 - id: "caps", - title: "Caps", - description: "Tahoe caps for your music and configuration files." - }, { - id: "lastfm", - title: "Last.fm", - description: 'Share the music your are listening to with the world via Last.fm.' - } -]; + id: "caps", + title: "Caps", + description: "Tahoe caps for your music and configuration files." +}]; // Renderers are used to render the interface elements for each setting (ie. the input boxes, checkboxes etc.) // Settings and renderers are bound together via "representAs" property which hunk ./contrib/musicplayer/src/controllers/Settings.js 99 this.dialog = new Dialog({ title: "Settings", html: new Element("div", {id: "settings"}), - hideOnOutsideClick: false + hideOnOutsideClick: false, + closeButton: true }); this._el = $("settings"); this.column = new GroupsColumn({ hunk ./contrib/musicplayer/src/controllers/Settings.js 263 } var GroupsColumn = new Class({ - Extends: da.ui.NavigationColumn, + Extends: NavigationColumn, view: null, hunk ./contrib/musicplayer/src/controllers/SongContext.js 27 }); this.tabs = new Element("div", { id: "context_tabs", - "class": "no_selection" + "class": "button_group no_selection" }); for(var id in CONTEXTS) hunk ./contrib/musicplayer/src/controllers/SongContext.js 31 - this.tabs.grab(new Element("a", { - id: id + TAB_SUFFIX, - "class": "tab", - href: "#", - html: CONTEXTS[id].title + this.tabs.grab(new Element("button", { + id: id + TAB_SUFFIX, + html: CONTEXTS[id].title })); hunk ./contrib/musicplayer/src/controllers/SongContext.js 35 - this.tabs.addEvent("click:relay(.tab)", function () { + this.tabs.addEvent("click:relay(button)", function () { SongContext.show(this.id.slice(0, _TS_L)); }); hunk ./contrib/musicplayer/src/controllers/SongContext.js 48 $("player_pane").adopt(this.tabs, this.loading_screen, this.el); + function adjustDimensions () { + SongContext.el.style.height = ( + window.getHeight() - $("song_info_block").getHeight() - SongContext.tabs.getHeight() + ) + "px"; + } + Player.addEvent("play", function (song) { hunk ./contrib/musicplayer/src/controllers/SongContext.js 55 + adjustDimensions(); + if(SongContext.active) SongContext.active.update(song); }); hunk ./contrib/musicplayer/src/controllers/SongContext.js 61 - window.addEvent("resize", function () { - SongContext.el.style.height = ( - window.getHeight() - $("song_info_block").getHeight() - SongContext.tabs.getHeight() - ) + "px"; - }); + window.addEvent("resize", adjustDimensions); window.fireEvent("resize"); hunk ./contrib/musicplayer/src/controllers/SongContext.js 63 + this.initialized = true; }, hunk ./contrib/musicplayer/src/controllers/SongContext.js 115 * **/ addTab: function (id) { - this.tabs.grab(new Element("a", { - id: id + TAB_SUFFIX, - "class": "tab", - href: "#", - html: CONTEXTS[id].title + this.tabs.grab(new Element("button", { + id: id + TAB_SUFFIX, + html: CONTEXTS[id].title })); } }; hunk ./contrib/musicplayer/src/controllers/controllers.js 17 //#require "controllers/Navigation.js" //#require "controllers/Player.js" //#require "controllers/SongContext.js" +//#require "controllers/Search.js" +//#require "controllers/Playlist.js" //#require "controllers/CollectionScanner.js" hunk ./contrib/musicplayer/src/controllers/default_columns.js 1 +//#require "controllers/Player.js" //#require "libs/ui/NavigationColumn.js" //#require "services/albumCover.js" hunk ./contrib/musicplayer/src/controllers/default_columns.js 7 (function () { var Navigation = da.controller.Navigation, + Player = da.controller.Player, NavigationColumn = da.ui.NavigationColumn, Album = da.db.DocumentTemplate.Album, Song = da.db.DocumentTemplate.Song, hunk ./contrib/musicplayer/src/controllers/default_columns.js 11 + Playlist = da.db.DocumentTemplate.Playlist, fetchAlbumCover = da.service.albumCover; /** section: Controller hunk ./contrib/musicplayer/src/controllers/default_columns.js 49 * * Displays artists. **/ -var the_regex = /^the\s*/i; +var THE_REGEX = /^the\s*/i; Navigation.registerColumn("Artists", ["Albums", "Songs"], new Class({ Extends: NavigationColumn, hunk ./contrib/musicplayer/src/controllers/default_columns.js 58 map: function (doc, emit) { // If there are no documents in the DB this function // will be called with "undefined" as first argument - if(!doc) return; + if(!doc || doc.type !== "Artist") return; hunk ./contrib/musicplayer/src/controllers/default_columns.js 60 - if(doc.type === "Artist") - emit(doc.id, { - title: doc.title - }); + emit(doc.id, { + title: doc.title + }); } }, hunk ./contrib/musicplayer/src/controllers/default_columns.js 71 }, compareFunction: function (a, b) { - a = a && a.value.title ? a.value.title.split(the_regex).slice(-1) : a; - b = b && b.value.title ? b.value.title.split(the_regex).slice(-1) : b; + a = a && a.value.title ? a.value.title.split(THE_REGEX).slice(-1) : a; + b = b && b.value.title ? b.value.title.split(THE_REGEX).slice(-1) : b; if(a < b) return -1; if(a > b) return 1; hunk ./contrib/musicplayer/src/controllers/default_columns.js 90 Extends: NavigationColumn, options: { - rowHeight: 72/3, - iconSize: 1, - renderImmediately: false + rowHeight: 50 }, hunk ./contrib/musicplayer/src/controllers/default_columns.js 93 - // We can't reuse "Album" view because of #_passesFilter(). view: { id: "albums_column", hunk ./contrib/musicplayer/src/controllers/default_columns.js 97 map: function (doc, emit) { - if(!doc || !this._passesFilter(doc)) return; + if(!doc || doc.type !== "Album" || !this._passesFilter(doc)) return; hunk ./contrib/musicplayer/src/controllers/default_columns.js 99 - if(doc.type === "Album") - emit(doc.id, { - title: doc.title, - icons: doc.album_cover_urls || [] - }); + emit(doc.id, { + title: doc.title, + icon: doc.album_cover_urls ? doc.album_cover_urls[1] : null + }); } }, hunk ./contrib/musicplayer/src/controllers/default_columns.js 106 - initialize: function (options) { - this.parent(options); - - // TODO: select icon size depending on the column's width - // also, adjust margins between the elements accordingly - this.options.iconSize = this.options.totalCount <= (this.getVisibleIndexes()[1] + 1) ? 2 : 1; - this._row_dim = this.options.iconSize === 1 ? 64 : 174; + renderItem: function (index) { + var item = this.getItem(index); + if(!item.value.icon) { + item.value.icon = "resources/images/album_cover_1.png"; + fetchAlbumCover(Album.findById(item.id), function (urls) { + item.value.icon = urls[1]; + }); + } hunk ./contrib/musicplayer/src/controllers/default_columns.js 115 - this._el.addEvent("resize", function () { - var width = this._el.getWidth(); - // 4 + 4 being padding on the element - this.options.rowHeight = width / (4 + this._row_dim + 4); - }.bind(this)); - this._el.fireEvent("resize"); hunk ./contrib/musicplayer/src/controllers/default_columns.js 116 - this.render(); + return this.parent(index); }, createFilter: function (item) { hunk ./contrib/musicplayer/src/controllers/default_columns.js 121 return {album_id: item.id}; + } +})); + +/** + * class da.controller.Navigation.columns.Genres < da.ui.NavigationColumn + * filters: [[da.controller.Navigation.columns.Songs]] + * + * Displays song genres. + **/ +var GENRES = da.util.GENRES; +Navigation.registerColumn("Genres", ["Songs"], new Class({ + Extends: NavigationColumn, + + view: { + id: "genres_column", + map: function (doc, emit) { + // If there are no documents in the DB this function + // will be called with "undefined" as first argument + if(!doc || doc.type !== "Song") return; + + emit(doc.genre || -1, 1); + }, + reduce: function (key, values, rereduce) { + //console.log("reduce", key, values); + + if(key !== null) { + var key_n = isNaN(+key) ? key : + key; + + return { + title: typeof key_n === "number" ? GENRES[key_n] || "Unknown" : key_n, + subtitle: values.length, + genre: key_n + } + } else { + var n = values.length, count = 0; + while(n--) + count += values[n].subtitle; + + return { + title: values[0].title, + subtitle: count, + genre: values[0].genre + } + } + } }, hunk ./contrib/musicplayer/src/controllers/default_columns.js 168 - renderItem: function (index) { - var item = this.getItem(index), - data = item.value, - el = new Element("a", { - id: this.options.id + "_column_item_" + item.id, - href: "#", - title: data.title - }), - cover = data.icons[this.options.iconSize]; + mapReduceFinished: function (view) { + this._addIdsToReducedView(view); + this.parent(view); + }, + + mapReduceUpdated: function (view) { + this._addIdsToReducedView(view); + this.parent(view); + }, + + _addIdsToReducedView: function (view) { + var n = view.rows.length; + while(n--) + view.rows[n].id = view.rows[n].value.genre; + return view; + }, + + createFilter: function (item) { + return {genre: item.value.genre}; + }, + + compareFunction: function (a, b) { + a = a && a.value.title ? a.value.title.split(THE_REGEX).slice(-1) : a; + b = b && b.value.title ? b.value.title.split(THE_REGEX).slice(-1) : b; hunk ./contrib/musicplayer/src/controllers/default_columns.js 193 - el.style.width = this._row_dim + "px"; - el.style.height = this._row_dim + "px"; - if(!cover || !cover.length) { - cover = "resources/images/album_cover_" + this.options.iconSize + ".png"; - fetchAlbumCover(Album.findById(item.id)); + if(a < b) return -1; + if(a > b) return 1; + return 0; + } +})); + + +/** + * class da.controller.Navigation.columns.Playlists < da.ui.NavigationColumn + * filters: [[da.controller.Navigation.columns.Songs]] + * + * Displays songs. + **/ +Navigation.registerColumn("Playlists", ["_PlaylistSongs"], new Class({ + Extends: NavigationColumn, + + view: { + id: "playlists_column", + map: function (doc, emit) { + if(!doc || doc.type !== "Playlist" || !this._passesFilter(doc)) return; + + emit(doc.id, { + title: doc.title, + song_ids: doc.song_ids + }); } hunk ./contrib/musicplayer/src/controllers/default_columns.js 219 + }, + + initialize: function (options) { + this.parent(options); hunk ./contrib/musicplayer/src/controllers/default_columns.js 224 - el.grab(new Element("img", {src: cover})); - return el; + this._el.addEvent("click:relay(.action)", function (event, el) { + var item = this.getItem(el.parentNode.retrieve("column_index")); + da.controller.Playlist.edit(Playlist.findById(item.id)); + }.bind(this)); }, hunk ./contrib/musicplayer/src/controllers/default_columns.js 230 - getBoxCoords: function(index) { - return [0, (this.options.rowHeight * index)/3]; + mapReduceUpdated: function (view) { + this.parent(view); + if(this._active_el) + this._el.fireEvent("click:relay(.column_item)", [null, this._active_el], 1); + }, + + createFilter: function (item) { + var id = item.id, + songs = item.value.song_ids; + + return function (song) { + return song.type === "Song" ? songs.contains(song.id) : song.id === id; + } + }, + + renderItem: function (index) { + var item = this.getItem(index), + data = item.value; + + return (new Element("a", { + id: this.options.id + "_column_item_" + item.id, + href: "#", + "class": index % 2 ? "even" : "odd" + })).adopt([ + new Element("a", { + href: "#", + html: "Edit", + title: "Edit the playlist", + "class": "action" + }), + new Element("span", { + html: data.title, + title: data.title, + "class": "title" + }) + ]); } })); hunk ./contrib/musicplayer/src/controllers/default_columns.js 269 + /** * class da.controller.Navigation.columns.Songs < da.ui.NavigationColumn * filters: none hunk ./contrib/musicplayer/src/controllers/default_columns.js 283 this.parent(options); this.addEvent("click", function (item, event, el) { - da.controller.Player.setPlaylist(this._playlist); - da.controller.Player.play(Song.findById(item.id)); - }.bind(this), true); + el.removeClass("active_column_item"); + }, true); + + this.addEvent("click", function (item, event, el) { + Player.play(Song.findById(item.id)); + Player.setPlaylist(this._playlist); + }.bind(this)); + + this._onSongChange = this._updateSelectedItem.bind(this); + da.controller.Player.addEvent("play", this._onSongChange); }, view: { hunk ./contrib/musicplayer/src/controllers/default_columns.js 298 id: "songs_column", map: function (doc, emit) { - if(!doc || !this._passesFilter(doc)) return; + if(!doc || doc.type !== "Song" || !this._passesFilter(doc)) + return; hunk ./contrib/musicplayer/src/controllers/default_columns.js 301 - if(doc.type === "Song" && doc.title) + if(doc.title && doc.title.length) emit(doc.title, { title: doc.title, track: doc.track hunk ./contrib/musicplayer/src/controllers/default_columns.js 312 mapReduceFinished: function (values) { this.parent(values); this.createPlaylist(); + this._updateSelectedItem(da.controller.Player.nowPlaying()); }, hunk ./contrib/musicplayer/src/controllers/default_columns.js 315 - mapReduceUpdated: function (values) { - this.parent(values); + mapReduceUpdated: function (values, rerender) { + this.parent(values, rerender); this.createPlaylist(); }, hunk ./contrib/musicplayer/src/controllers/default_columns.js 328 playlist[n] = this._rows[n].id; this._playlist = playlist; - delete playlist; + playlist = null; }, compareFunction: function (a, b) { hunk ./contrib/musicplayer/src/controllers/default_columns.js 337 if(a < b) return -1; if(a > b) return 1; - return 0; + return 0; + }, + + _updateSelectedItem: function (song) { + if(!song) + return false; + + var new_active_el = $(this.options.id + "_column_item_" + song.id); + if(!new_active_el) + return false; + + if(this._active_el) + this._active_el.removeClass("active_column_item"); + + this._active_el = new_active_el; + this._active_el.addClass("active_column_item"); + new_active_el = null; + }, + + destroy: function () { + da.controller.Player.removeEvent("play", this._onSongChange); + this.parent() + } +})); + + +/** + * class da.controller.Navigation.columns.PlaylistSongs < da.controller.Navigation.columns.Songs + * filters: none + * + * Displays songs from a playlist - adds drag&drop functionality. + **/ +Navigation.registerColumn("_PlaylistSongs", "Songs", [], new Class({ + Extends: Navigation.columns.Songs, + + view: { + id: "playlist_songs_column", + map: function (doc, emit) { + var type = doc.type; + if(!doc || (type !== "Song" && type !== "Playlist") || !this._passesFilter(doc)) + return; + + if(type === "Song") + emit(doc.title, { + title: doc.title + }); + else + emit("_playlist", { + id: doc.id, + title: doc.title + " (playlist)", + song_ids: doc.song_ids + }); + } + }, + + mapReduceFinished: function (view) { + var playlist_pos = view.findRow("_playlist"); + this.addPositions(view.rows[playlist_pos], view.rows); + return this.parent(view); + }, + + mapReduceUpdated: function (view) { + var full_view = da.db.DEFAULT.views[this.view.id].view, + new_rows = $A(full_view.rows); + // this is why we can't use this.parent(view, true), + // we need to add positions to the all elements, before + // the sorting occurs (remember that `view` contains only the playlist) + this.addPositions(new_rows[full_view.findRow("_playlist")], new_rows); + new_rows.sort(this.compareFunction); + + this.options.totalCount = new_rows.length; + this._rows = new_rows; + + var active = this.getActiveItem(); + this.rerender(); + + if(active) { + console.log("has_active_item"); + this._active_el = $(this.options.id + "_column_item_" + active.id); + this._active_el.addClass("active_column_item"); + } + + full_view = null; + new_rows = null; + active = null; + }, + + compareFunction: function (a, b) { + a = a && a.value ? a.value.playlist_pos : 0; + b = b && b.value ? b.value.playlist_pos : 0; + + if(a < b) return -1; + if(a > b) return 1; + return 0; + }, + + addPositions: function (playlist, rows) { + if(playlist) { + rows.erase(playlist); + this._playlist = playlist.value.song_ids; + } + var n = rows.length, + playlist = this._playlist; + + while(n--) + rows[n].value.playlist_pos = playlist.indexOf(rows[n].id); + }, + + createPlaylist: function () {} + + /* + mapReduceUpdated: function (view) { + this._rows = $A(da.db.DEFAULT.views[this.view.id].view.rows); + this._rows.sort(this.compareFunction); + this.options.totalCount = this._rows.length; + this.rerender(); + + var active = this.getActiveItem(); + if(active) { + this._active_el = $(this.options.id + "_column_item_" + active.id); + this._active_el.addClass("active_column_item"); + } } hunk ./contrib/musicplayer/src/controllers/default_columns.js 460 + */ })); hunk ./contrib/musicplayer/src/controllers/default_columns.js 463 + })(); hunk ./contrib/musicplayer/src/controllers/default_contexts.js 315 }, onHide: function () { this.video.src = "about:blank"; - setTimeout(function () { - Player.play(); - }, 1000); }.bind(this) }); hunk ./contrib/musicplayer/src/doctemplates/Artist.js 10 * - title (String): name of the artist * **/ + (function () { var DocumentTemplate = da.db.DocumentTemplate; addfile ./contrib/musicplayer/src/doctemplates/Playlist.js hunk ./contrib/musicplayer/src/doctemplates/Playlist.js 1 +//#require "libs/db/DocumentTemplate.js" + +/** + * class da.db.DocumentTemplate.Playlist < da.db.DocumentTemplate + * + * Class representing playlists + * + * #### Standard properties + * - `title` (String): name of the playlist. + * - `description` (String): a few words about the playlist. + * - `song_ids` (Array): list of ID's of songs belonging to the playlist. + **/ + +(function () { +var DocumentTemplate = da.db.DocumentTemplate; + +DocumentTemplate.registerType("Playlist", new Class({ + Extends: DocumentTemplate +})); + +/* +DocumentTemplate.Playlist.findOrCreate({ + properties: {id: "offline", }, + onSuccess: function (offline_playlist, was_created) { + if(was_created) + offline_playlist.update({ + title: "Offline", + description: "Songs on this playlist will be available even after you go offline." + }); + } +}); +*/ + +})(); hunk ./contrib/musicplayer/src/doctemplates/Setting.js 17 * id: "volume", * group_id: "general", * representAs: "Number", - * + * * title: "Volume", * help: "Configure the volume", * value: 64 hunk ./contrib/musicplayer/src/doctemplates/Setting.js 107 value: "" }); +/* Setting.register({ id: "lastfm_enabled", group_id: "lastfm", hunk ./contrib/musicplayer/src/doctemplates/Setting.js 137 value: "", position: 2 }); +*/ })(); hunk ./contrib/musicplayer/src/doctemplates/Song.js 2 //#require "libs/db/DocumentTemplate.js" +//#require "libs/util/genres.js" (function () { hunk ./contrib/musicplayer/src/doctemplates/Song.js 5 -var DocumentTemplate = da.db.DocumentTemplate; - +var DocumentTemplate = da.db.DocumentTemplate, + GENRES = da.util.GENRES; /** * class da.db.DocumentTemplate.Song < da.db.DocumentTemplate * belongsTo: [[da.db.DocumentTemplate.Artist]], [[da.db.DocumentTemplate.Album]] hunk ./contrib/musicplayer/src/doctemplates/Song.js 12 * * #### Standard properties - * * `id` - Read-only cap of the file - * * `title` - name of the song - * * `track` - track number - * * `year` - year in which track was published - * * `lyrics` - lyrics of the song - * * `artist_id` - id of an [[da.db.DocumentTemplate.Artist]] - * * `album_id` - id of an [[da.db.DocumentTemplate.Album]] + * - `id` ([[String]]): Read-only cap of the file. + * - `title` ([[String]]): name of the song. + * - `track` ([[Numner]]): track number. + * - `year` ([[Number]]): year in which the track was published, `0` if the year + * is unkown. + * - `duration` ([[Number]]): length of the song in milliseconds. + * - `artist_id` ([[String]]): id of an [[da.db.DocumentTemplate.Artist]] + * - `album_id` ([[String]]): id of an [[da.db.DocumentTemplate.Album]] + * - `plays` ([[Number]]): number of full plays + * - `genre` ([[String]] | [[Number]]): id of the genre or name of the genre + * itself. If it's a number, it's a index of an [[da.util.GENRES]]. + * `-1` if the genre isn't specified. + * - `mbid` ([[String]]): Musicbrainz ID + * - `lastfm_id` ([[String]]): Last.fm ID * **/ hunk ./contrib/musicplayer/src/doctemplates/Song.js 29 -// Defined by ID3 specs: -// http://www.id3.org/id3v2.3.0#head-129376727ebe5309c1de1888987d070288d7c7e7 -var GENRES = [ - "Blues","Classic Rock","Country","Dance","Disco","Funk","Grunge","Hip-Hop","Jazz", - "Metal","New Age","Oldies","Other","Pop","R&B","Rap","Reggae","Rock","Techno", - "Industrial","Alternative","Ska","Death Metal","Pranks","Soundtrack","Euro-Techno", - "Ambient","Trip-Hop","Vocal","Jazz+Funk","Fusion","Trance","Classical","Instrumental", - "Acid","House","Game","Sound Clip","Gospel","Noise","AlternRock","Bass","Soul","Punk", - "Space","Meditative","Instrumental Pop","Instrumental Rock","Ethnic","Gothic", - "Darkwave","Techno-Industrial","Electronic","Pop-Folk","Eurodance","Dream", - "Southern Rock","Comedy","Cult","Gangsta","Top 40","Christian Rap","Pop/Funk", - "Jungle","Native American","Cabaret","New Wave","Psychadelic","Rave","Showtunes", - "Trailer","Lo-Fi","Tribal","Acid Punk","Acid Jazz","Polka","Retro","Musical", - "Rock & Roll","Hard Rock","Folk","Folk-Rock","National Folk","Swing","Fast Fusion", - "Bebob","Latin","Revival","Celtic","Bluegrass","Avantgarde","Gothic Rock", - "Progressive Rock","Psychedelic Rock","Symphonic Rock","Slow Rock","Big Band", - "Chorus","Easy Listening","Acoustic","Humour","Speech","Chanson","Opera","Chamber Music", - "Sonata","Symphony","Booty Bass","Primus","Porn Groove","Satire","Slow Jam","Club","Tango", - "Samba","Folklore","Ballad","Power Ballad","Rhythmic Soul","Freestyle","Duet","Punk Rock", - "Drum Solo","A capella","Euro-House","Dance Hall" -]; - DocumentTemplate.registerType("Song", new Class({ Extends: DocumentTemplate, hunk ./contrib/musicplayer/src/doctemplates/doctemplates.js 12 //#require "doctemplates/Artist.js" //#require "doctemplates/Album.js" //#require "doctemplates/Song.js" - +//#require "doctemplates/Playlist.js" hunk ./contrib/musicplayer/src/index.html 3 + hunk ./contrib/musicplayer/src/index.html 6 + Music Player for Tahoe-LAFS hunk ./contrib/musicplayer/src/index.html 9 - - + + hunk ./contrib/musicplayer/src/index_devel.html 43 - - - - - - - - + + hunk ./contrib/musicplayer/src/index_devel.html 55 + + + + + + + + hunk ./contrib/musicplayer/src/index_devel.html 78 + hunk ./contrib/musicplayer/src/index_devel.html 80 +
Loading...
hunk ./contrib/musicplayer/src/libs/TahoeObject.js 75 } this._fetched = true; - new Request.JSON({ + var req = new Request.JSON({ url: "/uri/" + encodeURIComponent(this.uri), onSuccess: function (data) { hunk ./contrib/musicplayer/src/libs/TahoeObject.js 81 this.applyMeta(data); (success||$empty)(this); + + delete req; }.bind(this), onFailure: failure || $empty hunk ./contrib/musicplayer/src/libs/TahoeObject.js 86 - }).get({t: "json"}); + }); + req.get({t: "json"}); return this; }, hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 258 for(var view_name in views) this.view(views[view_name].options, dict); - }.bind(this), true); + }.bind(this), true); }, /** hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 309 put: function (doc, cb) { if ($type(doc) === "array") { this.dict.setDocs(doc); - //var n = doc.length, _doc; - //while(n--) { - // _doc = doc[n]; - // this.dict.set(_doc.id, _doc); - //} } else this.dict.set(doc.id, doc); hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 397 var full_view = this.views[id].view.rows.concat(view.rows), rereduce = {}, reduce = options.reduce, - n = full_view.length; + n = full_view.length, + row, key; while(n--) { hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 401 - var row = full_view[n], - key = row.key; + row = full_view[n]; + key = row.key; + if(!rereduce[key]) rereduce[key] = [row.value]; else hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 430 * - id (String): name of the view. **/ killView: function (id) { - delete this.views[id].view; + this.removeEvents("updated." + id); delete this.views[id]; return this; } hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 438 // Maximum number of items to process before giving the UI a chance // to breathe. -var DEFAULT_CHUNK_SIZE = 1000; - +var DEFAULT_CHUNK_SIZE = 1000, // If no progress callback is given, we'll automatically give the // UI a chance to breathe for this many milliseconds before continuing // processing. hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 442 -var DEFAULT_UI_BREATHE_TIME = 50; + DEFAULT_UI_BREATHE_TIME = 50; function defaultProgress(phase, percent, resume) { window.setTimeout(resume, DEFAULT_UI_BREATHE_TIME); hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 513 function findRowInReducedView (key, rows) { if(rows.length > 1) { - var midpoint = Math.floor(rows.length / 2); - var row = rows[midpoint]; - if(key < row.key) + var midpoint = Math.floor(rows.length / 2), + row = rows[midpoint], + row_key = row.key; + + if(key < row_key) return findRowInReducedView(key, rows.slice(0, midpoint)); hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 519 - if(key > row.key) - return midpoint + findRowInReducedView(key, rows.slice(midpoint)); - return row.key === key ? midpoint : -1; - } - - return rows[0].key === key ? 0 : -1; + if(key > row_key) { + var p = findRowInReducedView(key, rows.slice(midpoint)) + return p === -1 ? -1 : midpoint + p; + } + return midpoint; + } else + return rows[0] && rows[0].key === key ? 0 : -1; } /** hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 544 this.rows = []; var keyRows = []; - this._include = function (mapResult) { + this._include = function (mapResult) { var mapKeys = mapResult.keys, mapDict = mapResult.dict; hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 570 value: item.values[j] }; - if(has_key) + if(has_key && this.rows[ki]) newRows.shift(); this.rows = this.rows.concat(newRows); } hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 575 - this.rows.sort(idSort); + this.rows.sort(keySort); var keys = []; keyRows = []; hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 587 pos: keys.push(key) - 1 }); } - - //delete keys; }; /** hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 616 return this; } -function idSort (a, b) { - a = a.id; - b = b.id; - - if(a < b) return -1; - if(a > b) return 1; - return 0; -} - function findRowInMappedView (key, keyRows) { hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 617 - if (keyRows.length > 1) { + if(keyRows.length > 1) { var midpoint = Math.floor(keyRows.length / 2); var keyRow = keyRows[midpoint]; hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 620 - if (key < keyRow.key) + if(key < keyRow.key) return findRowInMappedView(key, keyRows.slice(0, midpoint)); hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 622 - if (key > keyRow.key) + if(key > keyRow.key) return findRowInMappedView(key, keyRows.slice(midpoint)); return keyRow ? keyRow.pos : -1; } else hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 646 * - worker (Object): reference to Web worker implementation. Defaults to `window.Worker`. **/ function WebWorkerMapReducer(numWorkers, Worker) { - if (!Worker) + if(!Worker) Worker = window.Worker; var pool = []; hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 666 }; } - for (var i = 0; i < numWorkers; i++) + for(var i = 0; i < numWorkers; i++) pool.push(new MapWorker(i)); this.map = function WWMR_map(map, dict, progress, chunkSize, finished) { hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 676 mapDict = {}; function getNextChunk() { - if (keys.length) { + if(keys.length) { var chunkKeys = keys.slice(0, chunkSize), chunk = {}, n = chunkKeys.length; hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 697 function nextJob(mapWorker) { var chunk = getNextChunk(); - if (chunk) { - mapWorker.map( - map, - chunk, - function jobDone(aMapDict) { - for (var name in aMapDict) - if (name in mapDict) { - var item = mapDict[name]; - item.keys = item.keys.concat(aMapDict[name].keys); - item.values = item.values.concat(aMapDict[name].values); - } else - mapDict[name] = aMapDict[name]; + if(chunk) { + mapWorker.map(map, chunk, function jobDone(aMapDict) { + for(var name in aMapDict) + if(name in mapDict) { + var item = mapDict[name]; + item.keys = item.keys.concat(aMapDict[name].keys); + item.values = item.values.concat(aMapDict[name].values); + } else + mapDict[name] = aMapDict[name]; hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 707 - if (keys.length) - progress("map", - (size - keys.length) / size, - function() { nextJob(mapWorker); }); - else - workerDone(); - }); + if(keys.length) + progress("map", + (size - keys.length) / size, + function() { nextJob(mapWorker); } + ); + else + workerDone(); + }); } else workerDone(); } hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 727 function allWorkersDone() { var mapKeys = []; - for (var name in mapDict) + for(var name in mapDict) mapKeys.push(name); mapKeys.sort(); finished({dict: mapDict, keys: mapKeys}); hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 733 } - for (var i = 0; i < numWorkers; i++) + for(var i = 0; i < numWorkers; i++) nextJob(pool[i]); }; hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 823 do { var key = mapKeys[i], - item = mapDict[key] + item = mapDict[key]; rows.push({ key: key, hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 844 } }; -da.db.BrowserCouch = BrowserCouch; -da.db.BrowserCouch.Dictionary = Dictionary; -da.db.SingleThreadedMapReducer = SingleThreadedMapReducer; -da.db.WebWorkerMapReducer = WebWorkerMapReducer; +da.db.BrowserCouch = BrowserCouch; +da.db.BrowserCouch.Dictionary = Dictionary; +da.db.SingleThreadedMapReducer = SingleThreadedMapReducer; +da.db.WebWorkerMapReducer = WebWorkerMapReducer; })(); hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 137 this[cache_key] = owner; callback(owner); - - /* - DocumentTemplate.find({ - properties: { - id: this.doc[key + "_id"], - type: this.belongsTo[key] - }, - - onSuccess: function (doc) { - this[cache_key] = doc; - callback(doc); - }.bind(this), - - onFailure: callback - }, this.constructor.db()); - */ } else if(key in this.hasMany) { var relation = this.hasMany[key], hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 139 - props = {type: relation[0]}; + props = {type: relation[0]}; props[relation[1]] = this.id; hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 159 * - properties (Object): updated properties. * fires propertyChange **/ - set: function (properties) { - if(arguments.length == 2) { + set: function (properties, value) { + if(typeof value !== "undefined") { var key = properties; properties = {}; hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 163 - properties[key] = arguments[1]; + properties[key] = value; } $extend(this.doc, properties); hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 178 * fires propertyRemove **/ remove: function (property) { - if(property !== "_id") + if(property !== "id") delete this.doc[property]; this.fireEvent("propertyRemove", [property, this]); hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 217 * da.db.DocumentTemplate#destroy([callback]) -> this * - callback (Function): function called after `destroy` event. * - * Removes all document's properties except for `id` and adds `_deleted` property. + * Destroys the document. + * + * #### Notes + * The document won't be completely destroyed from the db, all of its properties + * will be deleted, but it will get a `_deleted` property set to `true`. **/ destroy: function (callback) { hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 224 - for(var property in this.doc) - delete this.doc[property]; + for(var prop in this.doc) + delete this.doc[prop]; this.doc.id = this.id; this.doc._deleted = true; hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 234 this.fireEvent("destroy", [this]); if(callback) callback(this); - }); + }.bind(this)); return this; } hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 352 template.find = function (options) { options.properties.type = type; - if(options.id) + if(options.properties.id) template.findById(options.id, function (doc) { if(doc) options.onSuccess([doc]); hunk ./contrib/musicplayer/src/libs/ui/Column.js 42 **/ initialize: function (options) { this.setOptions(options); - if(!this.options.id) + if(!this.options.id || !this.options.id.length) this.options.id = "duC_" + (IDS++); this._populated = false; hunk ./contrib/musicplayer/src/libs/ui/Column.js 51 this._rendered = []; this._el = new Element("div", { - id: options.id + "_column", + id: this.options.id + "_column", "class": "column", styles: { overflowX: "hidden", hunk ./contrib/musicplayer/src/libs/ui/Column.js 91 // ask for it in every #render() - which can be quite expensive. this._el.addEvent("resize", function () { this._el_height = this._el.getHeight(); + this.render(); }.bind(this)); }, hunk ./contrib/musicplayer/src/libs/ui/Column.js 111 render: function () { if(!this._populated) this.populate(); - if(this._rendered.length === this.options.totalCount + 1) + if(this._rendered.length === this.options.totalCount) return false; // We're pre-fetching previous 5 and next 10 items hunk ./contrib/musicplayer/src/libs/ui/Column.js 119 var total_count = this.options.totalCount, ids = this.getVisibleIndexes(), n = Math.max(0, ids[0] - 6), - m = Math.max(Math.min(ids[1] + 10, total_count), total_count), + m = Math.min(ids[1] + 10, total_count), first_rendered = -1, box; hunk ./contrib/musicplayer/src/libs/ui/Column.js 144 if(first_rendered !== -1) { var coords = this.getBoxCoords(first_rendered); + console.log("rendering box at", this.options.id, [first_rendered, m], coords); box.setStyles({ position: "absolute", top: coords[1], hunk ./contrib/musicplayer/src/libs/ui/Column.js 164 populate: function () { var o = this.options; this._populated = true; - this._weight.setStyle("top", o.rowHeight * o.totalCount /*+ o.rowHeight*/); + this._weight.setStyle("top", o.rowHeight * o.totalCount); this._el.fireEvent("resize"); return this; hunk ./contrib/musicplayer/src/libs/ui/Column.js 171 }, /** - * da.ui.Column#rerender() -> this + * da.ui.Column#rerender() -> this | false **/ rerender: function () { if(!this._el) hunk ./contrib/musicplayer/src/libs/ui/Column.js 177 return false; - console.log("rerender", this.options.id, this._deleted); var weight = this._weight; this._el.empty(); this._el.grab(weight); hunk ./contrib/musicplayer/src/libs/ui/Column.js 219 }, /** - * da.ui.Column#getVisibleIndexes() -> Array + * da.ui.Column#getVisibleIndexes() -> [first_visible_index, last_visible_index] * * Returns an array with indexes of first and last item in visible portion of list. **/ hunk ./contrib/musicplayer/src/libs/ui/Column.js 226 getVisibleIndexes: function () { // Math.round() and Math.ceil() are used in such combination // to include items which could be only partially in viewport - var rh = this.options.rowHeight, - per_viewport = Math.round(this._el_height / rh), - first = Math.ceil(this._el.getScroll().y / rh); + var rh = this.options.rowHeight, + first = Math.ceil(this._el.getScroll().y / rh), + per_viewport = Math.round(this._el_height / rh); if(first > 0) first--; return [first, first + per_viewport]; hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 12 Implements: [Events, Options], options: { - title: null, + title: null, + closeButton: false, + show: false, + draggable: false, hideOnOutsideClick: true, hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 17 - show: false + destroyOnHide: false }, /** hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 26 * - options.hideOnOutsideClick (Boolean): if `true`, the dialog will be * hidden when the click outside the dialog element occurs (ie. on the dimmed * portion of screen) + * - options.closeButton (Boolean): toggle the close button. If `true`, the button + * will be injected at the top of `options.html`, before the title (if any). * - options.show (Boolean): if `true` the dialog will be shown immediately as it's created. * Defaults to `false`. hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 30 + * - options.draggable (Boolean): when set to `true`, the dialog will be draggable. + * There won't be a dialog wrapper, ie. the users will be able to interact with + * the content around the dialog. Defaults to `false`. + * - options.destroyOnHide (Boolean): destroy the dialog after the dialog has been hidden + * for the first time. * - options.html (Element): contents of the. * * To the `options.html` element `dialog` CSS class name will be added and hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 38 - * the element will be wrapped into a `div` with `dialog_wrapper` CSS class name. + * the element will be wrapped into a `div` with `dialog_wrapper` (or `draggable_dialog_wrapper`) CSS class name. * * If `options.title` is provided, the title element will be injected at the top of * `options.html` and will be given `dialog_title` CSS class name. hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 44 * * #### Notes - * All dialogs are hidden by default, use [[Dialog.show]] to show them immediately - * after they are created method. + * * All dialogs are hidden by default, use [[Dialog.show]] to show them immediately + * after they are created. + * * When the close button is clicked, before `hide` event is fired, a `dismiss` + * event will be fired. To cancel hiding of the dialog just throw an error from + * an listener. + * * If the dialog will be draggable, you're expected to privide a `options.title`, + * as that will be the handle. * * #### Example hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 53 - * new da.ui.Dialog({ - * title: "What's your name?" + * var hai = new da.ui.Dialog({ + * title: "Bonjur tout le monde!" * html: new Element("div", { hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 56 - * html: "Hello!" + * html: "Hai World!" * }), hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 58 - * show: true + * show: true, + * + * onHide: function () { + * hai.destroy(); + * delete hai; + * } * }); * **/ hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 71 this.setOptions(options); if(!this.options.html) throw "options.html must be provided when creating an Dialog"; - + this._el = new Element("div", { hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 73 - "class": "dialog_wrapper" + "class": this.options.draggable ? "draggable_dialog_wrapper" : "dialog_wrapper" }); if(!this.options.show) this._el.style.display = "none"; hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 78 - if(this.options.title) + if(this.options.title) { + var title; + if(typeof this.options.title === "string") hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 82 - (new Element("h2", { + title = new Element("h2", { html: this.options.title, hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 84 + href: "#", "class": "dialog_title no_selection" hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 86 - })).inject(this.options.html, "top"); + }); else if($type(this.options.title) === "element") hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 88 - this.options.title.inject(this.options.html, "top"); + title = this.options.title; + + title.inject(this.options.html, "top"); + delete title; + } + + if(this.options.closeButton) + (new Element("a", { + "class": "dialog_close no_selection", + html: "Close", + title: "Close", + events: { + click: function () { + this.fireEvent("dismiss"); + this.hide(); + }.bind(this) + } + })).inject(this.options.html, "top"); if(this.options.hideOnOutsideClick) this._el.addEvent("click", this.hide.bind(this)); hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 112 this._el.grab(options.html.addClass("dialog")); document.body.grab(this._el); + + if(this.options.draggable) + this._el.makeDraggable({ + handle: this.options.html.getElement(".dialog_title") + }); }, /** hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 143 this._el.hide(); this.fireEvent("hide", [this]); + + if(this.options.destroyOnHide) + this.destroy(); + return this; }, hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 159 /** * da.ui.Dialog#destroy() -> this - * fires hide **/ destroy: function () { hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 161 - this.hide(); - this._el.destroy(); delete this._el; delete this.options; hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 168 return this; } }); - hunk ./contrib/musicplayer/src/libs/ui/Menu.js 90 this._el = (new Element("ul")).addClass("menu").addClass("no_selection"); this._el.style.display = "none"; this._el.addEvent("click:relay(.menu_item a)", this.click.bind(this)); + this._el.addEvent("dragend:relay(.menu_item a)", this.click.bind(this)); this._id = "_menu_" + (ID++) + "_"; this.render(); hunk ./contrib/musicplayer/src/libs/ui/Menu.js 245 if(event) event.stop(); - if(event && event.target) + if(event && event.target) { this._el.position($extend({ relativeTo: event.target }, this.options.position)); hunk ./contrib/musicplayer/src/libs/ui/Menu.js 249 + } this._el.style.zIndex = 5; this._el.style.display = "block"; hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 25 if(!this._passesFilter(doc)) return false; - emit(doc.id, { + emit(doc.title, { title: doc.title || doc.id }); }, hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 73 this.view.finished = this.view.finished.bind(this); if(this.view.reduce) - this.view.reduce = this.view.reduced.bind(this); + this.view.reduce = this.view.reduce.bind(this); if(!this.view.updated && !this.view.temporary) this.view.updated = this.mapReduceUpdated; if(this.view.updated) hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 103 this.injectBottom(this.options.parentElement || document.body); if(this.options.renderImmediately !== false) this.render(); + return this; }, hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 108 /** - * da.ui.NavigationColumn#mapReduceUpdated(values) -> this + * da.ui.NavigationColumn#mapReduceUpdated(values[, forceRerender = false]) -> this * - values (Object): rows returned by map/reduce process. * * Note that this will have to re-render the whole column, as it's possible hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 115 * that one of the new documents should be rendered in the middle of already * rendered ones (due to sorting). **/ - mapReduceUpdated: function (values) { - var new_rows = $A(da.db.DEFAULT.views[this.view.id].view.rows); + mapReduceUpdated: function (values, rerender) { + var new_rows = $A(da.db.DEFAULT.views[this.view.id].view.rows), + active = this.getActiveItem(); new_rows.sort(this.compareFunction); // Noting new was added, so we can simply re-render those elements hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 121 - if(this.options.totalCount === new_rows.length) { + if(!rerender && this.options.totalCount === new_rows.length) { values = values.rows; var n = values.length, id_prefix = this.options.id + "_column_item_", hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 132 el = $(id_prefix + item.id); if(el) { index = el.retrieve("column_index"); + console.log("Rerendering item", id_prefix, index); this.renderItem(index) .addClass("column_item") hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 143 this._rows = new_rows; } else { + console.log("total count was changed, rerendering whole column", this.options.id); this.options.totalCount = new_rows.length; this._rows = new_rows; hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 146 - return this.rerender(); + this.rerender(); + } + + if(active) { + this._active_el = $(this.options.id + "_column_item_" + active.id); + this._active_el.addClass("active_column_item"); } }, hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 164 }, /** + * da.ui.NavigationColumn#getActiveItem() -> Object | undefined + **/ + getActiveItem: function () { + if(!this._active_el) + return; + + return this.getItem(this._active_el.retrieve("column_index")); + }, + + /** * da.ui.NavigationColumn#renderItem(index) -> Element * - index (Number): position of the item that needs to be rendered. * hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 227 * #### Examples * * function createFilter (item) { - * return {artist_id: item.id}; + * return {artist_id: item.id} + * } + * + * function createFilter(item) { + * var id = item.id; + * return function (doc) { + * return doc.chocolates.contains(id) + * } * } * **/ hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 239 createFilter: function (item) { - return {}; + return {}; }, click: function (event, el) { hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 248 this._active_el.removeClass("active_column_item"); this._active_el = el.addClass("active_column_item"); - this.fireEvent("click", [item, event, el]); + this.fireEvent("click", [item, event, el], 1); return item; }, hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 263 * [See meanings of return values](https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/sort#Description). **/ compareFunction: function (a, b) { - a = a.value.title; - b = b.value.title; + a = a && a.value ? a.value.title : -1; + b = b && b.value ? b.value.title : -1; if(a < b) return -1; if(a > b) return 1; hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 274 destroy: function () { this.parent(); delete this._rows; + delete this._active_el; hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 276 - if(this.view) + if(this.view && !this.view.temporary) if(this.options.killView) (this.options.db || da.db.DEFAULT).killView(this.view.id); else hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 33 * * progress_bar.toElement().setStyle("width", 100); * + * If you want your progress bar as a lovely gradient, just put a `LinearGradient` + * object to `options.foreground`. + * + * var pb = new da.ui.ProgressBar({width: 100, height: 5, foreground: "#ffa"}); + * var gradient = pb.ctx.createLinearGradient(0, 0, 0, 5); + * gradient.addColorStop(0, "#ffa"); + * gradient.addColorStop(1, "#ffe"); + * pb.options.foregound = gradient; + * gradient = null; + * **/ initialize: function (canvas, options) { this.setOptions(options); hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 125 this.ctx.fillRect(0, 0, this.progress * opts.width, opts.height); delete opts; - return this; + return this; }, /** hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 14 var SegmentedProgressBar = new Class({ /** - * new da.ui.SegmentedProgressBar(width, height, segments) + * new da.ui.SegmentedProgressBar(width, height, segments[, ticks = 0]) + * - width (Number): width of the progressbar in pixels. + * - height (Number): height of the progressbar in pixels. + * - segments (Object): names of individual progress bars and their forground + * color, see example below. + * - ticks (Number): number of 1px marks along the progress bar. * * #### Example * var mb = new da.ui.SegmentedProgressBar(100, 15, { hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 32 * * The first define progress bar will be in foreground, while * the last defined will be in background; + * + **/ + /** + * da.ui.SegmentedProgressBar.segments -> {segment1: da.ui.ProgressBar, ...} **/ hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 37 - initialize: function (width, height, segments) { + initialize: function (width, height, segments, ticks) { this._index = []; this.segments = {}; hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 40 + this.ticks = ticks; this._el = new Element("canvas"); this._el.width = width; hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 85 while(n--) this.segments[idx[n]].rerender(); + if(this.ticks) { + var inc = Math.round(this._el.width/this.ticks), + h = this._el.height; + + this.ctx.fillStyle = "rgba(255, 255, 255, 0.3)"; + //this.ctx.fillStyle = "#ddd"; + for(var n = 0, m = this._el.width; n < m; n += inc) { + if(n > 5) + this.ctx.fillRect(n, 0, 1, h); + } + this.ctx.fillStyle = "rgba(0, 0, 0, 1)"; + } + return this; }, hunk ./contrib/musicplayer/src/libs/util/ID3.js 54 _getFile: function (parser) { if(!parser) - return this.options.onFailure("fromID3"); + return this.options.onFailure("noParserFound"); this.request = new Request.Binary({ url: this.options.url, hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 22 * * TYER * * TIME * * TCON - * * USLT * * WOAR * * WXXX hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 24 + * * USLT - not parsed by default but frame decoder is present * * As well as their equivalents in ID3 v2.2 specification. * hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 46 **/ var BinaryFile = da.util.BinaryFile, CACHE = [], - GENRE_REGEXP = /^\(\d+\)/, - BE_BOM = "\xFE\xFF", - LE_BOM = "\xFF\xFE", + GENRE_REGEXP = /^\(?(\d+)\)?|(.+)/, + //BE_BOM = "\xFE\xFF", + //LE_BOM = "\xFF\xFE", UNSYNC_PAIR = /(\uF7FF\0)/g, hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 50 -FFLAGS = { - ALTER_TAG_23: 0x8000, - ALTER_FILE_23: 0x4000, - READONLY_23: 0x2000, - COMPRESS_23: 0x0080, - ENCRYPT_23: 0x0040, - GROUP_23: 0x0020, + FFLAGS = { + ALTER_TAG_23: 0x8000, + ALTER_FILE_23: 0x4000, + READONLY_23: 0x2000, + COMPRESS_23: 0x0080, + ENCRYPT_23: 0x0040, + GROUP_23: 0x0020, hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 58 - ALTER_TAG_24: 0x4000, - ALTER_FILE_24: 0x2000, - READONLY_24: 0x1000, - GROUPID_24: 0x0040, - COMPRESS_24: 0x0008, - ENCRYPT_24: 0x0004, - UNSYNC_24: 0x0002, - DATALEN_24: 0x0001 -}, + ALTER_TAG_24: 0x4000, + ALTER_FILE_24: 0x2000, + READONLY_24: 0x1000, + GROUPID_24: 0x0040, + COMPRESS_24: 0x0008, + ENCRYPT_24: 0x0004, + UNSYNC_24: 0x0002, + DATALEN_24: 0x0001 + }, FrameType = { /** hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 78 if(d.getByteAt(offset) === 1) { // Unicode is being used, and we're trying to detect Unicode BOM. // (we don't actually care if it's little or big endian) - var test_string = d.getStringAt(offset, 5), - bom_pos = test_string.indexOf(LE_BOM); - if(bom_pos === -1) - bom_pos = test_string.indexOf(BE_BOM); - if(bom_pos === -1) - window._ts = test_string; + //var test_string = d.getStringAt(offset, 5), + // bom_pos = test_string.indexOf(LE_BOM); + //if(bom_pos === -1) + // bom_pos = test_string.indexOf(BE_BOM); hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 83 - offset += bom_pos + 1; - size -= bom_pos + 1; - - console.log("Unicode BOM detected", [bom_pos, d.getStringAt(offset, size)]); + //offset += bom_pos + 1; + //size -= bom_pos + 1; hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 86 - //if(d.getByteAt(offset + 1) + d.getByteAt(offset + 2) === 255 + 254) { - // offset += 2; - // size -= 2; - //} + if(d.getByteAt(offset + 1) + d.getByteAt(offset + 2) === 255 + 254) { + console.log("Unicode BOM detected"); + offset += 2; + size -= 2; + } } return d.getStringAt(offset + 1, size - 1).strip(); hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 150 //TIME: $empty, TCON: function (offset, size) { // Genre, can be either "(123)Genre", "(123)" or "Genre". - var data = FrameType.text.call(this, offset, size); - return +((data.match(GENRE_REGEXP) || " ")[0].slice(1, -1)); + var data = FrameType.text.call(this, offset, size), + match = data.match(GENRE_REGEXP); + + if(!match) + return -1; + if(match[1]) + return +match[1]; + if(match[2]) + return match[2].strip(); + return -1; }, hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 161 - USLT: FrameType.unsyncedLyrics, + //USLT: FrameType.unsyncedLyrics, WOAR: FrameType.link, WXXX: FrameType.userLink }; hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 415 * **/ unsync: function (n, m) { - console.log("unsyncing file", this.options.url); - if(arguments.length) { var data = this.data.data, part = data hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 451 artist: f.TPE2 || f.TPE1 || f.TP2 || f.TP1 || "Unknown", track: f.TRCK || f.TRK || 0, year: f.TYER || f.TYE || 0, - genre: f.TCON || f.TCO || 0, - lyrics: f.USLT || f.ULT || "", + genre: f.TCON || f.TCO || -1, links: { official: f.WOAR || f.WXXX || f.WAR || f.WXX || "" } hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 495 * Use this method to add your own ID3v2 frame parsers. You can access this as `da.util.ID3v2Parser.addFrameParser`. * * `fn` will be called with following arguments: - * * offset - position at frame appears in data - * * size - size of the frame, including header - * + * * offset: position at frame appears in data + * * size: size of the frame, including header * hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 498 - * `this` keyword inside `fn` will refer to instance of ID3v2. + * `this` keyword inside `fn` will refer to an instance of [[da.util.ID3v2Parser]]. **/ addFrameParser: function (name, fn) { FRAMES[name] = fn; addfile ./contrib/musicplayer/src/libs/util/PlaylistExporters.js hunk ./contrib/musicplayer/src/libs/util/PlaylistExporters.js 1 +//#require +//#require + +(function () { +var Song = da.db.DocumentTemplate.Song, + SERVER = location.protocol + "//" + location.host, + XML_HEADER = '\n', + TRACKLIST_REGEXP = /tracklist/gi, + TRACKNUM_REGEXP = /tracknum/gi; + +/** + * da.util.playlistExporter.XSPF(playlist) -> String + * - playlist (da.util.Playlist + * + * #### External resources + * * [XSPF v1 specification](http://xspf.org/xspf-v1.html) + * * [XSPF Quickstart](http://xspf.org/quickstart/) + * * [XSPF Validator](http://validator.xspf.org/) - we're generating valid XSPF! + * + **/ +function XSPFExporter (playlist) { + var ids = playlist.get("song_ids"), + file = new Element("root"), + track_list = new Array(ids.length), + //track_list = new Element("trackList"), + song, artist, album, track, duration; + + for(var n = 0, m = ids.length; n < m; n++) { + song = Song.findById(ids[n]); + // getting a 'belongs to' relationship is always synchronous + song.get("artist", function (_a) { artist = _a }); + song.get("album", function (_a) { album = _a }); + // XSPF specification requires positive intergers, + // whereas we're using negative ones indicate that the value isn't present. + track = song.get("track"); + duration = song.get("duration"); + + track_list[n] = tag("track", [ + tag("location", makeURL(song)), + tag("title", song.get("title").stripTags()), + tag("creator", artist.get("title").stripTags()), + tag("album", album.get("title").stripTags()), + track < 1 ? "" : tag("trackNum", track), + duration < 1 ? "" : tag("duration", duration) + ].join("")); + } + + file.grab(new Element("playlist", { + version: 1, + xmlns: "http://xspf.org/ns/0/", + + html: [ + tag("title", playlist.get("title").stripTags()), + tag("annotation", playlist.get("description").stripTags()), + tag("trackList", track_list.join("")) + ].join("") + })); + + // As per some specification `document.createElement(tagName)`, lowercases + // tagName if the `document` is an (X)HTML document. + var output = file.innerHTML + .replace(TRACKLIST_REGEXP, "trackList") + .replace(TRACKNUM_REGEXP, "trackNum"); + + openDownloadWindow(makeDataURI("application/xspf+xml", XML_HEADER + output)); +} + +/** + * da.util.playlistExporter.M3U(playlist) -> undefined + * - playlist (da.db.DocumentTemplate.Playlist): playlist which will be exported + * + * #### Resources + * * [Wikipedia article on M3U](http://en.wikipedia.org/wiki/M3U) + * * [M3U specification](http://schworak.com/programming/music/playlist_m3u.asp) + * + **/ +function M3UExporter (playlist) { + var ids = playlist.get("song_ids"), + file = ["#EXTM3U"], + song; + + for(var n = 0, m = ids.length; n < m; n++) { + song = Song.findById(ids[n]); + song.get("artist", function (artist) { + file.push( + "#EXTINFO:-1,{0} - {1}".interpolate([artist.get("title"), song.get("title")]), + makeURL(song) + ); + }); + } + + openDownloadWindow(makeDataURI("audio/x-mpegurl", file.join("\n"))); +} + +/** + * da.util.playlistExporter.PLS(playlist) -> String + * + * #### Resources + * * [PLS article on Wikipedia](http://en.wikipedia.org/wiki/PLS_(file_format)) + **/ +function PLSExporter(playlist) { + var ids = playlist.get("song_ids"), + file = ["[playlist]", "NumberOfEntries=" + ids.length], + song; + + for(var n = 0, m = ids.length; n < m; n++) { + song = Song.findById(ids[n]); + file.push( + "File" + (n + 1) + "=" + makeURL(song), + "Title" + (n + 1) + "=" + song.get("title"), + "Length" + (n + 1) + "=" + song.get("duration") + ) + } + + file.push("Version=2"); + openDownloadWindow(makeDataURI("audio/x-scpls", file.join("\n"))); +} + +function makeURL(song) { + var named = "?@@named=" + encodeURIComponent(song.get("title")) + ".mp3"; + return [SERVER, "uri", encodeURIComponent(song.id)].join("/") + named; +} + +function makeDataURI(mime_type, data) { + var x = "data:" + mime_type + ";charset=utf-8," + encodeURIComponent(data); + return x; +} + +function openDownloadWindow(dataURI) { + var download_window = window.open(dataURI, "_blank", "width=400,height=200"); + window.wdx = download_window; + + // This allows Firefox to open the download dialog, + // while Chrome will show the blank page. + setTimeout(function () { + download_window.location = "playlist_download.html"; + download_window.onload = function () { + var dl = download_window.document.getElementById("download_link"); + dl.href = dataURI; + }; + }, 2*1000); +} + +function tag(tagName, text) { + return "<" + tagName + ">" + text + ""; +} + + +/** + * da.util.playlistExporter + * Methods for exporting playlists to other formats. + * + * #### External resources + * * [A survey of playlist formats](http://gonze.com/playlists/playlist-format-survey.html) + **/ +da.util.playlistExporter = { + XSPF: XSPFExporter, + M3U: M3UExporter, + PLS: PLSExporter +}; + +})(); addfile ./contrib/musicplayer/src/libs/util/genres.js hunk ./contrib/musicplayer/src/libs/util/genres.js 1 +//#require + +(function () { +/** + * da.util.GENRES -> [String, ...] + * List of genres defined by ID3 spec. + * + * #### Links + * * [List of genres](http://www.id3.org/id3v2.3.0#head-129376727ebe5309c1de1888987d070288d7c7e7) + **/ +da.util.GENRES = [ + "Blues","Classic Rock","Country","Dance","Disco","Funk","Grunge","Hip-Hop","Jazz", + "Metal","New Age","Oldies","Other","Pop","R&B","Rap","Reggae","Rock","Techno", + "Industrial","Alternative","Ska","Death Metal","Pranks","Soundtrack","Euro-Techno", + "Ambient","Trip-Hop","Vocal","Jazz+Funk","Fusion","Trance","Classical","Instrumental", + "Acid","House","Game","Sound Clip","Gospel","Noise","AlternRock","Bass","Soul","Punk", + "Space","Meditative","Instrumental Pop","Instrumental Rock","Ethnic","Gothic", + "Darkwave","Techno-Industrial","Electronic","Pop-Folk","Eurodance","Dream", + "Southern Rock","Comedy","Cult","Gangsta","Top 40","Christian Rap","Pop/Funk", + "Jungle","Native American","Cabaret","New Wave","Psychadelic","Rave","Showtunes", + "Trailer","Lo-Fi","Tribal","Acid Punk","Acid Jazz","Polka","Retro","Musical", + "Rock & Roll","Hard Rock","Folk","Folk-Rock","National Folk","Swing","Fast Fusion", + "Bebob","Latin","Revival","Celtic","Bluegrass","Avantgarde","Gothic Rock", + "Progressive Rock","Psychedelic Rock","Symphonic Rock","Slow Rock","Big Band", + "Chorus","Easy Listening","Acoustic","Humour","Speech","Chanson","Opera","Chamber Music", + "Sonata","Symphony","Booty Bass","Primus","Porn Groove","Satire","Slow Jam","Club","Tango", + "Samba","Folklore","Ballad","Power Ballad","Rhythmic Soul","Freestyle","Duet","Punk Rock", + "Drum Solo","A capella","Euro-House","Dance Hall" +]; +da.util.GENRES[-1] = "Unknown"; + +})(); addfile ./contrib/musicplayer/src/playlist_download.html hunk ./contrib/musicplayer/src/playlist_download.html 1 - + + + + + + Playlist download + + + + + + + Download + + Right-click on the button above and select Save Link As... + from the context menu. + + + hunk ./contrib/musicplayer/src/resources/css/app.css 2 /*** Global styles ***/ -@font-face { - font-family: Junction; - font-style: normal; - font-weight: normal; - src: local('Junction'), url('resources/fonts/Junction.ttf') format('truetype'); -} - body { font-family: 'Droid Sans', 'Lucida Grande', 'Lucida Sans', 'Bitstream Vera', sans-serif; overflow: hidden; hunk ./contrib/musicplayer/src/resources/css/app.css 5 - background: #c0c0c0; + background: #c0c0c0 url(../images/radio_pattern.png) 0 0 repeat; } a { hunk ./contrib/musicplayer/src/resources/css/app.css 13 color: inherit; } -input[type="text"], input[type="password"] { +input[type="text"], input[type="password"], textarea { border: 1px solid #ddd; border-top: 1px solid #c0c0c0; background: #fff; hunk ./contrib/musicplayer/src/resources/css/app.css 20 padding: 2px; } -input:focus, input:active { +input:focus, input:active, textarea:focus, input[type="button"]:focus, input[type="submit"]:focus, button:focus { border-color: #33519d; -webkit-box-shadow: #33519d 0 0 5px; -moz-box-shadow: #33519d 0 0 5px; hunk ./contrib/musicplayer/src/resources/css/app.css 24 - -o-box-shadow: #33519d 0 0 5px; box-shadow: #33519d 0 0 5px; } hunk ./contrib/musicplayer/src/resources/css/app.css 28 input[type="button"], input[type="submit"], button { - background: #ddd; - border: 1px transparent; + background: #ddd url(../images/selection_background.png) 0 50% repeat-x; + border: 0; border-bottom: 1px solid #c0c0c0; hunk ./contrib/musicplayer/src/resources/css/app.css 31 + border-top: 1px solid rgba(0,0,0,0); padding: 2px 7px; color: #000; text-shadow: #fff 0 1px 0; hunk ./contrib/musicplayer/src/resources/css/app.css 35 + outline: 0; -webkit-border-radius: 4px; -moz-border-radius: 4px; hunk ./contrib/musicplayer/src/resources/css/app.css 39 - -o-border-radius: 4px; border-radius: 4px; } hunk ./contrib/musicplayer/src/resources/css/app.css 42 -input[type="button"]:active, input[type="submit"]:active, button:active { - border-top: 1px solid #1e2128; - border-bottom: 0; - background: #33519d !important; +input[type="button"]:active, input[type="submit"]:active, button:active, button.active { + border-top: 1px solid #1e2128 !important; + border-bottom: 1px solid rgba(0,0,0,0) !important; + background: #33519d url(../images/selection_background_inverted.png) 0 100% repeat-x !important; color: #fff; text-shadow: #000 0 1px 1px; } hunk ./contrib/musicplayer/src/resources/css/app.css 50 +input[type="submit"] { + font-weight: bold; + font-size: 1.1em; +} + +.button_group button { + margin: 0; + border-left: 1px solid #c0c0c0; + border-top: 1px solid #ddd; + + -webkit-border-radius: 0; + -moz-border-radius: 0; + -o-border-radius: 0; + border-radius: 0; +} + +.button_group button:first-child { + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} + +.button_group button:last-child { + border-right: 1px solid #ddd; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +.button_group button:active:first-child, .button_group button.active:first-child { + border-left-color: #33519D; +} + +.button_group button:active:last-child, .button_group button.active:last-child { + border-right-color: #33519D; +} + .no_selection { -webkit-user-select: none; -moz-user-select: none; hunk ./contrib/musicplayer/src/resources/css/app.css 160 z-index: 2; } +.draggable_dialog_wrapper { + position: fixed; + top: 50px; + left: 100px; + z-index: 3; +} + .dialog { display: block; margin: 50px auto 0 auto; hunk ./contrib/musicplayer/src/resources/css/app.css 170 - background: #fff; - border: 1px solid #ddd; + border: 5px solid rgba(255, 255, 255, 0.3); + + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; -webkit-box-shadow: rgba(0, 0, 0, 0.4) 0 10px 40px; -moz-box-shadow: rgba(0, 0, 0, 0.4) 0 10px 40px; hunk ./contrib/musicplayer/src/resources/css/app.css 178 - -o-box-shadow: rgba(0, 0, 0, 0.4) 0 10px 40px; box-shadow: rgba(0, 0, 0, 0.4) 0 10px 40px; } hunk ./contrib/musicplayer/src/resources/css/app.css 181 +.draggable_dialog_wrapper .dialog { + margin: 0; +} + +iframe.dialog { + background: #000; + border: 0; +} + .dialog_title { margin: 0; padding: 5px; hunk ./contrib/musicplayer/src/resources/css/app.css 201 text-shadow: #1e2128 0 1px 0; } +.draggable_dialog_wrapper .dialog_title { + cursor: move; +} + +.dialog_close { + display: block; + float: right; + width: 16px; + height: 16px; + background: transparent url(../images/close.png) 0 100% no-repeat; + cursor: default; + margin: 10px; + text-indent: -4444px; + opacity: 0.3; +} + +.dialog_close:active { + opacity: 0.1; +} + #loader { font-size: 2em; width: 100%; hunk ./contrib/musicplayer/src/resources/css/app.css 308 background: url(bubble.png) bottom right; } +/** Drag&drop **/ +.drag_clone { + background: transparent !important; + z-index: 10 !important; +} + /*** Navigation columns ***/ .column_container { float: left; hunk ./contrib/musicplayer/src/resources/css/app.css 344 width: 100%; } -.column_container .column_header:active, .column_container .column_header:focus, .column_header.active { +.column_container .column_header:active, .column_container .column_header:focus, .column_header.active_menu { background-color: #1e2128; padding: 3px 0 1px 0; outline: 0; hunk ./contrib/musicplayer/src/resources/css/app.css 348 + -webkit-box-shadow: #000 0 3px 7px inset; } .column_header.active { hunk ./contrib/musicplayer/src/resources/css/app.css 363 .navigation_column { width: 100%; background: #fff url(../images/column_background.png) 0 0 repeat; -/* background-attachment: fixed; */ + background-attachment: scroll; z-index: 1; } hunk ./contrib/musicplayer/src/resources/css/app.css 388 background-color: #fff; } +.column_item .action { + display: block; + float: right; + margin-top: -1px; + cursor: default; +} + .navigation_column a.column_item { display: block; cursor: default; hunk ./contrib/musicplayer/src/resources/css/app.css 426 .navigation_column .active_column_item, .menu_item:hover, #next_song:hover, #prev_song:hover, #video_search_results a:active, #video_search_results a:focus { background-color: #33519d !important; + background-image: url(../images/selection_background.png); + background-position: bottom left; + background-repeat: repeat-x; text-shadow: #000 0 1px 0; color: #fff !important; outline: 0 !important; hunk ./contrib/musicplayer/src/resources/css/app.css 441 /** Albums column **/ #Albums_column { - background: #fff; + background-image: url(../images/column_background_tall.png); } #Albums_column .column_item { hunk ./contrib/musicplayer/src/resources/css/app.css 445 + height: 40px; +} + +#Albums_column .column_item img { + display: block; + float: left; + margin: -12px 0 0 0; width: 64px; height: 64px; hunk ./contrib/musicplayer/src/resources/css/app.css 454 - padding: 4px; - text-indent: 0; - border-radius: 2px; - display: inline-block; - margin: 0; + + -webkit-box-shadow: rgba(0, 0, 0, 0.5) 1px 0 5px; + -moz-box-shadow: rgba(0, 0, 0, 0.5) 1px 0 5px; + box-shadow: rgba(0, 0, 0, 0.5) 1px 0 5px; } hunk ./contrib/musicplayer/src/resources/css/app.css 460 -#Albums_column .column_item.even, #Albums_column .column_item.odd { - background: transparent; +#Albums_column .column_item span { + vertical-align: top; } hunk ./contrib/musicplayer/src/resources/css/app.css 464 -#Albums_column .column_item img { - display: block; - - -webkit-box-shadow: rgba(0, 0, 0, 0.5) 0 1px 3px; - -moz-box-shadow: rgba(0, 0, 0, 0.5) 0 1px 3px; - -o-box-shadow: rgba(0, 0, 0, 0.5) 0 1px 3px; - box-shadow: rgba(0, 0, 0, 0.5) 0 1px 3px; +/** Songs column **/ + +/** Genres column **/ +#Genres_column .column_item span { + display: inline; +} + +#Genres_column .subtitle { + float: right; + margin-right: 5px; +} + +/** Playlists column **/ +#Playlists_column .action { + display: none; + width: 16px; + height: 16px; + background: url(../images/cog.png) 50% 50% no-repeat; + text-indent: -4444px; + margin: 1px 4px 0 0; + opacity: 0.4; + cursor: default; + outline: 0; +} + +#Playlists_column .action:active { + opacity: 0.3 !important; +} + +#Playlists_column .column_item:hover .action, #Playlists_column .active_column_item .action { + display: block !important; +} + +#Playlists_column .active_column_item .action { + background-image: url(../images/cog_inverted.png); + opacity: 1; } /*** Menus ***/ hunk ./contrib/musicplayer/src/resources/css/app.css 507 display: block; text-indent: 0; margin: 0 0 0 -1px; - padding: 3px 0; + padding: 4px 0; position: fixed; background: #fff; color: #000; hunk ./contrib/musicplayer/src/resources/css/app.css 511 - min-width: 100px; + min-width: 170px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; hunk ./contrib/musicplayer/src/resources/css/app.css 518 list-style: none; cursor: default; z-index: 5; - border: 1px solid #ddd; -webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px; -moz-box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px; hunk ./contrib/musicplayer/src/resources/css/app.css 524 -o-box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px; box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - -o-border-radius: 3px; - border-radius: 3px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -o-border-radius: 4px; + border-radius: 4px; } .menu_item { hunk ./contrib/musicplayer/src/resources/css/app.css 541 color: inherit; text-decoration: none; cursor: default; + margin: 0 20px 0 0; } .menu_item .menu_separator { hunk ./contrib/musicplayer/src/resources/css/app.css 563 content: " ✔ "; } +.menu_item a.title { + background: #fff !important; + font-weight: bold; +} + +.menu_item:hover a.title { + color: #000 !important; + text-shadow: none !important; +} + +.menu_item.disabled { + color: #c0c0c0; + background: #fff !important; +} + .navigation_menu { border-top: 0; -webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px; hunk ./contrib/musicplayer/src/resources/css/app.css 586 box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px; } +.menu .button_group button { + background-color: #fff; +} + +#main_menu { + position: absolute; + top: 5px; + right: 5px; + color: #000; + cursor: default; + text-shadow: #fff 0 1px 0; + display: block; + padding: 3px; + z-index: 7; + width: 20px; + height: 20px; + text-indent: -4444px; + background: url(../images/cog.png) 50% 50% no-repeat; + + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -o-border-radius: 4px; + border-radius: 4px; +} + +#main_menu:hover { + background-color: #ddd; + + -webkit-box-shadow: #fff 0 0 4px; + -moz-box-shadow: #fff 0 0 4px; + -o-box-shadow: #fff 0 0 4px; + box-shadow: #fff 0 0 4px; +} + +#main_menu:active, #main_menu.active_menu { + background-color: #fff; + -webkit-box-shadow: #fff 0 0 6px; +} + +#main_menu.active_menu { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + /*** Settings ***/ #settings { width: 600px; hunk ./contrib/musicplayer/src/resources/css/app.css 710 #revert_settings { float: left; background: transparent; - border-bottom: 1px transparent; + border-bottom: 1px solid rgba(0, 0, 0, 0); } /** Navigation columns **/ hunk ./contrib/musicplayer/src/resources/css/app.css 735 position: fixed; top: 0; right: 0; + background: #f3f3f3 url(../images/song_details_background.png) 0 0 repeat; } #song_info_block { hunk ./contrib/musicplayer/src/resources/css/app.css 740 width: 100%; - background: #f3f3f3; +/* background: #f3f3f3 url(../images/radio_pattern.png) 0 0 repeat; + -webkit-box-shadow: rgba(0, 0, 0, 0.6) 0 1px 15px inset; */ } #song_details { hunk ./contrib/musicplayer/src/resources/css/app.css 748 font-size: 0.9em; color: #585858; float: left; - width: 300px; - padding: 10px; - margin-top: 20px; + width: 315px; + padding: 0 0 0 10px; + margin: 27px 0 0 0; cursor: default; hunk ./contrib/musicplayer/src/resources/css/app.css 752 + text-shadow: #fff 0 1px 0; } #song_details h2, #song_details span { hunk ./contrib/musicplayer/src/resources/css/app.css 775 width: 160px; max-width: 160px; overflow: hidden; - padding: 10px; + padding: 0; float: left; display: block; hunk ./contrib/musicplayer/src/resources/css/app.css 778 + margin: 12px 0 10px 5px; } #song_album_cover { hunk ./contrib/musicplayer/src/resources/css/app.css 782 - /*float: left; - display: block; - margin: 10px;*/ - max-width: 150px; - border: 1px solid #fff; + max-width: 140px; hunk ./contrib/musicplayer/src/resources/css/app.css 784 - -webkit-box-shadow: #c0c0c0 0 3px 5px; - -moz-box-shadow: #c0c0c0 0 3px 5px; - -o-box-shadow: #c0c0c0 0 3px 5px; - box-shadow: #c0c0c0 0 3px 5px; + -webkit-box-shadow: #000 0 1px 3px; + -moz-box-shadow: #000 0 1px 3px; + -o-box-shadow: #000 0 1px 3px; + box-shadow: #000 0 1px 3px; hunk ./contrib/musicplayer/src/resources/css/app.css 789 - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - -o-border-radius: 3px; - border-radius: 3px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -o-border-radius: 4px; + border-radius: 4px; } #song_title { hunk ./contrib/musicplayer/src/resources/css/app.css 816 #play_button_wrapper { display: inline-block; - width: 40px; - height: 40px; + width: 41px; + height: 41px; vertical-align: middle; } hunk ./contrib/musicplayer/src/resources/css/app.css 833 } #play_button:active, #play_button:focus, #play_button.active { - background-position: 0 -40px; + background-position: 0 -41px; } #play_button.active:active, #play_button.active:focus { hunk ./contrib/musicplayer/src/resources/css/app.css 837 - background-position: 0 -80px; + background-position: 0 -82px; } #next_song, #prev_song { hunk ./contrib/musicplayer/src/resources/css/app.css 888 #next_song:hover { background-image: url(../images/next_active.png); + background-position: 0% 50%; + background-repeat: no-repeat; } #prev_song { hunk ./contrib/musicplayer/src/resources/css/app.css 902 #prev_song:hover { background-image: url(../images/previous_active.png); + background-position: 100% 50%; + background-repeat: no-repeat; +} + +#song_position { + font-size: 0.8em; + text-align: center; + width: 100px; + position: relative; + top: 10px; + left: -100px; } /*** Song Context ***/ hunk ./contrib/musicplayer/src/resources/css/app.css 917 #context_tabs { - background: #ddd; +/* + background: #ddd url(../images/song_details_background.png) 0 0 repeat; border: 1px solid #c0c0c0; border-left: 0; border-right: 0; hunk ./contrib/musicplayer/src/resources/css/app.css 922 + -webkit-box-shadow: #c0c0c0 0px -1px 7px inset; +*/ + background: #fff; + border-top: 1px solid #c0c0c0; width: 100%; text-align: center; padding: 0; hunk ./contrib/musicplayer/src/resources/css/app.css 930 text-shadow: #fff 0 1px 0; - - -webkit-box-shadow: #c0c0c0 0px -1px 7px inset; + margin-top: 5px; } hunk ./contrib/musicplayer/src/resources/css/app.css 933 -#context_tabs a.tab { - text-decoration: none; - display: inline-block; - cursor: default; - padding: 2px 7px; - margin: 0 0 -1px 0; - outline: 0; - border-left: 1px solid #c0c0c0; -} hunk ./contrib/musicplayer/src/resources/css/app.css 934 -#context_tabs a.tab:last-child { - border-right: 1px solid #c0c0c0; +#context_tabs button { + position: relative; + top: -10px; + z-index: 2; + border-top: 1px solid #c0c0c0; + background-color: #f3f3f3; } hunk ./contrib/musicplayer/src/resources/css/app.css 942 -#context_tabs a.tab:active, #context_tabs a.tab:focus, #context_tabs a.tab.active { - -webkit-box-shadow: #c0c0c0 0px 1px 5px inset; - -moz-box-shadow: #c0c0c0 0px 1px 5px inset; - -o-box-shadow: #c0c0c0 0px 1px 5px inset; - box-shadow: #c0c0c0 0px 1px 5px inset; +#context_tabs button:first-child { + border-left-color: #c0c0c0; } hunk ./contrib/musicplayer/src/resources/css/app.css 946 -#context_tabs a.tab.active { - background: #fff; +#context_tabs button:last-child { + border-right-color: #c0c0c0; } #song_context { hunk ./contrib/musicplayer/src/resources/css/app.css 952 overflow: auto; + background: #fff; } #song_context_loading { hunk ./contrib/musicplayer/src/resources/css/app.css 961 text-align: center; padding-top: 20px; font-size: 1.5em; + z-index: 1 !important; } /** Artist context **/ hunk ./contrib/musicplayer/src/resources/css/app.css 1101 margin: auto; } -#recommended_songs li:nth-child(2n+1), #video_search_results li:nth-child(2n+1) { +#recommended_songs li:nth-child(odd), #video_search_results li:nth-child(odd) { background: #f3f3f3; } hunk ./contrib/musicplayer/src/resources/css/app.css 1152 float: left; width: 360px; } + +#youtube_music_video { + /* fun fact: Chromium won't play