7 patches for repository /home/josip/bin/tahoe-tmp: Mon Jul 12 16:26:21 CEST 2010 josip.lisec@gmail.com * add-navigation-and-settings-tests Added tests for da.controller.Navigation, da.controller.Settings and da.controller.CollectionScanner. Tue Jul 13 20:11:06 CEST 2010 josip.lisec@gmail.com * add-progress-bars Added da.ui.ProgressBar and da.ui.SegmentedProgressBar classes, the latter one will be used mainly for visualizing progress of currently playing song and load percentage. Mon Jul 19 14:13:57 CEST 2010 josip.lisec@gmail.com * player-interface * Fixed bugs in (Segmented)ProgressBar * Added basic player controls * Added album cover fetcing via Last.fm * Reorganised the way external libraries are being imported * Fixed tests Sun Jul 25 11:46:52 CEST 2010 josip.lisec@gmail.com * add-controller-tests Added tests for: * da.ui.Dialog * da.controller.CollectionScanner * da.controller.Settings Wed Jul 28 15:19:19 CEST 2010 josiplisec@gmail.com * add-player-controls * Added player controls (previos/play/next) with tests. * Made other, smaller, improvements to da.ui.NavigationColumn (smarter re-rendering) * Limited both scanner and indexer workers to allow only one request to Tahoe-LAFS, thus making network I/O almoast synchronous, mainly due to the fact that Tahoe never completes requests under high load. Sun Aug 1 01:12:11 CEST 2010 josip.lisec@gmail.com * updated-docs * Added NOTES file with instructions for running the tests. * Fixed typos in INSTALL Sun Aug 1 01:13:16 CEST 2010 josip.lisec@gmail.com * added-songcontext-and-services * Added da.controller.SongContext with default contexts: * 'Artist' - shows basic information about the artist the currently playing song, * 'Recommendations' - shows similar artists and songs, * 'Music Videos' - presents search results from YouTube of currently playing song. * Added da.service.* APIs which are used by contexts above * da.service.lastFm - interface to the Last.fm services * da.service.artistInfo * da.service.recommendations * da.service.musicVideo * UTF-8 related fixes to ID3v2 parser New patches: [add-navigation-and-settings-tests josip.lisec@gmail.com**20100712142621 Ignore-this: 559424eabbd88c496d69d076f2fcd5de Added tests for da.controller.Navigation, da.controller.Settings and da.controller.CollectionScanner. ] { hunk ./contrib/musicplayer/manage.py 274 subprocess.call(args) +class VerifyTests(Command): + description = 'runs syntax checks on tests' + user_options = [] + + def initialize_options(self): + pass + def finalize_options(self): + pass + + def run(self): + args = [ + 'java', + '-jar', CLOSURE_COMPILER_PATH, + '--js_output_file', '_tmp.out', + '--dev_mode', 'START_AND_END'] + + for filename in os.listdir('tests'): + if filename.endswith('.js'): + args.append('--js') + args.append('tests/' + filename) + + r = subprocess.call(args) + if r == 0: + print 'Everything seems to be okay! Yay!' + + os.remove('_tmp.out') + setup( name = 'tahoe-music-player', cmdclass = { hunk ./contrib/musicplayer/manage.py 307 'build': Build, 'install': Install, 'watch': Watch, + 'tests': VerifyTests, 'docs': Docs } ) hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 43 checkpoints: ["scanner", "indexer"], onFinish: function () { this.finished = true; - this.terminate() + this.destroy() }.bind(this) }) }, hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 52 * CollectionScanner#finished -> true | false **/ finished: false, - - /** - * CollectionScanner#terminate() -> undefined - * - * Instantly kills both workers. - **/ - terminate: function () { - this.indexer.terminate(); - this.scanner.terminate(); - }, - + onScannerMessage: function (event) { var cap = event.data; if(cap === "**FINISHED**") { hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 94 artist_id: artist_id, album_id: album_id }); + + delete links; } }); }, hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 126 links.checkpoint("artist"); } }); + }, + + /** + * CollectionScanner#destroy() -> undefined + * + * Instantly kills both workers. + **/ + destroy: function () { + this.indexer.terminate(); + this.scanner.terminate(); + + delete this.indexer; + delete this.scanner; + delete this.goal; } }); hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 146 var SCANNER; /** * da.controller.CollectionScanner + * Public interface of [[CollectionScanner]]. **/ da.controller.CollectionScanner = { /** hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 152 * da.controller.CollectionScanner.scan() -> undefined * Starts scanning music directory - * - * Part of public API. **/ scan: function (cap) { hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 154 - if(!SCANNER || (SCANNER && SCANNER.finished)) + if(!SCANNER || (SCANNER && SCANNER.finished)) { + delete SCANNER; SCANNER = new CollectionScanner(cap); hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 157 + } }, /** hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 162 * da.controller.CollectionScanner.isScanning() -> true | false - * - * Part of public API. **/ isScanning: function () { return SCANNER && !SCANNER.finished; hunk ./contrib/musicplayer/src/controllers/Navigation.js 76 if(!(this.menu = options.menu)) this.createMenu(); + else + this.header.store("menu", this.menu); hunk ./contrib/musicplayer/src/controllers/Navigation.js 79 - if(this.column.constructor.filters && this.column.constructor.filters.length) - this.column.addEvent("click", this.listItemClick.bind(this)); + if(this.column.constructor.filters.length) + this.column.addEvent("click", this.createFilteredColumn.bind(this)); var first_item = this.column._el.getElement(".column_item"); if(first_item) hunk ./contrib/musicplayer/src/controllers/Navigation.js 112 href: "#" }); - this.header.addEvent("click", function (e) { + this.header.addEvent("click", function (event) { var menu = this.retrieve("menu"); if(menu) hunk ./contrib/musicplayer/src/controllers/Navigation.js 115 - menu.show(e); + menu.show(event); }); this._el.grab(this.header.grab(new Element("span", { hunk ./contrib/musicplayer/src/controllers/Navigation.js 129 /** * NavigationColumnContainer#createMenu() -> this | false * - * Creates menu for current column if it has filters. + * Creates menu for current column (if it has filters). * [[da.ui.Menu]] instance is stored to `header` element with `menu` key. **/ createMenu: function () { hunk ./contrib/musicplayer/src/controllers/Navigation.js 146 this.menu = new Menu({ items: items }); - this.menu._el.addClass("navigation_menu"); this.header.store("menu", this.menu); hunk ./contrib/musicplayer/src/controllers/Navigation.js 149 - this.menu.addEvent("show", function () { + this.menu.addEvent("show", function () { var header = this.header; header.addClass("active"); hunk ./contrib/musicplayer/src/controllers/Navigation.js 152 - header.retrieve("menu")._el.style.width = header.getWidth() + "px"; + // adjusting menu's width to the width of the header + header.retrieve("menu").toElement().style.width = header.getWidth() + "px"; }.bind(this)); this.menu.addEvent("hide", function () { hunk ./contrib/musicplayer/src/controllers/Navigation.js 161 }.bind(this)); if(filters && filters.length) - this.menu.addEvent("click", this.menuItemClick.bind(this.parent_column || this)); + this.menu.addEvent("click", this.replace.bind(this.parent_column || this)); return this; }, hunk ./contrib/musicplayer/src/controllers/Navigation.js 167 /** - * NavigationColumnContainer#menuItemClick(filterName, event, element) -> undefined + * NavigationColumnContainer#replace(filterName[, event][, element]) -> undefined * - filterName (String): id of the menu item. * - event (Event): DOM event. hunk ./contrib/musicplayer/src/controllers/Navigation.js 170 - * - element (Element): clicked `Element`. + * - element (Element): clicked menu item. * * Function called on menu click. If `filterName` is name of an actual filter then * list in current column is replaced with a new one (provided by that filter). hunk ./contrib/musicplayer/src/controllers/Navigation.js 175 **/ - menuItemClick: function (filter_name, event, element) { + replace: function (filter_name, event, element) { if(!Navigation.columns[filter_name]) return; hunk ./contrib/musicplayer/src/controllers/Navigation.js 179 - var parent = this.filter_column._el, - header = this.filter_column.header, - menu = this.filter_column.menu; + var parent = this.filter_column._el, + header = this.filter_column.header, + menu = this.filter_column.menu, + filter = this.filter_column.column.options.filter; // we need to keep the menu and header, since hunk ./contrib/musicplayer/src/controllers/Navigation.js 185 - // all we need to do is to replace the list + // all we need to do is to replace the list. + // null-ifying those properties will make sure that + // filter_column's destroy won't destroy them this.filter_column.menu = null; this.filter_column._el = null; this.filter_column.destroy(); hunk ./contrib/musicplayer/src/controllers/Navigation.js 194 this.filter_column = new NavigationColumnContainer({ columnName: filter_name, - filter: this.filter_column.column.options.filter, - container: parent, - header: header, - menu: menu + filter: filter, + container: parent, + header: header, + menu: menu }); hunk ./contrib/musicplayer/src/controllers/Navigation.js 199 - + if(menu.last_clicked) menu.last_clicked.removeClass("checked"); hunk ./contrib/musicplayer/src/controllers/Navigation.js 202 - element.addClass("checked"); + if(element) + element.addClass("checked"); hunk ./contrib/musicplayer/src/controllers/Navigation.js 205 - header.getElement(".column_title").empty().appendText(filter_name); + header.getElement(".column_title").set("text", filter_name); }, /** hunk ./contrib/musicplayer/src/controllers/Navigation.js 209 - * NavigationColumnContainer#listItemClick(item) -> undefined + * NavigationColumnContainer#createFilteredColumn(item) -> undefined * - item (Object): clicked item. * hunk ./contrib/musicplayer/src/controllers/Navigation.js 212 - * Creates a new column after this one with applied filter. + * Creates a new column after (on the right) this one with applied filter which + * is generated using the data of the clicked item. **/ hunk ./contrib/musicplayer/src/controllers/Navigation.js 215 - listItemClick: function (item) { + createFilteredColumn: function (item) { if(this.filter_column) this.filter_column.destroy(); hunk ./contrib/musicplayer/src/controllers/Navigation.js 228 /** * NavigationColumnContainer#destroy() -> this * - * Destroys this column (including menu). + * Destroys this column (including menu and header). * Removes itself from [[da.controller.Navigation.activeColumns]]. **/ destroy: function () { hunk ./contrib/musicplayer/src/controllers/Navigation.js 240 this.menu.destroy(); delete this.menu; } - this.column.destroy(); - delete this.column; + if(this.column) { + this.column.destroy(); + delete this.column; + } if(this._el) { this._el.destroy(); delete this._el; hunk ./contrib/musicplayer/src/controllers/Navigation.js 249 } - var index = Navigation.activeColumns.indexOf(this); - delete Navigation.activeColumns[index]; - Navigation.activeColumns = Navigation.activeColumns.clean(); + Navigation.activeColumns.erase(this); return this; }, hunk ./contrib/musicplayer/src/controllers/Navigation.js 271 * da.controller.Navigation.columns * * Contains all known columns. - * + * * #### Notes * Use [[da.controller.Navigation.registerColumn]] to add new ones, * *do not* add them manually. hunk ./contrib/musicplayer/src/controllers/Navigation.js 281 /** * da.controller.Navigation.activeColumns -> [NavigationColumnContainer, ...] * - * Array of currently active columns. + * Array of currently active (visible) columns. * The first column is always [[da.controller.Navigation.columns.Root]]. **/ activeColumns: [], hunk ./contrib/musicplayer/src/controllers/Navigation.js 304 root_column.filter_column = artists_column; root_column.header = artists_column.header; - this._header_height = root_column.header.getHeight(); + this._header_height = artists_column.header.getHeight(); window.addEvent("resize", function () { var columns = Navigation.activeColumns, n = columns.length, hunk ./contrib/musicplayer/src/controllers/Navigation.js 344 this.columns[name] = col; if(name !== "Root") this.columns.Root.filters.push(name); + + // TODO: If Navigation is initialized + // then Root's menu has to be updated. } }; hunk ./contrib/musicplayer/src/controllers/Settings.js 266 } }); +// Settings dialog (DOM nodes) will be destroyed +// a minute after last da.controller.Settings.hide() call. +// Freeing up memory. +var destroy_timeout; +function destroySettingsDialog () { + Settings.initialized = false; + + Settings.column.destroy(); + Settings.dialog.destroy(); + + delete Settings.column; + delete Settings.dialog; + delete Settings._el; + delete Settings._controls; +} + /** * da.controller.Settings * hunk ./contrib/musicplayer/src/controllers/Settings.js 296 * The description will be displayed at the top of settings dialog. **/ registerGroup: function (config) { - GROUPS[name] = config; + GROUPS.push(config); + return this; }, hunk ./contrib/musicplayer/src/controllers/Settings.js 303 /** * da.controller.Settings.addRenderer(name, renderer) -> this - * - name (String): name of the renderer. [[da.db.DocumentTemplate.Setting]] uses this in `representAs` property. + * - name (String): name of the renderer. + * [[da.db.DocumentTemplate.Setting]] uses this in `representAs` property. * - renderer (Function): function which renderes specific setting. * * As first argument `renderer` function takes [[Setting]] object, hunk ./contrib/musicplayer/src/controllers/Settings.js 311 * while the second one is the result of [[da.db.DocumentTemplate.Setting.getDetails]]. * * The function *must* return an [[Element]] with `setting_box` CSS class name. - * The very same element *must* contain another element with `setting_` id property. + * The very same element *must* contain another element whose `id` attribute + * must mach following pattern: `setting_`. ie. it should + * return something like: + * + *
+ * + * + *
+ * * That element will be passed to the serializer function. * * #### Default renderers hunk ./contrib/musicplayer/src/controllers/Settings.js 324 * * `text` - * * `numeric` (same as `text`, the only difference is in serializer) + * * `numeric` (same as `text`, the only difference is in the serializer + * which will convert value into [[Number]]) * * `password` * * `checkbox` hunk ./contrib/musicplayer/src/controllers/Settings.js 328 + * **/ addRenderer: function (name, fn) { if(!(name in RENDERERS)) hunk ./contrib/musicplayer/src/controllers/Settings.js 347 if(!(name in SERIALIZERS)) SERIALIZERS[name] = serializer; - return this; + return this; }, /** hunk ./contrib/musicplayer/src/controllers/Settings.js 356 * Shows the settings dialog. **/ show: function () { + clearTimeout(destroy_timeout); if(!Settings.initialized) Settings.initialize(); hunk ./contrib/musicplayer/src/controllers/Settings.js 372 **/ hide: function () { Settings.hide(); + destroy_timeout = setTimeout(destroySettingsDialog, 60*60*1000); }, /** hunk ./contrib/musicplayer/src/controllers/Settings.js 381 **/ showGroup: function (group) { this.show(); + var n = GROUPS.length; while(n--) if(GROUPS[n].id === group) hunk ./contrib/musicplayer/src/controllers/Settings.js 387 break; - Settings.renderGroup({id: group, value: GROUPS[n+1]}); + Settings.renderGroup({id: group, value: GROUPS[n]}); } }; hunk ./contrib/musicplayer/src/doctemplates/Artist.js 5 /** * class da.db.DocumentTemplate.Artist < da.db.DocumentTemplate * hasMany: [[da.db.DocumentTemplate.Song]] - * belongsTo: [[da.db.DocumentTemplate.Artist]] * * #### Standard properties hunk ./contrib/musicplayer/src/doctemplates/Artist.js 7 - * * `title` - name of the artist + * - title (String): name of the artist * **/ (function () { hunk ./contrib/musicplayer/src/doctemplates/Artist.js 18 hasMany: { songs: "Song" - }, - - belongsTo: { - artist: "Artist" } })); hunk ./contrib/musicplayer/src/doctemplates/Setting.js 56 properties: {id: template.id}, onSuccess: function (doc, was_created) { if(was_created) - doc.update({ + doc.update({ group_id: template.group_id, value: template.value }); hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 433 * - id (String): name of the view. **/ killView: function (id) { + delete this.views[id].view; delete this.views[id]; return this; } hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 274 if(!result.rows.length) return options.onFailure(); - var n = result.rows.length; + var n = result.rows.length, row, type; while(n--) { hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 276 - var row = result.rows[n].value, - type = DocumentTemplate[row.type]; + row = result.rows[n].value; + type = DocumentTemplate[row.type]; result.rows[n] = type ? new type(row) : row; } hunk ./contrib/musicplayer/src/libs/ui/Column.js 3 //#require "libs/ui/ui.js" +(function () { /** section: UserInterface * class da.ui.Column * implements Events, Options hunk ./contrib/musicplayer/src/libs/ui/Column.js 10 * * Widget which can efficiently display large amounts of items in a list. **/ +var IDS = 0; da.ui.Column = new Class({ Implements: [Events, Options], hunk ./contrib/musicplayer/src/libs/ui/Column.js 15 options: { - id: undefined, + id: null, rowHeight: 30, totalCount: 0, hunk ./contrib/musicplayer/src/libs/ui/Column.js 18 - renderTimeout: 120, - itemClassNames: "column_item" + renderTimeout: 120 }, /** * new da.ui.Column(options) hunk ./contrib/musicplayer/src/libs/ui/Column.js 26 * if ommited, random one will be generated. * - options.rowHeight (Number): height of an row. Defaults to 30. * - options.totalCount (Number): number of items this column has to show in total. - * - options.itemClassNames (String): CSS class names added to each item. Defaults to `column_item`. * - options.renderTimeout (Number): milliseconds to wait during the scroll before rendering * items. Defaults to 120. * hunk ./contrib/musicplayer/src/libs/ui/Column.js 41 initialize: function (options) { this.setOptions(options); if(!this.options.id) - this.options.id = "column_" + Math.uuid(5); - + this.options.id = "column_" + (IDS++); + this._populated = false; // #_rendered will contain keys of items which have been rendered. // What is a key is up to particular implementation. hunk ./contrib/musicplayer/src/libs/ui/Column.js 49 this._rendered = []; this._el = new Element("div", { - id: options.id, - 'class': 'column', + id: options.id + "_column", + "class": "column", styles: { overflowX: "hidden", overflowY: "auto", hunk ./contrib/musicplayer/src/libs/ui/Column.js 72 this._weight.injectBottom(this._el); // scroll event is fired for even smallest changes - // of scrollbars positions, since rendering items can be + // of scrollbar's position, since rendering items can be // expensive a small timeout will be set in order to save hunk ./contrib/musicplayer/src/libs/ui/Column.js 74 - // some bandwidth - the negative side is that flicker is seen + // some bandwidth - the downside is that flickering will be seen // while scrolling. var scroll_timer = null, timeout = this.options.renderTimeout, hunk ./contrib/musicplayer/src/libs/ui/Column.js 86 }); // We're caching lists' height so we won't have to - // ask for it in every #render() - which can be quite expensiv. + // ask for it in every #render() - which can be quite expensive. this._el.addEvent("resize", function () { this._el_height = this._el.getHeight(); }.bind(this)); hunk ./contrib/musicplayer/src/libs/ui/Column.js 117 ids = this.getVisibleIndexes(), n = Math.max(0, ids[0] - 6), m = Math.max(Math.min(ids[1] + 10, total_count), total_count), - box = new Element("div", {"class": "column_items_box"}), - item_class = this.options.itemClassNames, - first_rendered = null; + first_rendered = -1, + box; for( ; n < m; n++) { if(!this._rendered.contains(n)) { hunk ./contrib/musicplayer/src/libs/ui/Column.js 122 - // First item in viewport could be already rendered - // this helps minimizing amount of DOM nodes that will be inserted - if(first_rendered === null) + // First item in viewport could be already rendered, + // by detecting the first item we're 'gonna render + // helps minimizing amount of DOM nodes that will be inserted + // (and avoids duplicaton). + if(first_rendered === -1) { first_rendered = n; hunk ./contrib/musicplayer/src/libs/ui/Column.js 128 + box = new Element("div", {"class": "column_items_box"}); + } hunk ./contrib/musicplayer/src/libs/ui/Column.js 131 - this.renderItem(n).addClass(item_class).store("column_index", n).injectBottom(box); + this.renderItem(n) + .addClass("column_item") + .store("column_index", n) + .injectBottom(box); this._rendered.push(n); } } hunk ./contrib/musicplayer/src/libs/ui/Column.js 139 - if(first_rendered !== null) { + if(first_rendered !== -1) { var coords = this.getBoxCoords(first_rendered); box.setStyles({ position: "absolute", hunk ./contrib/musicplayer/src/libs/ui/Column.js 143 - top: coords[0], - left: coords[1] + top: coords[0], + left: coords[1] }).injectBottom(this._el); } hunk ./contrib/musicplayer/src/libs/ui/Column.js 245 destroy: function (dispose) { this._el.destroy(); delete this._el; + + this._weight.destroy(); + delete this._weight; + return this; }, hunk ./contrib/musicplayer/src/libs/ui/Column.js 259 return this._el; } }); + +})(); + hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 19 /** * new da.ui.Dialog(options) - * - options.title (String): title of the dialog. optional. - * - options.hideOnOutsideClick (Boolean): if `true`, the dialog will be hidden when - * click outside the dialog element (ie. on the dimmed portion of screen) occurs. + * - options.title (String | Element): title of the dialog. optional. + * - 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.show (Boolean): if `true` the dialog will be shown immediately as it's created. * Defaults to `false`. * - options.html (Element): contents of the. hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 79 * fires show **/ show: function () { + if(this._el.style.display !== "none") + return this; + this._el.show(); this.fireEvent("show", [this]); return this; hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 88 }, /** - * da.ui.Dialog#hide(event) -> this + * da.ui.Dialog#hide([event]) -> this * fires hide **/ hide: function (event) { hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 92 - if(event && event.target !== this._el) + if((event && event.target !== this._el) + || this._el.style.display === "none") return this; hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 95 - + this._el.hide(); this.fireEvent("hide", [this]); return this; hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 101 }, + /** + * da.ui.Dialog#toElement() -> Element + **/ + toElement: function () { + return this._el; + }, + /** * da.ui.Dialog#destroy() -> this hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 110 + * fires hide **/ hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 112 - destory: function () { - this.options.html.destroy(); + destroy: function () { + this.hide(); + this._el.destroy(); hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 116 + delete this._el; + delete this.options; + return this; } }); hunk ./contrib/musicplayer/src/libs/ui/Menu.js 4 //#require "libs/ui/ui.js" (function () { -var VISIBLE_MENU; - /** section: UserInterface * class da.ui.Menu * implements Events, Options hunk ./contrib/musicplayer/src/libs/ui/Menu.js 61 * * [MooTools Element class](http://mootools.net/docs/core/Element/Element#Element:constructor) **/ +var VISIBLE_MENU, ID = 0; da.ui.Menu = new Class({ Implements: [Events, Options], 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.injectBottom(document.body); + this._id = "_menu_" + (ID++) + "_"; this.render(); }, hunk ./contrib/musicplayer/src/libs/ui/Menu.js 129 var options = this.options.items[id], el; if(typeof options === "function") - el = options(this).addClass("menu_item"); + el = options(id).addClass("menu_item"); else el = new Element("li").grab(new Element("a", options)); hunk ./contrib/musicplayer/src/libs/ui/Menu.js 132 - + + el.id = this._id + id; + return el.addClass("menu_item").store("menu_key", id); }, hunk ./contrib/musicplayer/src/libs/ui/Menu.js 157 * If `value` is an [[Object]] then it will be passed as second argument to MooTools's [[Element]] class. * If `value` is an [[Function]] then it has return an [[Element]], * first argument of the function is id of the item that needs to be rendered. + * + * #### Notes + * `id` attribute of the element will be overwritten in both cases. + * *Do not* depend on them in your code. **/ addItem: function (id, value) { this.options.items[id] = value; hunk ./contrib/musicplayer/src/libs/ui/Menu.js 180 }, /** + * da.ui.Menu#getItem(id) -> Element + * - id (String): id of the item. + * + * Returns DOM representing the menu item. + * + * #### Notes + * Never overwrite `id` attribute of the element, + * as this very method relies on it. + **/ + getItem: function (id) { + return $(this._id + id); + }, + + /** * da.ui.Menu#addSeparator() -> this * * Adds separator to the menu. hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 199 destroy: function () { this.parent(); + delete this._rows; + if(this.view) if(this.options.killView) (this.options.db || da.db.DEFAULT).killView(this.view.id); hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 204 - else + else (this.options.db || da.db.DEFAULT).removeEvent("update." + this.view.id, this.view.updated); }, hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 329 } return result; + }, + + destroy: function () { + delete this.data; } }); hunk ./contrib/musicplayer/src/libs/util/ID3.js 63 }, _onFileFetched: function (data) { - if(this.parsers[0] && this.parsers[0].test(data)) - this.parser = (new this.parsers[0](data, this.options, this.request)); - else + var parser = this.parsers[0]; + if(parser && parser.test(data)) { + this.parser = (new parser(data, this.options, this.request)); + delete this.parsers; + } else this._getFile(this.parsers.shift()); hunk ./contrib/musicplayer/src/libs/util/ID3.js 69 + }, + + /** + * ID3#destory() -> undefined + **/ + destroy: function () { + if(this.parser) + this.parser.destroy(); + delete this.parser; + delete this.request; + delete this.options; } }); hunk ./contrib/musicplayer/src/libs/util/ID3v1.js 48 this.tags.year = 0; this.options.onSuccess(CACHE[this.options.url] = this.tags); + }, + + destroy: function () { + delete this.data; + delete this.options; } }); hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 375 official: f.WOAR || f.WXXX || f.WAR || f.WXX || "" } }; + }, + + /** + * ID3v2Parser#destroy() -> undefined + **/ + destroy: function () { + this.data.destroy(); + delete this.data; + delete this.options; + delete this._request; } }); hunk ./contrib/musicplayer/src/workers/indexer.js 39 uri = "/uri/" + encodeURIComponent(cap); queue++; - new da.util.ID3({ + var parser = new da.util.ID3({ url: uri, onSuccess: function (tags) { hunk ./contrib/musicplayer/src/workers/indexer.js 42 - // To avoid duplication, we're using id property (which is mandatary) to store - // read-cap, which is probably already "more unique" than Math.uuid() if(tags && typeof tags.title !== "undefined" && typeof tags.artist !== "undefined") { tags.id = cap; postMessage(tags); hunk ./contrib/musicplayer/src/workers/indexer.js 46 } - + + parser.destroy(); + delete parser; + // Not all files are reporeted instantly so it might // take some time for scanner.js/CollectionScanner.js to // report the files, maximum delay we're allowing here hunk ./contrib/musicplayer/src/workers/indexer.js 58 setTimeout(checkQueue, 1000*60*1); }, onFailure: function () { + parser.destroy(); + delete parser; + if(!--queue) setTimeout(checkQueue, 1000*60*1); } hunk ./contrib/musicplayer/src/workers/scanner.js 27 obj.get(function () { queue--; - if(obj.type === "filenode") + if(obj.type === "filenode") { + delete obj; return postMessage(obj.uri); hunk ./contrib/musicplayer/src/workers/scanner.js 30 + } var n = obj.children.length; while(n--) { hunk ./contrib/musicplayer/src/workers/scanner.js 42 scan(child); } + delete obj; if(!queue) postMessage("**FINISHED**"); }); hunk ./contrib/musicplayer/tests/initialize.js 4 windmill.jsTest.require("shared.js"); windmill.jsTest.register([ -// 'test_utils', 'test_Goal', 'test_BinaryFile', 'test_ID3', hunk ./contrib/musicplayer/tests/initialize.js 9 'test_ID3v1', 'test_ID3v2', + 'test_BrowserCouchDict', 'test_BrowserCouch', hunk ./contrib/musicplayer/tests/initialize.js 11 + 'test_BrowserCouch_tempView', + 'test_BrowserCouch_liveView', 'test_DocumentTemplate', hunk ./contrib/musicplayer/tests/initialize.js 15 - 'test_NavigationController' + 'test_CollectionScannerController', + 'test_Menu', + 'test_NavigationController', + 'test_Dialog', + 'test_SettingsController' ]); hunk ./contrib/musicplayer/tests/shared.js 31 self.frames = frames; } }, {}); + data = null; }; this.test_waitForData = { hunk ./contrib/musicplayer/tests/shared.js 55 jum.assertSameObjects(SHARED.songs[vkey].frames,self.frames); }; + this.teardown = function () { + self.parser.destroy(); + delete self.parser; + }; + return this; }; } hunk ./contrib/musicplayer/tests/shared.js 71 // catches cases when one of args is null if(!a || !b) jum.assertEquals(a, b); - - for(var prop in a) - if(a.hasOwnProperty(prop)) - if(prop in a && prop in b) - if(typeof a[prop] === "object") - jum.assertSameObjects(a[prop], b[prop]); - else - jum.assertEquals(a[prop], b[prop]); + + if($type(a) === "array") { + jum.assertTrue("arrays should be of the same length", + a.length === b.length + ); + + // we're doing this in case of of the + // objects is an 'arguments' object, + // which lack .indexOf() + a = $A(a); + b = $A(b); + + for(var n = 0, m = a.length; n < m; n++) + if(typeof a[n] === "object") + jum.assertSameObjects(a[n], b[n]); else hunk ./contrib/musicplayer/tests/shared.js 87 - jum.assertTrue("missing '" + prop +"' property", false); + jum.assertTrue("should contain '" + a[n] + "'", + b.indexOf(a[n]) !== -1 + ); + } else { + for(var prop in a) + if(a.hasOwnProperty(prop)) + if(prop in a && prop in b) + if(typeof a[prop] === "object") + jum.assertSameObjects(a[prop], b[prop]); + else + jum.assertEquals(a[prop], b[prop]); + else + jum.assertTrue("missing '" + prop +"' property", false); + } hunk ./contrib/musicplayer/tests/shared.js 102 + delete a; + delete b; return true; }; hunk ./contrib/musicplayer/tests/shared.js 106 + hunk ./contrib/musicplayer/tests/test_BinaryFile.js 8 self = this; this.setup = function () { - this.file_le = new BinaryFile("\0\0\1\0"); - this.file_be = new BinaryFile("\0\1\0\0", {bigEndian: true}); - this.bond = new BinaryFile("A\0\0\7James Bond\0"); + self.file_le = new BinaryFile("\0\0\1\0"); + self.file_be = new BinaryFile("\0\1\0\0", {bigEndian: true}); + self.bond = new BinaryFile("A\0\0\7James Bond\0"); }; this.test_options = function () { hunk ./contrib/musicplayer/tests/test_BinaryFile.js 14 - jum.assertEquals(4, this.file_le.length); - jum.assertFalse(this.file_le.bigEndian); + jum.assertEquals(4, self.file_le.length); + jum.assertFalse(self.file_le.bigEndian); hunk ./contrib/musicplayer/tests/test_BinaryFile.js 17 - jum.assertEquals(4, this.file_be.length); - jum.assertTrue(this.file_be.bigEndian); + jum.assertEquals(4, self.file_be.length); + jum.assertTrue(self.file_be.bigEndian); }; this.test_getByte = function () { hunk ./contrib/musicplayer/tests/test_BinaryFile.js 22 - jum.assertEquals(0, this.file_le.getByteAt(0)); - jum.assertEquals(1, this.file_le.getByteAt(2)); + jum.assertEquals(0, self.file_le.getByteAt(0)); + jum.assertEquals(1, self.file_le.getByteAt(2)); hunk ./contrib/musicplayer/tests/test_BinaryFile.js 25 - jum.assertEquals(0, this.file_be.getByteAt(0)); - jum.assertEquals(1, this.file_be.getByteAt(1)); + jum.assertEquals(0, self.file_be.getByteAt(0)); + jum.assertEquals(1, self.file_be.getByteAt(1)); }; this.test_getShort = function () { hunk ./contrib/musicplayer/tests/test_BinaryFile.js 30 - jum.assertEquals(0, this.file_le.getShortAt(0)); // 00 - jum.assertEquals(256, this.file_le.getShortAt(1)); // 01 - jum.assertEquals(1, this.file_le.getShortAt(2)); // 10 + jum.assertEquals(0, self.file_le.getShortAt(0)); // 00 + jum.assertEquals(256, self.file_le.getShortAt(1)); // 01 + jum.assertEquals(1, self.file_le.getShortAt(2)); // 10 hunk ./contrib/musicplayer/tests/test_BinaryFile.js 34 - jum.assertEquals(1, this.file_be.getShortAt(0)); // 01 - jum.assertEquals(256, this.file_be.getShortAt(1)); // 10 - jum.assertEquals(0, this.file_be.getShortAt(2)); // 00 + jum.assertEquals(1, self.file_be.getShortAt(0)); // 01 + jum.assertEquals(256, self.file_be.getShortAt(1)); // 10 + jum.assertEquals(0, self.file_be.getShortAt(2)); // 00 }; this.test_getLong = function () { hunk ./contrib/musicplayer/tests/test_BinaryFile.js 40 - jum.assertEquals(65536, this.file_le.getLongAt(0)); - jum.assertEquals(65536, this.file_be.getLongAt(0)); + jum.assertEquals(65536, self.file_le.getLongAt(0)); + jum.assertEquals(65536, self.file_be.getLongAt(0)); }; this.test_getBits = function () { hunk ./contrib/musicplayer/tests/test_BinaryFile.js 45 - jum.assertSameObjects([0, 1], this.file_le.getBitsAt(2, 2)); - jum.assertSameObjects([0, 0, 0, 1], this.file_be.getBitsAt(1, 4)); + jum.assertSameObjects([0, 1], self.file_le.getBitsAt(2, 2)); + jum.assertSameObjects([0, 0, 0, 1], self.file_be.getBitsAt(1, 4)); }; this.test_unpack = function () { hunk ./contrib/musicplayer/tests/test_BinaryFile.js 50 - jum.assertSameObjects(["A", 0, 0, 7], this.bond.unpack("c3i")); - jum.assertSameObjects(["James Bond"], this.bond.unpack("4x10S")); + jum.assertSameObjects(["A", 0, 0, 7], self.bond.unpack("c3i")); + jum.assertSameObjects(["James Bond"], self.bond.unpack("4x10S")); }; this.test_toEncodedString = function () { hunk ./contrib/musicplayer/tests/test_BinaryFile.js 55 - jum.assertEquals("%00%00%01%00", this.file_le.toEncodedString()); - jum.assertEquals("%00%01%00%00", this.file_be.toEncodedString()); + jum.assertEquals("%00%00%01%00", self.file_le.toEncodedString()); + jum.assertEquals("%00%01%00%00", self.file_be.toEncodedString()); + }; + + this.teardown = function () { + self.file_le.destroy(); + self.file_be.destroy(); + self.bond.destroy(); + + delete self.file_le; + delete self.file_be; + delete self.bond; }; return this; hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 4 windmill.jsTest.require("shared.js"); var test_BrowserCouchDict = new function () { - var BrowserCouch = da.db.BrowserCouch, - self = this; + var self = this; this.setup = function () { hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 7 - self.dict = new BrowserCouch.Dictionary(); + self.dict = new da.db.BrowserCouch.Dictionary(); hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 9 - this.dict.set("a", 1); - this.dict.set("b", 2); + self.dict.set("a", 1); + self.dict.set("b", 2); hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 12 - this.dict.setDocs([ + self.dict.setDocs([ {id: "c", value: 3}, {id: "d", value: 4}, {id: "a", value: 5} hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 20 }; this.test_set = function () { - jum.assertTrue(this.dict.has("a")); - jum.assertTrue(this.dict.has("b")); - jum.assertFalse(this.dict.has("x")); + jum.assertTrue(self.dict.has("a")); + jum.assertTrue(self.dict.has("b")); + jum.assertFalse(self.dict.has("x")); jum.assertSameObjects({id:"a", value: 5}, this.dict.dict.a); hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 25 - jum.assertEquals(2, this.dict.dict.b); + jum.assertEquals(2, self.dict.dict.b); }; this.test_setDocs = function () { hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 29 - jum.assertTrue(this.dict.has("c")); - jum.assertTrue(this.dict.has("d")); + jum.assertTrue(self.dict.has("c")); + jum.assertTrue(self.dict.has("d")); hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 32 - jum.assertEquals(3, this.dict.dict.c.value); - jum.assertEquals(4, this.dict.dict.d.value); + jum.assertEquals(3, self.dict.dict.c.value); + jum.assertEquals(4, self.dict.dict.d.value); }; this.test_remove = function () { hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 37 - this.dict.remove("a"); - jum.assertEquals(3, this.dict.keys.length); - jum.assertFalse(this.dict.has("a")); + self.dict.remove("a"); + jum.assertEquals(3, self.dict.keys.length); + jum.assertFalse(self.dict.has("a")); }; this.test_unpickle = function () { hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 43 - this.dict.unpickle({ + self.dict.unpickle({ x: 2.2, y: 2.3 }); hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 48 - jum.assertEquals(2, this.dict.keys.length); - jum.assertTrue(this.dict.has("x")); - jum.assertTrue(this.dict.has("y")); - jum.assertFalse(this.dict.has("a")); + jum.assertEquals(2, self.dict.keys.length); + jum.assertTrue(self.dict.has("x")); + jum.assertTrue(self.dict.has("y")); + jum.assertFalse(self.dict.has("a")); }; this.test_clear = function () { hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 55 - this.dict.clear(); + self.dict.clear(); hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 57 - jum.assertEquals(0, this.dict.keys.length); - jum.assertFalse(this.dict.has("x")); - jum.assertFalse(this.dict.has("b")); + jum.assertEquals(0, self.dict.keys.length); + jum.assertFalse(self.dict.has("x")); + jum.assertFalse(self.dict.has("b")); }; }; hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 68 self = this; this.setup = function () { - this.db = false; - this.stored = {}; + self.db = false; + self.stored = {}; BrowserCouch.get("test1", function (db) { self.db = db; hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 92 this.test_put = function () { var cb = {doc1: 0, doc2: 0, doc3: 0}; - this.db.put({id: "doc1", test: 1}, function () { cb.doc1++ }); - this.db.put({id: "doc2", test: 2}, function () { cb.doc2++ }); - this.db.put({id: "doc3", test: 3}, function () { cb.doc3++ }); - this.db.put({id: "doc1", test: 4}, function () { cb.doc1++ }); + self.db.put({id: "doc1", test: 1}, function () { cb.doc1++ }); + self.db.put({id: "doc2", test: 2}, function () { cb.doc2++ }); + self.db.put({id: "doc3", test: 3}, function () { cb.doc3++ }); + self.db.put({id: "doc1", test: 4}, function () { cb.doc1++ }); jum.assertEquals(2, cb.doc1); jum.assertEquals(1, cb.doc2); hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 108 }; this.test_wipe = function () { - jum.assertEquals(3, this.db.getLength()); - this.db.wipe(); + jum.assertEquals(3, self.db.getLength()); + self.db.wipe(); BrowserCouch.get("test1", function (db) { jum.assertEquals(0, db.getLength()); hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 152 }; this.test_map = function () { + self.mapped_docs = []; this.db.view({ temporary: true, hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 157 map: function (doc, emit) { - self.map_called++; + self.mapped_docs.push(doc.id); if(doc.nr !== 2) emit(doc.id, doc.nr); }, hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 181 } }; + this.test_verifyMapArguments = function () { + jum.assertEquals("map() should have been called three times", + 3, self.mapped_docs.length + ); + jum.assertSameObjects(["doc1", "doc2", "doc3"], self.mapped_docs); + }; + this.test_verifyMapResult = function () { var mr = self.map_result; hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 191 - jum.assertEquals(3, self.map_called); jum.assertTrue("rows" in mr); jum.assertEquals(2, mr.rows.length); jum.assertEquals("function", typeof mr.findRow); hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 244 }; this.test_verifyReduceResult = function () { - var rr = this.reduce_result; - jum.assertFalse(this.reduce_updated_called); + var rr = self.reduce_result; + jum.assertFalse(self.reduce_updated_called); jum.assertEquals("function", typeof rr.findRow); jum.assertEquals("function", typeof rr.getRow); hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 255 }; this.test_verifyReduceFindRow = function () { - var rr = this.reduce_result; + var rr = self.reduce_result; jum.assertTrue(rr.findRow("even") !== -1); jum.assertTrue(rr.findRow("odd") !== -1); hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 266 }; this.teardown = function () { - this.db.wipe(); + self.db.wipe(); }; return this; hunk ./contrib/musicplayer/tests/test_Goal.js 5 var Goal = da.util.Goal, self = this; this.test_setup = function () { - this._timestamps = {}; - this._calls = {a: 0, b: 0, c: 0, afterC: 0, success: 0, setup: 0}; - this._calls.setup++; - this._goal = new Goal({ + self.timestamps = {}; + self.calls = {a: 0, b: 0, c: 0, afterC: 0, success: 0, setup: 0}; + self.calls.setup++; + self.goal = new Goal({ checkpoints: ["a", "b", "c"], onCheckpoint: function (name) { hunk ./contrib/musicplayer/tests/test_Goal.js 12 - self._timestamps[name] = new Date(); - self._calls[name]++; + self.timestamps[name] = new Date(); + self.calls[name]++; }, onFinish: function (name) { hunk ./contrib/musicplayer/tests/test_Goal.js 17 - self._timestamps.success = new Date(); - self._calls.success++; + self.timestamps.success = new Date(); + self.calls.success++; }, afterCheckpoint: { hunk ./contrib/musicplayer/tests/test_Goal.js 23 c: function () { - self._timestamps.afterC = new Date(); - self._calls.afterC++; + self.timestamps.afterC = new Date(); + self.calls.afterC++; } } }); hunk ./contrib/musicplayer/tests/test_Goal.js 29 - this._goal.checkpoint("b"); - this._goal.checkpoint("c"); - this._goal.checkpoint("a"); + self.goal.checkpoint("b"); + self.goal.checkpoint("c"); + self.goal.checkpoint("a"); hunk ./contrib/musicplayer/tests/test_Goal.js 33 - this._goal.checkpoint("c"); - this._goal.checkpoint("b"); + self.goal.checkpoint("c"); + self.goal.checkpoint("b"); }; this.test_allEventsCalledOnce = function () { hunk ./contrib/musicplayer/tests/test_Goal.js 38 - jum.assertTrue(this._calls.a === 1); - jum.assertTrue(this._calls.b === 1); - jum.assertTrue(this._calls.c === 1); - jum.assertTrue(this._calls.afterC === 1); - jum.assertTrue(this._calls.success === 1); - jum.assertTrue(this._goal.finished); + jum.assertTrue(self.calls.a === 1); + jum.assertTrue(self.calls.b === 1); + jum.assertTrue(self.calls.c === 1); + jum.assertTrue(self.calls.afterC === 1); + jum.assertTrue(self.calls.success === 1); + jum.assertTrue(self.goal.finished); }; this.test_timestamps = function () { hunk ./contrib/musicplayer/tests/test_Goal.js 47 - jum.assertTrue(this._timestamps.b <= this._timestamps.c); - jum.assertTrue(this._timestamps.c <= this._timestamps.a); - jum.assertTrue(this._timestamps.c <= this._timestamps.afterC); + jum.assertTrue(self.timestamps.b <= self.timestamps.c); + jum.assertTrue(self.timestamps.c <= self.timestamps.a); + jum.assertTrue(self.timestamps.c <= self.timestamps.afterC); }; this.test_successCalls = function () { hunk ./contrib/musicplayer/tests/test_Goal.js 53 - jum.assertTrue(this._timestamps.success >= this._timestamps.a); - jum.assertTrue(this._timestamps.success >= this._timestamps.b); - jum.assertTrue(this._timestamps.success >= this._timestamps.c); - jum.assertTrue(this._timestamps.success >= this._timestamps.afterC); + jum.assertTrue(self.timestamps.success >= self.timestamps.a); + jum.assertTrue(self.timestamps.success >= self.timestamps.b); + jum.assertTrue(self.timestamps.success >= self.timestamps.c); + jum.assertTrue(self.timestamps.success >= self.timestamps.afterC); }; }; hunk ./contrib/musicplayer/tests/test_ID3.js 23 var self = this; this.setup = function () { - this.called_onSuccess = false; - this.called_onFailure = false; + self.called_onSuccess = false; + self.called_onFailure = false; new ID3_patched({ url: "/fake/" + Math.uuid(), hunk ./contrib/musicplayer/tests/test_ID3v1.js 10 self = this; this.setup = function () { - this.tags = {}; + self.tags = {}; SHARED.parser = new ID3v1Parser(BinaryFile.fromEncodedString(SHARED.songs.v1.data), { url: "/fake/" + Math.uuid(), hunk ./contrib/musicplayer/tests/test_Menu.js 8 this.setup = function () { self.menu = new Menu({ items: { - a: {html: "a", id: "_test_first_menu_item"}, - b: {html: "b", id: "_test_second_menu_item"}, + a: {html: "a"}, + b: {html: "b"}, _sep: Menu.separator, hunk ./contrib/musicplayer/tests/test_Menu.js 11 - c: {html: "c", id: "_test_third_menu_item"} + c: function () { + return (new Element("li")).grab(new Element("span", {html: "c"})) + } } }); }; hunk ./contrib/musicplayer/tests/test_Menu.js 18 - this.test_domNode = function () { + this.test_domNodes = function () { var el = self.menu.toElement(); jum.assertEquals("menu's element should be inserted into body of the page", hunk ./contrib/musicplayer/tests/test_Menu.js 24 el.getParent(), document.body ); - //jum.assertEquals("should have four list items", ) + + jum.assertEquals("should have four child nodes", + 4, el.getChildren().length + ); + + jum.assertEquals("item added as Object should be element", + "element", + $type(self.menu.getItem("b")) + ); + jum.assertEquals("item added as Function should be element", + "element", + $type(self.menu.getItem("c")) + ); + jum.assertEquals("separator should be also an element", + "element", + $type(self.menu.getItem("_sep")) + ); }; this.test_events = function () { hunk ./contrib/musicplayer/tests/test_Menu.js 50 jum.assertEquals("clicked items' key should be 'b'", "b", key); }); // events are synchronous - self.menu.click(null, el.getElement("li:nth-child(2)")); + self.menu.click(null, self.menu.getItem("b")); var showed = 0, hidden = 0; hunk ./contrib/musicplayer/tests/test_Menu.js 80 ); }; + this.teardown = function () { self.menu.destroy(); }; hunk ./contrib/musicplayer/tests/test_NavigationController.js 5 var Navigation = da.controller.Navigation, self = this; + this.setup = function () { + self.maps_updated = false; + // We're doing this for sorting test + da.db.DocumentTemplate.Song.findFirst({ + properties: {title: "Maps"}, + onSuccess: function (maps) { + maps.update({track: 6}, function () { + self.maps_updated = true; + }) + } + }); + }; + // We can't use da.controller.CollectionScanner.isFinished() // here because scanner worker has one minute timeout this.test_waitForCollectionScanner = { hunk ./contrib/musicplayer/tests/test_NavigationController.js 24 method: "waits.forJS", params: { js: function () { - return da.db.DEFAULT.views.Song.view.rows.length === 3 + return da.db.DEFAULT.views.Song.view.rows.length === 3 && self.maps_updated; } } }; hunk ./contrib/musicplayer/tests/test_NavigationController.js 56 ); }; + // Indirectly tests column re-rendering as all these + // items were dynamically added when CollectionScanner + // discovered them, as well as filtering. this.test_items = function () { var ac = Navigation.activeColumns, artists = ac[1].column, hunk ./contrib/musicplayer/tests/test_NavigationController.js 95 ); }; + this.test_replaceFilter = function () { + var Root = Navigation.activeColumns[0]; + Root.menu.click(null, Root.menu.getItem("Songs")); + + var container = Navigation.activeColumns[1], + column = container.column; + + jum.assertEquals("there should be only two active columns", + 2, Navigation.activeColumns.length + ); + jum.assertEquals("header should have new title", + "Songs", container.header.get('text') + ); + + jum.assertEquals("there should be three songs", + 3, column.options.totalCount + ); + + // again, indirect sorting test as 'Maps' is 6th track on the album + // but first alphabetically + jum.assertEquals("last song should be 'Maps'", + "Maps", column.getItem(2).value.title + ); + jum.assertEquals("first song should be 'Persona'", + "Persona", column.getItem(0).value.title + ); + }; + return this; }; hunk ./src/allmydata/test/test_musicplayer.py 95 }\n""" % (self.music_cap, self.settings_cap)) config.close() d.addCallback(_write_config_file) - + persona = upload.Data(b64decode(DATA['persona']), None) d.addCallback(lambda ign: self.music_node.add_file(u'persona', persona)) hunk ./src/allmydata/test/test_musicplayer.py 98 - + bigbang = upload.Data(b64decode(DATA['bigbang']), None) d.addCallback(lambda ign: self.music_node.add_file(u'bigbang', bigbang)) hunk ./src/allmydata/test/test_musicplayer.py 101 - + maps = upload.Data(b64decode(DATA['maps']), None) d.addCallback(lambda ign: self.music_node.add_file(u'maps', maps)) hunk ./src/allmydata/test/test_musicplayer.py 110 class ChromeTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Chrome): pass -# . #class FirefoxTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Firefox): hunk ./src/allmydata/test/test_musicplayer.py 111 - #pass +# pass } [add-progress-bars josip.lisec@gmail.com**20100713181106 Ignore-this: b96a74ab38924967a55383f75f888439 Added da.ui.ProgressBar and da.ui.SegmentedProgressBar classes, the latter one will be used mainly for visualizing progress of currently playing song and load percentage. ] { hunk ./contrib/musicplayer/src/index_devel.html 43 + + addfile ./contrib/musicplayer/src/libs/ui/ProgressBar.js hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 1 +//#require + +(function () { +/** section: UserInterface + * class da.ui.ProgressBar + * + * Canvas-based progress bar. + **/ +var ProgressBar = new Class({ + Implements: Options, + + options: { + width: 200, + height: 20, + foreground: "#33519d" + }, + + /** + * new da.ui.ProgressBar([canvas], options) + * - canvas (Element): already existing DOM node. + * Note that in this case, this istance of [[da.ui.ProgressBar]] won't + * monitor width/height changes. + * - options.width (Number): width of the progress bar. + * - options.height (Number): height of the progress bar. + * - options.foreground (String): colour of the progress bar. + * + * #### Notes + * To resize the progress bar after initialization use MooTools' + * `setStyle` method, since it properly fires `resize` event. + * + * progress_bar.toElement().setStyle("width", 100); + * + **/ + initialize: function (canvas, options) { + this.setOptions(options); + + if(canvas) { + this._el = canvas; + } else { + this._el = new Element("canvas"); + this._el.width = this.options.width; + this._el.height = this.options.height; + this._el.addClass("progressbar"); + + this._el.addEvent("resize", function () { + this.options.width = this._el.getWidth(); + this.options.height = this._el.getHeight(); + + this.progress -= 0.0001; + this.setProgress(this.progress + 0.0001); + }.bind(this)); + } + + this.ctx = this._el.getContext("2d"); + + this.progress = 0; + }, + + /** + * da.ui.ProgressBar#setProgress(progress) -> this + * - progress (Number): in 0-1 range. + * + * #### Notes + * Use animation with care, it should not be necessary for small changes + * especially if the progress bar is narrow. + * + **/ + setProgress: function (p) { + var current_progress = this.progress, + el_width = this.options.width, + diff = p - current_progress, + increment = diff > 0, + x, width; + + if(!diff) + return this; + + if(increment) { + x = current_progress * el_width; + width = diff * el_width; + } else { + x = (current_progress - (-diff)) * el_width; + width = (current_progress - p) * el_width; + } + + this.ctx.fillStyle = this.options.foreground; + this.ctx[increment ? "fillRect" : "clearRect"](x, 0, width, this.options.height); + this.progress = p; + return this; + }, + + /** + * da.ui.ProgressBar#rerender() -> this + **/ + rerender: function () { + this.ctx.fillStyle = this.options.foreground; + this.ctx.fillRect(0, 0, this.progress * this.options.width, this.options.height); + + return this; + }, + + /** + * da.ui.ProgressBar#toElement() -> Element + **/ + toElement: function () { + return this._el; + }, + + /** + * da.ui.ProgressBar#destroy() -> Element + **/ + destroy: function () { + this._el.destroy(); + delete this._el; + delete this.ctx; + delete this._fx; + } +}); +da.ui.ProgressBar = ProgressBar; + +})(); addfile ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 1 +//#require +//#require "libs/ui/ProgressBar.js" + +(function () { +var ProgressBar = da.ui.ProgressBar; +/** section: UserInterface + * class da.ui.SegmentedProgressBar + * + * Display multiple progress bars inside one canvas tag. + **/ +var SegmentedProgressBar = new Class({ + + /** + * new da.ui.SegmentedProgressBar(width, height, segments) + * + * #### Example + * var mb = new da.ui.SegmentedProgressBar(100, 15, { + * track: "#f00", + * load: "#f3f3f3" + * }); + * + * mb.setProgress("track", 0.2); + * mb.setProgress("load", 0.52); + * + * The first define progress bar will be in foreground, while + * the last defined will be in background; + **/ + initialize: function (width, height, bars) { + this.index = []; + + for(var bar in bars) + this.index.push(bar); + + var last_bar_name = this.index.getLast(), + last_bar = new ProgressBar(null, { + width: width, + height: height, + foreground: bars[last_bar_name] + }); + this._el = last_bar.toElement(); + this.ctx = last_bar.ctx; + + this.bars = {}; + var bar; + for(var n = 0, m = this.index.length - 1; n < m; n++) { + bar = this.index[n]; + this.bars[bar] = new ProgressBar(this._el, { + width: width, + height: height, + foreground: bars[bar] + }); + } + this.bars[last_bar_name] = last_bar; + last_bar = null; + }, + + /** + * da.ui.SegmentedProgressBar#setProgress(segment, progress) -> this | false + * - segment (String): name of the bar whose progress needs to be updated + * - progress (Number): number in 0-1 range. + **/ + setProgress: function (bar, p) { + var current_p = this.bars[bar].progress, + increment = current_p < p, + n = this.index.indexOf(bar); + + bar = this.bars[bar]; + if(!bar) + return false; + + var idx = this.index; + if(increment) { + if(this.index.indexOf(bar) == 0) + return this.setProgress(p); + + m = n + 1; + bar.setProgress(p); + while(m--) { + var b = this.bars[idx[m]]; + if(b.progress <= p) + b.rerender(); + } + } else { + m = this.index.length; + bar.setProgress(p); + + while(m--) + this.bars[idx[m]].rerender(); + } + + return this; + }, + + /** + * da.ui.SegmentedProgressBar#destroy() -> undefined + **/ + destroy: function () { + this._el.destroy(); + delete this._el; + delete this.index; + delete this.bars; + } +}); +da.ui.SegmentedProgressBar = SegmentedProgressBar; +})(); hunk ./contrib/musicplayer/src/resources/css/app.css 353 border-bottom: 1px transparent; } +#player_pane { + width: 500px; +} + hunk ./contrib/musicplayer/tests/initialize.js 19 'test_Menu', 'test_NavigationController', 'test_Dialog', - 'test_SettingsController' + 'test_SettingsController', + 'test_ProgressBar', + 'test_SegmentedProgressBar' ]); addfile ./contrib/musicplayer/tests/test_ProgressBar.js hunk ./contrib/musicplayer/tests/test_ProgressBar.js 1 - +var test_ProgressBar = new function () { + var ProgressBar = da.ui.ProgressBar, + BLACK = [0, 0, 0, 255], + RED = [255, 0, 0, 255], + TRANSPARENT = [0, 0, 0, 0], + self = this; + + this.setup = function () { + self.pb = new ProgressBar(null, { + width: 100, + height: 1, + foreground: "rgba(0, 0, 0, 255)" + }); + }; + + function getPixel(x) { + return self.pb.ctx.getImageData(x, 0, 1, 1).data + } + + this.test_incrementation = function () { + self.pb.setProgress(0.5); + + jum.assertSameObjects(BLACK, getPixel(0)); + jum.assertSameObjects(BLACK, getPixel(49)); + jum.assertSameObjects(TRANSPARENT, getPixel(100)); + + self.pb.options.foreground = "rgba(255, 0, 0, 255)"; + self.pb.setProgress(0.7); + + jum.assertSameObjects(BLACK, getPixel(0)); + jum.assertSameObjects(RED, getPixel(61)); + jum.assertSameObjects(TRANSPARENT, getPixel(100)); + }; + + this.test_decrementation = function () { + self.pb.options.foreground = "rgba(0, 255, 0, 255)"; + self.pb.setProgress(0.6); + + jum.assertSameObjects(RED, getPixel(0)); + jum.assertSameObjects(TRANSPARENT, getPixel(61)); + jum.assertSameObjects(TRANSPARENT, getPixel(70)); + }; + + this.teardown = function () { + this.pb.destroy(); + }; + + return this; +}; addfile ./contrib/musicplayer/tests/test_SegmentedProgressBar.js hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 1 - +var test_SegmentedProgressBar = new function () { + var SegmentedProgressBar = da.ui.SegmentedProgressBar, + RED = [255, 0, 0, 255], + GREEN = [ 0, 255, 0, 255], + BLUE = [ 0, 0, 255, 255], + TRANS = [ 0, 0, 0, 0], + self = this; + + this.setup = function () { + self.pb = new SegmentedProgressBar(100, 1, { + r: "#f00", + g: "#0f0", + b: "#00f" + }); + }; + + function getPixel(x) { + return self.pb.ctx.getImageData(x, 0, 1, 1).data; + } + + this.test_incrementation = function () { + var ctx = self.pb.ctx; + + self.pb.setProgress("g", 0.666); + self.pb.setProgress("r", 0.333); + self.pb.setProgress("b", 0.999); + + jum.assertSameObjects(RED, getPixel(0)); + jum.assertSameObjects(RED, getPixel(20)); + jum.assertSameObjects(RED, getPixel(34)); + + jum.assertSameObjects(GREEN, getPixel(35)); + jum.assertSameObjects(GREEN, getPixel(50)); + jum.assertSameObjects(GREEN, getPixel(65)); + + jum.assertSameObjects(BLUE, getPixel(68)); + jum.assertSameObjects(BLUE, getPixel(80)); + jum.assertSameObjects(BLUE, getPixel(98)); + }; + + this.test_incrementingMiddleSegment = function () { + self.pb.setProgress("g", 0.8); + + jum.assertSameObjects(RED, getPixel(0)); + jum.assertSameObjects(RED, getPixel(30)); + + jum.assertSameObjects(GREEN, getPixel(34)); + jum.assertSameObjects(GREEN, getPixel(66)); + jum.assertSameObjects(GREEN, getPixel(80)); + + jum.assertSameObjects(BLUE, getPixel(81)); + }; + + this.test_incrementingFirstSegment = function () { + self.pb.setProgress("r", 0.9); + + jum.assertSameObjects(RED, getPixel(0)); + jum.assertSameObjects(RED, getPixel(66)); + jum.assertSameObjects(RED, getPixel(79)); + jum.assertSameObjects(RED, getPixel(79)); + jum.assertSameObjects(BLUE, getPixel(91)); + }; + + this.test_decrementingFirstSegment = function () { + self.pb.setProgress("r", 0.7); + + jum.assertSameObjects(RED, getPixel(69)); + jum.assertSameObjects(GREEN, getPixel(71)); + jum.assertSameObjects(GREEN, getPixel(79)); + jum.assertSameObjects(BLUE, getPixel(81)); + jum.assertSameObjects(BLUE, getPixel(91)); + }; + + this.test_decrementingLastSegment = function () { + self.pb.setProgress("b", 0.2); + + jum.assertSameObjects(GREEN, getPixel(79)); + jum.assertSameObjects(TRANS, getPixel(81)); + }; + + this.teardown = function () { + this.pb.destroy(); + }; + + return this; +}; } [player-interface josip.lisec@gmail.com**20100719121357 Ignore-this: b7287f386bcbfeb6e59b8686da0ec65c * Fixed bugs in (Segmented)ProgressBar * Added basic player controls * Added album cover fetcing via Last.fm * Reorganised the way external libraries are being imported * Fixed tests ] { hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 39 this.finished = false; + this._songs_found = 0; this._goal = new Goal({ checkpoints: ["scanner", "indexer"], onFinish: function () { hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 44 this.finished = true; - this.destroy() + this.destroy(); + + da.ui.ROAR.alert( + "Collection scanner finished", + "{0} songs have been found. {1}".interpolate([ + this._found_songs, + this._found_songs ? "Your patience has paid off." : "Make sure your files have proper ID3 tags." + ]) + ); }.bind(this) hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 54 - }) + }); + + da.ui.ROAR.alert( + "Collection scanner started", + "We're scanning your musical collection. Soon new artists and songs\ + should start popping up. Patience." + ); }, /** hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 72 var cap = event.data; if(cap === "**FINISHED**") { this._goal.checkpoint("scanner"); - return; // this.scanner.terminate(); + return; } hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 75 + this._found_files++; if(da.db.DEFAULT.views.Song.view.findRow(cap) === -1) this.indexer.postMessage(cap); }, hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 83 onIndexerMessage: function (event) { if(event.data === "**FINISHED**") { this._goal.checkpoint("indexer"); - return; //this.indexer.terminate(); + return; } // Lots of async stuff is going on, a short summary would look something like: hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 112 }); delete links; + delete artist_id; + delete album_id; } }); }, hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 172 * Starts scanning music directory **/ scan: function (cap) { - if(!SCANNER || (SCANNER && SCANNER.finished)) { - delete SCANNER; + if(!SCANNER || (SCANNER && SCANNER.finished)) SCANNER = new CollectionScanner(cap); hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 174 - } }, /** 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 }); hunk ./contrib/musicplayer/src/controllers/Navigation.js 83 if(this.column.constructor.filters.length) this.column.addEvent("click", this.createFilteredColumn.bind(this)); - var first_item = this.column._el.getElement(".column_item"); - if(first_item) - first_item.focus(); + this._el.focus(); }, /** hunk ./contrib/musicplayer/src/controllers/Navigation.js 307 window.addEvent("resize", function () { var columns = Navigation.activeColumns, n = columns.length, - windowHeight = window.getHeight(); + height = window.getHeight() - this._header_height, + width = (window.getWidth() - $("player_pane").getWidth())/3 - 1; while(n--) hunk ./contrib/musicplayer/src/controllers/Navigation.js 311 - columns[n].column._el.setStyle("height", windowHeight - this._header_height); + columns[n].column._el.setStyles({ + height: height, + width: width + }); + + //$("navigation_pane").style.width = width*3 + "px"; + $("navigation_pane").style.height = window.getHeight() + "px"; }.bind(this)); window.fireEvent("resize"); hunk ./contrib/musicplayer/src/controllers/Navigation.js 330 * Adjusts column's height to window. **/ adjustColumnSize: function (column) { - column._el.setStyle("height", window.getHeight() - this._header_height); + column._el.setStyles({ + height: window.getHeight() - this._header_height, + // -1 for te right border + width: (window.getWidth() - $("player_pane").getWidth())/3 - 1 + }); }, /** hunk ./contrib/musicplayer/src/controllers/Player.js 2 //#require "libs/vendor/soundmanager/script/soundmanager2.js" +//#require "libs/ui/SegmentedProgressBar.js" +//#require "services/albumCover.js" (function () { hunk ./contrib/musicplayer/src/controllers/Player.js 6 +var DocumentTemplate = da.db.DocumentTemplate, + Song = DocumentTemplate.Song, + Setting = DocumentTemplate.Setting, + SegmentedProgressBar = da.ui.SegmentedProgressBar; + /** section: Controllers * class Player * hunk ./contrib/musicplayer/src/controllers/Player.js 19 * #### Notes * This class is private. * Public interface is provided through [[da.controller.Player]]. + * + * #### Links + * * [soundManager2 API](http://www.schillmania.com/projects/soundmanager2/doc/) + * **/ var Player = { /** hunk ./contrib/musicplayer/src/controllers/Player.js 31 **/ initialize: function () { var path = location.protocol + "//" + location.host + location.pathname; + //path = path.slice(-path.lastIndexOf("/")); $extend(soundManager, { useHTML5Audio: false, url: path + 'resources/flash/', hunk ./contrib/musicplayer/src/controllers/Player.js 42 soundManager.onready(function () { da.app.startup.checkpoint("soundmanager"); }); + + da.app.addEvent("ready", this.initializeUI.bind(this)); + + // We're using term sound for soundManager objects, while + // song for DocumentTemplate.Song instances. + /** + * Player#sounds -> Object + * + * Cache of SoundManager's object. Keys are id's of [[da.db.DocumentTemplate.Song]]. + **/ + this.sounds = {}; + /** + * Player#playlist -> [Song, ...] + **/ + this.playlist = []; + /** + * Player#nowPlaying -> {song: , sound: } + **/ + this.nowPlaying = {song: null, sound: null}; + + this._loading = []; + }, + + initializeUI: function () { + this._el = $("player_pane"); + window.addEvent("resize", function () { + this._el.style.height = window.getHeight() + "px"; + }.bind(this)); + window.fireEvent("resize"); + + this.progress_bar = new SegmentedProgressBar(150, 5, { + track: "#33519d", + load: "#C1C6D4" + }); + + this.progress_bar.toElement().id = "track_progress"; + this.progress_bar.toElement().addEvents({ + // 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) + return; + + var p = e.event.offsetX / this.getWidth(); + sound.setPosition(sound.durationEstimate * p); + } + }); + + var info_block = new Element("div", {id: "song_info_block" }), + 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"}), + position = new Element("span", {id: "song_position"}), + controls = new Element("div", {id: "player_controls"}), + play = new Element("a", {id: "play_button", href: "#"}); + + controls.adopt(play, this.progress_bar.toElement()); + + wrapper.grab(song_title); + wrapper.appendText("from "); + wrapper.grab(album_title); + wrapper.appendText(" by "); + wrapper.adopt(artist_name, position, controls); + + cover_wrapper.grab(album_cover); + + info_block.adopt(cover_wrapper, wrapper, new Element("div", {"class": "clear"})); + info_block.style.visibility = "hidden"; + this._info_block_visible = false; + + this._el.adopt(info_block); + + play.addEvent("click", function () { + Player.playPause(); + }); + }, + + /** + * Player#play(song) -> undefined + * - song (da.db.DocumentTemplate.Song): song to play. + * + * If there is currently another song playing, it will be stopped. + **/ + play: function (song) { + var np = this.nowPlaying; + if(!song && np.song) + np.sound.resume(); + + $("play_button").addClass("active"); + + if(np.song && song.id === np.song.id) + return; + + if(this.sounds[song.id]) { + np.sound.stop(); + this.sounds[song.id].play(); + return; + } + + if(np.sound) + np.sound.stop(); + + this._loading.push(song.id); + this.sounds[song.id] = soundManager.createSound({ + id: song.id, + url: "/uri/" + encodeURIComponent(song.id), + autoLoad: false, + autoPlay: false, + stream: true, + + onload: function () { + this._loading.remove(song.id); + if(!song.get("duration")) + song.update({duration: this.duration}); + }, + + onplay: function () { + Player.setNowPlaying(song); + }, + whileloading: function () { + Player.progress_bar.setProgress("load", this.bytesLoaded/this.bytesTotal) + }, + whileplaying: function () { + Player.progress_bar.setProgress("track", this.position/this.durationEstimate); + }, + onfinish: function () { + Player.playbackFinished(); + } + }); + + this.sounds[song.id].play(); + np = null; + + if(!this._info_block_visible) { + this._info_block_visible = true; + $("song_info_block").style.visibility = "visible"; + } + }, + + /** + * Player#setNowPlaying(song) -> undefined + * fires nowPlaying + **/ + setNowPlaying: function (song) { + if(this.nowPlaying.sound) + this.nowPlaying.sound._last_played = +(new Date()); + + this.nowPlaying = { + song: song, + sound: this.sounds[song.id] + }; + + var song_title = song.get("title"), + song_title_el = $("song_title"); + song_title_el.set("text", song_title); + song_title_el.title = song_title; + $("song_position").title = "total " + song.get("duration"); + + song.get("album", function (album) { + var title = album.get("title"), + album_covers = album.get("album_cover_urls"), + el = $("song_album_title"); + + el.set("text", title); + el.title = title; + + title = null; + delete el; + + if(album_covers) + $("song_album_cover").src = album_covers[2]; + else + da.service.albumCover(album, function (urls) { + $("song_album_cover").src = urls[2]; + }); + + album = null; + album_covers = null; + }); + song.get("artist", function (artist) { + var aname = artist.get("title"), + el = $("song_artist_name"); + el.set("text", aname); + el.title = aname; + + aname = null; + delete el; + }); + + da.controller.Player.fireEvent("nowPlaying", [song]); + }, + + /** + * Player#playbackFinished() -> undefined + * + * Called when song finishes playing. + * Deterimnes what to do next - load next song in playlist, + * repeat this song, etc. + **/ + playbackFinished: function () { + $("play_button").removeClass("active"); + }, + + /** + * Player#pause() -> undefined + * fires pause + * + **/ + pause: function () { + if(!this.nowPlaying.song) + return; + + this.nowPlaying.sound.pause(); + $("play_button").removeClass("active"); + da.controller.Player.fireEvent("pause", [this.nowPlaying.song]); + }, + + /** + * Player#playPause([song]) -> undefined + * + * Checks if there is a paused song, if so, it'll be resumed. + * If there aren't any paused songs, the fallback `song`, + * if provided, will be played instead. + * + **/ + playPause: function (song) { + if(!this.nowPlaying.song && song) + this.play(song); + + if(this.nowPlaying.sound.paused) + this.play(); + else + this.pause(); + }, + + /** + * Player#free() -> undefined + * + * Frees memory taken by loaded songs. This method is ran about every + * 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 + * * (The Universality of Song Length?)[http://a-candle-in-the-dark.blogspot.com/2010/02/song-length.html] + * + **/ + free: function () { + var eight_mins_ago = (+ new Date()) - 8*60*1000; + + var sound; + 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 + )) + { + sound.destruct(); + delete this.sounds[id]; + } + } + + delete sound; } }; hunk ./contrib/musicplayer/src/controllers/Player.js 319 - Player.initialize(); hunk ./contrib/musicplayer/src/controllers/Player.js 321 +// Check is performed every four minutes +Player.free.periodical(8*60*1000, Player); + +/** +TODO: Should be moved to another controller, Statistics or something alike. +function findAverageDuration(callback) { + da.db.DEFAULT.view({ + temporary: true, + map: function (doc, emit) { + if(doc._deleted || doc.type !== "Song" || !doc.duration) + return; + + emit("duration", doc.duration); + }, + + reduce: function (key, values, rereduce) { + var sum = 0, n = count = values.length; + while(n--) sum += values[n]; + return {average: sum/count, sample: count}; + }, + + finished: function (row) { + var d = row.getRow("duration"); + if(d.average) + Setting.create({ + id: "average_duration", + value: d.average, + sample: d.sample + }); + + console.log("average", d.average || 4*60*1000); + } + }); +} +**/ + /** * da.controller.Player **/ hunk ./contrib/musicplayer/src/controllers/Player.js 362 da.controller.Player = { /** - * da.controller.Player.play([uri]) -> Boolean - * - uri (String): location of the audio. + * da.controller.Player.play([song]) -> Boolean + * - cap (da.db.DocumentTemplate.Song): the track to be played. * * If `uri` is omitted and there is paused playback, then the paused * file will resume playing. hunk ./contrib/musicplayer/src/controllers/Player.js 368 **/ - play: function (uri) { return false }, + play: function (song) { + Player.play(song); + }, /** * da.controller.Player.pause() -> Boolean hunk ./contrib/musicplayer/src/controllers/Player.js 377 * * Pauses the playback (if any). **/ - pause: function () { return false }, + pause: function () { + Player.pause() + }, /** hunk ./contrib/musicplayer/src/controllers/Player.js 382 - * da.controller.Player.queue(uri) -> Boolean + * da.controller.Player.queue(song) -> Boolean * - uri (String): location of the audio file. * * Adds file to the play queue and plays it as soon as currently playing hunk ./contrib/musicplayer/src/controllers/Player.js 388 * file finishes playing (if any). **/ - queue: function (uri) { return false } + queue: function (song) { return false }, + + /** + * da.controller.Player.nowPlaying() -> da.db.DocumentTemplate.Song + **/ + nowPlaying: function () { + return Player.nowPlaying.song; + } }; hunk ./contrib/musicplayer/src/controllers/Player.js 397 +$extend(da.controller.Player, new Events()); da.app.fireEvent("ready.controller.Player", [], 1); hunk ./contrib/musicplayer/src/controllers/Settings.js 4 //#require "doctemplates/Setting.js" //#require "libs/ui/NavigationColumn.js" //#require "libs/ui/Dialog.js" +//#require "libs/vendor/Roar.js" (function () { /** section: Controllers hunk ./contrib/musicplayer/src/controllers/Settings.js 29 } ]; -// Renderers are used render interface elements for each setting (input boxes, checkboxes etc.) +// 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 // defaults to "text" for each setting. // All renderer has to do is to renturn a DIV element with "setting_box" CSS class hunk ./contrib/musicplayer/src/controllers/Settings.js 79 RENDERERS.numeric = RENDERERS.text; // Serializers do the opposite job of the one that renderers do, -// they take an element and return its value. +// they take an element and return its value which will be then stored to the DB. var SERIALIZERS = { text: function (input) { return input.value; hunk ./contrib/musicplayer/src/controllers/Settings.js 210 }, save: function () { - var settings = this.serialize(); + var settings = this.serialize(), setting; + for(var id in settings) hunk ./contrib/musicplayer/src/controllers/Settings.js 213 - Setting.findFirst({ - properties: {id: id}, - onSuccess: function (setting) { - setting.update({value: settings[id]}); - } - }); + Setting.findById(id).update({value: settings[id]}); + + da.ui.ROAR.alert("Saved", "Your settings have been saved"); }, serialize: function () { hunk ./contrib/musicplayer/src/controllers/Settings.js 221 var values = this._controls.getElement("form").getElements("input[id^=setting_]"), serialized = {}, - // in combo with el.id.slice is approx. x10 faster + // fun fact: in combo with el.id.slice is approx. x10 faster // than el.id.split("setting_")[1] setting_l = "setting_".length, n = values.length; hunk ./contrib/musicplayer/src/controllers/Settings.js 234 } return serialized; + }, + + /** + * Settings#free() -> undefined + * + * About a minute after last [[da.controller.Settings.hide]] call, + * all DOM nodes created by settings dialog will be destroyes - thus + * freeing memory. + * + **/ + free: function () { + Settings.initialized = false; + + Settings.column.destroy(); + Settings.dialog.destroy(); + + delete Settings.column; + delete Settings.dialog; + delete Settings._el; + delete Settings._controls; } }; $extend(Settings, new Events()); hunk ./contrib/musicplayer/src/controllers/Settings.js 285 } }); -// Settings dialog (DOM nodes) will be destroyed -// a minute after last da.controller.Settings.hide() call. -// Freeing up memory. var destroy_timeout; hunk ./contrib/musicplayer/src/controllers/Settings.js 286 -function destroySettingsDialog () { - Settings.initialized = false; - - Settings.column.destroy(); - Settings.dialog.destroy(); - - delete Settings.column; - delete Settings.dialog; - delete Settings._el; - delete Settings._controls; -} - /** * da.controller.Settings * hunk ./contrib/musicplayer/src/controllers/Settings.js 376 **/ hide: function () { Settings.hide(); - destroy_timeout = setTimeout(destroySettingsDialog, 60*60*1000); + destroy_timeout = setTimeout(Settings.free, 60*60*1000); }, /** hunk ./contrib/musicplayer/src/controllers/default_columns.js 83 Navigation.registerColumn("Albums", ["Songs"], new Class({ Extends: NavigationColumn, + options: { + rowHeight: 72/3, + iconSize: 1, + renderImmediately: false + }, + // We can't reuse "Album" view because of #_passesFilter(). view: { id: "albums_column", hunk ./contrib/musicplayer/src/controllers/default_columns.js 98 if(doc.type === "Album") emit(doc.id, { - title: doc.title + title: doc.title, + icons: doc.album_cover_urls || [] }); } }, hunk ./contrib/musicplayer/src/controllers/default_columns.js 104 + initialize: function (options) { + this.parent(options); + + this.options.iconSize = this.options.totalCount <= (this.getVisibleIndexes()[1] + 1) ? 2 : 1; + this._row_dim = this.options.iconSize === 1 ? 64 : 174; + + this._el.addEvent("resize", function () { + this.options.rowHeight = this._el.getWidth() / (4 + this._row_dim + 4); + }.bind(this)); + this._el.fireEvent("resize"); + + this.render(); + }, + createFilter: function (item) { return {album_id: item.id}; hunk ./contrib/musicplayer/src/controllers/default_columns.js 120 + }, + + renderItem: function (index) { + var item = this.getItem(index).value, + el = new Element("a", { + href: "#", + title: item.title + }); + + el.style.width = this._row_dim + "px"; + el.style.height = this._row_dim + "px"; + + el.grab(new Element("img", {src: item.icons[this.options.iconSize]})); + return el; + }, + + getBoxCoords: function(index) { + return [0, (this.options.rowHeight * index)/3]; } })); hunk ./contrib/musicplayer/src/controllers/default_columns.js 156 this._el.style.width = "300px"; this.addEvent("click", function (item) { - if(this.sound) - soundManager.stop(item.id); - - this.sound = soundManager.createSound({ - id: item.id, - url: "/uri/" + encodeURIComponent(item.id), - autoLoad: true, - onload: function () { - this.play(); - } - }); - }.bind(this)); + da.controller.Player.play(new da.db.DocumentTemplate.Song( + da.db.DEFAULT.views.Song.view.getRow(item.id)) + ); + }); }, view: { hunk ./contrib/musicplayer/src/controllers/default_columns.js 170 if(doc.type === "Song" && doc.title) emit(doc.title, { title: doc.title, - subtitle: doc.track, track: doc.track }); } hunk ./contrib/musicplayer/src/controllers/default_columns.js 175 }, + /* renderItem: function (index) { var item = this.getItem(index).value, el = new Element("a", {href: "#", title: item.title}); hunk ./contrib/musicplayer/src/controllers/default_columns.js 183 el.grab(new Element("span", {html: item.title, "class": "title"})); return el; - }, + }, */ compareFunction: function (a, b) { a = a && a.value ? a.value.track : a; 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 4 + Music Player for Tahoe-LAFS hunk ./contrib/musicplayer/src/index.html 6 - hunk ./contrib/musicplayer/src/index_devel.html 4 + Music Player for Tahoe-LAFS hunk ./contrib/musicplayer/src/index_devel.html 15 + + + - + + + + + hunk ./contrib/musicplayer/src/index_devel.html 47 + hunk ./contrib/musicplayer/src/index_devel.html 56 + + + + + + hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 341 * Defaults to current database. **/ view: function DB_view(options, dict) { - if(!options.id && !options.temporary) + if(!options.temporary && !options.id) return false; if(!options.map) return false; hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 114 * - callback (Function): needed only if `key` points to an property defined by an relationship. **/ get: function (key, callback) { - if(key in this.doc) + if(!this.belongsTo[key] && !this.hasMany[key]) return this.doc[key]; if(!callback) hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 130 if(!this.doc[key + "_id"]) return callback(null); + var type = this.belongsTo[key], + owner = DocumentTemplate[type].findById(this.doc[key + "_id"]); + + if(owner) + this[cache_key] = owner; + + callback(owner); + + /* DocumentTemplate.find({ properties: { id: this.doc[key + "_id"], hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 152 onFailure: callback }, this.constructor.db()); + */ } else if(key in this.hasMany) { var relation = this.hasMany[key], props = {type: relation[0]}; hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 161 DocumentTemplate.find({ properties: props, - onSuccess: callback, - onFailure: callback + onSuccess: callback, + onFailure: callback }, DocumentTemplate[relation[0]].db()); } hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 236 * Removes all document's properties except for `id` and adds `_deleted` property. **/ destroy: function (callback) { - this.doc = {id: this.id, _deleted: true}; + for(var property in this.doc) + delete this.doc[property]; + + this.doc.id = this.id; + this.doc._deleted = true; + this.constructor.db().put(this.doc, function () { this.fireEvent("destroy", [this]); if(callback) hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 297 result.rows[n] = type ? new type(row) : row; } + delete n; + delete row; + delete type; options.onSuccess(options.onlyFirst ? result.rows[0] : result.rows); hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 301 + options = null; + result = null; } }); }, hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 355 template.find = function (options) { options.properties.type = type; - DocumentTemplate.find(options, db); + if(options.id) + template.findById(options.id, function (doc) { + if(doc) + options.onSuccess([doc]); + else + options.onFailure(); + }); + else + DocumentTemplate.find(options, db); }; template.findFirst = function (options) { hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 380 DocumentTemplate.findOrCreate(options, db); }; + template.findById = function (id) { + var doc = template.db().views[type].view.getRow(id); + return doc ? new template(doc) : null; + }; + template.db().view({ id: type, map: function (doc, emit) { hunk ./contrib/musicplayer/src/libs/db/PersistStorage.js 1 -//#require "libs/vendor/persist-js/src/persist.js" +//#require "libs/vendor/Persist.js" //#require "libs/db/db.js" (function () { hunk ./contrib/musicplayer/src/libs/db/PersistStorage.js 5 +var Persist = da.vendor.Persist; + /** section: Database * class da.db.PersistStorage * hunk ./contrib/musicplayer/src/libs/ui/Column.js 76 // expensive a small timeout will be set in order to save // some bandwidth - the downside is that flickering will be seen // while scrolling. - var scroll_timer = null, - timeout = this.options.renderTimeout, - timeout_fn = this.render.bind(this); + var timeout = this.options.renderTimeout, + timeout_fn = this.render.bind(this), + scroll_timer; this._el.addEvent("scroll", function () { clearTimeout(scroll_timer); hunk ./contrib/musicplayer/src/libs/ui/Column.js 82 - scroll_timer = setTimeout(scroll_timer, timeout); + scroll_timer = setTimeout(timeout_fn, timeout); }); // We're caching lists' height so we won't have to hunk ./contrib/musicplayer/src/libs/ui/Column.js 143 var coords = this.getBoxCoords(first_rendered); box.setStyles({ position: "absolute", - top: coords[0], - left: coords[1] + top: coords[1], + left: coords[0] }).injectBottom(this._el); } hunk ./contrib/musicplayer/src/libs/ui/Column.js 170 * da.ui.Column#rerender() -> this **/ rerender: function () { + if(!this._el) + 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 211 * Returns X and Y coordinates at which item with given `index` should be rendered at. **/ getBoxCoords: function(index) { - return [this.options.rowHeight * index, 0]; + return [0, this.options.rowHeight * index]; }, /** hunk ./contrib/musicplayer/src/libs/ui/Column.js 246 * * Removes column from DOM. **/ - destroy: function (dispose) { + destroy: function () { this._el.destroy(); delete this._el; hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 61 // Small speed-hack if(!this.options.filter) - this._passesFilter = $lambda(true); + this._passesFilter = function () { return true }; this._el.addEvent("click:relay(.column_item)", this.click.bind(this)); hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 82 (options.db || da.db.DEFAULT).view(this.view); } else if(this.options.totalCount) { this.injectBottom(this.options.parentElement || document.body); - this.render(); + if(!this.options.renderImmediately) + this.render(); } }, hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 101 this.updateTotalCount(values.rows.length); this.injectBottom(this.options.parentElement || document.body); - return this.render(); + if(this.options.renderImmediately !== false) + this.render(); + return this; }, /** hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 137 **/ renderItem: function (index) { var item = this.getItem(index).value, - el = new Element("a", {href: "#", title: item.title}); + el = new Element("a", { + href: "#", + title: item.title, + "class": index%2 ? "even" : "odd" + }); if(item.icon) el.grab(new Element("img", {src: item.icon})); hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 87 } this.ctx.fillStyle = this.options.foreground; - this.ctx[increment ? "fillRect" : "clearRect"](x, 0, width, this.options.height); + // We're adding +-1 here because some browsers (even different implementations + // of WebKit are unable to render this precisely when small changes are + // in place. + this.ctx[increment ? "fillRect" : "clearRect"](x - 1, 0, width + 1, this.options.height); this.progress = p; hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 92 + return this; }, hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 100 * da.ui.ProgressBar#rerender() -> this **/ rerender: function () { - this.ctx.fillStyle = this.options.foreground; - this.ctx.fillRect(0, 0, this.progress * this.options.width, this.options.height); + var opts = this.options; + this.ctx.fillStyle = opts.foreground; + this.ctx.fillRect(0, 0, this.progress * opts.width, opts.height); return this; }, hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 28 * The first define progress bar will be in foreground, while * the last defined will be in background; **/ - initialize: function (width, height, bars) { - this.index = []; + initialize: function (width, height, segments) { + this._index = []; + this.segments = {}; hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 32 - for(var bar in bars) - this.index.push(bar); + this._el = new Element("canvas"); + this._el.width = width; + this._el.height = height; + this._el.className = "progressbar"; + this.ctx = this._el.getContext("2d"); hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 38 - var last_bar_name = this.index.getLast(), - last_bar = new ProgressBar(null, { - width: width, - height: height, - foreground: bars[last_bar_name] - }); - this._el = last_bar.toElement(); - this.ctx = last_bar.ctx; + this._el.addEvent("resize", function () { + var idx = this._index, + n = idx.length; + + while(n--) + this.segments[idx[n]].rerender(); + }.bind(this)); hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 46 - this.bars = {}; - var bar; - for(var n = 0, m = this.index.length - 1; n < m; n++) { - bar = this.index[n]; - this.bars[bar] = new ProgressBar(this._el, { - width: width, - height: height, - foreground: bars[bar] - }); - } - this.bars[last_bar_name] = last_bar; - last_bar = null; + for(var segment in segments) + if(segments.hasOwnProperty(segment)) { + this._index.push(segment); + this.segments[segment] = new ProgressBar(this._el, { + width: width, + height: height, + foreground: segments[segment] + }); + } }, /** hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 62 * - segment (String): name of the bar whose progress needs to be updated * - progress (Number): number in 0-1 range. **/ - setProgress: function (bar, p) { - var current_p = this.bars[bar].progress, - increment = current_p < p, - n = this.index.indexOf(bar); - - bar = this.bars[bar]; - if(!bar) + setProgress: function (segment, p) { + segment = this.segments[segment]; + if(!segment) return false; hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 67 - var idx = this.index; - if(increment) { - if(this.index.indexOf(bar) == 0) - return this.setProgress(p); - - m = n + 1; - bar.setProgress(p); - while(m--) { - var b = this.bars[idx[m]]; - if(b.progress <= p) - b.rerender(); - } - } else { - m = this.index.length; - bar.setProgress(p); - - while(m--) - this.bars[idx[m]].rerender(); - } + var idx = this._index, + n = idx.length; + + // Indeed, this is quite naive implementation + segment.setProgress(p); + while(n--) + this.segments[idx[n]].rerender(); return this; }, hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 79 /** + * da.ui.SegmentedProgressBar#toElement() -> Element + **/ + toElement: function () { + return this._el; + }, + + /** * da.ui.SegmentedProgressBar#destroy() -> undefined **/ destroy: function () { hunk ./contrib/musicplayer/src/libs/ui/SegmentedProgressBar.js 91 this._el.destroy(); delete this._el; - delete this.index; - delete this.bars; + delete this._index; + delete this.segments; + delete this.ctx; } }); da.ui.SegmentedProgressBar = SegmentedProgressBar; hunk ./contrib/musicplayer/src/libs/util/ID3.js 51 _getFile: function (parser) { if(!parser) - return this.options.onFailure(); + return this.options.onFailure("fromID3"); this.request = new Request.Binary({ hunk ./contrib/musicplayer/src/libs/util/ID3.js 54 - url: this.options.url, - range: parser.range, - onSuccess: this._onFileFetched.bind(this) + url: this.options.url, + range: parser.range, + noCache: true, + onSuccess: this._onFileFetched.bind(this), + onFailure: function () { + this.options.onFailure("failedRequest"); + }.bind(this) }); this.request.send(); hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 143 WAR: FRAMES.WOAR, WXX: FRAMES.WXXX }); - var ID3v2Parser = new Class({ /** * new da.util.ID3v2Parser(data, options, request) addfile ./contrib/musicplayer/src/libs/vendor/JSON.js hunk ./contrib/musicplayer/src/libs/vendor/JSON.js 1 +(function () { +/** + * da.vendor.JSON + * + * JSON parser/serializer. + **/ + +var has_JSON = typeof JSON === "undefined", + broken_JSON = false; + +if(has_JSON && JSON.stringify([1,2,3]) === "[1,2,3]") + broken_JSON = true; + +if(!has_JSON || broken_JSON) { +//#require "libs/vendor/json/json2.js" + +} +})(); addfile ./contrib/musicplayer/src/libs/vendor/LastFM.js hunk ./contrib/musicplayer/src/libs/vendor/LastFM.js 1 +(function () { + +//#require "libs/vendor/javascript-last.fm-api/lastfm.api.md5.js" +//#require "libs/vendor/javascript-last.fm-api/lastfm.api.js" + +// TODO: cache is broken, fix it (something with agumenting localStorage) +///require "libs/vendor/javascript-last.fm-api/lastfm.api.cache.js" + +/** + * class da.vendor.LastFM + * + * Last.fm API wrapper. + * + * #### Links + * * [Last.fm API](http://last.fm/api/intro) + * * [Last.fm API wrapper](http://github.com/fxb/javascript-last.fm-api) + * + **/ +da.vendor.LastFM = LastFM; + +})(); addfile ./contrib/musicplayer/src/libs/vendor/Persist.js hunk ./contrib/musicplayer/src/libs/vendor/Persist.js 1 +//#require + +(function () { +/** + * class da.vendor.Persist + * + * Persist.JS + * + * #### Links + * * [PersistJS project page](http://google.com) + **/ + +// tiny hack which allows us to use +// index_devel.html +if(typeof Persist === "undefined") { + var Persist; +//#require "libs/vendor/persist-js/src/persist.js" + da.vendor.Persist = Persist; +} else + da.vendor.Persist = window.Persist; + +})(); addfile ./contrib/musicplayer/src/libs/vendor/Roar.js hunk ./contrib/musicplayer/src/libs/vendor/Roar.js 1 +//#require + +(function () { +//#require "libs/vendor/roar/Roar.js" + +/** + * class da.vendor.Roar + * + * Roar notifications library. + * + * #### Links + * * [Roar project page](http://digitarald.de/project/roar/) + * + **/ + +da.vendor.Roar = Roar; + +/** + * da.ui.ROAR = da.vendor.Roar + * + * The default instance of [[da.vendor.Roar]]. + * + **/ +da.ui.ROAR = new Roar({ + position: "lowerLeft" +}); + +})(); addfile ./contrib/musicplayer/src/libs/vendor/vendor.js hunk ./contrib/musicplayer/src/libs/vendor/vendor.js 1 +/** + * da.vendor + * + * Interfce to 3rd party libraries. + * + **/ + +if(!da.vendor) + da.vendor = {}; hunk ./contrib/musicplayer/src/resources/css/app.css 10 } body { - font-family: 'Liberation Sans', 'Helvetica Neue', Helvetica, sans-serif; + font-family: 'Droid Sans', 'Lucida Grande', 'Lucida Sans', 'Bitstream Vera', sans-serif; overflow: hidden; hunk ./contrib/musicplayer/src/resources/css/app.css 12 + background: #c0c0c0; } a { hunk ./contrib/musicplayer/src/resources/css/app.css 65 cursor: default; } +.clear { + display: block; + width: auto; + clear: both; + padding: 0; + margin: 0; +} + +/*** Scroll bars ***/ +::-webkit-scrollbar { + width: 6px; + height: 6px; + background: #fff; + padding: 0 1px; +} + +::-webkit-scrollbar:hover { + visibility: visible; +} + +::-webkit-scrollbar-button:start:decrement, +::-webkit-scrollbar-button:end:increment { + display: none; +} + +::-webkit-scrollbar-button:vertical:increment { + background: transparent; +} + +::-webkit-scrollbar-track-piece { + background-color: transparent; + -webkit-border-radius: 3px; +} + +::-webkit-scrollbar-thumb:vertical { + height: 50px; +} + +::-webkit-scrollbar-thumb:horizontal { + width: 50px; +} + +::-webkit-scrollbar-thumb:vertical, ::-webkit-scrollbar-thumb:horizontal { + background-color: rgba(0, 0, 0, 0.1); + -webkit-border-radius: 3px; +} + +::-webkit-scrollbar-thumb:vertical:hover, ::-webkit-scrollbar-thumb:horizontal:hover { + background-color: rgba(0, 0, 0, 0.4); +} + +::-webkit-scrollbar-thumb:vertical:active, ::-webkit-scrollbar-thumb:horizontal:active { + background-color: rgba(0, 0, 0, 0.7); +} + /*** Dialogs ***/ .dialog_wrapper { width: 100%; hunk ./contrib/musicplayer/src/resources/css/app.css 162 padding: 50px 0 0 0; } +/*** Notifications (Roar) ***/ +/* Contents of Roar.css (distributed under MIT) */ +.roar-body { + position: absolute; + color: #fff; + text-align: left; + z-index: 999; + font-size: 0.8em; +} + +.roar { + position: absolute; + width: 300px; + cursor: default; + padding: 5px; +} + +.roar-bg { + position: absolute; + z-index: 1000; + width: 100%; + height: 100%; + left: 0; + top: 0; + background-color: #000; + border: 2px solid #000; + + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + -o-border-radius: 5px; + border-radius: 5px; + + -webkit-box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px; + -moz-box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px; + -o-box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px; + box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px; +} + + +.roar h3 { + position: relative; + margin: 0; + border: 0; + font-size: 13px; + color: #fff; + z-index: 1002; +} + +.roar p { + position: relative; + margin: 0; + font-size: 12px; + color: #fff; + z-index: 1002; +} + /*** Navigation columns ***/ .column_container { float: left; hunk ./contrib/musicplayer/src/resources/css/app.css 221 + /** min-width: 200px; margin-right: 1px; hunk ./contrib/musicplayer/src/resources/css/app.css 224 + **/ } .column_container .column_header { hunk ./contrib/musicplayer/src/resources/css/app.css 262 border-right: 1px solid #ddd; } -.column_container .navigation_column:last { - border-right: 5px solid #ddd; -} .navigation_column { width: 100%; hunk ./contrib/musicplayer/src/resources/css/app.css 271 } .navigation_column .column_items_box { - width: inherit; + width: 100%; } .navigation_column .column_item { hunk ./contrib/musicplayer/src/resources/css/app.css 280 padding: 5px 0; width: inherit; overflow: hidden; - text-overflow: ellipsis; text-indent: 5px; hunk ./contrib/musicplayer/src/resources/css/app.css 281 - white-space: nowrap; +} + +.column_item.even { + background-color: #f0f6fd; +} + +.column_item.odd { + background-color: #fff; } .navigation_column a.column_item { hunk ./contrib/musicplayer/src/resources/css/app.css 301 } .navigation_column .column_item span { - /*display: block;*/ + display: block; vertical-align: middle; hunk ./contrib/musicplayer/src/resources/css/app.css 303 + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; } .navigation_column .column_item span.subtitle { hunk ./contrib/musicplayer/src/resources/css/app.css 319 margin-left: 20px; } -.navigation_column .active_column_item, .menu_item:hover, .navigation_column .column_item:focus, .menu_item a:focus { +.navigation_column .active_column_item, .menu_item:hover { background: #33519d !important; text-shadow: #000 0 1px 0; color: #fff !important; hunk ./contrib/musicplayer/src/resources/css/app.css 326 outline: 0 !important; } +.navigation_column .column_item:focus, .menu_item a:focus { + -webkit-box-shadow: #758FCF 0 0 5px; + outline: 0 !important; +} + +/** Albums column **/ +#Albums_column { + background: #595959; +} + +#Albums_column .column_item { + width: 64px; + height: 64px; + padding: 4px; + text-indent: 0; + border-radius: 2px; + display: inline-block; + margin: 0; +} + +#Albums_column .column_item.even, #Albums_column .column_item.odd { + background: transparent; +} + +#Albums_column .column_item img { + display: block; + + -webkit-box-shadow: rgba(0, 0, 0, 0.5) 0 1px 3px; +} + /*** Menus ***/ .menu { display: block; hunk ./contrib/musicplayer/src/resources/css/app.css 434 #settings .navigation_column { border-right: 1px solid #c0c0c0; width: 150px; - float: left; + display: inline-block; } #settings_controls { hunk ./contrib/musicplayer/src/resources/css/app.css 503 border-bottom: 1px transparent; } +/** Navigation columns **/ +#navigation_pane { + position: fixed; + top: 0; + left: 0; + overflow: hidden; +} + +#navigation_pane, #player_pane { + background: #fff; + -webkit-box-shadow: #585858 0 0 50px; + -moz-box-shadow: #585858 0 0 50px; + -o-box-shadow: #585858 0 0 50px; + -box-shadow: #585858 0 0 50px; +} + +/** Player **/ #player_pane { width: 500px; hunk ./contrib/musicplayer/src/resources/css/app.css 522 + position: fixed; + top: 0; + right: 0; +} + +#song_info_block { + width: 100%; + background: #f3f3f3; + border-bottom: 1px solid #ddd; +} + +#song_details { + font-size: 0.9em; + color: #585858; + float: left; + width: 300px; + padding: 10px; + margin-top: 20px; + cursor: default; +} + +#song_details h2, #song_details span { + color: #000; + margin: 0; + font-style: normal; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +#song_details span { + display: inline-block; + max-width: 125px; +} + +#song_album_cover_wrapper { + text-align: center; + width: 160px; + max-width: 160px; + overflow: hidden; + padding: 10px; + float: left; + display: block; +} + +#song_album_cover { + /*float: left; + display: block; + margin: 10px;*/ + max-width: 150px; + border: 1px solid #fff; + outline: 1px solid #ddd; + + -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; +} + +#song_title { + display: block; +} + +#player_controls { + vertical-align: middle; + margin-top: 20px; +} + +#track_progress { + display: inline-block; + vertical-align: middle; + margin: 0 0 0 10px; + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + -o-border-radius: 2px; + border-radius: 2px; +} + + +#play_button { + display: inline-block; + width: 40px; + height: 40px; + background: url(../images/play.png) 0 0; + outline: 0; + vertical-align: middle; + cursor: default; +} + +#play_button:active, #play_button:focus, #play_button.active { + background-position: 0 -40px; +} + +#play_button.active:active, #play_button.active:focus { + background-position: 0 -80px; } adddir ./contrib/musicplayer/src/services addfile ./contrib/musicplayer/src/services/albumCover.js hunk ./contrib/musicplayer/src/services/albumCover.js 1 +//#require +//#require "services/lastfm.js" +//#require "libs/util/Goal.js" + +(function () { +var lastfm = da.service.lastFm, + Goal = da.util.Goal, + Artists = da.db.DEFAULT.views.Artist.view; + +function fetchAlbumCover(search_params, album, callback) { + lastfm.album.getInfo(search_params, { + success: function (data) { + var urls = data.album.image ? data.album.image : null, + n = urls.length; + + while(n--) + urls[n] = urls[n]["#text"]; + + album.set({ + album_cover_urls: urls, + lastfm_id: data.album.id, + mbid: data.album.mbid.length ? data.album.mbid : null + }); + album.save(); + + // fun fact: typeof /.?/ === "function" + if(urls && typeof callback === "function") + callback(urls); + } + }); +} + +/** + * da.service.albumCover(song[, callback]) -> undefined + * - song (da.db.DocumentTemplate): song whose album art needs to be fetched + * - callback (Function): called once album cover is fetched, with first + * argument being the URL of the album cover. + * + * #### Notes + * The URL will be saved to the `song` under 'cover_art' propety. + **/ +da.service.albumCover = function (album, callback) { + var search_params = {}; + if(album.get("lastfm_id")) + search_params.id = album.get("lastfm_id"); + else if(album.get("mbid")) + search_params.mbid = album.get("mbid"); + + if(!search_params.id && !search_params.mbid) + search_params = { + album: album.get("title"), + artist: Artists.getRow(album.get("artist_id")).title + }; + + fetchAlbumCover(search_params, album, callback); + search_params = null; +}; + +})(); addfile ./contrib/musicplayer/src/services/lastfm.js hunk ./contrib/musicplayer/src/services/lastfm.js 1 +//#require "libs/vendor/LastFM.js" + +(function () { +//var cache = new LastFMCache(); +/** + * da.service.lastFm -> da.vendor.LastFM + * + * Default instance of `LastFM` API wrapper. + * + **/ +da.service.lastFm = new da.vendor.LastFM({ + apiKey: "d5219a762390c878548b338d67a28f67", + // not so secret anymore :) + apiSecret: "9ff1e4083ec6e86ef4467262db5b1509", + cache: null +}); + +})(); addfile ./contrib/musicplayer/src/services/services.js hunk ./contrib/musicplayer/src/services/services.js 1 +/** + * da.service + * Access to 3rd party services - last.fm, MusicBrainz, etc. + **/ + +if(!da.service) + da.service = {}; hunk ./contrib/musicplayer/src/workers/indexer.js 17 * an MP3 file stored in Tahoe (without /uri/ prefix). * * Messages sent from this worker are objects returned by ID3 parser. + * + * #### Notes + * It has been detected that Tahoe (to be correct, it's web server) can't + * handle well large number of simoultanious requests, therefore we're limiting + * the number of files that can be fetched at the same time to one. * hunk ./contrib/musicplayer/src/workers/indexer.js 23 + * Since it's also possible that it will take more time for the [[Scanner]] + * to find all the files than it will take the [[Indexer]] we're allowing + * about 30-second delay before finally sending the "I'm done" message to the + * [[da.controller.CollectionScanner]]. + * **/ var window = this, hunk ./contrib/musicplayer/src/workers/indexer.js 32 document = {}, - queue = 0; + queue = []; this.da = {}; importScripts("env.js"); hunk ./contrib/musicplayer/src/workers/indexer.js 44 * * When tags are parsed, `postMessage` is called. **/ -onmessage = function (event) { - var cap = event.data, - uri = "/uri/" + encodeURIComponent(cap); +onmessage = function (event) { + queue.push(event.data); hunk ./contrib/musicplayer/src/workers/indexer.js 47 - queue++; + if(queue.length < 3) + getTags(event.data); +}; + +function getTags(cap) { + if(!cap) return false; var parser = new da.util.ID3({ hunk ./contrib/musicplayer/src/workers/indexer.js 54 - url: uri, + url: "/uri/" + encodeURIComponent(cap), + onSuccess: function (tags) { if(tags && typeof tags.title !== "undefined" && typeof tags.artist !== "undefined") { tags.id = cap; hunk ./contrib/musicplayer/src/workers/indexer.js 65 parser.destroy(); delete parser; - // Not all files are reporeted instantly so it might - // take some time for scanner.js/CollectionScanner.js to - // report the files, maximum delay we're allowing here - // for new files to come in is one minute. - if(!--queue) - setTimeout(checkQueue, 1000*60*1); + queue.erase(cap); + checkQueue(); }, hunk ./contrib/musicplayer/src/workers/indexer.js 68 - onFailure: function () { + + onFailure: function (calledBy) { parser.destroy(); delete parser; hunk ./contrib/musicplayer/src/workers/indexer.js 73 - if(!--queue) - setTimeout(checkQueue, 1000*60*1); + queue.erase(cap); + checkQueue(); } }); hunk ./contrib/musicplayer/src/workers/indexer.js 77 -}; +} hunk ./contrib/musicplayer/src/workers/indexer.js 79 +var finish_timeout; function checkQueue() { hunk ./contrib/musicplayer/src/workers/indexer.js 81 - if(!queue) - postMessage("**FINISHED**"); + if(!queue.length) + finish_timeout = setTimeout(finish, 30*60*1000); + else { + clearTimeout(finish_timeout); + setTimeout(function () { + getTags(queue.shift()); + }, 444); + } } hunk ./contrib/musicplayer/src/workers/indexer.js 91 +function finish () { + postMessage("**FINISHED**"); +} hunk ./contrib/musicplayer/src/workers/scanner.js 25 function scan (obj) { queue++; obj.get(function () { - queue--; - if(obj.type === "filenode") { hunk ./contrib/musicplayer/src/workers/scanner.js 26 - delete obj; return postMessage(obj.uri); } hunk ./contrib/musicplayer/src/workers/scanner.js 39 scan(child); } - delete obj; - if(!queue) + if(!--queue) postMessage("**FINISHED**"); }); } hunk ./contrib/musicplayer/tests/shared.js 65 } }; -jum.assertSameObjects = function (a, b) { +jum.assertEqualArrays = function (a, b) { + jum.assertEquals("arrays should have same length", a.length, b.length); + + var aa = ["array", "arguments"]; + for(var n = 0, m = a.length; n < m; n++) + if(aa.contains($type(a[n]))) + jum.assertEqualArrays(a[n], b[n]); + else if($type(a[n]) === "object") + jum.assertSameObjects(a[n], b[n]); + else + jum.assertEquals( + "was '" + JSON.stringify(b) + "', expected '" + JSON.stringify(a) + "'", + a[n], b[n] + ); + + return true; +}; + +jum.assertSameArrays = function (a, b) { + jum.assertEquals("arrays should have same length", a.length, b.length); + + var aa = ["array", "arguments"]; + for(var n = 0, m = a.length; n < m; n++) + if(aa.contains($type(a[n]))) + jum.assertEqualArrays(a[n], b[n]); + else if($type(a[n]) === "object") + jum.assertSameObjects(a[n], b[n]); + else + jum.assertTrue("should contain '" + a[n] + "'", b.indexOf(a[n]) !== -1); + + return true; +}; + +jum.assertSameObjects = function (a, b, useSameArrays) { if(a === b) return true; hunk ./contrib/musicplayer/tests/shared.js 101 - // catches cases when one of args is null if(!a || !b) jum.assertEquals(a, b); hunk ./contrib/musicplayer/tests/shared.js 104 - if($type(a) === "array") { - jum.assertTrue("arrays should be of the same length", - a.length === b.length - ); - - // we're doing this in case of of the - // objects is an 'arguments' object, - // which lack .indexOf() - a = $A(a); - b = $A(b); - - for(var n = 0, m = a.length; n < m; n++) - if(typeof a[n] === "object") - jum.assertSameObjects(a[n], b[n]); - else - jum.assertTrue("should contain '" + a[n] + "'", - b.indexOf(a[n]) !== -1 - ); - } else { - for(var prop in a) - if(a.hasOwnProperty(prop)) - if(prop in a && prop in b) - if(typeof a[prop] === "object") - jum.assertSameObjects(a[prop], b[prop]); - else - jum.assertEquals(a[prop], b[prop]); + for(var prop in a) + if(a.hasOwnProperty(prop)) + if(prop in a && prop in b) + if($type(a[prop]) === "object") + jum.assertSameObjects(a[prop], b[prop]); + else if($type(a[prop]) === "array") + jum[useSameArrays ? "assertSameArrays" : "assertEqualArrays"](a[prop], b[prop]); else hunk ./contrib/musicplayer/tests/shared.js 112 - jum.assertTrue("missing '" + prop +"' property", false); - } + jum.assertEquals("propety '" + prop + "' differs", a[prop], b[prop]); + else + jum.assertTrue("missing '" + prop +"' property", false); hunk ./contrib/musicplayer/tests/shared.js 116 - delete a; - delete b; return true; }; hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 185 jum.assertEquals("map() should have been called three times", 3, self.mapped_docs.length ); - jum.assertSameObjects(["doc1", "doc2", "doc3"], self.mapped_docs); + jum.assertSameArrays(["doc1", "doc2", "doc3"], self.mapped_docs); }; this.test_verifyMapResult = function () { hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 403 emit("albums", doc.albums); }, - reduce: function (keys, values, rereduce) { + reduce: function (key, values, rereduce) { if(rereduce) { self.rereduce_args = arguments; self.rereduce_called++; hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 479 this.test_rereduce = function () { jum.assertEquals(1, self.rereduce_called); - jum.assertSameObjects([null, [2, 15], true], self.rereduce_args); - } + jum.assertEqualArrays([null, [2, 15], true], self.rereduce_args); + }; this.teardown = function () { self.db.killView("test2"); hunk ./contrib/musicplayer/tests/test_DocumentTemplate.js 134 }; this.test_belongsTo = function () { - self.herbie.get("owner", function (owners) { - jum.assertEquals(1, owners.length); - jum.assertEquals(self.jim.id, owners[0].id); + self.herbie.get("owner", function (owner) { + jum.assertEquals(self.jim.id, owner.id); self.got_jim = true; }); }; hunk ./contrib/musicplayer/tests/test_DocumentTemplate.js 209 jum.assertTrue(self.foc_john.created); }; + this.test_findById = function () { + var jim = self.Person.findById("jim"); + jum.assertEquals("jim", jim.id); + jum.assertEquals("Jim", jim.get("first")); + + jum.assertTrue(self.Car.findById("KITT") === null); + }; + this.test_destroy = function () { self.success_on_destroy = self.failure_on_destroy = false; self.jim.destroy(function () { hunk ./contrib/musicplayer/tests/test_DocumentTemplate.js 220 - self.Person.findFirst({ - properties: {id: "jim"}, - onSuccess: function() { - self.success_on_destory = true; - }, - onFailure: function () { - self.failure_on_destory = true; - } - }); + self.destroyed = true; + self.found_destroyed_doc = !!self.Person.findById("jim"); }); }; hunk ./contrib/musicplayer/tests/test_DocumentTemplate.js 228 this.wait_forDestroy = { method: "waits.forJS", params: { - js: function () { return self.failure_on_destroy || self.success_on_destroy } + js: function () { return self.destroyed } } }; hunk ./contrib/musicplayer/tests/test_DocumentTemplate.js 233 this.test_verifyDestroy = function () { - jum.assertTrue(self.failure_on_destroy); - jum.assertFalse(self.success_on_destroy); + jum.assertFalse(self.found_destroyed_doc); }; this.teardown = function () { hunk ./contrib/musicplayer/tests/test_ProgressBar.js 17 }; function getPixel(x) { - return self.pb.ctx.getImageData(x, 0, 1, 1).data + // px is an ImageData object which mimics Array. + var px = self.pb.ctx.getImageData(x, 0, 1, 1).data; + return [px[0], px[1], px[2], px[3]]; } hunk ./contrib/musicplayer/tests/test_ProgressBar.js 22 + // Precise pixels are not being used due to the fact + // that each percent is widend for apporx. two pixels, + // in order to fix browser inconsistencies. this.test_incrementation = function () { self.pb.setProgress(0.5); hunk ./contrib/musicplayer/tests/test_ProgressBar.js 28 - jum.assertSameObjects(BLACK, getPixel(0)); - jum.assertSameObjects(BLACK, getPixel(49)); - jum.assertSameObjects(TRANSPARENT, getPixel(100)); + jum.assertEqualArrays(BLACK, getPixel(1)); + jum.assertEqualArrays(BLACK, getPixel(49)); + jum.assertEqualArrays(TRANSPARENT, getPixel(100)); self.pb.options.foreground = "rgba(255, 0, 0, 255)"; self.pb.setProgress(0.7); hunk ./contrib/musicplayer/tests/test_ProgressBar.js 35 - jum.assertSameObjects(BLACK, getPixel(0)); - jum.assertSameObjects(RED, getPixel(61)); - jum.assertSameObjects(TRANSPARENT, getPixel(100)); + jum.assertEqualArrays(BLACK, getPixel(1)); + jum.assertEqualArrays(RED, getPixel(52)); + jum.assertEqualArrays(RED, getPixel(69)); + jum.assertEqualArrays(TRANSPARENT, getPixel(100)); }; this.test_decrementation = function () { hunk ./contrib/musicplayer/tests/test_ProgressBar.js 45 self.pb.options.foreground = "rgba(0, 255, 0, 255)"; self.pb.setProgress(0.6); - jum.assertSameObjects(RED, getPixel(0)); - jum.assertSameObjects(TRANSPARENT, getPixel(61)); - jum.assertSameObjects(TRANSPARENT, getPixel(70)); + jum.assertEqualArrays(RED, getPixel(52)); + jum.assertEqualArrays(RED, getPixel(58)); + jum.assertEqualArrays(TRANSPARENT, getPixel(60)); + jum.assertEqualArrays(TRANSPARENT, getPixel(70)); }; this.teardown = function () { hunk ./contrib/musicplayer/tests/test_ProgressBar.js 52 - this.pb.destroy(); + //this.pb.destroy(); }; return this; hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 18 }; function getPixel(x) { - return self.pb.ctx.getImageData(x, 0, 1, 1).data; + // px is an ImageData object which mimics Array. + var px = self.pb.ctx.getImageData(x, 0, 1, 1).data; + return [px[0], px[1], px[2], px[3]]; } this.test_incrementation = function () { hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 26 var ctx = self.pb.ctx; - self.pb.setProgress("g", 0.666); - self.pb.setProgress("r", 0.333); - self.pb.setProgress("b", 0.999); + self.pb.setProgress("g", 0.6); + self.pb.setProgress("r", 0.3); + self.pb.setProgress("b", 1); hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 30 - jum.assertSameObjects(RED, getPixel(0)); - jum.assertSameObjects(RED, getPixel(20)); - jum.assertSameObjects(RED, getPixel(34)); + jum.assertEqualArrays(RED, getPixel(2)); + jum.assertEqualArrays(RED, getPixel(20)); + jum.assertEqualArrays(RED, getPixel(29)); hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 34 - jum.assertSameObjects(GREEN, getPixel(35)); - jum.assertSameObjects(GREEN, getPixel(50)); - jum.assertSameObjects(GREEN, getPixel(65)); + jum.assertEqualArrays(GREEN, getPixel(31)); + jum.assertEqualArrays(GREEN, getPixel(50)); + jum.assertEqualArrays(GREEN, getPixel(59)); hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 38 - jum.assertSameObjects(BLUE, getPixel(68)); - jum.assertSameObjects(BLUE, getPixel(80)); - jum.assertSameObjects(BLUE, getPixel(98)); + jum.assertEqualArrays(BLUE, getPixel(61)); + jum.assertEqualArrays(BLUE, getPixel(85)); + jum.assertEqualArrays(BLUE, getPixel(99)); }; this.test_incrementingMiddleSegment = function () { hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 46 self.pb.setProgress("g", 0.8); - jum.assertSameObjects(RED, getPixel(0)); - jum.assertSameObjects(RED, getPixel(30)); + jum.assertEqualArrays(RED, getPixel(2)); + jum.assertEqualArrays(RED, getPixel(29)); hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 49 - jum.assertSameObjects(GREEN, getPixel(34)); - jum.assertSameObjects(GREEN, getPixel(66)); - jum.assertSameObjects(GREEN, getPixel(80)); + jum.assertEqualArrays(GREEN, getPixel(31)); + jum.assertEqualArrays(GREEN, getPixel(66)); + jum.assertEqualArrays(GREEN, getPixel(79)); hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 53 - jum.assertSameObjects(BLUE, getPixel(81)); + jum.assertEqualArrays(BLUE, getPixel(81)); }; this.test_incrementingFirstSegment = function () { hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 59 self.pb.setProgress("r", 0.9); - jum.assertSameObjects(RED, getPixel(0)); - jum.assertSameObjects(RED, getPixel(66)); - jum.assertSameObjects(RED, getPixel(79)); - jum.assertSameObjects(RED, getPixel(79)); - jum.assertSameObjects(BLUE, getPixel(91)); + jum.assertEqualArrays(RED, getPixel(2)); + jum.assertEqualArrays(RED, getPixel(66)); + jum.assertEqualArrays(RED, getPixel(79)); + + jum.assertEqualArrays(BLUE, getPixel(91)); }; this.test_decrementingFirstSegment = function () { hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 69 self.pb.setProgress("r", 0.7); - jum.assertSameObjects(RED, getPixel(69)); - jum.assertSameObjects(GREEN, getPixel(71)); - jum.assertSameObjects(GREEN, getPixel(79)); - jum.assertSameObjects(BLUE, getPixel(81)); - jum.assertSameObjects(BLUE, getPixel(91)); + jum.assertEqualArrays(RED, getPixel(69)); + jum.assertEqualArrays(GREEN, getPixel(71)); + jum.assertEqualArrays(GREEN, getPixel(79)); + jum.assertEqualArrays(BLUE, getPixel(81)); + jum.assertEqualArrays(BLUE, getPixel(91)); }; this.test_decrementingLastSegment = function () { hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 79 self.pb.setProgress("b", 0.2); - jum.assertSameObjects(GREEN, getPixel(79)); - jum.assertSameObjects(TRANS, getPixel(81)); + jum.assertEqualArrays(GREEN, getPixel(79)); + jum.assertEqualArrays(TRANS, getPixel(81)); }; this.teardown = function () { hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 84 - this.pb.destroy(); + //this.pb.destroy(); }; return this; hunk ./contrib/musicplayer/tests/test_utils.js 35 var test_ArrayZip = new function () { this.test_oneArg = function () { - jum.assertSameObjects([[1]], Array.zip([1])); + jum.assertEqualArrays([[1]], Array.zip([1])); }; this.test_twoArgs = function () { hunk ./contrib/musicplayer/tests/test_utils.js 39 - jum.assertSameObjects([[1, 1], [2, 2], [3, 3]], Array.zip([1, 2, 3], [1, 2, 3])); + jum.assertEqualArrays([[1, 1], [2, 2], [3, 3]], Array.zip([1, 2, 3], [1, 2, 3])); }; this.test_moreSimpleArgs = function () { hunk ./contrib/musicplayer/tests/test_utils.js 43 - jum.assertSameObjects([[1, 2, 3, 4, 5]], Array.zip([1], [2], [3], [4], [5])); + jum.assertEqualArrays([[1, 2, 3, 4, 5]], Array.zip([1], [2], [3], [4], [5])); }; this.test_notSameLength = function () { hunk ./contrib/musicplayer/tests/test_utils.js 47 - jum.assertSameObjects([[1, 2, 4]], Array.zip([1], [2, 3], [4, 5, 6])); - jum.assertSameObjects([ + jum.assertEqualArrays([[1, 2, 4]], Array.zip([1], [2, 3], [4, 5, 6])); + jum.assertEqualArrays([ [1, 4, 6], [2, 5, undefined], [3, undefined, undefined] } [add-controller-tests josip.lisec@gmail.com**20100725094652 Ignore-this: 506444f1ed082b7fd7caca82c1a2129f Added tests for: * da.ui.Dialog * da.controller.CollectionScanner * da.controller.Settings ] { addfile ./contrib/musicplayer/tests/test_CollectionScannerController.js hunk ./contrib/musicplayer/tests/test_CollectionScannerController.js 1 +var test_CollectionScannerController = new function () { + var CS = da.controller.CollectionScanner, + self = this; + + this.test_waitForScannerToFinish = { + method: "waits.forJS", + params: { + js: function () { + return da.db.DEFAULT.views.Song.view.rows.length === 3 + } + } + }; + + this.test_foundSongs = function () { + var songs = $A(da.db.DEFAULT.views.Song.view.rows); + for(var n = 0, m = songs.length; n < m; n++) + songs[n] = songs[n].value.title; + + jum.assertEquals("there should be three songs", + 3, songs.length + ); + + jum.assertSameArrays(["Maps", "Persona", "Hey Big Bang"], songs); + }; + + this.test_foundArtists = function () { + var artists = $A(da.db.DEFAULT.views.Artist.view.rows); + for(var n = 0, m = artists.length; n < m; n++) + artists[n] = artists[n].value.title; + + jum.assertEquals("there should be two artists", + 2, artists.length + ); + jum.assertSameArrays(["Keane", "Superhumanoids"], artists); + }; + + this.test_foundAlbums = function () { + var albums = $A(da.db.DEFAULT.views.Album.view.rows); + for(var n = 0, m = albums.length; n < m; n++) + albums[n] = albums[n].value.title; + + jum.assertEquals("there should be two albums", + 2, albums.length + ); + jum.assertSameArrays(["Sunshine Retrospective Collect", "Urgency"], albums); + }; +}; addfile ./contrib/musicplayer/tests/test_Dialog.js hunk ./contrib/musicplayer/tests/test_Dialog.js 1 +var test_Dialog = new function () { + var Dialog = da.ui.Dialog, + self = this; + + this.setup = function () { + self.dialog = new Dialog({ + title: "dialog_title", + html: new Element("div", {id: "_t_dialog", html: "dialog_content"}) + }); + }; + + this.test_domNodes = function () { + var el = self.dialog.toElement(); + jum.assertTrue("dialog should have been inserted into document's body", + !!$("_t_dialog") + ); + jum.assertTrue("dialog should be hidden", + el.style.display !== "block" + ); + jum.assertEquals("dialog should have title bar", + "dialog_title", + el.getElement(".dialog_title").get("text") + ); + jum.assertTrue("dialog should have its contents", + el.getElement(".dialog").get("text").indexOf("dialog_content") > 0 + ); + }; + + this.test_events = function () { + var show = hide = 0; + self.dialog.addEvent("show", function () { + show++; + }); + self.dialog.addEvent("hide", function () { + hide++; + }); + + var el = self.dialog.toElement(); + self.dialog.show(); + jum.assertEquals("dialog should be visible", + "block", el.style.display + ); + jum.assertEquals("show event should be fired", + 1, show + ); + + self.dialog.show(); + jum.assertEquals("show event should be fired only first time", + 1, show + ); + + self.dialog.hide(); + jum.assertEquals("dialog should be hidden", + "none", el.style.display + ); + jum.assertEquals("hide event should be fired", + 1, hide + ); + + self.dialog.hide(); + jum.assertEquals("hide event should be fired only first time", + 1, hide + ); + }; + + this.test_destroy = function () { + var el = self.dialog.toElement(); + self.dialog.destroy(); + + jum.assertEquals("nodes should be removed from document", + null, el.getParent() + ); + }; + + return this; +}; addfile ./contrib/musicplayer/tests/test_SettingsController.js hunk ./contrib/musicplayer/tests/test_SettingsController.js 1 - +var test_SettingsController = new function () { + var Settings = da.controller.Settings, + Setting = da.db.DocumentTemplate.Setting, + self = this; + + this.setup = function () { + Setting.register({ + id: "_is_this_working", + group_id: "_test", + representAs: "_test_question", + title: "Is this working?", + help: "Check this box if you think that it's going to work.", + value: false + }); + + Settings.registerGroup({ + id: "_test", + title: "Test", + description: "Help can be usually reached by calling 112." + }); + + self.renderer_called = 0; + Settings.addRenderer("_test_question", function (setting, details) { + self.renderer_called++; + + jum.assertEquals("'_is_this_working' setting should be passed", + "_is_this_working", + setting.id + ); + + return this.checkbox(setting, details); + }); + + self.serializer_called = 0; + Settings.addSerializer("_test_question", function (input) { + self.serializer_called++; + jum.assertEquals("right input element should be passed to serializer", + "setting__is_this_working", + input.id + ); + return input.checked; + }); + }; + + this.test_getDetails = function () { + jum.assertSameObjects({ + representAs: "_test_question", + title: "Is this working?", + help: "Check this box if you think that it's going to work." + }, Setting.getDetails("_is_this_working")); + }; + + this.test_waitForSetting = { + method: "waits.forJS", + params: { + js: function () { + return da.db.SETTINGS.views.Setting.view.findRow("_is_this_working") !== -1 + } + } + }; + + this.test_render = function () { + Settings.showGroup("_test"); + jum.assertEquals("renderer should have been called once", + 1, self.renderer_called + ); + jum.assertEquals("serializer shouldn't have been called yet", + 0, self.serializer_called + ); + }; + + this.test_revert = function () { + $("setting__is_this_working").checked = true; + $("revert_settings").click(); + + jum.assertFalse("checkbox should be unchecked", + $("setting__is_this_working").checked + ); + + jum.assertEquals("renderer should be called during revert", + 2, self.renderer_called + ); + }; + + this.test_serialize = function () { + $("setting__is_this_working").checked = true; + $("save_settings").click(); + + jum.assertEquals("serializer called once", + 1, self.serializer_called + ); + + self.saved_doc = null; + Setting.findFirst({ + properties: {id: "_is_this_working"}, + onSuccess: function (doc) { + self.saved_doc = doc; + } + }); + }; + + this.test_waitForDoc = { + method: "waits.forJS", + params: { + js: function () { return !!self.saved_doc } + } + }; + + this.test_verifySave = function () { + jum.assertTrue("setting's value should be saved to the db", + self.saved_doc.get("value") + ); + }; + + this.teardown = function () { + Settings.hide(); + }; + + return this; +}; } [add-player-controls josiplisec@gmail.com**20100728131919 Ignore-this: 56f8d094357df285a679a04bf536c582 * Added player controls (previos/play/next) with tests. * Made other, smaller, improvements to da.ui.NavigationColumn (smarter re-rendering) * Limited both scanner and indexer workers to allow only one request to Tahoe-LAFS, thus making network I/O almoast synchronous, mainly due to the fact that Tahoe never completes requests under high load. ] { hunk ./contrib/musicplayer/manage.py 6 import os, shutil, sys, subprocess, re from time import sleep +from tempfile import mkstemp from setuptools import setup from setuptools import Command hunk ./contrib/musicplayer/manage.py 125 shutil.copy('src/libs/vendor/browser-couch/js/worker-map-reducer.js', 'build/js/workers/map-reducer.js') print 'Calculating dependencies...' - appjs = JSDepsBuilder('src/') - appjs.write_to_file('build/app.js') - self._compress('build/js/app.js', ['build/app.js']) - os.remove('build/app.js') + self.deps = JSDepsBuilder('src/') hunk ./contrib/musicplayer/manage.py 127 - # Libraries used by web workers - self._compile_js('src/libs', 'build/js/workers/env.js', files = [ - 'vendor/mootools-1.2.4-core-server.js', - 'vendor/mootools-1.2.4-request.js', - - 'util/util.js', - 'util/BinaryFile.js', - 'util/ID3.js', - 'util/ID3v2.js', - 'util/ID3v1.js', - 'TahoeObject.js' - ]) + self._make_js('Application.js', 'build/js/app.js') + + #deps.write_to_file('build/app.js') + #self._compress('build/js/app.js', ['build/app.js']) + #os.remove('build/app.js') hunk ./contrib/musicplayer/manage.py 133 - # Compressing the workers themselves - self._compile_js('src/workers', 'build/js/workers/', join = False) + for worker in os.listdir('src/workers'): + if worker.endswith('.js'): + self._make_js('workers/' + worker, 'build/js/workers/' + worker) + #deps.write_to_file('build/worker_' + worker, root_file = 'workers/' + worker) print 'Done!' hunk ./contrib/musicplayer/manage.py 140 - def _compile_js(self, source, output_file, files = None, join = True): - js_files = files - if not js_files: - js_files = [] - for filename in os.listdir(source): - if filename.endswith('.js'): - js_files.append(os.path.join(source, filename)) - - js_files.sort() - else: - js_files = [os.path.join(source, path) for path in files] - - if join: - self._compress(output_file, js_files) - else: - for js_file in js_files: - self._compress(output_file + os.path.basename(js_file), [js_file]) + #def _compile_js(self, source, output_file, files = None, join = True): + # js_files = files + # if not js_files: + # js_files = [] + # for filename in os.listdir(source): + # if filename.endswith('.js'): + # js_files.append(os.path.join(source, filename)) + # + # js_files.sort() + # else: + # js_files = [os.path.join(source, path) for path in files] + # + # if join: + # self._compress(output_file, js_files) + # else: + # for js_file in js_files: + # self._compress(output_file + os.path.basename(js_file), [js_file]) + + def _make_js(self, root, output): + tmp_file = mkstemp()[1] + self.deps.write_to_file(tmp_file, root_file = root) + self._compress(output, [tmp_file]) + os.remove(tmp_file) def _compress(self, output_file, files): print 'Compressing %s...' % output_file hunk ./contrib/musicplayer/manage.py 264 root_dirs = [ 'src/', 'src/libs', 'src/libs/ui', 'src/libs/db', - 'src/libs/util', 'src/controllers', 'src/doctemplates' + 'src/libs/util', 'src/controllers', 'src/doctemplates', + 'src/services' ] for root_dir in root_dirs: for filename in os.listdir(root_dir): hunk ./contrib/musicplayer/src/Application.js 3 //#require "libs/vendor/mootools-1.2.4-core-ui.js" //#require "libs/vendor/mootools-1.2.4.4-more.js" +//#require "libs/util/console.js" /** * da hunk ./contrib/musicplayer/src/Application.js 55 da.db.DEFAULT = db; this.startup.checkpoint("data_db"); }.bind(this), new PersistStorage("tahoemp_data")); + + this.addEvent("ready.controller.CollectionScanner", function () { + if(!da.db.DEFAULT.getLength()) + da.controller.CollectionScanner.scan(); + }); }, loadInitialSettings: function () { hunk ./contrib/musicplayer/src/Application.js 73 {id: "settings_cap", type: "Setting", group_id: "caps", value: data.settings_cap} ], function () { this.startup.checkpoint("settings_db"); - - this.caps.music = data.music_cap, + + this.caps.music = data.music_cap; this.caps.settings = data.settings_cap; hunk ./contrib/musicplayer/src/Application.js 76 - + this.startup.checkpoint("caps"); hunk ./contrib/musicplayer/src/Application.js 78 + + if(!da.db.DEFAULT.getLength()) + da.controller.CollectionScanner.scan(); }.bind(this)); }.bind(this), hunk ./contrib/musicplayer/src/Application.js 110 }, finished: function (result) { + if(!result.rows.length) + return this.loadInitialSettings(); + this.caps.music = result.getRow("music_cap"); this.caps.settings = result.getRow("settings_cap"); if(!this.caps.music.length || !this.caps.music.length) hunk ./contrib/musicplayer/src/Application.js 122 }.bind(this), updated: function (result) { - if(result.findRow("music_cap") !== -1) { - this.caps.music = result.getRow("music_cap"); - da.controller.CollectionScanner.scan(this.caps.music); - } - if(result.findRow("settings_cap") !== -1) - this.caps.settings = result.getRow("settings_cap") + var music = result.getRow("music_cap"), + settings = result.getRow("settings_cap"); + + if(music) + da.controller.CollectionScanner.scan(this.caps.music = music); + + if(settings) + this.caps.settings = settings; + + this.startup.checkpoint("caps"); }.bind(this) }); }, hunk ./contrib/musicplayer/src/Application.js 146 $("loader").destroy(); $("panes").setStyle("display", "block"); - if(da.db.DEFAULT.getLength() === 0) - da.controller.CollectionScanner.scan(); - this.fireEvent("ready"); } }; hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 29 * Starts a new scan using [[Application.caps.music]] as root directory. **/ initialize: function (root) { + 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 40 this.finished = false; - this._songs_found = 0; + this._found_files = 0; this._goal = new Goal({ checkpoints: ["scanner", "indexer"], onFinish: function () { hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 50 da.ui.ROAR.alert( "Collection scanner finished", "{0} songs have been found. {1}".interpolate([ - this._found_songs, - this._found_songs ? "Your patience has paid off." : "Make sure your files have proper ID3 tags." + this._found_files, + this._found_files ? "Your patience has paid off." : "Make sure your files have proper ID3 tags." ]) ); }.bind(this) hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 76 return; } - this._found_files++; if(da.db.DEFAULT.views.Song.view.findRow(cap) === -1) this.indexer.postMessage(cap); }, hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 85 this._goal.checkpoint("indexer"); return; } - + // Lots of async stuff is going on, a short summary would look something like: // 1. find or create artist with given name and save its id // to artist_id. hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 92 // 2. look for an album with given artist_id (afterCheckpoint.artist) // 3. save the album data. // 4. look for song with given id and save the new data. + this._found_files++; var tags = event.data, album_id, artist_id, hunk ./contrib/musicplayer/src/controllers/Navigation.js 265 * da.controller.Navigation **/ var Navigation = { - // This is not really a class, but PDoc refuses to generate docs otherwise. /** hunk ./contrib/musicplayer/src/controllers/Navigation.js 266 - * da.controller.Navigation.columns + * da.controller.Navigation.columns * hunk ./contrib/musicplayer/src/controllers/Navigation.js 268 - * Contains all known columns. - * - * #### Notes - * Use [[da.controller.Navigation.registerColumn]] to add new ones, - * *do not* add them manually. + * Contains all known columns. + * + * #### Notes + * Use [[da.controller.Navigation.registerColumn]] to add new ones, + * *do not* add them manually. **/ columns: {}, hunk ./contrib/musicplayer/src/controllers/Player.js 1 -//#require "libs/vendor/soundmanager/script/soundmanager2.js" +//#require "libs/vendor/SoundManager.js" +//#require "doctemplates/Song.js" //#require "libs/ui/SegmentedProgressBar.js" hunk ./contrib/musicplayer/src/controllers/Player.js 4 -//#require "services/albumCover.js" (function () { hunk ./contrib/musicplayer/src/controllers/Player.js 6 -var DocumentTemplate = da.db.DocumentTemplate, - Song = DocumentTemplate.Song, - Setting = DocumentTemplate.Setting, +var soundManager = da.vendor.soundManager, + Song = da.db.DocumentTemplate.Song, SegmentedProgressBar = da.ui.SegmentedProgressBar; /** section: Controllers hunk ./contrib/musicplayer/src/controllers/Player.js 24 * **/ var Player = { + + // We're using term sound for soundManager objects, while + // song for DocumentTemplate.Song instances. + /** + * Player#sounds -> Object + * + * Cache of SoundManager's object. Keys are id's of [[da.db.DocumentTemplate.Song]]. + **/ + sounds: {}, + + /** + * Player#playlist -> [String, ...] + * Contains id's of songs in the playlist. + **/ + playlist: [], + _playlist_pos: 0, + + /** + * Player#queue -> [da.db.DocumentTemplate.Song, ...] + **/ + queue: [], + + /** + * Player#nowPlaying -> {song: , sound: } + **/ + nowPlaying: { + song: null, + sound: null + }, + _loading: [], + /** * new Player() * Sets up soundManager2 and initializes player's interface. hunk ./contrib/musicplayer/src/controllers/Player.js 60 **/ initialize: function () { - var path = location.protocol + "//" + location.host + location.pathname; - //path = path.slice(-path.lastIndexOf("/")); - $extend(soundManager, { - useHTML5Audio: false, - url: path + 'resources/flash/', - debugMode: false, - debugFlash: false - }); - soundManager.onready(function () { da.app.startup.checkpoint("soundmanager"); }); hunk ./contrib/musicplayer/src/controllers/Player.js 65 da.app.addEvent("ready", this.initializeUI.bind(this)); - - // We're using term sound for soundManager objects, while - // song for DocumentTemplate.Song instances. - /** - * Player#sounds -> Object - * - * Cache of SoundManager's object. Keys are id's of [[da.db.DocumentTemplate.Song]]. - **/ - this.sounds = {}; - /** - * Player#playlist -> [Song, ...] - **/ - this.playlist = []; - /** - * Player#nowPlaying -> {song: , sound: } - **/ - this.nowPlaying = {song: null, sound: null}; - - this._loading = []; }, initializeUI: function () { hunk ./contrib/musicplayer/src/controllers/Player.js 81 this.progress_bar.toElement().id = "track_progress"; this.progress_bar.toElement().addEvents({ - // Has some issues in firefox - the object's width also gets scaled + // Has some issues in Firefox - the object's width also gets scaled /* mouseenter: function () { this.tween("height", 15); hunk ./contrib/musicplayer/src/controllers/Player.js 100 } }); - var info_block = new Element("div", {id: "song_info_block" }), - 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"}), - position = new Element("span", {id: "song_position"}), - controls = new Element("div", {id: "player_controls"}), - play = new Element("a", {id: "play_button", href: "#"}); + var els = { + info_block: new Element("div", {id: "song_info_block" }), + 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: "#"}) + }; + + var play_wrapper = new Element("div", {id: "play_button_wrapper"}); + play_wrapper.grab(els.play); + els.controls.adopt(els.prev, play_wrapper, els.next, this.progress_bar.toElement()); + + els.wrapper.grab(els.song_title); + els.wrapper.appendText("from "); + els.wrapper.grab(els.album_title); + els.wrapper.appendText(" by "); + els.wrapper.adopt(els.artist_name, els.controls); + + els.cover_wrapper.grab(els.album_cover); hunk ./contrib/musicplayer/src/controllers/Player.js 126 - controls.adopt(play, this.progress_bar.toElement()); + els.info_block.adopt(els.cover_wrapper, els.wrapper, new Element("div", {"class": "clear"})); + + this._el.adopt(els.info_block); hunk ./contrib/musicplayer/src/controllers/Player.js 130 - wrapper.grab(song_title); - wrapper.appendText("from "); - wrapper.grab(album_title); - wrapper.appendText(" by "); - wrapper.adopt(artist_name, position, controls); + this._el.style.visibility = "hidden"; + this._visible = false; hunk ./contrib/musicplayer/src/controllers/Player.js 133 - cover_wrapper.grab(album_cover); + var next = els.next, prev = els.prev; + els.play.addEvents({ + click: function () { + Player.playPause(); + }, + + mouseover: function () { + if(!next.hasClass("inactive")) + next.fade("show"); + if(!prev.hasClass("inactive")) + prev.fade("show"); + } + }); hunk ./contrib/musicplayer/src/controllers/Player.js 147 - info_block.adopt(cover_wrapper, wrapper, new Element("div", {"class": "clear"})); - info_block.style.visibility = "hidden"; - this._info_block_visible = false; + var hideNextPrev = function () { + next.fade("out"); + prev.fade("out"); + }; hunk ./contrib/musicplayer/src/controllers/Player.js 152 - this._el.adopt(info_block); + els.next.addEvents({ + click: function () { Player.playNext() } + }); + els.next.set("tween", {duration: "short", link: "cancel"}); hunk ./contrib/musicplayer/src/controllers/Player.js 157 - play.addEvent("click", function () { - Player.playPause(); + els.prev.addEvents({ + click: function () { Player.playPrev() } }); hunk ./contrib/musicplayer/src/controllers/Player.js 160 + els.prev.set("tween", {duration: "short", link: "cancel"}); + + els.controls.addEvent("mouseleave", hideNextPrev); + + this.elements = els; + delete els; + delete play_wrapper; }, /** hunk ./contrib/musicplayer/src/controllers/Player.js 177 **/ play: function (song) { var np = this.nowPlaying; - if(!song && np.song) - np.sound.resume(); - - $("play_button").addClass("active"); - - if(np.song && song.id === np.song.id) + if(!song || (song && np.song && np.song.id === song.id)) return; hunk ./contrib/musicplayer/src/controllers/Player.js 180 + this.elements.play.addClass("active"); + if(this.sounds[song.id]) { np.sound.stop(); this.sounds[song.id].play(); hunk ./contrib/musicplayer/src/controllers/Player.js 185 + this.progress_bar.setProgress("load", 1); return; } hunk ./contrib/musicplayer/src/controllers/Player.js 209 onplay: function () { Player.setNowPlaying(song); }, + whileloading: function () { hunk ./contrib/musicplayer/src/controllers/Player.js 211 - Player.progress_bar.setProgress("load", this.bytesLoaded/this.bytesTotal) + if(Player.nowPlaying.sound === this) + Player.progress_bar.setProgress("load", this.bytesLoaded/this.bytesTotal); }, hunk ./contrib/musicplayer/src/controllers/Player.js 214 + whileplaying: function () { hunk ./contrib/musicplayer/src/controllers/Player.js 216 - Player.progress_bar.setProgress("track", this.position/this.durationEstimate); + if(Player.nowPlaying.sound === this) + Player.progress_bar.setProgress("track", this.position/this.durationEstimate); }, hunk ./contrib/musicplayer/src/controllers/Player.js 219 + onfinish: function () { hunk ./contrib/musicplayer/src/controllers/Player.js 221 - Player.playbackFinished(); + if(Player.nowPlaying.sound === this) + Player.playbackFinished(); } }); hunk ./contrib/musicplayer/src/controllers/Player.js 229 this.sounds[song.id].play(); np = null; - if(!this._info_block_visible) { - this._info_block_visible = true; - $("song_info_block").style.visibility = "visible"; + if(!this._visible) { + this._visible = true; + this._el.style.visibility = "visible"; } }, hunk ./contrib/musicplayer/src/controllers/Player.js 237 /** * Player#setNowPlaying(song) -> undefined - * fires nowPlaying + * fires play **/ setNowPlaying: function (song) { if(this.nowPlaying.sound) hunk ./contrib/musicplayer/src/controllers/Player.js 243 this.nowPlaying.sound._last_played = +(new Date()); + var pp = this.playlist.indexOf(song.id); + if(pp !== -1) + this._playlist_pos = pp; + delete pp; + this.nowPlaying = { song: song, sound: this.sounds[song.id] hunk ./contrib/musicplayer/src/controllers/Player.js 254 }; var song_title = song.get("title"), - song_title_el = $("song_title"); - song_title_el.set("text", song_title); - song_title_el.title = song_title; - $("song_position").title = "total " + song.get("duration"); + els = this.elements; + + els.song_title.set("text", song_title); + els.song_title.title = song_title; song.get("album", function (album) { var title = album.get("title"), hunk ./contrib/musicplayer/src/controllers/Player.js 262 album_covers = album.get("album_cover_urls"), - el = $("song_album_title"); - - el.set("text", title); - el.title = title; + has_album_cover = album_covers && album_covers[2] && album_covers[2].length; hunk ./contrib/musicplayer/src/controllers/Player.js 264 - title = null; - delete el; + els.album_title.set("text", title); + els.album_title.title = title; + els.album_cover.src = has_album_cover ? album_covers[2] : "resources/images/album_cover_2.png"; hunk ./contrib/musicplayer/src/controllers/Player.js 268 - if(album_covers) - $("song_album_cover").src = album_covers[2]; - else - da.service.albumCover(album, function (urls) { - $("song_album_cover").src = urls[2]; - }); - - album = null; - album_covers = null; + delete album; + delete title; + delete album_covers; + delete has_album_cover; }); hunk ./contrib/musicplayer/src/controllers/Player.js 273 + song.get("artist", function (artist) { hunk ./contrib/musicplayer/src/controllers/Player.js 275 - var aname = artist.get("title"), - el = $("song_artist_name"); - el.set("text", aname); - el.title = aname; + var aname = artist.get("title"); + els.artist_name.set("text", aname); + els.artist_name.title = aname; aname = null; hunk ./contrib/musicplayer/src/controllers/Player.js 280 - delete el; + delete els; }); hunk ./contrib/musicplayer/src/controllers/Player.js 283 - da.controller.Player.fireEvent("nowPlaying", [song]); + da.controller.Player.fireEvent("play", [song]); + song = null; + + this.updateNextPrev(); }, /** hunk ./contrib/musicplayer/src/controllers/Player.js 297 * repeat this song, etc. **/ playbackFinished: function () { - $("play_button").removeClass("active"); + this.elements.play.removeClass("active"); + + this.playNext(); }, /** hunk ./contrib/musicplayer/src/controllers/Player.js 305 * Player#pause() -> undefined * fires pause - * **/ pause: function () { if(!this.nowPlaying.song) hunk ./contrib/musicplayer/src/controllers/Player.js 311 return; this.nowPlaying.sound.pause(); - $("play_button").removeClass("active"); + this.elements.play.removeClass("active"); da.controller.Player.fireEvent("pause", [this.nowPlaying.song]); }, hunk ./contrib/musicplayer/src/controllers/Player.js 327 if(!this.nowPlaying.song && song) this.play(song); - if(this.nowPlaying.sound.paused) - this.play(); - else + if(this.nowPlaying.sound.paused) { + this.nowPlaying.sound.resume(); + } else this.pause(); }, hunk ./contrib/musicplayer/src/controllers/Player.js 334 /** + * Player#playPrev() -> undefined + **/ + playPrev: function () { + this.play(Song.findById(this.playlist[this._playlist_pos - 1])); + }, + + /** + * Player#getNext() -> String + * + * Returns the ID of the song that will be played next. + **/ + getNext: function () { + if(this.queue.length) + return this.queue[0]; + + if(this.playlist.length) + return this.playlist[this._playlist_pos + 1]; + }, + + /** + * Player#playNext() -> undefined + **/ + playNext: function () { + var next = this.getNext(); + if(!next) + return; + + if(this.queue.length) + this.queue.shift(); + if(this.playlist.length) + this._playlist_pos++; + + this.play(Song.findById(next)); + }, + + /** + * Player#positionNextPrev() -> undefined + **/ + updateNextPrev: function () { + var els = this.elements, + prev = this.playlist[this._playlist_pos - 1], + next = this.getNext(); + + prev = prev ? Song.findById(prev) : null; + next = next ? Song.findById(next) : null; + + if(prev) + els.prev + .set("text", prev.get("title")) + .show() + .position({ + position: "centerLeft", + edge: "centerRight", + relativeTo: els.play + }) + .removeClass("inactive"); + else + els.prev.hide().addClass("inactive"); + + if(next) + els.next + .set("text", next.get("title")) + .show() + .position({ + position: "centerRight", + edge: "centerLeft", + relativeTo: els.play + }) + .removeClass("inactive"); + else + els.next.hide().addClass("inactive"); + + delete els; + delete next; + delete prev; + }, + + /** * Player#free() -> undefined * * Frees memory taken by loaded songs. This method is ran about every hunk ./contrib/musicplayer/src/controllers/Player.js 424 **/ free: function () { var eight_mins_ago = (+ new Date()) - 8*60*1000; - + var sound; for(var id in this.sounds) { sound = this.sounds[id]; hunk ./contrib/musicplayer/src/controllers/Player.js 442 delete sound; } }; + Player.initialize(); // Check is performed every four minutes hunk ./contrib/musicplayer/src/controllers/Player.js 446 -Player.free.periodical(8*60*1000, Player); +setTimeout(function () { + Player.free(); +}, 8*60*1000); hunk ./contrib/musicplayer/src/controllers/Player.js 450 -/** +/* TODO: Should be moved to another controller, Statistics or something alike. function findAverageDuration(callback) { da.db.DEFAULT.view({ hunk ./contrib/musicplayer/src/controllers/Player.js 481 } }); } -**/ +*/ /** * da.controller.Player hunk ./contrib/musicplayer/src/controllers/Player.js 488 **/ da.controller.Player = { /** - * da.controller.Player.play([song]) -> Boolean + * da.controller.Player.play([song]) -> undefined * - cap (da.db.DocumentTemplate.Song): the track to be played. hunk ./contrib/musicplayer/src/controllers/Player.js 490 - * - * If `uri` is omitted and there is paused playback, then the paused - * file will resume playing. + * fires play **/ play: function (song) { Player.play(song); hunk ./contrib/musicplayer/src/controllers/Player.js 497 }, /** - * da.controller.Player.pause() -> Boolean + * da.controller.Player.pause() -> undefined + * fires pause * * Pauses the playback (if any). **/ hunk ./contrib/musicplayer/src/controllers/Player.js 503 pause: function () { - Player.pause() + Player.pause(); }, /** hunk ./contrib/musicplayer/src/controllers/Player.js 507 - * da.controller.Player.queue(song) -> Boolean - * - uri (String): location of the audio file. + * da.controller.Player.queue(id) -> [String, ...] + * - id (String): location of the audio file. * * Adds file to the play queue and plays it as soon as currently playing * file finishes playing (if any). hunk ./contrib/musicplayer/src/controllers/Player.js 512 + * + * Returned array contains queued songs. **/ hunk ./contrib/musicplayer/src/controllers/Player.js 515 - queue: function (song) { return false }, + queue: function (id) { + Player.queue.include(id); + return Player.queue; + }, + + /** + * da.controller.Player.getNext() -> String + * Returns ID of the [[da.db.DocumentTemplate.Song]] which will be played next. + **/ + getNext: function () { + return Player.getNext(); + }, + + /** + * da.controller.Player.getPrev() -> String + * Returns ID of the [[da.db.DocumentTemplate.Song]] which which was played before this one. + **/ + getPrev: function () { + return Player.playlist[Player._playlist_pos - 1]; + }, + + /** + * da.controller.Player.setPlaylist(playlist) -> undefined + * - playlist ([String, ...]): array containing id's of [[da.db.DocumentTemplate.Song]] documents. + **/ + setPlaylist: function (playlist) { + if(!playlist || $type(playlist) !== "array") + return false; + + Player.playlist = playlist; + Player._playlist_pos = 0; + }, /** * da.controller.Player.nowPlaying() -> da.db.DocumentTemplate.Song hunk ./contrib/musicplayer/src/controllers/default_columns.js 2 //#require "libs/ui/NavigationColumn.js" +//#require "services/albumCover.js" (function () { hunk ./contrib/musicplayer/src/controllers/default_columns.js 5 -var Navigation = da.controller.Navigation, - NavigationColumn = da.ui.NavigationColumn; +var Navigation = da.controller.Navigation, + NavigationColumn = da.ui.NavigationColumn, + Album = da.db.DocumentTemplate.Album, + Song = da.db.DocumentTemplate.Song, + fetchAlbumCover = da.service.albumCover; /** section: Controller * class da.controller.Navigation.columns.Root < da.ui.NavigationColumn hunk ./contrib/musicplayer/src/controllers/default_columns.js 111 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; hunk ./contrib/musicplayer/src/controllers/default_columns.js 117 this._el.addEvent("resize", function () { - this.options.rowHeight = this._el.getWidth() / (4 + this._row_dim + 4); + 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 125 this.render(); }, - + createFilter: function (item) { return {album_id: item.id}; }, hunk ./contrib/musicplayer/src/controllers/default_columns.js 131 renderItem: function (index) { - var item = this.getItem(index).value, + var item = this.getItem(index), + data = item.value, el = new Element("a", { hunk ./contrib/musicplayer/src/controllers/default_columns.js 134 - href: "#", - title: item.title - }); + id: this.options.id + "_column_item_" + item.id, + href: "#", + title: data.title + }), + cover = data.icons[this.options.iconSize]; el.style.width = this._row_dim + "px"; el.style.height = this._row_dim + "px"; hunk ./contrib/musicplayer/src/controllers/default_columns.js 142 + if(!cover || !cover.length) { + cover = "resources/images/album_cover_" + this.options.iconSize + ".png"; + fetchAlbumCover(Album.findById(item.id)); + } hunk ./contrib/musicplayer/src/controllers/default_columns.js 147 - el.grab(new Element("img", {src: item.icons[this.options.iconSize]})); + el.grab(new Element("img", {src: cover})); return el; }, hunk ./contrib/musicplayer/src/controllers/default_columns.js 169 this.parent(options); this._el.style.width = "300px"; + this._playlist = []; hunk ./contrib/musicplayer/src/controllers/default_columns.js 171 - this.addEvent("click", function (item) { - da.controller.Player.play(new da.db.DocumentTemplate.Song( - da.db.DEFAULT.views.Song.view.getRow(item.id)) - ); + this.addEvent("click", function (item, event, el) { + da.controller.Player.setPlaylist(this._playlist); + da.controller.Player.play(Song.findById(item.id)); }); }, hunk ./contrib/musicplayer/src/controllers/default_columns.js 190 } }, - /* - renderItem: function (index) { - var item = this.getItem(index).value, - el = new Element("a", {href: "#", title: item.title}); + mapReduceUpdated: function (result) { + this.parent(result); hunk ./contrib/musicplayer/src/controllers/default_columns.js 193 - el.grab(new Element("span", {html: item.title, "class": "title"})); + var n = this.options.totalCount, + playlist = new Array(n); hunk ./contrib/musicplayer/src/controllers/default_columns.js 196 - return el; - }, */ + while(n--) + playlist = this._rows[n].id; + + this._playlist = playlist; + delete playlist; + }, compareFunction: function (a, b) { a = a && a.value ? a.value.track : a; hunk ./contrib/musicplayer/src/index_devel.html 11 - - - - - - - - hunk ./contrib/musicplayer/src/index_devel.html 16 - + + + + + + hunk ./contrib/musicplayer/src/index_devel.html 27 - + + + + + hunk ./contrib/musicplayer/src/index_devel.html 65 - + hunk ./contrib/musicplayer/src/libs/TahoeObject.js 1 +//#require + (function () { /** * == Tahoe == hunk ./contrib/musicplayer/src/libs/TahoeObject.js 121 return result; } }); -window.TahoeObject = TahoeObject; + +da.util.TahoeObject = TahoeObject; })(); hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 310 /** * da.db.DocumentTemplate.findFirst(options[, db]) -> undefined * - options (Object): same options as in [[da.db.DocumentTemplate.find]] apply here. + * + * #### Notes + * This method is also available on all classes which inherit from [[da.db.DocumentTemplate]]. **/ findFirst: function (options, db) { options.onlyFirst = true; hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 323 * da.db.DocumentTemplate.findOrCreate(options[, db]) -> undefined * - options (Object): same options as in [[da.db.DocumentTemplate.find]] apply here. * - options.properties.type (String): must be set to the desired [[da.db.DocumentTemplate]] type. + * + * #### Notes + * This method is also available on all classes which inherit from [[da.db.DocumentTemplate]]. **/ findOrCreate: function (options, db) { options.onSuccess = options.onSuccess || $empty; hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 336 }, /** - * da.db.DocumentTemplate.registerType(typeName[, db = Application.db], template) -> da.db.DocumentTemplate - * - typeName (String): name of the type. ex.: `Car`, `Chocolate` etc. + * da.db.DocumentTemplate.registerType(typeName[, db = da.db.DEFAULT], template) -> da.db.DocumentTemplate + * - typeName (String): name of the type. ex.: `Car`, `Chocolate`, `Song`, etc. * - db (BrowserCouch): database to be used. hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 339 - * - template (da.db.DocumentTemplate): the actual [[da.db.DocumentTemplate]] [[Class]]. + * - template (da.db.DocumentTemplate): class which extends [[da.db.DocumentTemplate]]. * * New classes are accessible from `da.db.DocumentTemplate.`. hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 342 + * + * #### Notes + * You _must_ use this method in order to **/ registerType: function (type, db, template) { if(arguments.length === 2) { hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 389 DocumentTemplate.findOrCreate(options, db); }; + /** + * da.db.DocumentTemplate.findById(id) -> da.db.DocumentTemplate + * - id (String): id of the document + * + * #### Notes + * This method is available *only* on classes which extend [[da.db.DocumentTemplate]] + * and are registered using [[da.db.DocumentTemplate.registerType]]. + **/ template.findById = function (id) { hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 398 - var doc = template.db().views[type].view.getRow(id); - return doc ? new template(doc) : null; + var doc = template.view().getRow(id); + return doc && !doc._deleted ? new template(doc) : null; + }; + + template.view = function () { + return template.db().views[type].view; }; template.db().view({ hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 409 id: type, map: function (doc, emit) { - if(doc && doc.type === type) + if(doc && !doc._deleted && doc.type === type) emit(doc.id, doc); }, finished: $empty hunk ./contrib/musicplayer/src/libs/ui/Column.js 10 * * Widget which can efficiently display large amounts of items in a list. **/ + var IDS = 0; da.ui.Column = new Class({ Implements: [Events, Options], hunk ./contrib/musicplayer/src/libs/ui/Column.js 21 totalCount: 0, renderTimeout: 120 }, + /** * new da.ui.Column(options) * - options.id (String): desired ID of the column's DIV element, `_column` will be appended. hunk ./contrib/musicplayer/src/libs/ui/Column.js 43 initialize: function (options) { this.setOptions(options); if(!this.options.id) - this.options.id = "column_" + (IDS++); + this.options.id = "duC_" + (IDS++); this._populated = false; // #_rendered will contain keys of items which have been rendered. hunk ./contrib/musicplayer/src/libs/ui/Column.js 184 this._populated = false; return this.render(); }, + /** * da.ui.Column#updateTotalCount(totalCount) -> this | false * - totalCount (Number): total number of items this column is going to display hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 115 * rendered ones (due to sorting). **/ mapReduceUpdated: function (values) { - this._rows = $A(da.db.DEFAULT.views[this.view.id].view.rows); - this._rows.sort(this.compareFunction); - this.options.totalCount = this._rows.length; - return this.rerender(); + var new_rows = $A(da.db.DEFAULT.views[this.view.id].view.rows); + new_rows.sort(this.compareFunction); + + // Noting new was added, so we can simply re-render those elements + if(this.options.totalCount === new_rows.length) { + values = values.rows; + var n = values.length, + id_prefix = this.options.id + "_column_item_", + item, el, index; + + while(n--) { + item = values[n]; + el = $(id_prefix + item.id); + if(el) { + index = el.retrieve("column_index"); + + this.renderItem(index) + .addClass("column_item") + .store("column_index", index) + .replaces(el); + } + } + + this._rows = new_rows; + } else { + this.options.totalCount = new_rows.length; + this._rows = new_rows; + return this.rerender(); + } }, /** hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 158 * da.ui.NavigationColumn#renderItem(index) -> Element * - index (Number): position of the item that needs to be rendered. * - * This function relies on `title`, `subtitle` and `icon` properties from emitted documents. + * This function relies on `title`, `subtitle` and `icon` properties from emitted documents. + * + * #### Note + * If you are overwriting this method, make sure that the returned element has the `id` attribute + * that follows this convention: + * + * this.options.id + "_column_item_" + item.id + * + * Where `item.id` represents unique identifier of the item that is being rendered (not to be mistaken + * with `index` argument). + * + * This is necessary for updating views which are bound to [[da.db.BrowserCouch]] views. + * **/ renderItem: function (index) { hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 173 - var item = this.getItem(index).value, + var item = this.getItem(index), + data = this.getItem(index).value, el = new Element("a", { hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 176 - href: "#", - title: item.title, - "class": index%2 ? "even" : "odd" + id: this.options.id + "_column_item_" + item.id, + href: "#", + title: data.title, + "class": index%2 ? "even" : "odd" }); hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 182 - if(item.icon) - el.grab(new Element("img", {src: item.icon})); - if(item.title) - el.grab(new Element("span", {html: item.title, "class": "title"})); - if(item.subtitle) - el.grab(new Element("span", {html: item.subtitle, "class": "subtitle"})); + if(data.icon) + el.grab(new Element("img", {src: data.icon})); + if(data.title) + el.grab(new Element("span", {html: data.title, "class": "title"})); + if(data.subtitle) + el.grab(new Element("span", {html: data.subtitle, "class": "subtitle"})); hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 189 + delete item; + delete data; return el; }, hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 73 el_width = this.options.width, diff = p - current_progress, increment = diff > 0, - x, width; + // Since most of the time we'll be incrementing + // progress we can save one if/else condition + // by "caching" the results immediately + x = current_progress, + width = diff; if(!diff) return this; hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 82 - if(increment) { - x = current_progress * el_width; - width = diff * el_width; - } else { - x = (current_progress - (-diff)) * el_width; - width = (current_progress - p) * el_width; + if(!increment) { + x = current_progress - (-diff); + width = current_progress - p; } hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 87 + // This allows SegmentedProgressBar to acutally + // draw bars with different colours. this.ctx.fillStyle = this.options.foreground; hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 90 - // We're adding +-1 here because some browsers (even different implementations - // of WebKit are unable to render this precisely when small changes are - // in place. - this.ctx[increment ? "fillRect" : "clearRect"](x - 1, 0, width + 1, this.options.height); + // We're adding +-1px here because some browsers are unable + // to render small changes precisely. (even different implementations of WebKit) + this.ctx[increment ? "fillRect" : "clearRect"]( + (x * el_width) - 1, 0, + (width * el_width) + 1, this.options.height + ); + this.progress = p; return this; hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 104 /** * da.ui.ProgressBar#rerender() -> this + * + * #### Notes + * Unlike [[da.ui.ProgressBar.setProgress]] this method will render the whole bar, + * and thus is not really efficient (but indeed, needed in some situations). **/ rerender: function () { var opts = this.options; hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 114 this.ctx.fillStyle = opts.foreground; this.ctx.fillRect(0, 0, this.progress * opts.width, opts.height); + delete opts; return this; }, hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 130 **/ destroy: function () { this._el.destroy(); + delete this._el; delete this.ctx; hunk ./contrib/musicplayer/src/libs/ui/ProgressBar.js 133 - delete this._fx; + delete this.progress; + delete this.options; } }); da.ui.ProgressBar = ProgressBar; hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 1 +//#require + /* * Binary Ajax 0.2 * hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 20 * * Class containing methods for working with files as binary data. **/ + +var UNPACK_FORMAT = /(\d+\w|\w)/g, + WHITESPACE = /\s./g; + var BinaryFile = new Class({ /** * new da.util.BinaryFile(data[, options]) hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 45 // In this case we're probably dealing with IE, // and in order for this to work, VisualBasic-script magic is needed, // for which we don't have enough of mana. - throw Exception("This browser is not supported"); + throw Exception("We're lacking some mana. Please use different browser."); } }, hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 268 * #### External resources * * [Python implementation of `unpack`](http://docs.python.org/library/struct.html#format-strings) **/ - _unpack_format: /(\d+\w|\w)/g, - _whitespace: /\s./g, unpack: function (format) { hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 269 - format = format.replace(this._whitespace, ""); - var pairs = format.match(this._unpack_format), + format = format.replace(WHITESPACE, ""); + var pairs = format.match(UNPACK_FORMAT), n = pairs.length, result = []; hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 333 } return result; - }, - - destroy: function () { - delete this.data; + }, + + destroy: function () { + delete this.data; } }); hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 414 range[0] += +this.headRequest.getHeader("Content-Length"); range[1] = range[0] + range[1] - 1; this.options.range = range; - + if(this.headRequest.isSuccess()) this.send(this._send_options || {}); }, hunk ./contrib/musicplayer/src/libs/util/ID3.js 1 +//#require +//#require "libs/util/BinaryFile.js" + /** * == ID3 == * hunk ./contrib/musicplayer/src/libs/util/ID3.js 79 }, /** - * ID3#destory() -> undefined + * da.util.ID3#destory() -> undefined **/ destroy: function () { if(this.parser) hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 377 }, /** - * ID3v2Parser#destroy() -> undefined + * da.util.ID3v2Parser#destroy() -> undefined **/ destroy: function () { this.data.destroy(); addfile ./contrib/musicplayer/src/libs/util/console.js hunk ./contrib/musicplayer/src/libs/util/console.js 1 +if(!this.console) + this.console = { + log: function () {} + }; addfile ./contrib/musicplayer/src/libs/vendor/SoundManager.js hunk ./contrib/musicplayer/src/libs/vendor/SoundManager.js 1 +//#require + +(function () { +/** + * class da.vendor.SoundManager + * SoundManager2 class + * + * #### Links + * * http://www.schillmania.com/projects/soundmanager2/ + * + **/ +/** + * da.vendor.soundManager + * + * Default instance of [[da.vendor.SoundManager]]. + * + **/ + +(function (window) { +//#require "libs/vendor/soundmanager/script/soundmanager2.js" +})(da.vendor); + +var url = location.protocol + "//" + location.host + location.pathname.split("/").slice(0, -1).join("/"), + path = location.pathname.contains("devel.html") ? "/libs/vendor/soundmanager/swf/" : "/resources/flash/"; + +$extend(da.vendor.soundManager, { + useHTML5Audio: false, + url: url + path, + debugMode: false, + debugFlash: false +}); + +})(); hunk ./contrib/musicplayer/src/resources/css/app.css 160 height: 100%; text-align: center; padding: 50px 0 0 0; + text-shadow: #fff 0 1px 0; } /*** Notifications (Roar) ***/ hunk ./contrib/musicplayer/src/resources/css/app.css 166 /* Contents of Roar.css (distributed under MIT) */ .roar-body { - position: absolute; - color: #fff; - text-align: left; - z-index: 999; - font-size: 0.8em; + position: absolute; + color: #fff; + text-align: left; + z-index: 999; + font-size: 0.8em; } .roar { hunk ./contrib/musicplayer/src/resources/css/app.css 174 - position: absolute; - width: 300px; - cursor: default; - padding: 5px; + position: absolute; + width: 300px; + cursor: default; + padding: 5px; } .roar-bg { hunk ./contrib/musicplayer/src/resources/css/app.css 181 - position: absolute; - z-index: 1000; - width: 100%; - height: 100%; - left: 0; - top: 0; - background-color: #000; - border: 2px solid #000; + position: absolute; + z-index: 1000; + width: 100%; + height: 100%; + left: 0; + top: 0; + background-color: #000; + border: 2px solid #000; + + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + -o-border-radius: 5px; + border-radius: 5px; hunk ./contrib/musicplayer/src/resources/css/app.css 195 - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - -o-border-radius: 5px; - border-radius: 5px; - - -webkit-box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px; + -webkit-box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px; -moz-box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px; hunk ./contrib/musicplayer/src/resources/css/app.css 197 - -o-box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px; - box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px; + -o-box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px; + box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px; } hunk ./contrib/musicplayer/src/resources/css/app.css 203 .roar h3 { - position: relative; - margin: 0; - border: 0; - font-size: 13px; - color: #fff; - z-index: 1002; + position: relative; + margin: 0; + border: 0; + font-size: 13px; + color: #fff; + z-index: 1002; } .roar p { hunk ./contrib/musicplayer/src/resources/css/app.css 212 - position: relative; - margin: 0; - font-size: 12px; - color: #fff; - z-index: 1002; + position: relative; + margin: 0; + font-size: 12px; + color: #fff; + z-index: 1002; } /*** Navigation columns ***/ hunk ./contrib/musicplayer/src/resources/css/app.css 320 margin-left: 20px; } -.navigation_column .active_column_item, .menu_item:hover { - background: #33519d !important; +.navigation_column .active_column_item, .menu_item:hover, #next_song:hover, #prev_song:hover { + background-color: #33519d !important; text-shadow: #000 0 1px 0; color: #fff !important; outline: 0 !important; hunk ./contrib/musicplayer/src/resources/css/app.css 334 /** Albums column **/ #Albums_column { - background: #595959; + background: #fff; } #Albums_column .column_item { hunk ./contrib/musicplayer/src/resources/css/app.css 556 #song_details span { display: inline-block; max-width: 125px; + vertical-align: bottom; } #song_album_cover_wrapper { hunk ./contrib/musicplayer/src/resources/css/app.css 602 border-radius: 2px; } - -#play_button { +#play_button_wrapper { display: inline-block; width: 40px; height: 40px; hunk ./contrib/musicplayer/src/resources/css/app.css 606 + vertical-align: middle; +} + +#play_button { + display: block; + width: inherit; + height: inherit; background: url(../images/play.png) 0 0; outline: 0; hunk ./contrib/musicplayer/src/resources/css/app.css 615 - vertical-align: middle; cursor: default; hunk ./contrib/musicplayer/src/resources/css/app.css 616 + position: absolute; + z-index: 2; } #play_button:active, #play_button:focus, #play_button.active { hunk ./contrib/musicplayer/src/resources/css/app.css 628 background-position: 0 -80px; } +#next_song, #prev_song { + min-height: 12px; + min-width: 12px; + max-width: 200px; + position: absolute; + z-index: 1; + display: block; + cursor: default; + color: #000; + background: rgba(255, 255, 255, 0.8) no-repeat; + text-shadow: #fff 0 1px 0; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + -webkit-box-shadow: rgba(0, 0, 0, 0.4) 0 1px 3px; +} + +#next_song:hover, #prev_song:hover { + color: #fff; +} + +#next_song:active, #prev_song:active { + text-shadow: #000 0 -1px 0; +} + +#next_song { + text-align: left; + background-image: url(../images/next.png); + background-position: 0% 50%; + padding: 2px 4px 2px 15px; + margin-left: -5px; +} + +#next_song:hover { + background-image: url(../images/next_active.png); +} + +#prev_song { + text-align: right; + background-image: url(../images/previous.png); + background-position: 100% 50%; + padding: 2px 15px 2px 4px; + margin-right: -5px; +} + +#prev_song:hover { + background-image: url(../images/previous_active.png); +} addfile ./contrib/musicplayer/src/resources/images/album_cover_1.png binary ./contrib/musicplayer/src/resources/images/album_cover_1.png oldhex * newhex *89504e470d0a1a0a0000000d4948445200000040000000400806000000aa6971de000000047342 *4954080808087c086488000000097048597300000ddb00000ddb01995f140f0000001974455874 *536f667477617265007777772e696e6b73636170652e6f72679bee3c1a00000af449444154789c *d55b6d8c5547197eced9b3df0b5d7661615b2a4b972db1b61aaad590968fa669ac1f241a6b53ad *a49416132b89d9c6567f6862fd20fd61b14634a9fcf68728ea4f112220b471ed87b6b1600aa5cb *22fb01b78dcbb2ecbd7bcecceb8f33336766ceccb977776f280cb97bef9df79d99f799f76bde39 *97e0c1ad8f110080d23700c83ee98d0c429e87729db91929708d421064e3f52902c54319b34736 *ca3eba65977f2d7a449c3b04d67aaa8142e0e47183a23c2800c4e168049e7d74d2d5a72af43c4f *f6252222ac5c791316b5772061896fbc060ad535e56d8526848581aac66df6742e5e8ca1575e43 *4400a28610a7de7907b371ac189ca03c42d5627e7e81ab8132d72f02e5e6719bf0da5b07c089a7 *2e401c20226df51494cb67ec49af2550e4a2399724101188840b1008200e12cee8063e075035d0 *6b05250576b61c4fe0a4b9b626d5372122019c88c0b9bdd0f505ca8258340c00078823e29c403c *054f4436bb655ef925af255041d1f65b669dba0009172002692e70dd827276fbe44f959eba800c *0a5c9fcc02e5587dfea0aaf1b869540d94e2a94ee742e911a5aea032c1f50caa882117dd940b80 *83c0c53fd3e1af6950808859850cd60ca69b122744693e103ddc0c82570bd447efb803a74f9fc6 *9599998581320575aca41dfb45fa0fd3e8cf413c4b87fa8b730257343f9d7bc6a73cc5f41ddbb7 *61d3867b040f419ec9d44b642ae2046ed3889b2f489ba6ec9f477622429811807a824a4f98bc26 *508d8d8d58b76e5ddd40291ae700e7c6c2363dca095dc5fc4cd39a9ff9d93c04a077452f08dc9f *b5b4f86417d58654e4a299e353516510e45c1c843838b952dffc40193d55401101611461f9b21e *4c4c4ccc1b94fa486470bac24a6ac50c61dea774f3f2981ffce607399e4b13f49b1f781a784908 *b9edd14753c1d478e1269a5b32cec058822449c012862466489244bd581283b144f03024090363 *fa2ba531418b6c50be469a2603a3dfafa99cb67c964029bdaf6f357a962dc7d8f8a8ca06f6f1dc *9d24b2f5dd4b982642429e34088a4056a429b726dd9ab203112711081df4349032657d1cc0e053 *8308c3109c3341e342fb59bd92b3524977d1885b59c4e409eb0d4af148817996613867ca84e358 *9a2d533a220216dfd089c1c1a7325005a07da0b2a0aeb990c4a65e69265269707ea060f0caac93 *651e019ac54892182c61e0090363dc9a5fba41fade3fb0160f7fe5118736ab8122636394b21c74 *883922b5c30294ed337e9fb3d24a20fd8bd4620a557ed6fc6c1a2b01b8f7befb71766418c78ffd *cd62a7bc9f3b3ac84b97518b8b2b312bfae740f956d1f01311883115b86c76fbf490a313a13120 *742c5a8ca92b6554e2040460ebb627303e3a8653a7deae0994bfdb91c085d24323c94953963d3e *f36204c61892244612cf822571e63e35985fe6668207c04f77efc6f6c71fc31ff6fd064d418c20 *080004187cfabb686d6fcbfc5c9bdff6796eace13ebe7361e98b3a3ad2209832430b26467d9ce5 *6a4ee02c4192c488935930963840ebc0c901da8cce9227b502a0542a61ff1fff84ed8f3f81d2e8 *59440d21c2a8113b777eab2a285e03bdb5a505f7dcbd1e777de2e3f8d71b6f72227a2b82166840 *e2286c99677a4ad4ce08054e98b73ae7d9d05a0300b8da8c4aa5826f3ffd0cbef9e493b8ebee4d *b8e5d6dbb06ad587303c3c6c8d71cf6ff2a4f4d57d7d58bab41b070f1ec2f9736731333dfdeb24 *49be11a6c0f269837161e2490cc65981f9c92ca2650883a79aa6b80a8259be4ffbf7fc720fde9f *180507b073f0196d0eb7a63319cd42adb3b313cdcdcd3870e0004e9d7c0b972f5d0263ac0700c2 *cce2d39492e6e859b02401e37cfea02c7aa1d042532efa8f7ef803b43645e8b8a10babfa561782 *266eb99f780df4afc13f5f7f1de747ce82678f027bd30da0f4349606b4188cb1fa809a8ba6903f *53487aa954c2f1c37f0101d8b869b313b48c273670e21c3dcb96e1ccbb6770fedc08f4e7a0006e *0480308e6753d08cd715149f8ba6002008bca0f6eedd8b300830b0f6c3063d3b0c650158d1c53d *4477d7128c8e8de1caf4653b44ac0080c806e58b58de50a3051a7f9de23b02893e6102727dfb98 *5429971182a36b698f0a94949bc0dde224c1ccf4b48bd41c04417764044139511d40d942e5bed9 *8384267d3c33d353686c6e1316e3af2ce55825a29d72ac16d9e6e9163813d282e115d8cde7e621 *7984e6fe72bc5cae2049123d883957b18d2108033435b7b8065488e83d752344220fd70b544aa9 *4d5324df73a69c816a696bc799b74f583c6ed03a7d627c1c37af5c89f323c398b97245278e0340 *68a4b45c30b1d395ccf77ab0b1798aca658d474fb102855efd49ba2cd6c2a6161c3f7a240bc0ea *2a2f3fb74ebf70e122bababab074f90a71bc566d14102ec091a5bd224d55d56671a4f46a2bed0b *d4fa36cb6db7df0e20c4cb2f1f877c7ce77546471176eebf23b873dd3a946766509a1897563406 *001117b73d4610ac0328a35c74807205d15c1014ed6bdb7600952954ca1577e875169a59e7c50b *257046d8b86123868686f0dec50ba854ca1300ac6b712d0d2e14949bc7a32911ad8d342ce86118 *a2f7e6d57861d7f7abd42356b2b6e8a5d245542a656cdabc114110e2c4c9935fefeb1ff85464fb *4cdd4055a1675fe55faecc5b6f0f3db215212be3d5d75e758076ac52c03379e9128e1d7b0900b0 *79d3c68623478fde1999d74b949fa51a28278f1f74da656da1dc58690adad0fb3efd79bcb8e767 *d6e6589298a722c77ac8edd0e4e4248808917da1385f5039589e2f411862fdfa0de8eceac2c8f0 *bbf8f79b6fa814689c0409e8bdf12634508223870fd704caa057db14d27f20a10e428e205803a8 *5accefcb5fdd8a7befff2c3a96748328fbdd07c51575e3ce898cc92e4f4de2d9ef7d273bfe5601 *25bb5d79c21e266faa22fb36672ea06a31bf8628c2aee77f81debe0180c41378ad058dcd606203 *ec343c393989c9c9c99a4115542c4a581d1791fc8d90fea8cb89c511f60acc0f4801ade8edc573 *3f7f11514b7b51bd2206643742f306e514d9bd75b25c8ee4bdbcbcd181935d0e72ae6074e9cb3d *fbdc6e34b4b41741506d627c2cdd8005807208e93160520a8fec1b1fd7a2f331bfd5fd6bd0b6a4 *c73dc66a5118e03f274fc0fb78def2351f2887585a473ef30817302f38f4d10b31bf2d5ff8927f *acd51aa9823849cc4e07a85c8fc7afa880960d4d6b1af5535999090a616b4255d3d4d2eeee4201 *646b6b6ac00b3ffe89b6f90b005595273bdacb1ba32c0bd857dfe6acf9ae023a001cfdeb410c7c *ec93b9a8afb7eef626fceaf95d3874e86081c8a819d45ce852e9a17eb7e6bb70344a61c593a7eb *e5f2c1037f467372192d8da1b17010a45aef6a4ab063eb43f8fdfefd05e57276b7e72a795549ed *a153215d640113b8bb1c9e8ff9254982871ffc223ef3c003f8dc962d58b3f623f8dffb25bcf2f7 *973034f40f1c3e7204e572d99ea17af5491ef7d3e8da9b9f2e9e84454a8bdccac30e50ce650be8 *535397b0ef77fbf0db7dfb8a66a81ba83c8f7b2001ca1244390c65054031a8ea9a123c1f00a82a *038df02e5d3db27dcadfae0228efc7da40791637b954b2219905ccbb359fd05713942572cda05c *f2b9931401b218e2fa3940a6c1eb1594fdad808713cc721894bf91216bd0b50e2a1f979c025b41 *d02887a530750495e3991b28c006e6075544b7a552c550aa7d7988c9bb803dcbb50ccacfe31aaf *b240fa1f25e4ef6bbc023bd7b9c64019c38a4b6ae502b21ceebfa51fb3b3b3f9856bf02b8f2896 *c005dc7502e5dd3a87d9767777831321686d6ba720009a9b9b8d018502d7c2f30180ca7d2ba82c *1963886763042dadad068eab092a2fb07bed3c4f81157ae82629a3ff1f4bca40d105b38e8e0000 *000049454e44ae426082 addfile ./contrib/musicplayer/src/resources/images/album_cover_2.png binary ./contrib/musicplayer/src/resources/images/album_cover_2.png oldhex * newhex *89504e470d0a1a0a0000000d49484452000000ae000000ae0806000000af0c36ee000000047342 *4954080808087c086488000000097048597300000ddb00000ddb01995f140f0000001974455874 *536f667477617265007777772e696e6b73636170652e6f72679bee3c1a0000200049444154789c *ed9d7974dcc67dc7bfd805efe52152a44899b244d122255bb6654bb6d334579336796eea28499d *c4ce693b8edcb8b5d3b47e2fe97bbd92a6699a1e69d3367d4efb9a363de3d7f63549dba43eeaba *b55b5b8ea548b2adc3966c51e6215ea22851229700a67f60710c30339801b0d4ee12a3077131f3 *fb7d6776f0d91f7ec06201edd68fdc4910ab10ea0fa73575dd787d10e16abc3e822d9ab276d05c *d338da9253a271ea89c833f6862282b524ba7c67e27ba1339bca05a38448f5c0c8c38476898291 *aa62d4699cd72c1849e8856ab9b430aa149d94664bd33474b477c41b4ccdc028d7474d45c64425 *5d18a39ce6e6cec2e1d58db875ba8e9eeeb5989c9a92eab47a604c2732fa6b2b3e32262a2b0ba3 *acd9dab56b71fefc028ac5650004babb1508c1858b17317b664ebd875516196ddfb4f329091853 *d20ed5961146d6873c8e5e4b4b73c986d839ae634e9c4a857c34daac7a22a3edcb81278391b652 *d2568388674d88b700f0475c50dc6630f28a60d75f066daab6c6608ca75bca714969c5ffaf1260 *f4f754d330c6d2ae3518d9a6c139f2222e816eb76a00f12d1cbd9584d1d5cb60a4ad6a0946254d *3a25d03d9288b7f8742a0bc6b073b961a45a6a1a4675ed68af181b83e3e2e5b84eaa507a414a30 *3b9398c1a8ae4959653046ba90288390b567a76b7060243e830c46aea5b4690663ecc27375822b *88177103298494627a30f29d3318c566550fa3db1c34d002ed7e3c35e874a42561810c46caecd2 *c2a83e824a8631d426d273c975735c5f7d64c405d7a09a61541c452cb30c46f94e592efe93b580 *7bad02092c01810c46a6d9aa85516a38d130aa178f4feaac822f12470e44289f368c3ed30c4639 *d7ea8231bad589b6be884be7b8dc54a08a61147b66302a0c41b224fcf8070c34385f311068a11c *9780ba8841b1ab90696dc1184353d26d35c3a8e00d67a6d4226eb9606498961b46ba69a56004f8 *40d60e8c80ff5dca8d503633254e1eab1671e507c2ec54d6505197dd94c118bf956d502e1893bc *639d76e6e7b8198c198c720df1df3109bdf02a08f11600ced56170994d9ce34698971b46db4436 *6fac62181946d5066392a23b1b995004478d82575d093096da3218251ae2bdeb72c1482905a5a2 *226ef840ac7a61941c86b2a6b27e06a3ad94668eeb8fb8f4976aac9e250697c1987c04296de06a *8391af5d22938ab84e5f81884b0f256d189d4e796b714ab482ea672483b19cda49735ce72b5f70 *b94506a36c630663eac5bd242194e3068eac23de6006a35c116fcb940e05ab1dc60412ba930848 *e5b8fc9124b74808a3d0328331a05d99300ab57d0b81ef86200ceb683d45838a863145cd904a06 *2357db5de51a6a003410df0230ae55888cb8550023e5b962309643bbb66194d323de6bea5a8552 *0f6e1dc9602caff62a8791a1196ae6d4f93086ee5cdfe80fc69183cc600c686730c68131429053 *e78fb80845e2585d853c5724b74b57bb5c30eebee5a7b0beaf0f737373f8bb6f3d94aa36802a86 *51ce231471bdc1059b385d663032c5a39487b60c6170f3004cd3c443fff4cf300c435adb5de51a *56278cd29604d139ae7353b1b44af9614c4f33289ea6eaa68d970300f2f93c2eebebc3c9919188 *3e56018c094a20e2ca2956e5e99d8ac819ed32343c8457474e2969869a258695d46ba561a41503 *62a16fcebc7a37f2a63d8ada8751323296ca95c35bf1f0c38f86bb545289e7555930d2aba2fb1c *3b754ebd770ba6881c3734880c469f265f89d5d2df7f5944dfab0746b62b4b9313719df6b4735c *5b983ba454b5a3f5cb07a3946ce96fa1b5154d4d8db878f1620a7d10e6cb2465e5608c5f7c3704 *518bb825a734c6c0d5755785c69712c6187811408386ed576fc7debdcf467bac108c00ffd6b261 *2d8e48192379f8eab090c7cac028eea33a6054b6269efdeb6fbc097b9fd9abd457582e5d18995b *7e05d30a9542455c4214af0dcb60f4ac239de8b91d1a1a422e97836559022bf6806a15469e0829 *f5238eb880c2ae3a1d18c57da4e3511e1865d5484832afebd87ed5553870f02080d509a39276c0 *2e1c7109a07ce58e5c5feaa32b8775b9724686b61fc6d06c11e0965b6ec1c18307ab0a4642fde7 *cb7689af0f496d420068721775599605cb32619a264a3776f64d2f71d42206ae54aa07465b4aee *883a6964240036f46fc0f6abb7e3d0c14331469a0e8c3683be91123a75b148d2398e70248025a1 *4d0801b1887b124177ae2877f25bb9f19513df4b0763f075582761642461d3ddb7eca6c04d7337 *ed9ddaf47243200d18a39d23e25f0c5d02ce2f2048e06ffc2eb896d50423a72a4e219c1e0880fe *0dfdb8e6da6b70e087079455dd57bee0636fdfb8035f6918218d46e9adb9767ad0258351aea846 *c6523ae733f5e66ff7bb7697c0e56c0b275253e7dc6501ad5c1895347d1117a01e42ed6c0cc6fe *2c41491f4686c825825156957e456f550260fdfacbb063c70eecdfbfdf05d4fd4b680d9981c587 *51a05d1618d9262c0f3ae212ff43a849900e413fe9c048bbae108ca968f32363a41f0780f7bcf7 *a771f0e001148b45b6670dc2a8ace9445d0039afca89b9f4ed6edc93bebec5fff4548d7857ed38 *0b7197c03f22d205bd0887cff8c7d0262c5dae366d24d6a5a643a0ebd3f3edcc58f1b3a7a7077b *f6dc13d2f6ed1d85e37517e7004630dee0940bf518daa155aeb6ff960751ba115d06cc727e1891 *c1a804236b2184c022049665c1342d188609c3305d4f9692b372cdb5d7e13def7d2f77ebd53a8c *2c067802a51cd73322209190a994f4f346c25e4ba42b768eda4d07bfc4616a93508dbbe25fbff9 *e677e2d4c8293cfb2cef3a068937aa3ce50a93c731e52b486a4799f9830aa81c57be9f15833191 *763218edc2cad00908b12858835df2a445c07a5d6ab8e3cebb30717a1ca74a3fef6189d6048c94 *49c4f6a2a26e20c795d94d3377d5dc21d10b5f9bde75f0b5c5bb27a95d29d140483011e21d425a *2096056299304d0386b10cc330609a162c8bc0b202fd8486ec7fef74152b2e3b75f50d0db8efbe *9f474ba1106f370dc2af662d01eda8dd7464d7ae59f43fea9d4bbc2d473cb75230f2f3c69586d1 *73646b3bf9a99d9b9a9609d3f222acfc5623d4aadb35630a48608510a0634d27eebfffd3d0345f *9f350ea3dc3fdb3cb73a60646d549f37b16059064c7319a669c0b24c106271c8e08d970d8cbf37 *16b024b0e2b719d83c883beeba3b1e8c20dcea4a8551a8e76d2c0024ddc745f17d9d22008f5547 *f8add145e445dc0b36fcf7078eafa766cafa9c7366003ff2fa37607c6c14dffbf77f53d4569c35 *2973d9b9521d42d89055e3400e846e7a1776a91e18c3cdf407d50195befa497a241c33be3711ac *79953c60fd65f77b6ec5f8f804f6effb8168842a830b19a50f63d838d2551c6ba8c89a13eda6c3 *bb6a8e6282ddb4fcfe8c04766f8c45731602020b84986e0a605906083119da115d5266f177d3d4 *7bf669b266226897cbe5f1f14fdc830d1b2e979ab2d477d3fe255021a31d4a2b1963150d8235db *ee5985d014fa042b0646ffd7750c4d6259b00c13e6b201d3306199168845c25da60423ff6db134 *6d1b4dd390d7f3ccd90a4dbfafaabea111f77ffa01145a5b6b16c6a02b5bc2aee444dcc0fb4a02 *236b7bcbc2486933aa2dc0322d9886593a4d65c2221673facb0123bf0ff6941100f5ba8e6d03fd *b872733f36adefc1baae0e149a1ab8c0fa15db3bd6e0171ff82cf4bc5e23304652caa590795621 *551879402aefa64b53439cd3554e0a10380320d02e078cbcad14d476cab261e06b0f7e1d8f3ff1 *04cecc4e636d472b36f7f76268631f3ada9a91cb692160ddae09b0beff72dc7bdfa76a12469184 *b3bd9c151dc4330625075f5a1b9c424ee19889bde5b48949e0ffc68aaf223956a52110356586a1 *53659a26bef1577fedd6373535e0865dbb70dbfbdf871b76ee84699a989899c3f4dc79d727f896 *b75f731d3e70fb87f10f7ff737f2039069556f90ef23b1366da713ad544145c668dd34608c32b3 *4a796bf84c40726d6518958c89600d5404be7071114ffcf79378e2bf9fc4e0e6cdb8fd03efc3cd *ef783bda0b2d3839318de2b2c9f4fdb19f78074e9d1ac153fff3847868150d23c393c79bef580b *0072c1382ddc4df3771c0181886637c487774b16b1dcaf572dd3a0a115ed757cfb189914c0fd88 *466afab4b9b321b19ba6b6023d5ea7e2f889e3f8c26f7d09b7def641bc74ec28b66d5a8f8e4293 *cfcd13d2340db77de8a3686f5fb3e2bb69596db70f8166e872029f4e2ea7614d473b06366d44ef *ba7514d539ff66e402299d33469310ba5d3f018845609a16cc65039661ba67022a1e46c18727d8 *e036513db181191b9fc09e7b7f0e7ffaf53fc386759db8acbb8382d699b7fa8646dcf3b3f75515 *8c224da7343536617868086f7ee39b70c5e0200cd3c00ff6edc3e2d292ab493f84daffd7f75213 *7462b707df95d01c80ddb9594a05a44aa426db50ca4d62bcb15a790d44d46817cb32f1177ff957 *78fa9967f0c5dff83c36f474e2d4e46c68573ab865186f7ddbdbf1d8a30f4b8d555478bbe9249a *7cedb06663630386b60c41d7f3383d3989ff79ea492c2f2fb3e44ce159052a3aca44af88c86881 *c0b44c18a6bd10d32a4f64f486131d193946dcc44818a444d12b1813e522e30b2f1ec15d7b7e06 *0b67cfa0bfa7b374d10dbd49def3fedbd1d1d1c184a19c91514693afed154dd33030b0093bafbf *1e274746b06fdf0f31fada188ac56514978a989d99192784bc9b10b29310d24708a9a7236e4833 *38e1128561ec9cc2a2cf0854416494eb546c4dc2ab5e4a2557666666f0897b7f167ff6b53f417f *7727462667a9f6ba8606ecb9f73efcf66f7e4e69acac221319e309b35e12e4f379ecb8f61a1886 *81a79fd9eb3e1b636e761653a7c771f1c20500d0474ebcfc6dbf5c4e267ac94545e75d972216b1 *a3ebb2b10cc3344a5f0c5443646447ca28eda89c31ea631a1519a7a7a771f727efc585f93358d3 *daec8dabd4c5e0d0366c1ad81c0686a91d2f32b2c5ed25529bb1951a1a1ab06be7f59898388d03 *070fc1300c9c3f378f970ebf8891578e3bd00240b7a6696bfcdd86ce2a88600c831206c724a4f4 *3b2b031637158809236323941f46156d3e30a0fec6db4d3b91b7a3b90e757a9e6a2600de77db87 *571646c2da4ad1ba20f6834dafdcba15070f3d8fd746474108c1c4d8284e1c3b8a8b171658de5b *fd2b6e8eeb75af9e33da5fbd9a3096976119ce29ac32c018b181cb0323bd31a222231f186fdb71 *b7ab84e6d4d4147eed739fc7e0fa6e689a46a90f0e6f436f6f2f471c6586d17b03d15117181c1c *c4c953a770e1c2055896859113c731393e26ea69d8bfe25eabc0de66e20d6b5a160cd380612edb *bf12e0b8d62e8cd11bd71d0a00276148ba9b7ef2a9ffc577bffb1df4ac29d00d9a860f7ce863c9 *61847f18b23012b1b24fafabb313200433d3d300211839711c67e7ce448d880237f4106a6f7a81 *d0892e62b712cb0adc90586222a2ac85ef58bd9094f5c4dad19a24f4426c1815a3bff2875fc50d *bb7642cfd7c130eda001026cdbbe03854201e7cf9f8f1c44e299907853c196fafa7a6cd8d08f1f *1eb06faf3a31fa1acecd9f95e92d1871bd6b6fdd6b7089ef96a3ce27ce32ed9fb618f6852d9191 *9176174746c948b612915166572aabe98907fb4abe9b5e2a16f12bbffe39aceb2878d30740cbe5 *f08637bd2572572d1319790b4f2f726608b06d782b0e1f3e026259983f7306d393a7a3e7cf2e34 *b8a22e09084ce2a403a67dedc08ac118dec01507a3a4b69722482987e0e1e9bef0e261bc74f879 *e8f91ce576e3eb7e54a897368c51daced2d9d58999d9195c5c5c844508c6c75e93990da750a74c *72549fee6b62ffcad539d8b2ac4b00237b1357128cca39a3ef4dc8680b954b8d5f7bf0eb58b7a6 *40d9f5f55fbe6230ca683b4b5f6f2f4647470142303b3d8565ce7dd238a551d3b47a67857e246a *e90217d6d558843b83ce3b8d5fc2dac9f45852245891a2b6f342a4ec07446e2a391fdcc0fae123 *47f0f2e1e7d1d4b3b10429a0e5eb303078054e1c7f595a57d447b443b447634303968b4bee1751 *d3a727547b018056003340e9ac826559f6d7b08659125ee591d1176d124746d08d843139224d99 *c8f8b5071f444b83eeed1001bce14d6f891d19c118a370094e1d63e9ea5e8bf1d313202058387f *5eeee9f1e1d2eabcd00dc30ed7ba9ee74d8f373066534a11ac4222635c4d6eb3625b9cc8f8e2e1 *23c81183aabb62689801bafa3b971e4584615ba115a3af8d0204b2671158c503d7792ffe1c3783 *511d46195d4250baf5a52038286bdb1e87f6efc3866dd7b9d56b3abbb8fda405a384825bb49c56 *7af60449055cce5985881155db6edaa799da6e5a429bde4d33dc787dc4d84dffc7c3df4783ee9d *5da8ab6f086897fe49e9b2df307fdc9eb69bfe0516fbeb7f02588477a9a24cf1226e7044a9465c *4aaa0a22a38476923edc3d9ae42e5b25323ef7dc3ee8790d8b86bd9ecbeb52fd44c6fe1436582e *9f876199202030cc65e9f7cf28fe54c1014a3132fa5eac5a18636e00a951284a1b8681a5c54500 *75b6b3966348a4072309be1268e6733998cb064000a318eba0cc298188eb74ec8b06ab16464019 *48e5b896ea419207ced2e245688dce26d512bc8ff40007005dcfc13096e12415a968fa23ae54bc *cd60a42d2465ddd431f00b8634b49db2706e1e85463b28e57df76748a2c92f2165aef6f2b2095d *af032180aed725e9f49cf32290e312e606ce604cb30f80c4cff1027dd0e33dbfb08042b7fd3ae7 *1c88c55497dc6c52a5582ca2bede4e61f47c0ed0d4f706a5e281ab1c71810cc698da22b3342263 *2e9fb7dd086059fe5c327d187942bccde7c5440d757575aa5ff73a2510719d31f8725c5ea92818 *e58c15fb28e3018cb39630e2fa35fd3036b7b4ba15cba59f72c7d5a56a120fd786cb0eb4042d85 *02e6666723bd18c5072e71226df87ca3782c69c3e8b3aa261815a3ad4c58881b199b5a0aaedb85 *85e0f5b8c10f8f9a76b8708e7138bab367e6d0d9d985e9e969b414da92834bf5c3c9717965b5c3 *184b9da41571696d00d0ebeae1dcb0e9b5932762f42398edd843b61d2726c671e5b66d989e9e42 *537313f2791da6a97c6a8c8eb8fe9155048c09b4c3eee5835139321250671544daee9acab873f6 *0f28f3390dfbdce7a4950fc6a86affaa695ab088fd7374c330b166edda3857888523ae9b28a47e *52bdc6618ca19d3ce286fdbb7b7a6012fbc7932d0d3a9edebb57b11f751855b5c74647b1f1f2cb *71fcc409b47774e0ecec8ccad7bf8b8410f7882efc803ee941a6034fb5c3e8ae45682b1d70065b *24c6fd8e77eef65291e2459c3d7b96afa9bcad254522aa67cfcca1bf7f031a1a1ab1b8b888ceee *1e9c1e1b95edf0847fc51771fd5157568b3f48ea558dc2184bb3f47fdabbe95d37fda8db7470df *b3700ebad3d056ac165a1d3d76145b878771e0e041145a5bb1b4a6137367a40ed48efa57e81c97 *c80c328351f588da69235c3b9163b4652e97434b47172c02d4eb1a1e7ef8e1f0f68adfa5ba85a0 *7969710967cfcea3afaf0f6363e3e8ecee41b158649c0509151a5c370238f0a47cc45bcb302aeb *1130afc74d1a19dff0e6b7c02a557534d563fffefd31fa4896eb70668f59fbcaabafe09aabafc6 *d2d212666666b0aeaf0f93131358387f8e695f2abc881b3db80cc664919104e74f465ba2cb1ffb *f19b01d89f89a9d15751740f781227de0c13c9c98b303b74e8795c77dd0e148bcb989f9f474f5f *1fceccd4636e7686e71200d7cbbae05e689c5aa97d1855b5ed0328c18198a2662e97c3e5570cc3 *02d0daa4e3cfbffef79193512e18c366a2034e821f1e3880eb775c8793232731393985359d5d68 *6a6ac2ecf434969616832e47fc2bee4f77e4226e78309509235fb31c30ca74e9df4b29832368de *fddef7c32a3daeaead3e8747fff371615c57ed9a6d963c920380699878eeb9e7b06ddb5674ae59 *8323c78ea1a1a9097d1b3660e1dc399c9d3b83e2d212004c1142a87b34e95e4fde92c1a8dca594 *05158952da4dbfede6770100eaf31abefb2fff647f1ba5300132d151a699e710e5661282e75f78 *11bdebd6e1c65dbbf0f2f1e3989999454ba1152d8556188681c58b178d812b867603e414803100 *93ded561c45b5447bf9a615412281d9cd1f3153f326e1ad88cc6d60e0040777b33bef5d04381cf *c4a581514ddb369c383d81d933b3b8627010570c0ee2e51327303d3d8dbc9e474b6ba18f80fc8b *cfc564def40ea85418e5b5e5fb8815fa229aa3725822dd7558d17bf5c13bef0600e434e0e5170e *60726a2a368c52c3518451c99d004b4b45bcf0e261d4d7d561f3e6cdd83a3484f3e71730393585 *e9e969148b4547254fff3c5d18713318a58a649e4b9baa47c65c2e872d575e0b0b40576b23fef4 *9bdf0cd84946c732c31847bbb85cc491a34780a340737333d6ae5d8b1b6fbc017b9f7d164b4b4b *00e1dc663483319e196d2a8aba72392e4f9d00b8f5f60fc1420e390dc82f9dc3d37b9f610d424a *4fae361d6d6e2bc77c61e102161646505f574f4556ce0d41140793c128dd4c4276f122e3db6e7e *3700a0774d337ef70b9f0b877395e1950b46296dd9cee979e2e6b85509a382695a308a9c427cba *53ab9ae3d28637bceef5a86b6a819ed3303f398a871f7d24ca455a5baa35bdfc83b616b8513733 *01020fa18e93e3c66eae011815b4a9681b0ebb32126ec3ed1fb50fcafabb5af1c0a73e2bbfdf15 *595c02186327a104d0bd6b88827fa375573b8c718d89c21c070dfbd65f86cede7ed4e91a5e7a61 *3f7ef0dc0ff852e58251e81a1fc6a876efee9e40e8017d4a572ec534ad1518636f3bb970c4b4b8 *eb67ee0301b0b1bb1d1ffbf4ef26db5ed2c3290f8c49b475efc20fce0fd4d386514133e8500930 *e672390c6ddd8681c12dd8b071137afbd6a3adbd03e7cecd63667212e363af61e4e42b387eec28 *6639178c84ae94557863859616f4b637e1c9c71fc14b2fbd14ed514530f253ffe0fd94191197bd *17ab6d186574b76ebb0ab77ef0a3d872e535d0f2de7d549c17dd00366fa525cf4e8de3c1affe1e *5e387480d16fbcc8d89c37b0695d073efa953f4825da02970ec624451bd8324400a0a1a1019b36 *6ec4d163c7126857138cd1c65a2e873beefe247ee4cd6f45634b5bd84b32559d9d38859ef51b60 *58407d0eb8fd5d3f2ed4128d71d3c68d585e5ec6e898f061765509234bc2d93b0d6dd982919111 *2cda17dd8823ae5fa95660940d52bb6e7c1d3ef90b9f417d53216caa786cd5d96b43ebfa24f83e *fd95574f4658a40d63024d8e7b1a37be0be5b8eaf997d8b092609431686e69c1677ee537b079db *35616b4560654b34c7ab03469e364000618e1b72aa0d18233d4a2f765cbf13bff0cb5f40ae7447 *c172019be8e6d9c2b154198cac6a49f7f02f206247dc68e378c7126a93a79a333ae5aaabafc503 *bffa4520972f7b84651022e8a7766154d10c5e0516fe0584225d950ca3943601b65e79153efbf9 *df01c9e54372e902ebf499c128651668f3e319b856c1fe5b0b304af68e2bb60ce197bff8fb403e *5f76602587e4ab4a6914150a63b436095432222efb7adcea8451a57cf6f35f0672dee77725804d *e507a93501a3fc5022226e3857a84618fdaea29cf1d6db3eec9e9f5da9089bd7c28f9b758b009c *d8e3ab601895b45d3c1911d7178955bbf1fabbc430ca967c3e8fdd1ff8c88aa7046671c99da455 *0fa3829b72c405aa07c628097fcef8c94f3d004dafc74a97c98951f63b5b85308a0d821b8fce65 *05396ef5c1a8526e7ae35b938f254679f1d0016624507e17d50823201305a5fa908ab87e8f4a86 *51461b20d0751d39bd7ec5d384babc86ff7dea4989c385d509a3488f7ad42a02bf80f0eaab0f46 *6635a75cbb73d78a430b009777b7e3d8d1a3911b5a7a6c550e639212f8058420e2324653493046 *eb7995bb6e785d5cd5d8a5b12e87bd4ffe17e6cf09ef48b8aa61a4fa60e4b8c26b1552bde9dd0a *c228576b97e16ddbe38e2076d9d8dd86fbeef86de58dad7c10a358d8539a2e905c1813142ae24a *45d0b4611468cad5cae8d29ec48afdd8f958a5bdb90edff9e77fc4a953a718a36194550a6350dd *2f1dfe05846347650ad507a3ca50e6cfcca057b5df98a5ad51c740771b6efbca5712edcd6a11c6 *2485f14854f91cb7926054d17ee2b14730bceb8de5d93ebed2d6a463fba65e7cfceebb31373727 *1e9e5bb17a61f46bba6b4e0a8bc8df9ca50da3bc77b98fa61d8ffd070ea0b95ec7c292f203e2a4 *4b5b531daedad8833beffa381e798471c38e04859edab4815c39189314c62f2064c75059308a0d *688b99e969743769585cd6605ae96ef8bc06f4af6dc5655d6db8e3cebbf0e8638f49fb962f3a56 *078c3c6d27a8fa236e8eb2f39fcce52d3ec02317ff0b095dff80a3b57dbb8e48ed70f9dbbff926 *b65ed685c8073d2a94ae42036edaba016df51a3ef2b18fe1d1c71e638e97b788c61b2ea56d21a1 *2b2f4bcfb28c665c6dc751469b26c32eeec34bc40fd608f4cf5e911abad820f9c7563667fcea1f *fd31aedb711daede79134e4ecdc3881979350085461d837d5d686d6ec037bef197f8d297bf5c7a *409e6af17d70538b602b1c19c3b5a968063f25f6e3a242613276370c83958351499310fcdcfdf7 *e33fbeff3dbcfecacd7875620663b30b52006b1a5068d0d1ddde82beae36d4e93afeefff9ec667 *7ee99770f8f061eee83318d9ab11d56e9b177989ef71513ed76a8591db0747fbdcb973b8f927df *897bf67c02f7ecd9834dbd5d3873ee0216168bb8b058c4e2b201d322a8d7f368a8cba3b1be0e4d *f575e86c6b413e9fc399b9393cf4d043f8f6b7bf83ff7cfcf1b4462b1a722ada9502a3b46530dd *04a0f5f4ad2700d0d8d080814d9b70f8c811be806ad79700c6b8a5bdbd1d7b3e7137def1f6b763 *6060006d6d6d219bf9f9798c8d8fe399a79fc177fff55ff1e4534fc130446726563b8cf14582cd *c3c3c33879f2241617edc748693dbd7d36b88d8d4c70ab1946bfbaaa7457672706060650281430 *363e86b1b1719c3f7f1eac192f570a50eb30aa580c0fd1e09672dcd2d11cd27b405f25c218a5e7 *2fd33333989ee13ee530be7606a38a336526c87125875015307a9aee5a992323a325154d896a39 *4d4911b93e920d90b1df8af6089d55007ce7cfd224278351b41a4fbf966094d7f6f6542570d5b7 *7c15c3e8abca6094722e2f8cae79f41bf59e016157518f44f57fa596bcd41e8c6a7d6430a6a917 *74a61e892ae340ad9531ea66302a39d7048c2209278df55d8f4b1fb39523c70daed6028c722669 *c328e19c368c3134838e5cf758bacec199031391152a338c02b10c46d93eaa134651ad773daebd *4ee5b8ee92c128adcd37cb6094aa95d526ce7f6ec4f5725cf1256aab0b46b66906a354ad92b6ec *b7a2779500000208494441541140e9525a2fe2d2392e8fdc0c4659fd4b08632ced4b09a382b94b *2d23c7e5a7b9f18f826a0146db257d4dbfe3aa8451499bbe209e7963e70cc6f89a7ec70c46092b *d9e30837da9622ae7b201627c7951f1fc7b4fa60144a940b4625edea8131e6060620ba0593ec27 *4175301505a3e79cc11861b50230f23ded88ea5cc108701ed097c128e790c118ed2284316e21a2 *07f4ad5218992d3509a3f200ca0ea348890081af7c399982e790c1a86c95b6768dc298449b1d71 *3318c396198c4cd73461e4b9fa6fbf241d71fd8ae9c1c877ca60149bd7128c490a15719db82bea *505c3218958ad454c7dcc05508235bdb992402a845dc0c46a592c128a14dc4cd1c093f0fd43767 *a1dbde55118c800a90198c894b0a30ca3b1004bf21a3afc7a50432183318cb0ca3824b3021605f *ab80ea8651ec592e18d3d3f6aa2b1046a153c259118d551471c3d72a54008c3e970c469e7635c1 *28ef4d02af0511974a816b0246765506a392b994a13a8c628360541144dc969602d6adeb653b2a *8caa16604c948726ed3a965315c2a8505a5b5ba9b0aa3535371300c8e572e8e9e9e1769ac198b0 *eb584e29242f11915cad679641fc398d7edbb4c5e9d3a761591600406b6c6a964228ee68ca0f63 *0abbea6ada4d338dab17c6b87de85242198c114ed500a3bca6741f29c3a8a2e9fd02822f157304 *aca6f2c218d1bda4a6b8a12260a48c6a074695f2ffb8cbbd2492de9dad0000000049454e44ae42 *6082 addfile ./contrib/musicplayer/src/resources/images/album_cover_3.png binary ./contrib/musicplayer/src/resources/images/album_cover_3.png oldhex * newhex *89504e470d0a1a0a0000000d494844520000012c0000012c0806000000797d8e75000000047342 *4954080808087c086488000000097048597300000ddb00000ddb01995f140f0000001974455874 *536f667477617265007777772e696e6b73636170652e6f72679bee3c1a0000200049444154789c *ed9d797824477df7bfdd33d2e8be565a69b5da4bbb6b6ce313db188231c609e18ac11c36873184 *c380c107040281e409a72140de246fc843c0604802210fd809e1b20dbc1c068cf101c6dec580ed *f55e5aed4a7b49ab7b35d3f5fe319a511f55d555d5553d3d527df7697ba6abeaf7fb764df747d5 *353dddcecbaf7e3d416a22dcb7122d0d89505f2688924031513424910bc1abed2409ac94dd7178 *a5ecc6aad69cf82abe1c59dbb1d949b8e96bec8d5633af3d61d63e2b0b21c5da6108c91cb26ad9 *79108a64b710524b9f4108c934e3004b6d8f587d108a89a42989e22e1052ad2114ac610242d4b8 *0c5908c928c11ea8d15f1e243e9a859099241642e2928190701e0b21f59a46fcc507cd9bed170b *21b59a1642c1741642e2aa3f080945590a433d25dcbc69239ce81e4c3790e2943d5d598250ad83 *06c3323fc29886f674ac06e9b95a99108a0bb36bf79ec8ba3cada50360efde7d2002a78b12f9f5 *44cad44808081ca2358190585f590849a4b72321f5289abc6dddb209344fcc392c424879d1939f *9a5ca4485306c9da32105233af0a21e2e9c82ef70d99279a25cb1012aaa043624956238474f861 *ce6111a1f0f50c21b3f3427110e25d4694068484ff1459080927b110d21386a0ec8fe691328745 *428b516f096b5b080573d40784624b33e08f59cb4248298c2e6bf4534206af78b2101297859029 *590871221909a3692b85d773af74b7101297859029590871221909631642c9c250e7b096075782 *100290a99f6e641942e5ca2948715e2843108ad434e62d79e0d501214d916242c4cc6105c626a1 *961983908673620b2153b210e2443212a6de202417883e2795a74229e1fc9585908c2c84c495a5 *53b25500a198308a7b472231e7b0c2ccb21092918590b82c8494031b08530b0831c3524f091d4a *a903380e9b3c164212e92d84d4a35808690993290809562dd3877a4a189d5eaf4c7889ec301642 *a664211413497b88950921f9daba4312ce3bd904d46f091dd8df8f999185504c24ed212c84cc84 *548350022da5c8d3c0447cff440399d50a84905c5309ad400885c258089909592b0831cb687358 *542811301be8938590b82c8494821a0a6321249c50b22909bc0eb2a93cb4caebdd060b21715908 *29053514a67e2124d958459a201455f8fcce09bd8e9eff093c84c242485c16424a410d85b11092 *4e2ad84c1542125e58a784b44123730ecb42483d8a859096301642d249059ba5002129d18955fe *96901658fb1c9658300b213d612c84cc84b41052f312d79885a0b012ce6159087122690f632164 *26a485909a97b8c6267a51ee067e3c331642ca61346da5962899869044d8550fa1726045a50ba1 *782d670dcc6111dfff09741ee8f4e489a2580869096321249d54b09985105f7159597358dae6af *926ff6ea8090a6481642eab2103224b5ac32088ab9acc1424839b08110e2192c84c49b5908f1a5 *0f42520d29c47240b9ac8159db5f6a21a42d8485905452c16616427cd5084214457b9f54d73bb4 *53c240551fdd44efd620e52e41984c4128264cbd40285ab55e210444af92160eac280b219e829f *80be195dfa1c16b5aaa68cfaa3ea8b6421a4260b21435a1d1062ad8f9491d853420153ab0242f2 *b57587ac1708959b8a9e925908996d15df384d08495526a09ee2d127dd43bcb210321372654288 *f69e1954d98df690896421245e593412bd1ef59450627c2561403d8c85907042c9a616426c5908 *895736ff095532702e6b1047566c16b9a2c4b57587b41052776324acb22c84c42ba70721da8ae8 *1961f90d630e4b268b5051e2daba435a08a9bb311256591642e2956b0b2181daa132d61c5678bd *ef9cb07e2124d958558a29b205217ee37a8150a296c2104a94452dc28a81507209cc6159088937 *b31062cb4248bcf2ea82509cfc8c4a7e5983bf8972550b21715908b164212493323b10a2948290 *f21216ffb784164294f77abdc435b610d296452d8a8590b0e220a443ec1bf851e7af2c84c46521 *c49285904cca950f2119511ff3b57c3f774d86b204a1726045650942ea59571f84a422298b30df *706ba6aef89fe3d5dedff26f99835e049e9ac38aaadacc42882f0b21f18a1642d5ec19e82b6e76 *f18ef4d5899bc322a1ba82db682114270b21f1ca1642010719f6270fa1e462ce61f11f556f2164 *212453d94228e020c3fe6a01216111129ec32aeffac4f75a2c90b203fd2113c94248bcb28550c0 *4186fd651d4281b7be178ccb1a1860529e73b710e2c9fe7443265d960ef448695a36e8d923e933 *0422168434887e4a28d1191642164216427ab45a212423e695eefc392c134a1942318d2d846452 *66ed400f94a665839edd42485db45342b3262d84c42b5b0805b2d795b70c1dea7508a16a51a01a *7db8a4701d96859078650ba140f6baf296a1437d8540285ee1db6747e7d7f3f4b0c4b7b0652164 *4616427a946908010183197366084209b4e427328745208a2b5a50769185904ccaac1de881d2b4 *6cd0b35b08a92ba31062142ace61094328a6b2a02c84ccc84228812c84c4a5f89465d6395e5891 *cb1a1c000e292fe5320b2189daa9ca4228812c84c4a509423a94a73d0e7a399da6392c0b216165 *194240d85fedfd04642124ae0c41881e99ce9fc8b784c273581642c2b2104a200b2171651e42c9 *95a7eeab4c62a50ca20c1fe81642f27acab9e7e0e52f7949f5bde779f8c0473e8a62b158435721 *5908a51e9916913387c5ab6ee0dbbc0c1fe82bef9e42294ae05aa14263013d3d3d8175eb0707b1 *77df3e83c660215483c8fa22062329dfc0cf42488fea1d42326a686c88acdbb861831ab02c8452 *8f6c0a42f42116a1f66900588133427a7d4e867495e553b2d50421193d69dbb6c8bae12d9bf1b3 *bbef5e32a3e94b1e0ba11a4414809006f99e4be8cfa0f794505416428acaf24f37620efaadc3c3 *d4fb1e456521947ec47420242a02a2f0a87ad924750321a0d67e02ca328400a55332dafabebe3e *b86e0e9ee719f112db543d6baa5157328458ebe59f4bc84a6221a4a61508a1a8e44642aeeba2bf *7f2d0e1e3c24ec254e1642da022b897b5b2a4e51e0a7cf84be275581e5df3f828fd989c992822c *8412a8061092d5a6a10d38387a905bc742485b6025e98050b2fc65e5e9f307e6e7b02c8412284b *df9025180955b479cb66dc73df7dc9bd00c836da2c84f82948e055c0dfd24bf5e712d20c5808a9 *6b854048a665a5ee29946f0fd5a3cae7d71ac542483c29afaf18e3a55860651a4240c060c69c65 *0b424066be210bc75b37b80e4d4d05cccfcf6b8fad25529d4208d0b3e768839006e5698f832684 *54979ac842485c1981906854dadf3fd77171e693cfc0fdbf7a4031aaa82c84d8296a07a138f9fb *caf8650dcb592d8484556710d211f6ecb3cfc27d0c600907cad0814529e46a354388520830064c *c9e6b02c84c4b50a20141fd5614ef99c71c693e1baeed2f5587502a17205a6cccc0bd52984ca15 *122b0a2c42b884aba92c84ea382a59ba2b243d6e7353334ed9b60dbfffc31fb465643bb1101255 *1a109211e3b286147d5808a51eb9667342845fed9cb3ce4e04ac6c7d43965d08952d64c91f2d61 *f96bc270896b263f612e24bc54ad4597e547fd882c6a7e228bbf99c422d941da23eb8be8ab19db *5f7236799f120170d6d96752d673fe85f625bf1f27bc4069cf09f51ddf0bb36f92ed2cc252ed2b *33fed809a2de18bb16a5b9f81c961d09a51e595f44c27d5b4bf9adac59d38bf5ebd7636464845a *37f5919040b169657f24c42835e4cdb52321ae21ed91f5450cd5161d0919da9184feba53dc8775 *e92597981d0945fa2ad2381b7d95819150c4a9dae1a9d50ff5315fcb10d2203b12528f64ae1384 *a47b4ec801fb91279582a75e7001fee71bdfc0cccc4ca86e4c67d4695f995019cad1c4956b2e49 *b0323d06a57d1247f12aef35ced2cee37904a552f40e1e79faee25301252908590d6c04ad20da1 *c422c1970d8d055c7cf1c5b8fd8edbebb6aff4e446043ad5d161b82e1183905ea927a1e38304ca *884762aec30a148a8fed2c84b4065652ad21443984e2eb11c67a00175f7c31eebcf34e7824c13d *b218aaf548a8cc201f847cdb183e40eb1f42ba45580fa130750a9fe5881642fc141a0e74075c50 *55d4ddd58df3cf3f1ff709dec1a1d610aaa6a9fe87044ea9fc90b2108a64e1ac8fee2c9c39aca4 *09d565211454a621a4c547b4fcd24b2fc5bdf7ddab16c080fca7630118ad88d331dd124cc2393c *09e85e199735104408a7417aa25908f153d47eb4c133c0b3e02fdbbc7933ce78f219d8b973a751 *3f91774b4789178693f1be5bf910d2a1c43f7eb6105ad66a8490545f09555d7e75e595afc0238f *3c2279bf777692ea9fe00a8cbce88849bfd4e2660a42a1aae6ac45c658911ad411969ef1958510 *3fc50a819094a2bd490355e565ffdab578ce73fe14dffbde1de27688ef7f64792e49efef622d84 *9492c836a39d12ca874b652b8594e4c0b2b7f30814a62e1ea8fc2f9fffbce7e1eebb7f8ea9a929 *769cca3c124902260b21a5249a9ad106e3b4ea6eb078696251e3efc76495e42a60d1df8ec5fda6 *2db8885e311df593e5be32e38d9d64a9c7422949b019c552537333ae78f995e5b2a56df13c0f9e *e7a1542ac12b95e0791e88e75160c5db68762764e18aeee031e97b69ecf054eb2bae6de5c3835d *937959834ef1a75ac1edddd5f64bfa247d6546ec849192186fcb9f2509fc2faee9054f7d2a7efc *931fe1f1c71f8f4fc2901d09e96996cc6bf22d55be5b43dcefb584474264e5ff923e495f99f1c7 *4e10f5c6195d28792391ffc535755c076fbdf66de8ecea0c4612197154bc2a4b62e4c11b5d7076 *41637e049b253b3c14fd28868800cb42484c16420a7e9ce5be1009efafd3d9d9891baebf11b95c *de4248a0593d4148269b6bef2914f66a21a4e28796a43cdf54fe116bb15842e557287116583637 *6edc84375d734d726f71073763179493e241cfb39de8f050f4a31846724f617888aecfb37efa1c *fa1505d537eb9d68515a2299f2c74e182949c59b5a92f008a7fcde0774856ccc32b2fcbff3cebb *007f76d908bef3ed6f711b9be93ac5a8c6763f4d5b2918462e9b196f795a5999652463077a387d *96bcf11312e61b53d203216ef4ea68533d5f201ea7205c7ed9652fc6c8c87e3cf8eb0713e58dc9 *9ea8997a8fac3e0831eb50fe08b9fed30aa1b1a6c0a85155a2a763f57663333ddee406d9d55631 *def813d3ce726642a2971250cf37c53f1866972c15b0ca5dd7c51bdef8660cad5fafb5afe2aa8a *9e8eb1b734e181251842f4102635f016dfe5ec7f00812bea4755d982103809ea074200df5b3c84 *f8b391e5b6049e5744a95484572ac2f34a28df0a85768484dec67861765b281cafd79a9a9a70dd *0def404b4b736c5ff1bab55e2044e9664eb6f4bdf19bf12124ec078a9735d42f842426a70d7889 *3d0438def82092b94575d88307423c78a4b40c28af08af542affd68e76608717d1ae8b3e9630d0 *65225def2f5fd3db8beb6f78079c4a609657ca56333e018925c6a4e0225e357d6ffc66fa2024e3 *cd5dae1b4c682124e625f610e078330721d6d1515e88e7c12b954fef8ac5d2d2699e07cf23f210 *92ea2b875a552407abceb66da7e0ead7fd39ef13905814372bdccdc255d3f7c66f561b08b14346 *9dd19f4ba8b6a70aba1728d19e3736a35c4be5a632d7ed0b26a1ec84e13555a857a025ad041f08 *ad29118fcaace38b71d1339f859191fdf8e1fffb41726f89ab6ada79b575b9a18349392ca1bca2 *d7a395277b547d4c5a0ba14816a56ab2d62a60e25f62a01942a251893e50f9f5f22b5e89032323 *f8fdef7e27103d2647829aa6c2ac0c082987ad8a3187c51bc48a4f4eeb1ba9a98d2933753a5639 *522b2f798b74d791f23c945742a9b4183f494e8bcab31de38ff3094496b8ed616e73a80bc3cae7 *f378ebb5d7614d6fafc4dea2b65fc56ca26297f3ff291f4ccafed4dc99f1b22c976e867d3025e9 *3b1deeeb1642c1aa09fbca0b00aa1400945817d77c629a1195552812b1a5ad0def78e7bbd0d8d0 *90cc5f824d0c16af0c0871dd69db1dd80dfdff5c0b21dad1ec7b19b728779b5c5f9547b31ebc52 *09a56269e95b3c8fef55a8f7e5bd24da2446135ebcb8bef5c7e85f3788eb6e7c879ab7188fd1c3 *c7424875a3e2bcb152085cd6a0e6de4248e293a65621205ee59bbc224ac5124a250f1e617fb85a *bc2408139fc58984e5e58c73c72a3fedf43370d555afe56e62dce1b3e22024e32741101508c978 *63dcd39d1e3a5313d3a1aacad66492686a1629aafe70932070f9882e2f09c2c8658ca9cdba0e2b *d43c2ea7c831f5ac4bff18074647f0e31ffd30aeb69c943f02c27997405a02f18368da64c98674 *b4e5a957351030463f7e5908718b44ba87a00c27cf8bce3fc9aa5610d2114623a896eb38b8f255 *afc1d8a14378e491df260c2cd6305b10e207aa0d8492b70c9d12dad331ae5787bd447fe1c20aea *81782578a562f91b3dafc886955a170b6cbd625f69eaf2707b91cf9b5beedb77fccae7f278cbdb *aec7dab5fd12de8285a99f8e2508a43a2fa4ee4d7da3845b8656b83a2054dd61580bd3769c143b *84673bbce88050ac1fb27475f9f255e5fe9fbd0876714c36f59dc74c97b30ef1e8f689d86115d2 *40e557734b2bdef9aef7a0a9a910e3cc4228e94609b7944a115ce1f28e10bdd70a25ec104eb59a *402866b38847e0153d94168b4bdfea952115f7ed5d309b196fea5d2e7a98b3fc3881ddcc751d0c *f5af416f573bda9a9bd090cf4572b3b6270e54fef66bfad6e2c6bff84bc071e2f7554d5dce6b68 *2124ea23ea873e8745adca935c6d91669122992933553f8a61aadd5b9d8f8a3b9434485b976bf2 *2312920299ee8eb6c07bcf23985b58c0dcfc49ccce2fe0c4cc5cf529ccb4f6a2a9b76e7f12def0 *c66b70cbe76f8e89a0123d412fa6df50bca5540a03fb11252ae7a739f2d8a266e015d7018462eb *e99a3497491adb2c4508493416f9b85dd7416b73135a9b9b0000a592872393277074721a8b8ba5 *44f62efca38b71e0c001dc79fb77855ad68025ca8dcdb0251d08d10a595ffa712e6b10cdb0f220 *145b9b90f2f550228f505fe110920a13be590301fef8792fc0d6adc3d8be751bb66d1dc6b6adc3 *d83a3c8c96961600402ee7a2bfa70bfd3d5d98989ac6f8f129cccd9f94caeddff12f7fd995181d *3d80877e2379b7520b21f5a84a1b421f30e559bc0a102ec31092ab9ac09b1f5216422a2123ab1d *00274e4ce1c1071fc2830f3e542d731c07ebd60de0f4534fc54b2f7f312e38ff3c0040577b1bba *dadb303bbf80f16393989c9ee3a7a6e476dd1cdef496b7e3e31ffd0046470e286e54650b0cb75a *71104aae3ca1c0a83a39ad3b61d620141766e9024e8f3392b210526f4a18c1082138303a8a03a3 *a3f8c18f7e84adc3c378d59557e0f9cffd53140a05b43415b079702d4ecccc62ffa1a3582c79a1 *f67c0f85a626bcf35d7f850ffcf57b313b3bcb70a7b64dfa2aaf1e083133d0fee02c0fbdfc0b28 *ef5951c517f1aa124135795b6e531e45154b25144b25943c0fc9bf1d4be88fd330fe9fa0334d5d *2edb50a4e6ae279ec0473ffe09bce0c52fc5673e7b33c60f1f060074b4b6e0499b07d1d1529eef *8abbd8d97f6945677737def5def7c3097c532cb61511afc295b575b2b857125c346d8894bf8057 *22bad0f3d27f4b28e15bbcaaa6ce48102658e47356bdf34111c5521125af04046ecd2221657ffe *9eaa1f08c97d454fbfe3a8a8bfc9c9497cf1dfff032f7ae9cbf1371ff810c60f1f463e97c3f050 *3f86fabae1baf4b98bc035603e6dd8b419afbcea6a7a3a296f5a3a59bc6b08ea1442327ee87259 *75c5c26aea8c04618245fcc68e6f2104d5a7c094024f81d1e9cfdf532b154212fec2f5022bc537 *ac582ce2ceef7f1faf7ccd6bf1c31fff0400d0dbdd8153370da0b990f745a483caaf8b9ffd1c0c *0f6fa378d5d6d1625b48b00a20c4f6d3d0d080cece0eac1b18c0f0962dcc7a2e3da486ce48b00d *325efc108a530052c5a2f015e7bc0d5bf51092e82ba0dcd7fe8b67690d45534d9e3881f7bcefaf *f1e19b3e8ed9b939141a1b71eae6f5e8ed6a431ca82a079bebbab8e6daeb964667c21d2de69560 *5543284e8d8d8d58bb762db66fdb866d5bb7a27f6d3f72b93c26264f60cfdebd81d3c3cac2b8ac *81b5c5cade024de3e022f5a5242b896f55e5624ed1db0527d84ccd41f88112a5506eacd690b57b *33012911f99bdffe367efde083b8e9c31fc4934f3f1d1bfad720e7ba38746c325a9b127b4d6f1f *ae7aedebf1e57fbb253ebda437d38afb822154db940d7a364a3ac771d0dede86eeae2eb4b4b460 *7e7e1e478e1cc1ccec2c161616626396e7b014411a2ce237961909319308fa23a88ca48a281517 *e17925788437792ef1f742cb1f1d7ee34c8c84041a4ab5e2fdada05696f3b37f6404afbfe62df8 *c297fe0d9ee761b0af1b033d1dcb6908ffe07ec6c5cfc6b6eda7d89150626fc43738082edd5d9d *d8be6d2b06d7adc3e2e222f6ecdd8bdd7bf6e0d8f1e342b00228d76105378fbf913a47422a0dc3 *612a9720f07f1aa3d38f78104d9b6cbca1542bc9ca0e758f49706087de174b257ce67337e3fe07 *1ec03ffefda730d8d703003878f4446c1cc77571cdb5d7e37defbe11a512fb8a7a65afe63a36b1 *e2bd25f3d3d4d484750303682c34e2e891a3383e3121d3c7f32018057010c0a1c82961f912aca5 *89691957862014db9a60e95b3ecafdcc13cb424863e5482b697c09a6bbff815fe3ba1bdf814fff *d33f72a1150ed7d5b306af7dc335f8d2e73f2b946735434844b95c0e6bfbfad0d5d58513274e60 *ffc8088ac562247de565b158c4d4e4719c989c58dcb079f83cd775471e7ae0bee3fe98fcc77cd5 *084222612b579d4b8fa6621cd50b84a45ad6f8c0229cd724bc5238125fbf79e8615c77e33bf02f *fff79f96a0e5e0e0d1c9d82817fed13371cfcf7f8adf3df288563fbac4dfddd3f7c34a5f686ac2 *86a121785e097bf6eec3ecec0cb5c9c985054c4e1cc7898909cc4c4f5556374c1effd53421e478 *b83ef3b286e8b6072b897e3b26047ac1a572ca572c2e3d4a3df0e9b11b2a7f4326e14d5343f196 *5229d4fdb024e22ff2ed1835907e6f153df4f00ebcfd861b313333539dd3e24521001cc7c5ebaf *799b113fccbc84b710eebc90093f4173cb4b9cb7cabfb6b6566cdeb41127a626f1c4eedd54582d *cccf63efaec7f1fb9d0fe3e0c87e3fac2a7a12cd4ee04af75a40282e1001599e405f7a9c15ddab *6996a86f94704ba914660ef4387f425fd1732b8492294a7452faa18777e2daebcbd05abfb6076d *cd05fa36fbec75f5acc1332fbe44dd5cacbfec400831def84758546b7a7a3034b41e636363181b *1b8f9c011517177160df1e3cfac84e4c4e4406507ed18125dc35da8e0f7643ff3f8f78282e8da6 *4a5e29f2b4183dded4374ab8a5540a6d9d2cee35b4c36ada10ba1cdf537328d54c7d3bb663e74e *5c7bfd0d989999c1d6f56b91737d5f8e332c3fefb2cb25fdd527842487225c0d0c0ca0b7b717fb *f6efc7f189898037e2118c8d1ec0ef773e8ca3870f8b4ce53046585a8e0f764399911081ffc2ce *1248c953f4a6b6515287a4f68a723b0b3752f8e0d6b321caa20327ead5545f01c08e9dbfc50def *7c1772ae83cd033dcb06687e01f4ae1dc0d9e73e65d54348d45bef9a35686f6dc5eeddbb313d3d *13f0b6585cc4138ffd01630747b937120889754a28e8c8b7e83e1d231e293f776fd177f5b9841f *5e56a956c295cd1c587129f543487d67161f0dc5e5d27f60b1bcfceac1dfe0e62fdc82ee8e36f4 *b4b7309d5474f9cb5ea1cd53244965df0ff555962014ee4096b78ece76ac59d3837dfbf763e1e4 *c940c8f9b939ecfafdef68735471a202ab7a5983bf0bfcdd23b4910a2240f9e931e2c4554b295c *d9cc4ec08d6a664394153f4aafd63469839e515347de7ccb17f1f4a75d8833cf3813d373235858 *ba8b292dc2e0d0266cdd7e0a763df6688c39dedbf4fb8a9f5eafbb969616ac1b5887fd2323980f *5dfc79627202fbf7ec86a7765ddb7ac7715a092181197b970aa6447fa0f90d08f15f81ee09b652 *1d40981969c4a5acff9190192f015f953f941c3f264ec73ccfc3fbfee66f3137378badebfbe0f0 *1e4ce10057bce22a20c65bea23218452088e8474bb6b6c68c086a1211c3a740833d333013f1347 *8f62efaec7556155d129e115eccb1a0252db992b6b3c02943c0f8bc5228aa5e804badcb16be6c0 *8a4b692194dc5bf9e7c595c306beff9af31334575e46470fe2a6bffb245a9b9b30d0d3ceab8acd *db4e41ffba75e6bdf993c64128022203eee20e0242303030808989e398989808549e9999c1c8be *3d3a5c444e0ba56fe017aded3096f213504ac5e53b23e839d0c5b5322124e3478fe2a0283c1272 *947fccc531175cb8fe7cffbe7bc71db8fdceef61b0b71bf99c1b09b76cd9c52baf7a9d267fd987 *10eb8008af69efe840636303c6c60f07d69f5c3c897dbb7741ed82ee8822c0ca57b623ba5d8ca1 *72ac8ff27553e5d33d7d078d507aa974fabd453264cc4f209bbe8ed42fc27d4b5da3a28f7efcef *70ce5967a1bfab1d078e4e2e832aa4534e3fa37cea58e9b4409d6083d47b2e9250dc81aa57d775 *31d0df8fd183070360f23c0ffb9ed885e2e2a262e488a2232c02da1347213dda20c443c92ba2b8 *747704d98e632e7cf00b79d339fa303312d2b39b6b1b0999102d2ccb1f77aca1cfdfcccc0c3efc *b18f615d6f17f2aecb0c9dcb37e09c73cfabcb9190894f796d5f1f666767313d351df07260df1e *cc05ee919f58945342ea81455b47dfccea247aa91878ec9585d02a80108229e24ec72a3e96fbd2 *1fc0ac37d6e9d82feef9251edaf130d676b7319b03c0854f7f86513fb58690a8b7c6a53b831e3a *7428907166661a93c7b957aeab8832e92eb199fe2e2c791e164bbe49740b214ddeea08428437d6 *a0f9737ca052dc020108c98e843efbb9cf07e6b26875b69e72aab41fde41912508450f587698ae *ee6e1c9f98c462b118587f6834c923d3986a731ca7c3bf224feb10ff3606d713108f722b976a2b *b30afb89a96dca063d5b6cba74fdf0d2d377c91a2a929eb08b0ce89e7befc5c33b76a07fdd1046 *8ed0ef9dd5ded583b6b6364c4fb12f804ca517397d95288c801cc74157672776efd913587f6272 *027333f4bb3168503b80ea87c2ff694ee5af0129a1545aacdebd53e7df81fa1d0905471b69f889 *1a5c5ef48e84f47aa3fef5ab548b8c8488196731a38dcfde7c33067bbbd19063fcf8c37170d1b3 *2e4d612444f7c73e75e184d1e1d597bbb3a3037373b338b9b0505d473c0fe36646571505ae3b61 *5cd6505ebceafc54f8562e7c651942f1fe2c8444bc850facb88b29c38eb438143dc8054ec7eebe *e797d8b173077ada9ba9294080739e72bea2b7187f358490c0015a554f77378e1d0bce534d1e3f *2249cce10000200049444154267c7b6345052617297f4eca23aa62713170cfa99502a15aff8875 *a54148d6596c7d8d1092fd84fff5739fc79aceb6a04f5fa3a18d5b561c8462c32e2d85a602dc5c *0e53d3d3017f13fc5bc4e8506084559dc32264e97ee89e87528950b6cdd001c311bf6fd3f7c34a *1f75921d6fe1153571c6485a0602db512a5e7d49eefec53dd8f3c4e3c83574a0588a666f283461 *cbf030763fb18b1746a337333da012b5a5a515d333d381d65eb184d9e9696dbe180a9e1212af84 *52b13c3f4598f353fa3aae5e4642d1d1d0ea1a0925f1c31e6d44af7437f2092b792b2fdffcd677 *d0c1bac91f80d39e7c6672af064742220bb725c34b4b73136667660355a7a64e54cfc00c2a082c *d9f9299a562284f85f849bf1569f1062639bd78304949506bc8593c479fcc9cf7e86ee8ed648fd *8a7a7afb18fe04212471ac99865079e106aaaaa5a505b3a18b42a74e449ffb6840c15342562d7e *df1aa72a5f84f7365bde6aee2ec68f70334d0a80ca4128915a569d5ec7c6c670747c148e5300ed *b66c43434352d0094b5bcb1476a44a8ac6860610029cf4fde4861082e913fcc7a7695270d2dd8e *84e4bd85ff4aa5753b0f513f494742897cf2bc3924109c441a70c2e8f22a3002fae94f7f869642 *03b57977cf9af84d16f6cadde9434b928d56f55a7ed5dcd28cd99999809f85b939a57bd9298875 *598301651942117ff50921a401a1883f716fd29bade255f3e9d84feefa29bada9aa85e9b5b5a57 *0484e2bd2dfb6bc837e064e84ea2c5a2b61f38c749ec949029c27b6b102ea222cc37e9bb8b2414 *77908a574e5f29858895c4ed65129c7631430ad6dbf1dbdf02a522b55d4363c188b7b0e43210ea *4b5d725d37f2a4668d776488130558948d5cfe836421c44fb8da21241fb812df01121ffcda5afb *5f12821d3b1e42d7d02991723727ff379e9242d95b5af2a7745c17a5c5934b63f972e1628d80e5 *66fe742ce2cf80bbc8d899774a115ce486e33afcc50ce3c54f56f4f8943c1d13899fac4f7da582 *a73cb4403fbdebae60b8ca1b3778adb59cd7d01a456faaa2edbdd57fa163ceef25e738c11b7022 *d553c2c0a4bbfa9f0b9a08f34dfa7f242209c51da4e295d357ca617429a5d331f1e391505f9a12 *01b0eb89dd701d2c7d53e8c30d011a1a1b7d733a24da3845d1ff2c310b95e4baeed26f8897555c *2c326a6b97e41c56962014496a21241ed44c0f48458d4c6155461adaec30259782e0f0e171e45d *072797be090b7eb4c4587f467285d7d4e0a0735db73cc20a98a8c9d18f7cb8e3cb7f45fc9737a4 *200b21c5a0198010ab654c1082f2550e493a461642328d8f1d3d869ceb8220f8d5bdeb38916fcc *14b2a3d6108a8a505e95e5380e4a9e17e0413e4fbfecc38002f7f3a1de0f4b8b2c841483d62f84 *a4a21150bf3434092119953c0fae130d98cf3928140a585858a86b08c9f82b168b68c8e731e75b *976f480d58811f2bcacd61d50d84d4b3ae3a08250b249f31744c730f7aca5bd3f2a7f38a4584a9 *9a775d9c3c7932bdb38f88f44048460b2717d0582800be9b17e6f37aa7bf390a8eb0a810aacc2e *723e140b210b212d7fb26a724d93585f2d164b8013bcf2c70131708577fa101217c1c2c202dada *da026672350316a5b3886fd12e0b21f5965986500d0eace8ec2bfbad8a4a0420fe011601bc92e8 *d7f9d98610b38452343fbf809e9e9e4059cd46585aface4248bde5aa829013084bbbeedd348464 *441c3792f7e4c23c0052f71092d1c9930b2834066fb9d358685abadcc1f8ef09c3232c862243ac *0c41083002a29509a150ed1a8f84c2e96b37175475c07ee7441f2a353b3595c6592ccd4db024c5 *6ef33c8252a9847c3e8762b17cfd95e300ad6ded69dc622634e94edbf2c015d3946203ae96f36a *0ea9aba585102b7bd480a01f73b639ee0493160a4d91dbcb1000c78e1e49628c6ba036ec164b3a *3f3f879696564c4e2e03aab53d1560d14758e1bf82dae6b02c84d46ad72984cc29398464b461d3 *266af883074729b5eb134280b8bf89c913e8eaea0c02abad0d8ee3981e256b98c3b21052ab6d21 *14485dbecc8931395d63400eae1f8a58701d078ffefe1100a40620d20f21194d4d9dc0e0ba01e4 *f3f9a5d34202d775d1dcd252be57963931e6b0fc5b2979ff20962c84cc28ab10a21988cc59f95f *38698f40f8c9fc5e86366c8c94b716f23874684ca3e7da4288928559e2790427a64ea0b3b30347 *8f1ead566defe84c1958923da10d611642acecd1355986908cb7d02d91f56d96fe53b2f5435160 *75b535616c7c5cd94ba4668d2124539500387e7c120303fd3872e468757d5b47278e1f3da2f473 *254145af74a7fd15149fc3b21012c81e5db35220644cb59d17ea5dbb36e2c4599cc7f8f818b75d *bd414846333333c8e7f2d59f2601e5df19f6f4adc5a1032392d184253287e54356962124df38b1 *2c84d455b9ee8a04de97df656d4ea8bbb73f50a3c17570dfbdbf34e8b33610924d3831711c7d7d *bd18195906545b7b3b9a9a9b313f37476b9c540160b9ac1bad25fd9a90482c9135a23736d3f4e9 *443d89ddd8ccd48dd678ee22ff02fe421fa3716f517f01af91bea38ce67dbb9c492f516fec5d6c *70fd06905c43206c7b73237e7ef72f8cf9e116c51c026a1fb39ab723478ea2ada515ad2dad81f5 *6bfad6d29224d5342124f0681ea9ebebe53b44b9716245d3115e61ca229c77b415698b6d40e708 *432c9478425dde9efbc217455277b735e19e5ffe52ca0fafaa998f588f375ed562a98483636358 *b76e1d1edfb5ab7a494353730bda3a3a743ffaebd1f00ae6650d74625b08f14528af582bd2563a *10e28afa0c0a7ef25acc093df9eca7045739c0b1f183983c31590308494436c3ab48cb8989e3e8 *eeea44ef9a1e1c3eb27c21eddafe012c2e9cc4c2c2bc72f490fe105ee17bcc176bfcc9198b6a10 *65b0ce38dd89f1634c5467547fccd33163fed8c3789e37da0c80693fd45165dcee96c89bda298f *ebb8e8ece90b4469cabbf8f9cf7faef1744cc25f5c52fa66487c12927db5a4d18307d1dbdb8b86 *7c43b5c8715c0cac5f8f5c2e27dd1b0c458095a7b84f2c7a87702ba42c4279c55a91b6d806b27c *d5b48837022761f74ab4e654e545b9f8d23f89f8ec6c2908ce5f09fad3b319c65a8a345d985fc0 *b163c731383888bd7bf7569be5f20de81f1cc2c1917d3aae80a78db0c444f91bce1c6da43132e3 *39b42321753f516f6c3fcade027d2df1d79d5714b3cb897ec4cf78d6a5913aedcd8d78f0370fc6 *788b492ab619315ed55baa36e5551b3f3c8e7c431eeb0607e07f7e545373137afbfb29bd2bade8 *082be88ff84ca6784f77a608e5156b45da621ba8f7915072d1933881220202876bdb9c5576e48d *5bb607de37e41c3cfac8c3b14f8951f79a602b159bf29b8907f53c82bd7bf6607878186bfb8a18 *3f7cb85ad6dede09d771313e760844fd1634d14977ea1e2cf2a748591642e2124f5a4b10c955a3 *fd89d42189488caa83431be03404effbd4d7d98caffdef2f147c1aa18962334dbd4c09b3b858c4 *9e3d7b31bc650b16178b387efc78b5acb5ad1deb1b1a7168f480ca730c0f104222bff9d174db40 *0b2171ad3c0889da24a1ffc73fb83e398464aa3dff452f09bc771c6073ff1afcf4673f97f7239a *54b9993908c96a6161017bf6edc3e64d9bb0582c626a6aaa1ab8a1d088c18d1b317e7054f6c2d2 *c8e920c00556709845c245359585504c162dd54c58a5cfcb243390cce772ebf39e7651a0a4b3b9 *010f3ffc1076efd9ad2d69bd40881e861d78767606fb47f663c3d010460f8e626262f936343937 *8781c1214c1c3f86c9e3c744a79ae8c0aadeecd517c33fd99a9e2c8462b268a966c6aa68d4e098 *8a709aea8290a8ce3ee73c3434b506d66de8ebc2473ef38f09fbb47e212419085327a670e0c028 *d6af5f8fd6e6568c1e3c588593e338e8ee59838e8e4e1c3f7654e4c67f8c1196d1832de6a34c1d *44750aa198aab585503203129f88d104575cf5bac0fb42de05595cc01ddffbbebe833cb530e99e *bafa35393989f9f9796cdcb0015bb70e63dfbe7d813b39b8f91cd6ac5d8b8eae2e1c3b7a0473ec *5bd3c89e12b264211493454bd57a82908a5782e80dfc8c26e4346b6e69c1ba4d5b03e5ebbadbf0 *b55b6f959f2c5e81108a0b1c4e31bf308fc79fd88575ebd661ebd6ad38303a1ab8532900343436 *a27f60108b274f6276661ab33333e12be479c00aa724bec542c8424846ec960eef2a0663c7637c *8d575dfd7a10dfe9aaeb0083bd1df8afaf7d2d91b77827f50321d9105ec9c381910398e99ac1d0 *e07a747574627c7c1c73f3c19fed343436a2b3b1079ddd3d28158b989d9dc1eccccc625f7f7f7e *cbb6edcdbb1f7f2c30539fa74d8099bb5851b0a68550adabea69c96a5659efb0eba4392f74e133 *9f1d58ddd3d684bbeeba0b07470f8a86e0ae91f1a25f1a2094a0f1c4c404e6e6e630d0df8faddb *8631353585f1f1c398a37c6398cbe7d0ded181f68e8e06023c4a006cdeb67d02c0288083000e25 *bcacc142484bc274aaea6999b47348f0a5a6c3891a5f44e73ff569c8175a02eb36f577e3931ffe *6a726f2b1442b2411616e6b177df5e149a9ad0d7d787e1e1614c4d4f97475cac4b1d96c3762d2d *a703d4392c125a38562d846a5d554f4b6393e6228105ea186159f9dd4b5f7575606d6b218723e3 *8770f72f7e414fbc2a20241e4826ddfcdc3cf6efdb8ff142017dbdbdd8ba6518274f9ec4d4f434 *a6a7a731333313fb60d67ce463244ba8d27e4a18c8a2a5aa85909184c64fc7fc678449c3d12024 *aab6f6760c6cd81268b5bea7035fbcf93320e187122674b75221a4e26d61610123070e606c7c1c *5d9d9d686d6b454ff7101cc7c1ecec2ca6a7a731373fbf74016a50797d50b21032de32c310120d *23029824109269f6eaabdf10986c2fe45d74b71570dbff7c4328b08550b2408b8b2771f8c8611c *3e72188ee3a0b9b9196d6d6d686d6bc3dafe7eecd8b933d226660e2bcb1092889c650825689a15 *088987895e384a641368dc992eb8e892c0fb2dfd5df8f677be53be519f8a2c84d45a1180108299 *9959cccccc0200ce3ce30c6aa3c829e1727862216421241126fdbe92091a4ef1cc4b2e45aeb1b9 *fabea5318781359df8f72f7fc590b7d5072175b1e7d00357ba13df0bc2e79582017dd524ab6a6b *6921a41c885ae4d0f749e1a0caee08f0b257bd2eb06adbe01adc71e79d78e491df29fbd15233cb *10520f9ba461406a8faa97316021a4ae3a829088e8975de98390a8366f194657ef40b54947731e *9dad4df8d4fff907a9401642fa87c6c4f77f424991a7570f34e1471634a0267313aef2cdb20ca1 *0481533c1d23e115fcfbf7193bd05fffd6ebfc16b07d7d1fbef6f55bb17bf76e0b218310924e41 *2156700e2b8c3746600ba1b4c2641f4252a23daa3e8503cb5fb3b5b50d9bb69f5e5dd7dd5a40de *21f8a74fffb3c6cb780c9d2bac040809578e99c3a28dad0c0c8e53686621a41a34cde381807603 *3ff3a7637f7ecdb5a85ccae038c0f6a15e7cf1965b303e7698d258cd9b54cb5505a16422843b87 *2587ac403315332682a616c642483410afefd2383ece7fc625d5d76b3b9a313f378bcfde7cb374 *c07a8150246a0621c45a4f2be35cd6c07e2b6c2641a9a62409c25808e908145753fd6092cff8e2 *97bd02ced223e8730eb06d7d2ffeee139fa85e516d21a447eaa7d6e1869c53c2483dc6002bde8b *860e5915100a06aefdf1606802203cd7a01c4c6d43fdad9e7ff9cbabafd775b7627c6c0cfff1e5 *ff5438c02c84245aeab451fe9690c6b520af2c845402d7fe334e01421272141e552fdd825178ce *53ce47535b170020ef3a181eecc5bbdff35edfdd302d84245aeab4c14e41826f01ff3ddda9b505 *8c19619985908e403a8e8fe6e6166c3ff5546cdd760a86366e42cf9a3e2c2e9ec4dcdc2c666766 *303b338d2387c7f1ebfbefc5d8a143b259c5bc6ae8f4e7bce032b8a4847c430336ac69c7a38f3d *866ffcefff2a05b710d293226eca88c626fa6f092578c54e17bf46319026691a3f1a9e7749944e *d2db868d9b70c5ab5f8b0d9bb7a0bda30b0d4dcd70dc5c340c25ee2bdef07638c4c3ecd42446f7 *efc137befe55ec78f837f116533a1dfbd447ff16777ef73b38edb46d0080d7bfff3d815b995808 *e949617ade5aea211416426682d4e6f828072a140a78e92b5e8d675efa5cb475ade1a78b99d324 *8e8be68e6e6c7d7237defda173515c98c5afeef9196ef9d77f09dfaf9b97459bfc519f7ee18538 *edb4d30000f7de771f7ef8a31fa7ee2790c142484994392c12f827ac550121f140b58490889e7e *d1c578d1cb5e81f59bb7018ecb8f14032a96f285165c78c973f1d48b9e8d7ffd878f316ac96dbc *c8688fa69e9e1e7cf14b5f0200dc7adb7f4be71551b20b4fd305519620444de93bc3f373883187 *c508a24d1642a603b15abde9add7e1a24b9f8b5c63537c3b455085e5e41bf1f6f77c10289d64ce *98aa251177f3dddb6fc7776fbf5db87e3543367650a114998750a04864068b3687c56a97706ea1 *f623ded503219190679d732eaeffcbbf46535ba75873a9d382781100c83546d7abd34add8b8590 *ba12404887a46ee06721a43f50120889442e140a78f7fb3f8853cfb9402c856650c54b4f060ba1 *04aa3184b80aa5a79e120acd605908a9b54af1c0fa93e73e1fafb9e63ae41a0af191520755bc2c *8412a88e20442d5cfa6d4eb86a9e56b73ae195c290cac074859140b1ad6a3ffcacaaa7b70feffb *e04d18d8301c9f4dd33c95aae4a094250889d5d0aaba8550b0828853167e242e6bb010d2183979 *44468aa73eed8f70fd5f7d008e9b936e5f9b5dde42885db4b220945884f9e3e78a050543358650 *6ccb3a8490606b3cef852fc26bde7c43e03730d9055542499dc25a08854c0815a605217659b490 *797b9900ae2c84d423a674ca73d5ebde88e7bfecd5523e3270e8046521a4ae7a8190b0037aaf4a *dfad41dd8064cb0c4128123563f32e2f7fd5d55558d51ba8887ac76a48ce2bca402fad3808c9e0 *8a2ec6650deab3ee16427a243a197dd1b32ec1e5af7c5ddd816a599a5dc584ab3988ea0642e22e *8c79a51ca09c392c463ba942230dc5a3d62984282da96bb73fe954bcf52fde0fe2bf6f4bdd804a *421642e2aa4308b1eb09ce61556e4f6a6f6c2690a146c02e140af89b9bfe01a87c1b58afa06298 *ac1708c556d5250da3869a434832a4f81c16db8d365908254bf1be0f7e0cb94253fd820a9587e8 *a4e8d642485c86209454fc47d54ba6b010d293829f91e0e9175d8c6d679c2bd92e7b725dea2d48 *e5b402212457534222139d4943ea0ac438c58bf92da18590b2242124aa7c3e8f6bffe2fd8126f5 *06aa8a72f47b265b08c9a89e20a421729e76d0b3e7b02c84c4339af1f3ba37bea5fadbc07a0555 *456ef5c1aa1642fca02b1b42ac88f4392c8da7844c0389c2a40ba25a404826e5c57ffac2ba0755 *450e29e99dc35a6510d216254508a9355e6e1d7b4a281043a57592c6d229b20ea16011bbf0052f *7a09728dcd060cd546dec9d9f84a16429a82641742ccf2b839ac4a3961d7e76590a9aca6150821 *19bdf88a57c757aa234d1c1e4b7460ad3a08450299d9cfd56765cc1f77d4392c9f03e306563b84 *4495cfe7d1deddbb624e071b720e76fce1774865feca4228d3100aa4f3fd5f610e2b6156a12829 *1f82198210c50053cfbaf43920d070194046d4d9dc88bbefb927c18114bb424f582d812c84f815 *38dffc85243e876521a4a6f84f4ba8ea45975caac34d6634d4d781fbef7f20b8d242a86e2004c4 *4d2f9af153bdd2dd1f9e7e59838590cf8070055d4e376ed9ae2952edd556c863d7638f62766646 *3986859019d5024232e23ce62be169212b24b3a8f69d21fa8d542a4e43499a5a5ad3c89a8a867a *3bf0f95bff23b2de42c88cb20e21bf027358894e09e332508b32d0191986904421dc7c1e5e06ba *33a99a1b5c74b634e22b5ffdaa5c9f5b080553720bb3b5a3043f3a516ff47a746011df422dcef2 *2959962024ee22ae1621f53fe1ee38c0e99bfaf1f5db6ec5e4e4a4b1392b2d112d84b852835072 *f9e6b002b3581078d0975ead4208892a979378564886b5bebb156dcd057ce10bb724dac92d84cc *a856101253d90fff51f57a72c416ae4608f10307b3948a8bc83940296bfb90849a1b5c6c1d5a8b *2f7ff92b78ecb1c722e5164266540f10aabe23c1d77ae6b024e6616a0fa2ec4248468ee3209773 *502a666d87135343cec1d95bd763f4c0017ce8231f51bb2ec7b02c8474890d211dca537284e6b0 *2c84d881f564118992775d9c44494bbe3495731c9cb565004d8d79bcf35defc2f4d4546ab92d84 *74c92c846494a73d0e9ac0c01c960608c9d5949081c95f9d5fd11342d09403047e2a9c29390e70 *fac65eb4b734e10bb77c1177ddf5d3c431eb164240c6fc6507424b0e28efcbebfcde98cf2554ca *215ea85853421987906c646f7e1a40fddca921e700a76de8c59ace36fce0073fc0df7ee003ccba *1642ba947508f94a62bcb16ebe109dc322a145d0807a4d0919fa1a5c4b1443de2a3a78601f3a36 *9c8ac53a98796fc839386bf300da5b9bf0eb071fc49bdef256144bbed3d9da1f49015908c9481d *423ae4067e8743c2b40a2efc520ee7788a3426ec45228b16af86bca9f8fcf14f7e82c19e36e1b8 *b55273838bf3b60fa1bdb509bb763d81ab5e7335e66667437d655ed18f8e3097e86799ae3b9eb7 *74ac718ef9183f697b732d846a07216a4486973beeb8139bfa7bd0d4e00afb48530e80755d2d78 *eaa99bd0d498c7030f3c80175e76198e1e3daa2d878590ba9f2c4328b2f80c84fd28ddad81b142 *d9bafe40667a5a39aa864f7e7c7c1cf7dd7f3f4e39ed0cecd8336e680bd554c8bb386d431fbada *5b0000dff9ce7771eddbdf8e858585d8b6d1dd2a4b5b06f81dd6de1adb40edbd01dc238408d58a *8de652471b21cad564241409c4f93310f1272e65af717e34ff9922006efad8c7d1d3d18a27adef *01eb8133692ae73a18ea69c385a76e44577b0b3ccfc33f7ffad378e39bdf8cf985858c8e8400ff *a7ca1f099114ac657924c4f7171c09f1fda91d9d512f428faaafd8d6a2153e12924ec92d0c96de *7befbdf8fad76fc595575e01c775f187912335f94174ce7530d0d582e1c15ee4dcf229eaeeddbb *71dd0d37e2befbee4bdf10807a19090119f7a730124a2656960a14836be98faa87240d2d848229 *b985c9fc7cf0c31fc6d39e7621366edc88b6a646fc6edf38a6178a89628ac801d0d298436f670b *36aeed412e57061521045ff8c22df8c84d37616e6e4e73560b2171d50384244c30468eec39ac00 *b12c844c424846478e1cc18b2f7f09fee7bf6fc3962d5b70fe933660f4f004f61e9ec442d1d39a *cb0fa9f5bd5d686c58de5d0821b8fd8e3bf0c94ffd3d1e79e41189a81642e25a611012afc6ac97 *a77e2aa4f21ff9aeb0103223bf9b91030770d98b2fc77ffde75770e6996762b0af0b837d5d383a *398d91c393989c3ba97caac8835445dffbfef7f1894f7e0a3b76ec88b8cb46b7b14dd4de9f1884 *626a6a94b89f84d52423d1f923f42da185901905cfa4e5bc8d8d8de139cf7d1edef4c637e2afde *fb1eb4b5b5614d6779210026a766716c6a16276617b0582aa1e41194bcf2c4683ee7a021e7a231 *9f43a12187a6c606343736a0b9a9012d8546b86ef4d289871f7e18dffcd6b7f1cd6f7d0b7bf6ec *49b2d98ab21012575621245995764a283d876521c4551208c9aa542ae17337df8c6f7eeb5bb8ee *ed6fc395575e89eeae2e3800bada5baa9719a8ca3ca42c84c4951c429255f544514cc8e28fb376 *dd2009ef1d679e792676eedc09dafd6874c942489796fd140a055cf66797e125975f8ef32f381f *3dddddc25116171771686c0cfbf7edc30f7ff4e30490b21012d7ea8390a8ce39e76cfce6370f45 *5ad3e7b0146521a44b413fa2f6e6e71770eb6db7e1d6db6e03006cdbb60d4f39f75cf4f7f7a3bb *bb0b5d9d5d28341530363686d1d183181d1dc5e8c1f2ff0f1f3eccf903652124ae2c4148225282 *84ea4de35a9248b5d8392c0b215d528350123dfef8e378fcf1c785fc044a6ade7516425a226512 *42825519e784d139acc095ab35df732d84a46421242e0b21232d3574086f0e5deb29a1a8a890cc *8c2c84c46521a425d22a80507c18da5784d116c99f4bc83253fba3c9270b2171590869896421a4 *9e8433c4e202cb424846750aa150b1859099a416426acdc2cad3e6a902bfa44f551642e2b210d2 *16c942c8445065f19e29a1ed9430ce42f555860ff4da7b03440f7c0b2133492d84d49ac587d5e3 *2701b02c84c4652154cba416426acde2c31af423368745aaffaffd650d1642e2b21032d2d24248 *a6610a228c392c8309b9a53507519d4048c2848590b6241261b275d0671d42f477c6e6b02c84c4 *652164a4a585904cc314c482904433b153c2706b7aaa7a81504c4d8d4a7e4ab63a2024d13acb10 *4ad8941d72054248ba325fd57bbafb2194fa3301ca59858b2c84f427b5104ad6941dd242482d28 *7dc09437ffc40db1220b21fd492d8492356587b4103211948baba5370a73582b0f429255f544b1 *1032155831a4859089a0dc9a7161d87358e112b2bc5808194f682194ac293ba4859089a0892024 *25ca292148f4633533879525084944aac931986e87c487c8ce81af0ca172e3144428afa49a6954 *d620943c10f59eee5a0c5808a9b7b41092699c822c844c070acf57b1064c94392c125ad4bc5908 *694b2211c64248258985909940b449f3a4623f973019af626421241ec642482589859099409128 *5ac2d2825000045d8faa672655afa6b7a985103ba4859089a016429abc85c2c45cd660fe40b710 *526b161f365b7e58496a0f21f1c016427a02897c99c799c3e28cb12c844c0455968590fec01642 *7a02e9bba280f8fe4f3925143d7db410526b161f365b7e78498453661942e261d20892690801ba *40a46f87e05ed6c02eb310e287cd961f5e120b21ed412c847484659f12d20cc44cbb5b08c9344c *41164286835808690a2b9a81306a2fcf6149f04ad09b74a9ee66f161b3e58797a45e20145b3343 *07faea80106064a7509e3292435658f4392ca914d93ae82d846464219420b25a948cf9e185d40f *a1045a4ac1b80e8bfd981dd1c03ab52221245d597fd07a8150244ad6bc65cc0f2f641621c42a93 *b8ac4122b0a22c84cc04b510b210126e68423129c4cfd6e87352fc5342c9edb3103213d442c842 *48aab10971521899326228f6067e164266825a085908493536a18c4048264d9e0624525d12f5b8 *a22c841264518b92356f19f3c30b69212421891454fea032c20aaf27be459b08e59554338db210 *528caa1e25637e78212d84242405a1e47e842e6be05988be926aa65116428a51d5a364cc0f2fa4 *8590845286908cb837f093326321a41e454bd85500a150580b2109651842d4748133bce50adc39 *2c0ba104512c8494c25a0849a8deb1cda45200000204494441542024586179b8449bc38aa59ba8 *c41a5808e9096421944016426a4a08211dd272033f0b213d812c8412c842485d8a07b0d11330c6 *a08979033fee0c56c60e2c0b213d612d84246421242ea9a07c62057f4bb8f48610667d8984c994 *650801ba406421249edd42a8fe21245e93851fc1cb1a2c8424a2e808c20d6b2124210b21711986 *506291b87bba0ba45a1d10028cec2284fb56bca109294348ac466209a658f510920a5c1b0831d7 *530e5ee6a47b0057193be82d84146521a42e0b217129f69548bd3cf5e0979ac3b210126e684216 *42eab2101297160889a760cf61319b2acfba2f47b11052534c8a7a9917b2109209bcf22124dd90 *845788dec0cfdfcc42484d1642eab2101257d621241c893e60ca03510855e6bbe4e1942508c9d7 *5692f22959762054ae9a3288ea1642e2952d84f48b3e8765da808590ba2c84c46521a4a9a1f9fd *8835951e16e79450720ecb42485d2b1042f1a58ab210d2d4b0361012abbcf446e6b286b3cf3ebb *ae2164a8a111a5eec6424853ba1a4328d2385bfbb509394dcd2deac32813b22321355908694ab7 *fa2094782494a2a4bf25549285909a12cc2fd60b84a4c32a07499e65754048ba7562f1d3074b63 *9f9a239e45a828b6548b2c84c46521a4a9b18590787a752f41605908a92b4bdf90651942b181b2 *34396d21249e3e1d2f9ccb1a2c842c8474a5b41012afbcfa20242a4208fe3fb1ed16c7a87a91db *0000000049454e44ae426082 addfile ./contrib/musicplayer/src/resources/images/album_covers.svg hunk ./contrib/musicplayer/src/resources/images/album_covers.svg 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + addfile ./contrib/musicplayer/src/resources/images/next.png binary ./contrib/musicplayer/src/resources/images/next.png oldhex * newhex *89504e470d0a1a0a0000000d494844520000000c0000000c080600000056755ce7000000197445 *5874536f6674776172650041646f626520496d616765526561647971c9653c0000006d49444154 *78da6258b566fd7920d6672012300215ff87b21bc242021bb12902aaa9c7a601042e00710250e3 *45340d70354c6886198034219b880e98708837e0f21bba93b06a8662bc363090ea24580018a087 *1c0b2e67e00a62162ca662042b2e0d384d45f63403a9490320c000c18231b06a136a8100000000 *49454e44ae426082 addfile ./contrib/musicplayer/src/resources/images/next_active.png binary ./contrib/musicplayer/src/resources/images/next_active.png oldhex * newhex *89504e470d0a1a0a0000000d494844520000000c0000000c080600000056755ce7000000197445 *5874536f6674776172650041646f626520496d616765526561647971c9653c0000005549444154 *78da62f8ffffff7920d6672012300215ff87b21b1819191bb129022aa947e62003acb6212b40d7 *0003f5a46a40b18d580d70db90394c0ca4026a3989684f130c561624719c11079243d64d52d200 *083000b0c621774561a5bd0000000049454e44ae426082 addfile ./contrib/musicplayer/src/resources/images/play.png binary ./contrib/musicplayer/src/resources/images/play.png oldhex * newhex *89504e470d0a1a0a0000000d49484452000000280000007808060000008070b09a000000047342 *4954080808087c086488000000097048597300000dd700000dd70142289b780000001974455874 *536f667477617265007777772e696e6b73636170652e6f72679bee3c1a000013cd49444154789c *ad5b7b9015d599ff9dc7edee3b330e0c030e8f0b988cbb8802098b24881544a694adac60b4023e *8810145722082aec4682c435d172b792182ba58b0fd06cb1a861acb5c4ec26d9dd02190bb59652 *b22b6aca9555c60b0c0cc3639899dbddb7fb9cfd63fab4e79edb7def9d49beaa53ddd3ddf79c5f *ffbe73bef33d7ac8f9f3e7311c3977eedcc50016039805603c21640221643c0048298f49298f02 *3806e05d00bb478c18f1c970c621430178f6ecd9b100d612426ee49c4fe59c8331064208082125 *cf4a2921a544188608820041107c24a5fc17004f8e1c39b2eb4f0af0cc99331700d8c818db68db *761de77cf0c71aa8248026d82008e0fbfe4018863f05f0d3a6a6a6aa83570578faf4e92594d2ad *b66d37673299188879d4cf4d703a4805d4f3bc1e21c4f7468d1ad53e2c803d3d3d04c08f6ddbde *6c5956ac46bd99004dd1c19920a594f03c0fbeef3f0a604b7373b34cea2311e0a953a7ea09212f *398eb388730e4a6922b0a4b9970630ad158b45789ef7ba94f296d1a347f79b7d50f34277773721 *84bcec38ce22b500cadeaa02d83496d35e88730ec7711611425eeeeeee2e1bac0c2080476ddbbe *4eb156099cd94ca6d31836af11426059d675001ea908f0c489134b2ccbda446912eee48e4d7069 *204db0665f94526432991f9c3871624922c0aeaeae0b38e74f33c6e29bfa6a343bafa4dea4395b *8b30c6c0397fbaababeb8232808490ef673299512630536a9977d5d84a123526e77c1421e4fb25 *008f1d3b369631767f357069809398d3ef992fa68f93643329a5f71f3b766c6c0c90527a2f632c *6b9a854a800f1e3c8842a15006b29a7d4c33e2fa35c6589610b25e0778a3092cad3325fbf6edc3 *0d37de88679f7bae0c4412b86afd9af69110722300d07c3e7f31a5f4cf86dae185175e88f3e7cf *63c78e1d58b478313a3a3aaab25e4bffea0529a57f1e61fb82bda174aa240802747777e3079b37 *63c58a15f8fcf3cf13770cf5fb5ac78940de4029a5b3cd9b95e649d220524af8be8f3f7cfc316e *bbed366cd9b2059ee7559dcb9558040042c8e514c0f8b41f24814beb0c00a410182814b077ef5e *2cbefe7abcf8d24b15f7e14a20a3f3f114404bda83953aafa4aa200cd1dbdb8be79e7d164b6fba *09efbdf75eea0b579906632980b1d5549ad659352906014e9c3c8907366dc2baf5ebd1dddd5d91 *c5040d8ee3e6e095409ae7b588140242084ccce54029ad49e5fa185c4ad925a56cd5ec4f458042 *889267aac955f3e661fdfaf5686e6e2e5938d534145d3fc6a594c700b49ae875104208a4793869 *d2fae52fe3fe0d1bf0951933502814e0791e841035b1a7b5e31cc0d16af32109641a838d8d8df8 *deead558bc78310a85020606065241a501d6fa3fcaa594ef4a296faec59428f5266d658c312cf9 *f6b771c71d77803186bebebeb2fe2a81d3ff1642a8dfbccb01bc26a5fc896268386a9d33670eee *bbf75eb4b4b4c0755d846158115c2de62b3abec6274c98f0bf478f1efd08c054730525a9553128 *a5c4a4499370effaf5b8fcf2cbd1dfdf8f81818112e0b52c840a203fcce5729ff0a8af57a59453 *15bd3a285dadba2f376fde3cdc7aebad705d174991611ab85ad41db5578128eccce7f36309219f *1042ea6b0980d47d7dc527c95019d480f603b83897cb755100c8e5725d001e371f4e6aea7e1886 *f1df959e359fa9654503f85984e98bc03d9fcf5f00e03021644c2d716ed23189bd4aea5553c8b8 *d60da03597cb9d07b4a029bab026eded2ab152893dd37c549a7b91ac51e04a004620db013c6a02 *4bfa3be97a2590d5541dc9a311865838ca650b806952caeb4d9529f3a29b1a5dcc88cd5477da8a *8ee4b568ecd23e934c443e9faf07b013c0f5fac0fa792d71af390f938e1ab865b95cae7af20800 *a2076f00f0a8eaacd2aaabb6e22ba814d118372481036a4860e6f3f925009e0230a6ecc743c818 *244837061744c50466d5cd36eaa015c08f0094bc658d5b9629fd515fadd5c001434ca2e7f3f9b1 *00d66250fd97d6fcc341f910c0ab009e5446b8161912405df2f97c4919426bc060f941b57701ec *cee572c32a432499999aa4b3b3b35e08318610d22ca51c01a04e08c100104a699d94720421c48b *76a6fa5c2e37ac7186c4e05b6fbd75919472939472a194727252360bf8626118fbeb1142c8ef28 *a58fcd9d3bf7b33f29c077de79a74908f18810e27642884329856a00121ddb249324844018862e *63ec05dff71fbcfaeaab4fffd100f7efdf7f9794f21f28a52328a5608c95a47b4d1695a4eddbca *0b12429c638c3d3077eedca78705309fcf93cececead52cabb1863508d520abd3491c4a2527118 *8689e0c2308c1b21e4d9c99327afcee572b5d7490e1e3cd85828147603b84ad5e3a2fc71095013 *a42eba8a7540aa76a79f03e820842cbef2ca2bcf99fd94ade27c3e4f0a85c26e42c8559cf31854 *26932901a900aaa4bbae629d41210438e708820042080441004a697c24842008827952caddf97c *7ebec96419c0cecece7f54e032990cf4a302c739af9aee9552c6ccea20f5978bd80300044130ef *c891234fe772b9bbf47e4a54fcf6db6faf96526eb52c2b06a637a5de6a796813a83a4a3958fa52 *2a2e168b28168bf07d3fbece395f3d67ce9c67ca0046a6e4ff38e7232dcb826559c86432b02c2b *56b15e7daae66e5572b5822088c1e9207ddf471004e73299cc97e6cc9973062855f18f1963234d *d638e750d54e05a6160675a3ad3bba524aa87ab3fe029a511f21847804c09a98c103070e4c0ec3 *f00f9c73c7719c9839d5aa9519861a76aaf3a8c00ddff7e1799ecea4cb189b7ac515577c460120 *0cc30728a58ebe52d3c029b372f4e85184615866b44df3631ef54592a4ada83952ca077415ffa5 *5a00fac349c1bb62ecf5d75fc7ecaf7d0d19ce316bd6ac54f6f4bf5553aa9652c2b2ac446fbd58 *2c2e0400d6d6d63603c0265da5aa99ace8ecfce217bfc0cf1f7f1c4d4d4dc85816c230c4a851a3 *4a58ae56f9d45b42983bb2ababeb554a29bd39896653a5a69a0821f07d1f4f3df514d6ad5b87ff *3a7000fbf7efc7f9f3e7535fccec47357d87d2370321c4cd9c1032d3bc99a4dea4bf95e9e93975 *0a3fd8b409b366cdc2da7bee8163dbf8fad7bf0ec658995a4d75ab7b994c2636e40a4710043339 *21a4459b9c65fbac0e4e37338c31f008208b9e3f74e810d6ae5983c58b17833286fafa7acc983e *3d11a4298a49b547178b4510425ab8cac528d529f6aacd1fc61832ea59eda528a5f8b7dffc061d *6fbe89952b57a2bfbf1f93274dc2f8f1e313d9532d93c9c4ce836251083186534a47eb5b581a38 *9d45f5324ac571d380168b456cdbb60d13274ec4aa55ab70e4f3cf3163fa74d4d7d59530a7b36a *9a2400a3b9c94a926ad36c21d700324a13c19e3c79128f3df61866cf9e8d712d2d68f8d2974a80 *2970a633ac1aa7949e2284e4d218ab04b08c41a3314a315028005262de37be81d6d6d68a73d124 *87527a8a13426280fa22a8a662ce79ea1c54aed4a953a7b06ad52aac5dbb16d96cb66cbb4b1263 *43e8e194d293690f993b88ba0700445331d3d42aa5c4a9ee6ecc9f3f1f0f3ffc30264c98909630 *2a11fd9e06f004c760607dad7eb3961d80192a2684e0dcd9b3b8f0c20bf1e28b2fe28a2bae484b *b3a50234934f8490f72821e465750128cfeaa7aa5cdbb7dd4201bde7cee1c1071f44474707e6ce *9d5b114c922465c084102fd2a953a7fe0f21a4d3445f4d2821088200c78f1dc3b7bef52d1c3870 *00cb972f8fdd7c7d5ad4225a38aaa2c1cee9d3a7bfcf23f4bf1542fcb51e1aea5f2025c9c48913 *d1dbdb8bf65dbbd0dada3a6440bae8c0d4f88490df0291c37ae8d0a149b66d7f9ccd666dc771a0 *5aa5b9d8d7d787c6c6c632372c6dbfadd43ccf83ebbaf1b15028789ee74d993973e6110a00d3a6 *4deb0cc370bb0a5cd4d1147df0868686b23c6092e75c4d92e26500cfcf9c39f308a025303399cc *e620087a4d90692074d0464c91c8629228b75f6fbeeff78661b8593d13036c6d6d3d5b2c1637a8 *1050b54a9da73197042ce99e8a47f43109217f337dfaf433650001e0d24b2fdd562c16b7eaa1a0 *ebbaa94970d30baef68c2e66d8592c161186e1d353a74e7d567fae2ca9327efcf8359ee775441f *c0c611975e034e63aad24250f7c33084e779ea035b3d92eb983061c2dd269e32808d8d8df2dcb9 *738b060606de705d376651753894d569828b3ea82d01e8ba2e5cd7ddc7185bd4d8d858369f52d3 *6fbdbdbde4f8f1e34f5a967577369b8d43513389544d947d538b409f3e11d0adb95c6e4d12b88a *00957cf4d147776432999f398e33c20cac92fc477dcb542a552ebc3a46e07a8510f75f72c925db *2b8d5f530af8f0e1c3238bc5e2df5996b5dab66ddb4c24e9ae98ce9cbe332980aeeb7a41103c63 *59d643adadad67ab8d3da424fac71f7f3c514af9b794d2459cf3c92ab164eebffaca56f6340882 *cf8410bf0e82e027d3a64debac75cc61d7493ef8e083698cb1258490bfa0948e2384b400181ddd *3e25a53c1104c17100ef0921da2fbbecb243c31967d87592f7df7fdff17dbf290882116118d609 *213261181200608c6528a5758cb1119cf326cbb29ccb2ebb6c58e30c89c15ffdea57adaeeb3eec *baee0221c4383302d4455773f425c971c771f6388ef3d04d37dd74f84f0a70d7ae5dcdaeebfe6c *6060e0164288a5672292c0e9119b5ac92a3f2da5f4ebeaea5e721c67c3d2a54b7bfe68803b77ee *bcafafafef47849006ddb4e82646813217090881d4cc8d021901ed6b6868f8e1b265cb7e3e2c80 *bb76eda2aeebfed3c0c0c07794fda39482710e9a14b7500ac5a3b98be8056e95ed5706bbaeaeee *9f1dc759b174e9d244373e11e0ae5dbb46f6f5f5fd7b1004b3952931eb2395f28709b145ecbaa9 *e28ee65e81737ea0a1a1e1daa54b9796d9c53280edededb4afafef9d2008669730c71818e7e009 *60930cb519a5e98ea902ac8e9ee7299073962c5952c264999929140abfd4c1e9b511fd3ca9e294 *0630a9c24408898f00e079de6cd7757f0960792a833b77eebcafbfbfff71bd4ea280e9451dc526 *ab854129213470dace525633f17d1f0d0d0df7dd72cb2d4f94016c6f6f6f3e7bf6ec678cb10633 *b9adb2feba27a3a7e92a89623108020461883002a67bd186a7d33772e4c88b962c59d253a262cf *f37e4a296dd0d5a897239252c4b5489c48620c2c0810180b4b673c62bdc1f3bc9f00b81d881cd6 *f6f6f656d7756f8dd3af3a7b5ac549011d4e0c4c0829eb471df5f9cd1883ebbacb5e79e595d698 *41cff31e228458716252abd3595aa7ba3a3dcf43c6b250152a21f133849012f3a433a7e62ce71c *524acbf3bc1f02584101c0f7fd365d157a09563735ba3a3ef8f043504ae1ba6ee53c4e048c68aa *658c95943b32d151b7b74110b401006b6d6d9de579de0635e76cdb866559254773ce1142f0daee *ddd8be7d3b265f7411c68d1b8762b19858bed081c68c2638b9a1e689034010048d9f7efae9afb9 *10e2b69839cd94e87344b1664eec8181013cf1c41398386912d6dd730f9aa3428efa4d25b55342 *60dbf620b8304426caf033c6e2125bb158fc0e0dc3f0ab3ab5ba19d11744996ba5cd9dce2347b0 *71e3463cb76d1b42213030305031534b0989992c2922191884105fa552cad1f1feaa81d3cd49da *9e1b0641497bb3a303b7af5c89bd7bf7220843f4f5f7c740cadc322921012491a3762829e5182a *84685613576f8abdc47430210885403108ca9ae7fb78fe8517b06ad52a7c72f8709cb1aab48074 *428c15decca5944d31f5caa0320692b243c481b810081332604ace9c3e8d87b66cc1942953b061 *e346148300f5757565c56c0550f733d50b08219a68d9fcd04a08496a55f755a6a05a3b74e81056 *7ef7bb78e1f9e7d1d3d35336964a279b5502f50c678c9da1948ed5e75a9203a027278141731044 *f99a4a22c210239b9a306dda348c1d3b367e495d28ca1761c4e669ce18eb01305677dd9326b632 *333ac04a2a1642c0b22c2c5fbe1ccb962d83e33825e18129a626010c02e49c7787061304286330 *0940b1584cbc4729455b5b1bd6ac5983969696448fa7acef84ad8f317692673299834110cc57ea *d319531dcb0874124813d8942953b061c306cc983123be6eb29fd48ff9516e1886c866b3bfe7d9 *6c7647a150b84f75a43f18771a4568fa80261ba3468dc2dd77df8d6f7ef39b3565bdca006ae180 *aad55896b5832e5cb8f02063eca859a730d59e26b66d63c58a15686f6fc775d75d17aff0a14868 *808bb6bca3d75e7bed410e008ee3fca7ebba2bf49841455f69aa696969c1fcf9f3b16edd3ae472 *b9125043f117f5149d8a5b8410b06dfb3f80c81fcc66b30f7b9e77731004b6f24a941db32c2bb1 *e365cb96a1bebebe0454b5b9962409796a1042bc6c36fb2320f2a8afb9e69a4febebeb7724c407 *830bc118941052024ebf3e14d103f820622f0ae6775c73cd359fc60001c0b6edef33c67af56046 *25d1875fe4aa0cbca404119d534a7b6ddb2efd1f7700686b6b3b5d5f5fbf390c43785afd42b55a *07ade539424859ffbeef430881fafafacd6d6d6df1c7b7251674e1c2854fd6d5d56d2f4609ee28 *031f7ff895387805a0959833fbf77d1fd96c76fbc2850b9fd49f2dcb2cd8b67da79472aaeffb73 *cdad4e4809cb88eac820925490bae8a508139ce3386fd9b67d67d94b26258ff6ecd933c2f7fd7f *f57dff4a15a7388e3318e4d836ac2834188aa805a0d74794662ccbda6f59d65f2d58b0a0ec23db *d4f4dbdebd7b491004cf140a853b1963b06d1bb66d7f112b1bb16c92e8b64d65b2f4624eb49d3d *c739bfebeaabaf1e5e9d64cf9e3ddf735df7efa5948d2a5c8cbf698df23366a63529b3aa5b86c8 *d6f53a8ef3c082050bb6561abfa614f01b6fbc312a0cc3477cdf5f298470f4cf96f50c17b4bddb *dc369511a694ba9665bdc0187b70fefcf97ffca7f2baecdbb7ef2221c4a62008160a2126ab4ddd *4ca49b15d0e8058e70ce7f47297decaaabaefaacd631875d27e9e8e8f88a10e26629e55785102d *52ca3142886600a094f61042ba29a5270821bfa794be3c6fdebcff1ece38ff0f8edc041b2aa64d *f20000000049454e44ae426082 addfile ./contrib/musicplayer/src/resources/images/play.svg hunk ./contrib/musicplayer/src/resources/images/play.svg 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + addfile ./contrib/musicplayer/src/resources/images/previous.png binary ./contrib/musicplayer/src/resources/images/previous.png oldhex * newhex *89504e470d0a1a0a0000000d494844520000000c0000000c080600000056755ce7000000197445 *5874536f6674776172650041646f626520496d616765526561647971c9653c0000006f49444154 *78da62642012ac5ab35e1f482d600132ea61826121818d381483d43480d82c3006143462331588 *0d60624c789c0032f502b262980d0c844c45064cc4988a53033100450334940ca0b610670350d3 *452036440b3dc24ec2651b0b2e9360b6012943e48863243569000418005c3728cdb5c9bd650000 *000049454e44ae426082 addfile ./contrib/musicplayer/src/resources/images/previous_active.png binary ./contrib/musicplayer/src/resources/images/previous_active.png oldhex * newhex *89504e470d0a1a0a0000000d494844520000000c0000000c080600000056755ce7000000197445 *5874536f6674776172650041646f626520496d616765526561647971c9653c0000005a49444154 *78da62602012fcffff5f1f88cf3302897a9820232363230ec520350d300e1ce03215450d2e0d20 *53ff6301181ab0998a53032e5391011303a980222791e56942c1ca028f102c001891178194214a *c4919a3400020c008f8b24f4b5e305de0000000049454e44ae426082 hunk ./contrib/musicplayer/src/services/albumCover.js 3 //#require //#require "services/lastfm.js" -//#require "libs/util/Goal.js" (function () { var lastfm = da.service.lastFm, hunk ./contrib/musicplayer/src/services/albumCover.js 6 - Goal = da.util.Goal, - Artists = da.db.DEFAULT.views.Artist.view; + Artists = da.db.DocumentTemplate.Artist.view(); function fetchAlbumCover(search_params, album, callback) { lastfm.album.getInfo(search_params, { hunk ./contrib/musicplayer/src/services/albumCover.js 12 success: function (data) { var urls = data.album.image ? data.album.image : null, - n = urls.length; + n = urls.length, + url; hunk ./contrib/musicplayer/src/services/albumCover.js 15 - while(n--) - urls[n] = urls[n]["#text"]; + while(n--) { + url = urls[n]["#text"]; + if(!url || !url.length) + url = "resources/images/album_cover_" + n + ".png"; + + urls[n] = url; + } hunk ./contrib/musicplayer/src/services/albumCover.js 23 - album.set({ + album.update({ album_cover_urls: urls, lastfm_id: data.album.id, mbid: data.album.mbid.length ? data.album.mbid : null hunk ./contrib/musicplayer/src/services/albumCover.js 28 }); - album.save(); // fun fact: typeof /.?/ === "function" hunk ./contrib/musicplayer/src/services/albumCover.js 30 - if(urls && typeof callback === "function") + if(urls && callback) callback(urls); hunk ./contrib/musicplayer/src/services/albumCover.js 32 + + delete callback; + delete urls; + delete data; + delete search_params; + }, + failure: function () { + if(!callback) + return; + + callback([ + "resources/images/album_cover_0.png", + "resources/images/album_cover_1.png", + "resources/images/album_cover_2.png", + "resources/images/album_cover_3.png" + ]); + + delete callback; } }); } hunk ./contrib/musicplayer/src/services/albumCover.js 58 * da.service.albumCover(song[, callback]) -> undefined * - song (da.db.DocumentTemplate): song whose album art needs to be fetched * - callback (Function): called once album cover is fetched, with first - * argument being the URL of the album cover. - * + * argument being an array of four URLs. + * * #### Notes hunk ./contrib/musicplayer/src/services/albumCover.js 61 - * The URL will be saved to the `song` under 'cover_art' propety. + * Fetched URLs will be saved to the `song` under 'album_cover_urls' propety. **/ da.service.albumCover = function (album, callback) { var search_params = {}; hunk ./contrib/musicplayer/src/services/albumCover.js 65 - if(album.get("lastfm_id")) - search_params.id = album.get("lastfm_id"); - else if(album.get("mbid")) + if(album.get("mbid")) search_params.mbid = album.get("mbid"); hunk ./contrib/musicplayer/src/services/albumCover.js 68 - if(!search_params.id && !search_params.mbid) + if(!search_params.mbid) search_params = { album: album.get("title"), artist: Artists.getRow(album.get("artist_id")).title hunk ./contrib/musicplayer/src/workers/indexer.js 35 queue = []; this.da = {}; -importScripts("env.js"); hunk ./contrib/musicplayer/src/workers/indexer.js 36 +//#require "libs/vendor/mootools-1.2.4-core-server.js" +//#require "libs/vendor/mootools-1.2.4-request.js" +//#require +//#require "libs/util/BinaryFile.js" +//#require "libs/util/ID3.js" + +var ID3 = da.util.ID3; /** * Indexer.onMessage(event) -> undefined * - event (Event): DOM event. hunk ./contrib/musicplayer/src/workers/indexer.js 59 function getTags(cap) { if(!cap) return false; - var parser = new da.util.ID3({ + var parser = new ID3({ url: "/uri/" + encodeURIComponent(cap), onSuccess: function (tags) { hunk ./contrib/musicplayer/src/workers/indexer.js 88 var finish_timeout; function checkQueue() { if(!queue.length) - finish_timeout = setTimeout(finish, 30*60*1000); + finish_timeout = setTimeout(finish, 3*60*1000); else { clearTimeout(finish_timeout); setTimeout(function () { hunk ./contrib/musicplayer/src/workers/indexer.js 92 - getTags(queue.shift()); + getTags(queue[0]); }, 444); } } hunk ./contrib/musicplayer/src/workers/scanner.js 11 var window = this, document = {}, - queue = 0; + queue = []; this.da = {}; hunk ./contrib/musicplayer/src/workers/scanner.js 14 -importScripts("env.js"); + +//#require "libs/vendor/mootools-1.2.4-core-server.js" +//#require "libs/vendor/mootools-1.2.4-request.js" +//#require "libs/TahoeObject.js" + +var TahoeObject = da.util.TahoeObject; /** * Scanner.scan(object) -> undefined hunk ./contrib/musicplayer/src/workers/scanner.js 25 * - object (TahoeObject): an Tahoe object. * - * Traverses the `object` until it finds a file, whose cap is then reported to main thread via `postMessage`. + * Traverses the `object` until it finds a file. + * File's cap is then reported to the main thread via `postMessage`. + * **/ function scan (obj) { hunk ./contrib/musicplayer/src/workers/scanner.js 30 - queue++; obj.get(function () { hunk ./contrib/musicplayer/src/workers/scanner.js 31 + queue.erase(obj.uri); + if(obj.type === "filenode") { hunk ./contrib/musicplayer/src/workers/scanner.js 34 - return postMessage(obj.uri); + postMessage(obj.uri); + checkQueue(); + return; } var n = obj.children.length; hunk ./contrib/musicplayer/src/workers/scanner.js 49 scan(child); } - if(!--queue) - postMessage("**FINISHED**"); + checkQueue(); }); } hunk ./contrib/musicplayer/src/workers/scanner.js 53 +function checkQueue() { + if(queue.length) + setTimeout(function() { + scan(new TahoeObject(queue.shift())) + }, 444); + else + postMessage("**FINISHED**"); +} + /** * Scanner.onmessage(event) -> undefined * - event.data (String): Tahoe cap pointing to root directory from which scanning should begin. hunk ./contrib/musicplayer/src/workers/scanner.js 67 **/ onmessage = function (event) { - scan(new TahoeObject(event.data)); + queue.push(event.data); + + if(queue.length < 2) + scan(new TahoeObject(event.data)); }; hunk ./contrib/musicplayer/tests/initialize.js 20 'test_NavigationController', 'test_Dialog', 'test_SettingsController', + 'test_ProgressBar', hunk ./contrib/musicplayer/tests/initialize.js 22 - 'test_SegmentedProgressBar' + 'test_SegmentedProgressBar', + 'test_PlayerController' ]); hunk ./contrib/musicplayer/tests/test_NavigationController.js 34 this.test_navigationBehaviour = [ {"params": {"xpath": "//div[@id='Artists_column_container']/div/div[2]/a[2]/span"}, "method": "click"}, - {"params": {"xpath": "//div[@id='Albums_column_container']/div/div[2]/a/span"}, - "method": "click"}, - {"params": {"xpath": "//div[@id='Albums_column_container']/a/span", "validator": "Albums"}, - "method": "asserts.assertText"} + {"params": {"xpath": "//div[@id='Albums_column']/div[2]/a[@title='Urgency']"}, + "method": "click"} ]; this.test_activeColumns = function () { addfile ./contrib/musicplayer/tests/test_PlayerController.js hunk ./contrib/musicplayer/tests/test_PlayerController.js 1 +var test_PlayerController = new function () { + var Song = da.db.DocumentTemplate.Song, + // Songs column was set as only one in test_NavigationController + Songs = null, + Player = da.controller.Player, + self = this; + + this.setup = function () { + Songs = da.controller.Navigation.activeColumns[1].column; + + self.songs = [ + Song.findById(Songs.getItem(0).id), + Song.findById(Songs.getItem(1).id), + Song.findById(Songs.getItem(2).id) + ]; + }; + + this.test_play = function () { + jum.assertTrue("Should return 'undefined if nothing is playing", + !Player.nowPlaying() + ); + + var play_event_fired = false; + function test_playEvent (song) { + jum.assertEquals("first argument to callback function should be playing song", + self.songs[0].id, + song.id + ); + + Player.removeEvent("play", test_PlayEvent); + delete test_playEvent; + } + + Player.addEvent("play", test_playEvent); + Player.play(self.songs[0]); + + jum.assertTrue("'play' event should have been fired", + play_event_fired + ); + }; + + this.test_getNext = function () { + jum.assertTrue("there shouldn't be a next song, since there is no playlist", + !Player.getNext() + ); + + var playlist = self.songs.map(function (s) { return s.id }); + Player.setPlaylist(playlist); + Player.play(self.songs[1]); + + jum.assertEquals("next song in playlist should be returned", + playlist[2], + Player.getNext() + ); + }; + + this.test_getPrev = function () { + jum.assertEquals("previous song in playlist should have been returned", + self.songs[0].id, + Player.getPrev() + ); + + Player.play(self.songs[2]); + jum.assertEquals("previous song in playlist should have been returned", + self.songs[1].id, + Player.getPrev() + ); + }; + + this.test_queue = function () { + jum.assertTrue("there shouldn't be anything left in the playlist", + !Player.getNext() + ); + + Player.queue(self.songs[0].id); + + jum.assertEquals("next song should be from queue", + self.songs[0].id, + Player.getNext() + ); + }; + + return this; +}; hunk ./contrib/musicplayer/tests/test_ProgressBar.js 27 // in order to fix browser inconsistencies. this.test_incrementation = function () { self.pb.setProgress(0.5); - - jum.assertEqualArrays(BLACK, getPixel(1)); - jum.assertEqualArrays(BLACK, getPixel(49)); + + jum.assertEqualArrays(BLACK, getPixel( 1)); + jum.assertEqualArrays(BLACK, getPixel( 49)); jum.assertEqualArrays(TRANSPARENT, getPixel(100)); self.pb.options.foreground = "rgba(255, 0, 0, 255)"; hunk ./contrib/musicplayer/tests/test_ProgressBar.js 35 self.pb.setProgress(0.7); - jum.assertEqualArrays(BLACK, getPixel(1)); - jum.assertEqualArrays(RED, getPixel(52)); - jum.assertEqualArrays(RED, getPixel(69)); - jum.assertEqualArrays(TRANSPARENT, getPixel(100)); + jun.assertEqualArrays(BLACK, getPixel( 1)); + jun.assertEqualArrays(RED, getPixel( 52)); + jun.assertEqualArrays(RED, getPixel( 69)); + jun.assertEqualArrays(TRANSPARENT, getPixel(100)); }; this.test_decrementation = function () { hunk ./contrib/musicplayer/tests/test_ProgressBar.js 46 self.pb.setProgress(0.6); jum.assertEqualArrays(RED, getPixel(52)); - jum.assertEqualArrays(RED, getPixel(58)); + jum.assertEqualArrays(RED, getPixel(55)); jum.assertEqualArrays(TRANSPARENT, getPixel(60)); jum.assertEqualArrays(TRANSPARENT, getPixel(70)); }; hunk ./contrib/musicplayer/tests/test_ProgressBar.js 52 this.teardown = function () { - //this.pb.destroy(); + this.pb.destroy(); }; return this; hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 24 } this.test_incrementation = function () { - var ctx = self.pb.ctx; - self.pb.setProgress("g", 0.6); self.pb.setProgress("r", 0.3); hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 26 - self.pb.setProgress("b", 1); + self.pb.setProgress("b", 1); hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 28 - jum.assertEqualArrays(RED, getPixel(2)); + jum.assertEqualArrays(RED, getPixel( 2)); jum.assertEqualArrays(RED, getPixel(20)); jum.assertEqualArrays(RED, getPixel(29)); hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 44 this.test_incrementingMiddleSegment = function () { self.pb.setProgress("g", 0.8); - jum.assertEqualArrays(RED, getPixel(2)); + jum.assertEqualArrays(RED, getPixel( 2)); jum.assertEqualArrays(RED, getPixel(29)); jum.assertEqualArrays(GREEN, getPixel(31)); hunk ./contrib/musicplayer/tests/test_SegmentedProgressBar.js 57 this.test_incrementingFirstSegment = function () { self.pb.setProgress("r", 0.9); - jum.assertEqualArrays(RED, getPixel(2)); + jum.assertEqualArrays(RED, getPixel( 2)); jum.assertEqualArrays(RED, getPixel(66)); jum.assertEqualArrays(RED, getPixel(79)); hunk ./src/allmydata/test/test_musicplayer.py 107 return d -class ChromeTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Chrome): - pass - -#class FirefoxTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Firefox): +#class ChromeTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Chrome): # pass hunk ./src/allmydata/test/test_musicplayer.py 109 + +class FirefoxTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Firefox): + pass } [updated-docs josip.lisec@gmail.com**20100731231211 Ignore-this: cbfb98d6ebeed2f2826250eb43d912ff * Added NOTES file with instructions for running the tests. * Fixed typos in INSTALL ] { hunk ./contrib/musicplayer/INSTALL 26 (And if you're one of those who prefer to do it by-hand (and keyboard), this file isn't a place for you.) -== Battle for Configuration File == +== 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 )! addfile ./contrib/musicplayer/NOTES hunk ./contrib/musicplayer/NOTES 1 +=== Running the tests === +== Prerequsites == += Windmill= +In order to run the tests make sure you have the latest Windmill[1][2] +(as of July 30th 2010 it means you'll have to build it from the trunk because +it contains a crutial fix) + +You can obtain the latest copy of Windmill by running the following commands +from your shell: + $ cd ~/bin + $ git pull git://github.com/windmill/windmill + $ python setup.py build + $ sudo python setup.py install + += Working browser = +You'll also need a working browser, the tests are intented to be ran in +Google Chrome (not Chromium) and/or Firefox. + +By default Google Chrome will be used. + +If you do not have Google Chrome installed on your computer, to use Firefox +you'll have to edit the src/allmydata/tests/test_musicplayer.py file. + +== Building the project == +Components needed by da.controller.Player and da.controller.CollectionScanner tests +are only available after we've built the from project. (Web workers and SoundManger's Flash files). + + $ cd + $ python manage.py build + +== Running the tests == +To run the music player-specific tests simply: + + $ cd + $ python setup.py test -s allmydata.test.test_musicplayer + +If you have any Google Chrome winows open, remember to close them before +running the tests, otherwise Windmill won't be able to work its magick. + +== Problematic tests == +* test_ProgressBar, test_SegmentedProgressBar: these might or might not fail, + the reason is that browsers don't render the graphics precisely the same. + They will probably (partially) fail in Firefox. +* test_ID3v1: This test usually fails on Linux version of Google Chrome. + Reasons are still unknown. + +== Debugging tips == +* Chrome is definitely faster, but Firefox gives better error messages. + To view more detailed messages than those presented by Windmill type + 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 +lovely web site for easier navigation. + +To generate the API documentation you'll need latest version of PDoc: + + $ sudo gem install pdoc + +After you've installed PDoc just run: + + $ cd + $ python manage.py docs + $ open-in-web-browser docs/index.html + +Note: You will be probably greeted with few 'missing dependencies' errors +from PDoc, just install them manually and you're good to go. + +[1] http://getwindmill.com - Windmill +[2] http://github.com/windmill - Windmill's Github repository +[3] http://pdoc.org/ - PDoc's website } [added-songcontext-and-services josip.lisec@gmail.com**20100731231316 Ignore-this: e8cd467b7d5681d1b9fbf9decb8b1f4b * Added da.controller.SongContext with default contexts: * 'Artist' - shows basic information about the artist the currently playing song, * 'Recommendations' - shows similar artists and songs, * 'Music Videos' - presents search results from YouTube of currently playing song. * Added da.service.* APIs which are used by contexts above * da.service.lastFm - interface to the Last.fm services * da.service.artistInfo * da.service.recommendations * da.service.musicVideo * UTF-8 related fixes to ID3v2 parser ] { hunk ./contrib/musicplayer/manage.py 12 CLOSURE_COMPILER_PATH = 'tools/closure-compiler-20100514/compiler.jar' +class ClosureCompiler: + def __init__(self, input_files, output, warnings = 'QUIET'): + self.input = input_files + self.output = output + self.warnings = warnings + + def compile(self, compression = 'WHITESPACE'): + print 'Compressing %s...' % self.output + + if compression == 'NONE': + output_file = open(self.output, 'a') + for filename in self.input: + f = open(filename) + output_file.write(f.read()) + output_file.write('\n') + f.close() + + output_file.close() + else: + args = [ + 'java', + '-jar', CLOSURE_COMPILER_PATH, + '--warning_level', self.warnings, + '--compilation_level', compression, + '--js_output_file', self.output] + + for filename in self.input: + args.append('--js') + args.append(filename) + + subprocess.call(args) + + def syntax_check(self): + args = [ + 'java', + '-jar', CLOSURE_COMPILER_PATH, + '--js_output_file', '_tmp.out', + '--dev_mode', 'START_AND_END'] + + for filename in self.input: + args.append('--js') + args.append(filename) + + r = subprocess.call(args) + os.remove('_tmp.out') + return r == 0 + class JSDepsBuilder: """ Looks for hunk ./contrib/musicplayer/manage.py 80 for (dirname, dirs, files) in os.walk(self.root): for filename in files: if filename.endswith('.js'): - self.detect_requires(os.path.join(dirname, filename)) + self.detect_requires(os.path.join(dirname, filename)) def detect_requires(self, path): reqs = [] hunk ./contrib/musicplayer/manage.py 95 if not os.path.isdir(req_path) and not req_path.endswith('.js'): reqs[i] += '.js' - #if len(reqs): - # print '%s depends on:' % os.path.basename(path) - # print '\t', '\n\t'.join(reqs) - self.files[path] = reqs hunk ./contrib/musicplayer/manage.py 97 - def parse(self, path): + def parse(self, path, syntax_check = False): if path in self.included: return '' if not path.endswith('.js'): hunk ./contrib/musicplayer/manage.py 104 # TODO: If path points to a directory, require all the files within that directory. return '' + if syntax_check: + compiler = ClosureCompiler([path], None) + if not compiler.syntax_check(): + raise Exception('There seems to be a syntax problem. Fix it.') + def insert_code(match): req_path = os.path.join(self.root, match.group(1)) if not req_path in self.included: hunk ./contrib/musicplayer/manage.py 115 if not os.path.isfile(req_path): raise Exception('%s requires non existing file: %s' % (path, req_path)) - return self.parse(req_path) + return self.parse(req_path, syntax_check) script_file = open(path, 'r') script = script_file.read() hunk ./contrib/musicplayer/manage.py 161 shutil.rmtree('build') shutil.copytree('src/resources', 'build/resources') - shutil.copytree('src/plugins', 'build/plugins') shutil.copy('src/config.example.json', 'build/') shutil.copy('src/index.html', 'build/') hunk ./contrib/musicplayer/manage.py 176 self._make_js('Application.js', 'build/js/app.js') - #deps.write_to_file('build/app.js') - #self._compress('build/js/app.js', ['build/app.js']) - #os.remove('build/app.js') - for worker in os.listdir('src/workers'): if worker.endswith('.js'): self._make_js('workers/' + worker, 'build/js/workers/' + worker) hunk ./contrib/musicplayer/manage.py 179 - #deps.write_to_file('build/worker_' + worker, root_file = 'workers/' + worker) print 'Done!' hunk ./contrib/musicplayer/manage.py 182 - #def _compile_js(self, source, output_file, files = None, join = True): - # js_files = files - # if not js_files: - # js_files = [] - # for filename in os.listdir(source): - # if filename.endswith('.js'): - # js_files.append(os.path.join(source, filename)) - # - # js_files.sort() - # else: - # js_files = [os.path.join(source, path) for path in files] - # - # if join: - # self._compress(output_file, js_files) - # else: - # for js_file in js_files: - # self._compress(output_file + os.path.basename(js_file), [js_file]) - def _make_js(self, root, output): tmp_file = mkstemp()[1] self.deps.write_to_file(tmp_file, root_file = root) hunk ./contrib/musicplayer/manage.py 185 - self._compress(output, [tmp_file]) + compiler = ClosureCompiler([tmp_file], output) + compiler.compile(self.compilation_level) os.remove(tmp_file) hunk ./contrib/musicplayer/manage.py 188 - - def _compress(self, output_file, files): - print 'Compressing %s...' % output_file - - if self.compilation_level == 'NONE': - output_file = open(output_file, 'a') - for filename in files: - f = open(filename) - output_file.write(f.read()) - output_file.write('\n') - f.close() - - output_file.close() - else: - args = [ - 'java', - '-jar', CLOSURE_COMPILER_PATH, - '--warning_level', 'QUIET', - '--compilation_level', self.compilation_level, - '--js_output_file', output_file] - - for filename in files: - args.append('--js') - args.append(filename) - - subprocess.call(args) class Watch(Build): description = 'watches src directory for changes and runs build command when they occur' hunk ./contrib/musicplayer/manage.py 283 pass def run(self): - args = [ - 'java', - '-jar', CLOSURE_COMPILER_PATH, - '--js_output_file', '_tmp.out', - '--dev_mode', 'START_AND_END'] - + test_files = [] for filename in os.listdir('tests'): if filename.endswith('.js'): hunk ./contrib/musicplayer/manage.py 286 - args.append('--js') - args.append('tests/' + filename) + test_files.append('tests/' + filename) hunk ./contrib/musicplayer/manage.py 288 - r = subprocess.call(args) - if r == 0: - print 'Everything seems to be okay! Yay!' - - os.remove('_tmp.out') + compiler = ClosureCompiler(test_files, None) + if compiler.syntax_check(): + print 'Everything seems okay. Yay!' setup( name = 'tahoe-music-player', hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 75 this._goal.checkpoint("scanner"); return; } + if(cap.debug) { + console.log("SCANNER", cap.msg, cap.obj); + return; + } if(da.db.DEFAULT.views.Song.view.findRow(cap) === -1) this.indexer.postMessage(cap); hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 90 return; } + if(event.data.debug) { + console.log("INDEXER", event.data.msg, event.data.obj); + return; + } + // Lots of async stuff is going on, a short summary would look something like: // 1. find or create artist with given name and save its id // to artist_id. hunk ./contrib/musicplayer/src/controllers/Player.js 65 }); da.app.addEvent("ready", this.initializeUI.bind(this)); + + this.preloadImages() }, initializeUI: function () { hunk ./contrib/musicplayer/src/controllers/Player.js 179 **/ play: function (song) { var np = this.nowPlaying; - if(!song || (song && np.song && np.song.id === song.id)) + if(song && np.song && np.song.id === song.id) return; this.elements.play.addClass("active"); hunk ./contrib/musicplayer/src/controllers/Player.js 184 + if(!song && np.sound.paused) { + np.sound.resume(); + return; + } + if(this.sounds[song.id]) { np.sound.stop(); this.sounds[song.id].play(); hunk ./contrib/musicplayer/src/controllers/Player.js 209 onload: function () { this._loading.remove(song.id); + if(!song.get("duration")) song.update({duration: this.duration}); }, hunk ./contrib/musicplayer/src/controllers/Player.js 224 }, whileplaying: function () { - if(Player.nowPlaying.sound === this) - Player.progress_bar.setProgress("track", this.position/this.durationEstimate); + if(Player.nowPlaying.sound === this) { + Player.progress_bar.setProgress("track", this.position / this.durationEstimate); + Player.progress_bar.toElement().title = this.position + "/" + this.durationEstimate; + } }, onfinish: function () { hunk ./contrib/musicplayer/src/controllers/Player.js 338 this.play(song); if(this.nowPlaying.sound.paused) { - this.nowPlaying.sound.resume(); + this.play(); } else this.pause(); }, hunk ./contrib/musicplayer/src/controllers/Player.js 422 }, /** + * Player#preloadImages() -> undefined + **/ + preloadImages: function () { + var images = [ + "next", "next_active", "previous", "previous_active", "play", + "album_cover_1", "album_cover_2", "album_cover_3" + ], + n = images.length, + i = null; + + while(n--) { + i = new Image(); + i.src = "resources/images/" + images[n] + ".png"; + } + + delete images; + delete n; + }, + + /** * Player#free() -> undefined * * Frees memory taken by loaded songs. This method is ran about every hunk ./contrib/musicplayer/src/controllers/Player.js 460 sound = this.sounds[id]; if(this.sounds.hasOwnProperty(id) && this.nowPlaying.song.id !== id - && ( - sound._last_played < eight_mins_ago || !sound.loaded - )) + && (sound._last_played >= eight_mins_ago || !sound.loaded)) { hunk ./contrib/musicplayer/src/controllers/Player.js 462 + console.log("Freed sound ", id, sound._last_played); sound.destruct(); delete this.sounds[id]; } addfile ./contrib/musicplayer/src/controllers/SongContext.js hunk ./contrib/musicplayer/src/controllers/SongContext.js 1 +//#require "controllers/Player.js" + +(function () { +var Song = da.db.DocumentTemplate.Song, + Player = da.controller.Player; + +var CONTEXTS = {}, + TAB_SUFFIX = "_context_tab", + _TS_L = -TAB_SUFFIX.length, + TAB_CONTAINER_SUFFIX = "_context_tab_container"; + +/** section: Controllers + * SongContext + **/ +var SongContext = { + /** + * SongContext.active -> Object + **/ + active: null, + + initialize: function () { + this.el = new Element("div", { + id: "song_context" + }); + this.tabs = new Element("div", { + id: "context_tabs", + "class": "no_selection" + }); + + for(var id in CONTEXTS) + this.tabs.grab(new Element("a", { + id: id + TAB_SUFFIX, + "class": "tab", + href: "#", + html: CONTEXTS[id].title + })); + this.tabs.addEvent("click:relay(.tab)", function () { + SongContext.show(this.id.slice(0, _TS_L)); + }); + + this.loading_screen = new Element("div", { + id: "song_context_loading", + html: "Loading...", + "class": "no_selection", + style: "display: none" + }); + + $("player_pane").adopt(this.tabs, this.loading_screen, this.el); + + Player.addEvent("play", function (song) { + if(SongContext.active) + SongContext.active.update(song); + }); + + window.addEvent("resize", function () { + SongContext.el.style.height = ( + window.getHeight() - $("song_info_block").getHeight() - SongContext.tabs.getHeight() + ) + "px"; + }); + window.fireEvent("resize"); + this.initialized = true; + }, + + /** + * SongContext#show(id) -> undefined + * - id (String): id of the context. + **/ + show: function (id) { + this.hide(); + + var context = CONTEXTS[id]; + if(!context.__initialized) { + var container = new Element("div", {id: id + TAB_CONTAINER_SUFFIX}); + context.initialize(container); + container.hide(); + this.el.grab(container); + + delete container; + context.__initialized = true; + } + + $(id + TAB_SUFFIX).addClass("active"); + this.active = context; + + da.controller.SongContext.showLoadingScreen(); + $(id + TAB_CONTAINER_SUFFIX).show(); + context.show(); + context.update(Player.nowPlaying()); + }, + + /** + * SongContext#hide() -> undefined + * + * Hides active tab. + * + **/ + hide: function () { + if(!this.active) + return; + + this.active.hide(); + $(this.active.id + TAB_CONTAINER_SUFFIX).hide(); + $(this.active.id + TAB_SUFFIX).removeClass("active"); + }, + + /** + * SongContext#addTab(id) -> undefined + * - id (String): id of the context. + * + **/ + addTab: function (id) { + this.tabs.grab(new Element("a", { + id: id + TAB_SUFFIX, + "class": "tab", + href: "#", + html: CONTEXTS[id].title + })); + } +}; + +da.app.addEvent("ready", function () { + SongContext.initialize(); +}); + +/** + * da.controller.SongContext + **/ +da.controller.SongContext = { + /** + * da.controller.SongContext.register(context) -> undefined + * - context.id (String): id of the context. Note that the "root" element of + * the context also has to have the same id. + * - context.title (String): human-frendly name of the context. + * - context.initialize (Function): function called only once, with the container + * element as the first argument. All DOM nodes should be added to that element. + * - context.show (Function): called every time context's tab is activated. + * - context.hide (Function): called when context's tab gets hidden. + * - context.update (Function): called when another song starts playing. + * The first argument of the function is the new song ([[da.db.DocumentTemplate.Song]]). + * + * #### Example + * da.controller.SongContext.register({ + * id: "artist-info", + * initialize: function (container) { + * this.el = new Element("div", {id: "artist-info", html: "Hai world!"}); + * this._shown = false; + * // Try to limit youself by putting all needed nodes into + * // container element. + * container.grab(this.el); + * + * }, + * show: function () { + * // Called everytime this tab is activated. + * if(!this._shown) { + * this.el.position({relativeTo: this.el.parent()}); + * this._shown = true; + * } + * }, + * hide: function () { + * // Called when tab is hidden, use this to stop updating document + * // nodes, etc. + * }, + * update: function (song) { + * // Called when new song starts playing. + * } + * })); + * + * #### Notes + * When the context is activated for the first time, functions all called in + * following order: + * * `initialize(container)` + * * `show` + * * `update(song)` + * + * `show` and `hide` methods should not implement hiding of the "root" element, + * rather, adding/removing obsolete events and/or start/stop updating nodes. + * + **/ + register: function (context) { + if(CONTEXTS[context.id]) + return; + + CONTEXTS[context.id] = context; + + if(SongContext.initialized) + SongContext.addTab(context.id); + }, + + /** + * da.controller.SongContext.show(id) -> undefined + * - id (String): id of the tab/context. + **/ + show: function (id) { + var active = SongContext.active; + if((active && active.id === id) || !CONTEXTS[id]) + return; + + delete active; + SongContext.show(id); + }, + + /** + * da.controller.SongContext.showLoadingScreen() -> undefined + **/ + showLoadingScreen: function () { + var screen = SongContext.loading_screen, + el = SongContext.el; + screen.style.width = el.getWidth() + "px"; + screen.style.height = el.getHeight() + "px"; + screen.show(); + + delete screen; + delete el; + }, + + /** + * da.controller.SongContext.hideLoadingScreen() -> undefined + **/ + hideLoadingScreen: function () { + SongContext.loading_screen.hide(); + } +}; + +da.app.fireEvent("ready.controller.SongContext", [], 1); + +})(); + +//#require "controllers/default_contexts.js" hunk ./contrib/musicplayer/src/controllers/controllers.js 13 if(typeof da.controller === "undefined") da.controller = {}; +//#require "controllers/Settings.js" //#require "controllers/Navigation.js" //#require "controllers/Player.js" hunk ./contrib/musicplayer/src/controllers/controllers.js 16 -//#require "controllers/Settings.js" +//#require "controllers/SongContext.js" //#require "controllers/CollectionScanner.js" hunk ./contrib/musicplayer/src/controllers/default_columns.js 168 initialize: function (options) { this.parent(options); - this._el.style.width = "300px"; - this._playlist = []; - this.addEvent("click", function (item, event, el) { da.controller.Player.setPlaylist(this._playlist); da.controller.Player.play(Song.findById(item.id)); hunk ./contrib/musicplayer/src/controllers/default_columns.js 171 - }); + }.bind(this), true); }, view: { hunk ./contrib/musicplayer/src/controllers/default_columns.js 187 } }, - mapReduceUpdated: function (result) { - this.parent(result); - + mapReduceFinished: function (values) { + this.parent(values); + this.createPlaylist(); + }, + + mapReduceUpdated: function (values) { + this.parent(values); + this.createPlaylist(); + }, + + createPlaylist: function () { var n = this.options.totalCount, playlist = new Array(n); hunk ./contrib/musicplayer/src/controllers/default_columns.js 202 while(n--) - playlist = this._rows[n].id; + playlist[n] = this._rows[n].id; this._playlist = playlist; delete playlist; addfile ./contrib/musicplayer/src/controllers/default_contexts.js hunk ./contrib/musicplayer/src/controllers/default_contexts.js 1 +//#require "services/artistInfo.js" +//#require "services/recommendations.js" +//#require "services/musicVideo.js" +//#require "libs/ui/Dialog.js" +//#require "doctemplates/Song.js" +//#require "controllers/Player.js" + +(function () { +var SongContext = da.controller.SongContext, + Song = da.db.DocumentTemplate.Song, + Player = da.controller.Player, + Dialog = da.ui.Dialog, + fetchArtistInfo = da.service.artistInfo, + fetchRecommendations = da.service.recommendations; + +SongContext.register({ + id: "artist_info", + title: "Artist", + + initialize: function (container) { + this.el = container; + var els = { + photo_wrapper: new Element("div", {id: "artist_photo_wrapper"}), + photo: new Element("img", {id: "artist_photo"}), + photo_chooser: new Element("div", {id: "artist_photo_chooser"}), + zoomed_photo: new Element("img", {id: "artist_photo_zoomed"}), + bio: new Element("div", {id: "artist_bio"}), + stats: new Element("div", {id: "artist_stats"}), + top_songs: new Element("ol", {id: "artist_top_tracks", "class": "context_column"}), + top_albums: new Element("ol", {id: "artist_top_albums", "class": "context_column middle_context_column"}), + events: new Element("ul", {id: "artist_events", "class": "context_column"}) + }; + + var clear = new Element("div", {"class": "clear"}); + + els.stats.adopt(els.top_songs, els.top_albums, els.events, clear); + els.photo_wrapper.adopt(els.photo, els.photo_chooser); + this.el.adopt(els.photo_wrapper, els.bio, clear.clone(), els.stats); + + els.photo_chooser.addEvent("click:relay(a)", function (event) { + var index = event.target.retrieve("photo_index"); + if(typeof index === "number") + this.switchPhoto(index); + }.bind(this)); + + this.photo_zoom = new Dialog({ + html: els.zoomed_photo + }); + + els.photo.addEvent("click", function (event) { + this.elements.zoomed_photo.src = this.active_photo.original; + this.photo_zoom.show(); + }.bind(this)); + + els.top_songs.addEvent("click:relay(li)", function (event) { + var index; + if(event.target.nodeName.toLowerCase() === "a") + index = event.target.parentNode.retrieve("list_position") + else + index = event.target.retrieve("list_position"); + + if(typeof index !== "number") + return; + + var song_data = this.artist_info.top_tracks[index]; + // TODO: Make an API for this type of things, it should also + // update the navigation columns to show the + song_data.artist_id = this._current_artist.id; + Song.findFirst({ + properties: song_data, + onSuccess: function (song) { + Player.play(song); + } + }); + + }.bind(this)); + + this.elements = els; + this._current_artist = {id: null}; + delete els; + delete clear; + }, + + show: $empty, + hide: $empty, + + update: function (song) { + song.get("artist", function (artist) { + if(this._current_artist.id === artist.id) + return !!SongContext.hideLoadingScreen(); + + this._current_artist = artist; + fetchArtistInfo(artist, function (info) { + this.artist_info = info; + var els = this.elements; + + els.bio.innerHTML = info.bio.summary; + els.photo.title = artist.get("title"); + this.switchPhoto(0); + + this.updatePhotoChooser(); + this.updateLists(); + + SongContext.hideLoadingScreen(); + + delete els; + }.bind(this)); + }.bind(this)); + }, + + updatePhotoChooser: function () { + var pc = this.elements.photo_chooser, + info = this.artist_info, + photos = info.photos, + n = photos.length, + bullets = new Array(n); + + pc.dispose(); + pc.empty(); + + while(n--) + bullets[n] = (new Element("a", { + html: "•", + href: "#" + })).store("photo_index", n); + + bullets.push(new Element("a", { + html: "+", + href: info.more_photos_url, + title: "More photos of " + this._current_artist.get("title"), + target: "_blank" + })); + + pc.adopt(bullets); + this.elements.photo_wrapper.grab(pc); + + delete pc; + delete bullets; + delete photos; + delete info; + }, + + updateLists: function () { + var els = this.elements, + info = this.artist_info, + events = info.events, + n; + + this.renderList("Top Songs", $A(info.top_tracks || []), els.top_songs); + this.renderList("Top Albums", $A(info.top_albums || []), els.top_albums); + this.renderList("Events", $A(info.events || []), els.events); + + var max_height = Math.max( + els.top_songs.getHeight(), + els.top_albums.getHeight(), + els.events.getHeight() + ); + els.top_albums.style.height = max_height + "px"; + + delete els; + delete info; + delete events; + }, + + renderList: function (title, items, el) { + var n = items.length; + if(!n) { + el.empty(); + return; + } + + var item; + while(n--) { + item = items[n]; + items[n] = (new Element("li")) + .store("list_position", n) + .grab(new Element("a", { + html: item.title, + title: item.title, + href: item.url ? item.url : "#", + target: item.url ? "_blank" : "" + })); + } + + el.empty().adopt(items); + (new Element("li", { + "class": "title", + "html": title + })).inject(el, "top"); + + delete el; + delete items; + delete item; + }, + + switchPhoto: function (n) { + this.active_photo = this.artist_info.photos[n]; + this.elements.photo.src = this.active_photo.extralarge; + } +}); + +SongContext.register({ + id: "recommendations", + title: "Recommendations", + + initialize: function (container) { + this.el = container; + var els = { + artists_title: new Element("h4", {html: "Artist you might like"}), + artists: new Element("div", {id: "recommended_artists"}), + songs_title: new Element("h4", {html: "Songs you should check out"}), + songs: new Element("ul", {id: "recommended_songs", "class": "context_column"}) + }; + + this.el.adopt(els.artists_title, els.artists, els.songs_title, els.songs); + this.elements = els; + delete els; + }, + + show: $empty, + hide: $empty, + + update: function (song) { + fetchRecommendations(song, function (rec) { + this.updateArtists($A(rec.artists || [])); + this.updateSongs($A(rec.songs || [])); + delete rec; + SongContext.hideLoadingScreen(); + }.bind(this)); + }, + + updateArtists: function (recommendations) { + this.elements.artists.empty(); + if(!recommendations.length) + return !!this.elements.artists_title.hide(); + else + this.elements.artists_title.show(); + + recommendations = recommendations.slice(0, 5); + var n = recommendations.length, rec; + while(n--) { + rec = recommendations[n]; + recommendations[n] = (new Element("a", {href: "#"})) + .grab(new Element("img", { + src: rec.image, + title: rec.title + })) + .appendText(rec.title); + } + + this.elements.artists.adopt(recommendations); + delete recommendations; + delete rec; + }, + + updateSongs: function (recommendations) { + this.elements.songs.empty(); + if(!recommendations.length) + return !!this.elements.songs_title.hide(); + else + this.elements.songs_title.show(); + + var n = recommendations.length, rec; + while(n--) { + rec = recommendations[n]; + recommendations[n] = (new Element("li")) + .grab(new Element("a", { + href: "#", + html: "{title} by {artist}".interpolate(rec) + })); + } + + this.elements.songs.adopt(recommendations); + delete recommendations; + delete rec; + } +}); + +SongContext.register({ + id: "videos", + title: "Music Videos", + + initialize: function (container) { + this.el = container; + this.video = new Element("iframe", { + id: "youtube_music_video", + type: "text/html", + width: 640, + height: 385, + frameborder: 0, + "class": "youtube-player" + }); + this.search_results = new Element("ul", { + id: "video_search_results", + "class": "context_column no_selection" + }); + + this.search_results.addEvent("click:relay(li)", function (event, el) { + var video_id = el.retrieve("video_id"); + + if(typeof video_id === "undefined") + return; + + this.video.src = "http://www.youtube.com/embed/" + video_id; + this.video_dialog.show(); + }.bind(this)); + + this.video_dialog = new Dialog({ + html: this.video, + onShow: function () { + Player.pause() + }, + onHide: function () { + this.video.src = "about:blank"; + setTimeout(function () { + Player.play(); + }, 1000); + }.bind(this) + }); + + this.el.grab(this.search_results); + }, + + show: $empty, + hide: $empty, + + update: function (song) { + SongContext.showLoadingScreen(); + da.service.musicVideo(song, this.updateSearchResults.bind(this)); + }, + + updateSearchResults: function (results) { + this.search_results.empty(); + + if(!results) + return; + + var n = results.length, video; + + while(n--) { + video = results[n]; + results[n] = (new Element("li")) + .store("video_id", video.id) + .grab((new Element("a")).adopt( + new Element("img", { + src: video.thumbnail.sqDefault, + title: video.title + }), + new Element("strong", { + html: video.title + }), +// new Element("small", { +// html: (new Date(video.duration)).format("%M:%S") +// }), + new Element("p", { + html: video.description.slice(0, 110) + "…" + }) + )); + } + + this.search_results.adopt(results); + SongContext.hideLoadingScreen(); + delete results; + delete video; + } +}); + +})(); 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_devel.html 65 + + + hunk ./contrib/musicplayer/src/index_devel.html 75 + +
Loading...
hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 36 options = options || {}; this.data = data; this.offset = options.offset || 0; - this.length = options.length || 0; + this.length = options.length || data.length || 0; this.bigEndian = options.bigEndian || false; hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 39 - if(typeof data === "string") { - this.length = this.length || data.length; - } else { + //if(typeof data === "string") { + // this.length = this.length || data.length; + //} else { // In this case we're probably dealing with IE, // and in order for this to work, VisualBasic-script magic is needed, // for which we don't have enough of mana. hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 45 - throw Exception("We're lacking some mana. Please use different browser."); - } + // throw Exception("We're lacking some mana. Please use different browser."); + //} }, /** hunk ./contrib/musicplayer/src/libs/util/ID3.js 72 _onFileFetched: function (data) { var parser = this.parsers[0]; if(parser && parser.test(data)) { - this.parser = (new parser(data, this.options, this.request)); - delete this.parsers; + try { + this.parser = (new parser(data, this.options, this.request)); + } catch(e) { + this.options.onFailure("parserFailure", e); + delete e; + } finally { + delete this.parsers; + } } else this._getFile(this.parsers.shift()); }, hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 44 * * Contains know ID3v2 frame types. **/ -var BinaryFile = da.util.BinaryFile, - CACHE = [], +var BinaryFile = da.util.BinaryFile, + CACHE = [], + GENRE_REGEXP = /^\(\d+\)/, + BE_BOM = "\xFE\xFF", + LE_BOM = "\xFF\xFE", + UNSYNC_PAIR = /(\uF7FF\0)/g, +FFLAGS = { + ALTER_TAG_23: 0x8000, + ALTER_FILE_23: 0x4000, + READONLY_23: 0x2000, + COMPRESS_23: 0x0080, + ENCRYPT_23: 0x0040, + GROUP_23: 0x0020, + + 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 = { /** * da.util.ID3v2Parser.frameTypes.text(offset, size) -> String hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 74 **/ text: function (offset, size) { var d = this.data; + 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) hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 78 - if(d.getByteAt(offset + 1) + d.getByteAt(offset + 2) === 255 + 254) { - offset += 2; - size -= 2; - } + 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; + + offset += bom_pos + 1; + size -= bom_pos + 1; + + console.log("Unicode BOM detected", [bom_pos, d.getStringAt(offset, size)]); + + //if(d.getByteAt(offset + 1) + d.getByteAt(offset + 2) === 255 + 254) { + // offset += 2; + // size -= 2; + //} } return d.getStringAt(offset + 1, size - 1).strip(); hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 133 ignore: $empty }, + FRAMES = { // ID3v2.4 tags hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 136 - SEEK: $empty, + //SEEK: $empty, // ID3v2.3 tags TRCK: function (offset, size) { hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 141 var data = FrameType.text.call(this, offset, size); - return +data.split("/")[0] + return +data.split("/")[0]; }, TIT1: FrameType.text, TIT2: FrameType.text, hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 150 TPE2: FrameType.text, TALB: FrameType.text, TYER: FrameType.textNumeric, - TIME: $empty, + //TIME: $empty, TCON: function (offset, size) { // Genre, can be either "(123)Genre", "(123)" or "Genre". var data = FrameType.text.call(this, offset, size); hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 154 - return +((data.match(/^\(\d+\)/) || " ")[0].slice(1, -1)); + return +((data.match(GENRE_REGEXP) || " ")[0].slice(1, -1)); }, USLT: FrameType.unsyncedLyrics, WOAR: FrameType.link, hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 179 WAR: FRAMES.WOAR, WXX: FRAMES.WXXX }); + var ID3v2Parser = new Class({ /** * new da.util.ID3v2Parser(data, options, request) hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 221 'majorVersion', 'minorVersion', "flags", "size" ]); this.version = 2 + (this.header.majorVersion/10) + this.header.minorVersion; - this.header.size = this.unsync(this.header.size) + 10; + this.header.size = this.unpad(this.header.size, 7) + 10; this.parseFlags(); hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 249 parseFlags: function () { var flags = this.header.flags; this.flags = { - unsync_all: flags & 0x80, - extended: flags & 0x40, - experimental: flags & 0x20, - footer: flags & 0x10 + unsync_all: !!(flags & 0x80), + extended: !!(flags & 0x40), + experimental: !!(flags & 0x20), + footer: !!(flags & 0x10) }; }, hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 255 - + /** * da.util.ID3v2Parser#parseFrames() -> undefined * Calls proper function for parsing frames depending on tag's version. hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 261 **/ parseFrames: function () { + console.log("parsing ID3v" + this.version + " tag", this.options.url); + if(this.version >= 2.3) this.parseFrames_23(); else hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 278 **/ parseFrames_23: function () { if(this.version >= 2.4 && this.flags.unsync_all) - this.data.data = this.unsync(0, this.header.size); - + this.unsync(); + var offset = 10, ext_header_size = this.data.getStringAt(offset, 4), tag_size = this.header.size; hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 283 - + // Some tagging software is apparently know for setting // "extended header present" flag but then ommiting it from the file, // which means that ext_header_size will be equal to name of a frame. hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 289 if(this.flags.extended && !FRAMES[ext_header_size]) { if(this.version >= 2.4) - ext_header_size = this.unsync(ext_header_size) - 4; + ext_header_size = this.unpad(ext_header_size) - 4; else ext_header_size = this.data.getLongAt(10); hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 296 offset += ext_header_size; } + var foffset, frame_name, frame_size, frame_size_ajd, frame_flags; while(offset < tag_size) { hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 298 - var foffset = offset, - frame_name = this.data.getStringAt(foffset, 4), - frame_size = this.unsync(foffset += 4, 4), - frame_flags = [this.data.getByteAt(foffset += 4), this.data.getByteAt(foffset += 1)]; - foffset++; // frame_flags + foffset = offset; + frame_name = this.data.getStringAt(foffset, 4); + frame_size = frame_size_adj = this.unpad(foffset += 4, 4); + frame_flags = this.data.getShortAt(foffset += 5); + foffset += 1; hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 304 - if(!frame_size) + if(!frame_size) { break; hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 306 + } hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 308 - if(FRAMES[frame_name] && frame_size) - this.frames[frame_name] = FRAMES[frame_name].call(this, foffset, frame_size); + if(this.version >= 2.4) { + if(frame_flags & (FFLAGS.COMPRESS_24 | FFLAGS.DATALEN_24)) { + foffset += 4; + frame_size -= 1; + frame_size_adj -= 5; + } + //if(!this.flags.unsync_all && (frame_flags & FFLAGS.UNSYNC_24)) + // this.unsync(offset, frame_size_); + } else { + if(frame_flags & FFLAGS.COMPRESS_23) { + console.log("Compressed frame. Sorry.", self.options.url); + foffset += 4; + } + } hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 323 - //console.log(frame_name, this.frames[frame_name], [foffset, frame_size]); + if(FRAMES[frame_name]) + this.frames[frame_name] = FRAMES[frame_name].call(this, foffset, frame_size_adj); + + //console.log("parsed frame", [frame_name, this.frames[frame_name]]); offset += frame_size + 10; } }, hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 337 **/ parseFrames_22: function () { var offset = 10, - tag_size = this.header.size; + tag_size = this.header.size, + foffset, frame_name, frame_size; while(offset < tag_size) { hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 341 - var foffset = offset, - frame_name = this.data.getStringAt(foffset, 3), - frame_size = (new BinaryFile( - "\0" + this.data.getStringAt(foffset += 3, 3), - {bigEndian:true} - )).getLongAt(0); + foffset = offset; + frame_name = this.data.getStringAt(foffset, 3); + frame_size = (new BinaryFile( + "\0" + this.data.getStringAt(foffset += 3, 3), + {bigEndian:true} + )).getLongAt(0); foffset += 3; if(!frame_size) hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 361 }, /** - * da.util.ID3v2Parser#unsync(offset, length[, bits = 7]) -> Number - * da.util.ID3v2Parser#unsync(string) -> Number - * - offset (Number): offset from which so start unsyncing. - * - length (Number): length string to unsync. + * da.util.ID3v2Parser#unpad(offset, length[, bits = 8]) -> Number + * da.util.ID3v2Parser#unpad(string, bits) -> Number + * - offset (Number): offset from which so start unpadding. + * - length (Number): length string to unpad. * - bits (Number): number of bits used. hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 366 - * - string (String): String to unsync. - * - * Performs unsyncing process defined in ID3 specification. + * - string (String): String to unpad. **/ hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 368 - unsync: function (offset, length, bits) { - bits = bits || 7; + unpad: function (offset, size, bits) { + bits = bits || 8; var mask = (1 << bits) - 1, bytes = [], numeric_value = 0, hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 377 if(typeof offset === "string") { data = new BinaryFile(offset, {bigEndian: true}); - length = offset.length; + if(size) + bits = size; + size = offset.length; offset = 0; } hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 383 - if(length) { - for(var n = offset, m = offset + length; n < m; n++) + if(size) { + for(var n = offset, m = n + size; n < m; n++) bytes.push(data.getByteAt(n) & mask); bytes.reverse(); hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 403 }, /** + * da.util.ID3v2Parser#unsync() -> undefined + * da.util.ID3v2Parser#unsync(offset, length) -> undefined + * + * Unsyncs the data as per ID3 specification. + * + **/ + unsync: function (n, m) { + console.log("unsyncing file", this.options.url); + + if(arguments.length) { + var data = this.data.data, + part = data + .slice(n, m) + .replace(UNSYNC_PAIR, String.fromCharCode(0xF7FF)); + + this.data.data = data.slice(0, n).concat(part, data.slice(n + m)); + } else + this.data.data = this.data.data.replace(UNSYNC_PAIR, String.fromCharCode(0xF7FF)); + + this.data.length = this.data.data.length; + }, + + /** * da.util.ID3v2Parser#simplify() -> Object * * Returns humanised version of data parsed from frames. hunk ./contrib/musicplayer/src/libs/vendor/SoundManager.js 19 * **/ -(function (window) { +// SoundManager depends on too much of window.* functions, +// thus making it in-efficient to load it inside another closure +// with da.vendor as alias for window. //#require "libs/vendor/soundmanager/script/soundmanager2.js" hunk ./contrib/musicplayer/src/libs/vendor/SoundManager.js 23 -})(da.vendor); + +da.vendor.SoundManager = SoundManager; +da.vendor.soundManager = soundManager; +//delete SoundManager; +//delete soundmanager; var url = location.protocol + "//" + location.host + location.pathname.split("/").slice(0, -1).join("/"), path = location.pathname.contains("devel.html") ? "/libs/vendor/soundmanager/swf/" : "/resources/flash/"; hunk ./contrib/musicplayer/src/libs/vendor/SoundManager.js 33 $extend(da.vendor.soundManager, { - useHTML5Audio: false, + useHTML5Audio: true, url: url + path, debugMode: false, debugFlash: false hunk ./contrib/musicplayer/src/resources/css/app.css 133 } .dialog { + display: block; margin: 50px auto 0 auto; background: #fff; border: 1px solid #ddd; hunk ./contrib/musicplayer/src/resources/css/app.css 220 z-index: 1002; } +/** Tooltips **/ +.tooltip { + color: #fff; + width: 139px; + z-index: 13000; +} + +.tool-title { + font-weight: bold; + font-size: 11px; + margin: 0; + color: #9FD4FF; + padding: 8px 8px 4px; + background: url(bubble.png) top left; +} + +.tool-text { + font-size: 11px; + padding: 4px 8px 8px; + background: url(bubble.png) bottom right; +} + /*** Navigation columns ***/ .column_container { float: left; hunk ./contrib/musicplayer/src/resources/css/app.css 343 margin-left: 20px; } -.navigation_column .active_column_item, .menu_item:hover, #next_song:hover, #prev_song:hover { +.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; text-shadow: #000 0 1px 0; color: #fff !important; hunk ./contrib/musicplayer/src/resources/css/app.css 379 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; } /*** Menus ***/ hunk ./contrib/musicplayer/src/resources/css/app.css 505 width: 150px; text-align: right; display: inline-block; + margin: 0 5px 0 0; } #settings_controls .setting_box label.no_indent { hunk ./contrib/musicplayer/src/resources/css/app.css 513 text-align: left; } +#settings_controls input[type="text"], #settings_controls input[type="password"] { + width: 250px; +} + #settings_controls .settings_footer { border-top: 1px solid #c0c0c0; text-align: right; hunk ./contrib/musicplayer/src/resources/css/app.css 563 #song_info_block { width: 100%; background: #f3f3f3; - border-bottom: 1px solid #ddd; } #song_details { hunk ./contrib/musicplayer/src/resources/css/app.css 606 margin: 10px;*/ max-width: 150px; border: 1px solid #fff; - outline: 1px solid #ddd; -webkit-box-shadow: #c0c0c0 0 3px 5px; -moz-box-shadow: #c0c0c0 0 3px 5px; hunk ./contrib/musicplayer/src/resources/css/app.css 611 -o-box-shadow: #c0c0c0 0 3px 5px; box-shadow: #c0c0c0 0 3px 5px; + + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + -o-border-radius: 3px; + border-radius: 3px; } #song_title { hunk ./contrib/musicplayer/src/resources/css/app.css 674 color: #000; background: rgba(255, 255, 255, 0.8) no-repeat; text-shadow: #fff 0 1px 0; + padding: 1px 0; overflow: hidden; text-overflow: ellipsis; hunk ./contrib/musicplayer/src/resources/css/app.css 681 white-space: nowrap; -webkit-box-shadow: rgba(0, 0, 0, 0.4) 0 1px 3px; + -moz-box-shadow: rgba(0, 0, 0, 0.4) 0 1px 3px; + -o-box-shadow: rgba(0, 0, 0, 0.4) 0 1px 3px; + box-shadow: rgba(0, 0, 0, 0.4) 0 1px 3px; + + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + -o-border-radius: 3px; + border-radius: 3px; } #next_song:hover, #prev_song:hover { hunk ./contrib/musicplayer/src/resources/css/app.css 697 #next_song:active, #prev_song:active { text-shadow: #000 0 -1px 0; + padding-top: 3px; + padding-bottom: 1px; } #next_song { hunk ./contrib/musicplayer/src/resources/css/app.css 724 #prev_song:hover { background-image: url(../images/previous_active.png); } + +/*** Song Context ***/ +#context_tabs { + background: #ddd; + border: 1px solid #c0c0c0; + border-left: 0; + border-right: 0; + width: 100%; + text-align: center; + padding: 0; + text-shadow: #fff 0 1px 0; + + -webkit-box-shadow: #c0c0c0 0px -1px 7px inset; +} + +#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; +} + +#context_tabs a.tab:last-child { + border-right: 1px solid #c0c0c0; +} + +#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 a.tab.active { + background: #fff; +} + +#song_context { + overflow: auto; +} + +#song_context_loading { + background: rgba(255, 255, 255, 0.7); + position: absolute; + text-align: center; + padding-top: 20px; + font-size: 1.5em; +} + +/** Artist context **/ +#artist_photo_wrapper { + float: left; + margin: 10px; + width: 200px; +} + +#artist_photo_wrapper:hover #artist_photo_chooser { + visibility: visible; +} + +#artist_photo { + max-width: 200px; + + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -o-border-radius: 4px; + border-radius: 4px; +} + +#artist_photo_chooser { + visibility: hidden; + text-align: center; + position: relative; + margin: -30px auto 0 auto; + font-size: 1.1em; + background: rgba(0, 0, 0, 0.7); + width: 90%; + overflow: hidden; + + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -o-border-radius: 4px; + border-radius: 4px; +} + +#artist_photo_chooser a { + display: inline-block; + padding: 0 5px; + cursor: default; + color: #fff; + outline: 0; +} + +#artist_photo_chooser a:hover, #artist_photo_chooser a:focus { + text-shadow: #fff 0 0 5px; +} + +#artist_bio { + padding-top: 10px; + width: 270px; + float: left; +} + +#artist_bio a { + color: #00f; + text-decoration: underline; +} + +#artist_stats { + margin: 30px 0 0 5px; +} + +.context_column { + width: 32%; + float: left; + margin-bottom: 0; +} + +.context_column li { + list-style: none; + padding: 2px; + margin: 0; +} + +.context_column li.title { + margin-bottom: 10px; + font-weight: bold; + text-align: center; +} + +.context_column li:nth-child(2n+3) { + background: #f3f3f3; +} + +.context_column li a { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: default; +} + +.middle_context_column { + border: 1px solid #ddd; + border-top: 0; + border-bottom: 0; + margin: 0 2px; + padding: 0 2px; +} + +#recommended_artists { + width: 98%; + margin: 5px 0 30px 5px; + overflow: hidden; + white-space: nowrap; +} + +#recommended_artists a { + text-align: center; + display: inline-block; + margin: 0 2px; + font-size: 0.9em; + + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -o-border-radius: 4px; + border-radius: 4px; +} + +#recommended_artists img { + display: block; + margin: 0 auto; + padding: 0; + + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -o-border-radius: 4px; + border-radius: 4px; +} + +#recommended_songs { + float: none; + width: 98%; + margin: auto; +} + +#recommended_songs li:nth-child(2n+1), #video_search_results li:nth-child(2n+1) { + background: #f3f3f3; +} + +#recommendations_context_tab_container h4 { + font-size: 1.1em; + margin: 5px 0 0 5px; +} + +#video_search_results { + width: 100%; + margin: 0; + overflow: hidden; + white-space: nowrap; +} + +#video_search_results li { + list-style: none; + border-bottom: 1px solid #ddd; + min-height: 90px; + padding: 0; + overflow: hidden; +} + +#video_search_results a { + white-space: normal; +} + +#video_search_results img { + float: left; + margin: 0 5px 0 0; + width: 120px; + height: 90px; +} + +#video_search_results strong { + display: block; +} + +#video_search_results small { + color: #c0c0c0; + font-size: 0.9em; + display: block; +} + +#video_search_results p { + display: block; + margin: 5px 0 0 0; + float: left; + width: 360px; +} + hunk ./contrib/musicplayer/src/services/albumCover.js 1 -//#require //#require "services/lastfm.js" (function () { addfile ./contrib/musicplayer/src/services/artistInfo.js hunk ./contrib/musicplayer/src/services/artistInfo.js 1 +//#require "services/lastfm.js" +//#require "doctemplates/Artist.js" +//#require "libs/util/Goal.js" + +(function () { +var lastfm = da.service.lastFm, + Artist = da.db.DocumentTemplate.Artist, + Goal = da.util.Goal; + +var CACHE = {}; + +function fetchArtistInfo(search_params, artist, callback) { + if(CACHE[artist.id]) { + if(callback) + callback(CACHE[artist.id]); + + return; + } + + var info = {}, + fetch_data = new Goal({ + checkpoints: ["bio", "photos", "events", "toptracks", "topalbums"], + onFinish: function () { + CACHE[artist.id] = info; + + if(callback) + callback(info); + + delete info; + delete fetch_data; + } + /*, + after: { + mbid: function () { + fetchArtistLinks(info.mbid, function (links) { + if(links) + info.links = links; + + fetch_data.checkpoint("links"); + }); + } + } */ + }); + + lastfm.artist.getInfo(search_params, { + success: function (data) { + data = data.artist; + if(data.mbid && data.mbid.length) { + info.mbid = data.mbid; + search_params.mbid = data.mbid; + } + + info.bio = data.bio; + fetch_data.checkpoint("bio"); + }, + failure: function () { + fetch_data.checkpoint("bio"); + } + }); + + lastfm.artist.getImages(search_params, { + success: function (data) { + data = data.images; + if(!data.image) + return fetch_data.checkpoint("photos"); + + if($type(data.image) !== "array") + data.image = [data.image]; + + var images = data.image.slice(0, 10), + n = images.length, + sizes, m; + + while(n--) { + sizes = images[n].sizes.size; + m = sizes.length; + images[n] = {}; + while(m--) + images[n][sizes[m].name] = sizes[m]["#text"]; + } + + info.photos = images; + info.more_photos_url = data.image[0].url; + + delete images; + delete data; + + fetch_data.checkpoint("photos"); + }, + failure: function () { + fetch_data.checkpoint("photos"); + } + }); + + lastfm.artist.getEvents(search_params, { + success: function (data) { + data = data.events; + if(!data.event) + return fetch_data.checkpoint("events"); + + if($type(data.event) !== "array") + data.event = [data.event]; + + var events = data.event.slice(0, 10), + n = events.length, event; + + while(n--) { + event = events[n]; + if(+event.cancelled) { + delete events[n]; + continue; + } + + var loc = event.venue.location; + events[n] = { + id: event.id, + title: [event.venue.name, loc.city, loc.country].join(", "), + time: event.startDate, + url: event.url + }; + } + + info.events = events.clean(); + delete events; + delete event; + delete data; + + fetch_data.checkpoint("events"); + }, + failure: function () { + fetch_data.checkpoint("events"); + } + }); + + lastfm.artist.getTopTracks(search_params, { + success: parseTop("track", info, fetch_data), + failure: function () { + fetch_data.checkpoint("toptracks"); + } + }); + + lastfm.artist.getTopAlbums(search_params, { + success: parseTop("album", info, fetch_data), + failure: function () { + fetch_data.checkpoint("topalbums"); + } + }); +} + +function parseTop(what, info, goal) { + return function (data) { + data = data["top" + what + "s"]; + if(!data[what]) + return goal.checkpoint("top" + what + "s"); + + if($type(data[what]) !== "array") + data[what] = [data[what]]; + + var items = data[what].slice(0, 10), + n = items.length, + item; + + while(n--) { + item = items[n]; + items[n] = {title: item.name}; + if(item.mbid && item.mbid.length) + items[n].mbid = item.mbid; + } + + info["top_" + what + "s"] = items; + delete items; + delete item; + delete data; + + goal.checkpoint("top" + what + "s"); + } +} + +/* +// Cross-domain XML: Fail (unless you use Flash) +function fetchArtistLinks(mbid, callback) { + var req = new Request({ + url: "http://musicbrainz.org/ws/1/artist/" + mbid, + onSuccess: function (_, data) { + window.dx = data; + console.log(data); + + //delete req; + }, + onFailure: function () { + if(callback) + callback(false); + + //delete req; + } + }); + + req.get({ + type: "xml", + inc: "url-rels" + }); +} +*/ + +/** + * da.service.artistInfo(artist, callback) -> undefined + * - artist (da.db.DocumentTemplate.Artist): artist whose info needs to be retireved. + * - callback (Function): function called when informations are fetched. + **/ +da.service.artistInfo = function artistInfo (artist, callback) { + var search_params = {}; + if(artist.get("mbid")) + search_params.mbid = artist.get("mbid"); + else + search_params.artist = artist.get("title"); + + fetchArtistInfo(search_params, artist, callback); +}; + +})(); hunk ./contrib/musicplayer/src/services/lastfm.js 1 +//#require //#require "libs/vendor/LastFM.js" (function () { addfile ./contrib/musicplayer/src/services/musicVideo.js hunk ./contrib/musicplayer/src/services/musicVideo.js 1 +//#require +//#require + +(function () { +var Song = da.db.DocumentTemplate.Song, + CACHE = {}; + +/** + * da.service.musicVideo(song, callback) -> undefined + * - song (da.db.DocumentTemplate.Song): song who's music videos are needed. + * - callback (Function): function to which results will be passed. + **/ +da.service.musicVideo = function (song, callback) { + if(CACHE[song.id]) + return !!callback(CACHE[song.id]); + + song.get("artist", function (artist) { + var req = new Request.JSONP({ + url: "http://gdata.youtube.com/feeds/api/videos", + data: { + v: 2, + alt: "jsonc", + category: "Music", + format: 5, + orderby: "relevance", + time: "all_time", + q: artist.get("title") + " " + song.get("title") + }, + onSuccess: function (results) { + callback(results.data.items); + delete req; + }, + onFailure: function () { + delete req; + callback(false); + } + }); + req.send(); + }); +}; + +})(); addfile ./contrib/musicplayer/src/services/recommendations.js hunk ./contrib/musicplayer/src/services/recommendations.js 1 +//#require "services/lastfm.js" +//#require "libs/util/Goal.js" hunk ./contrib/musicplayer/src/services/recommendations.js 4 +(function () { +var DocumentTemplate = da.db.DocumentTemplate, + Artist = DocumentTemplate.Artist, + Album = DocumentTemplate.Album, + Song = DocumentTemplate.Song, + Goal = da.util.Goal, + lastfm = da.service.lastFm, + + CACHE = {}; + +function getSimilar (what, search_params, callback) { + console.log("getSimilar", what); + lastfm[what].getSimilar(search_params, { + success: function (data) { + data = data["similar" + what + "s"]; + var items = data[what]; + + if(!items || typeof items === "string") + return callback(false); + + if($type(items) !== "array") + items = [items]; + else + items = items.slice(0, 10); + + var n = items.length, item; + while(n--) { + item = items[n]; + items[n] = { + title: item.name, + image: item.image && item.image.length ? item.image[2]["#text"] : "" + }; + + if(item.mbid && item.mbid.length) + items[n].mbid = item.mbid; + + if(what === "track") { + items[n].artist = item.artist.name; + if(item.artist.mbid && item.artist.mbid.length) + items[n].artist_mbid = item.artist.mbid; + } + } + + callback(items); + delete items; + delete data; + }, + failure: function () { + callback(false); + } + }); +} + +/** + * da.service.recommendations(song, callback) -> undefined + * - song (da.db.DocumentTemplate.Song): songs for which recommendations are needed. + * - callback (Function): called once recommended songs and albums are fetched. + **/ +da.service.recommendations = function (song, callback) { + var recommendations = {}, + fetch_data = new Goal({ + checkpoints: ["artists", "songs"], + onFinish: function () { + callback(recommendations); + delete recommendations; + } + }); + + song.get("artist", function (artist) { + if(CACHE[artist.id]) { + recommendations.artists = CACHE[artist.id]; + fetch_data.checkpoint("artists"); + } else + getSimilar("artist", {artist: artist.get("title"), limit: 10}, function (data) { + if(data) + CACHE[artist.id] = recommendations.artists = data; + fetch_data.checkpoint("artists"); + }); + + if(CACHE[song.id]) { + recommendations.songs = CACHE[song.id]; + fetch_data.checkpoint("songs"); + } else { + var song_search_params; + if(song.get("mbid")) + song_search_params = {mbid: song.get("mbid")}; + else + song_search_params = { + track: song.get("title"), + artist: artist.get("title") + }; + + getSimilar("track", song_search_params, function (data) { + if(data) + CACHE[song.id] = recommendations.songs = data; + + fetch_data.checkpoint("songs"); + }); + } + }); + +}; + +})(); hunk ./contrib/musicplayer/src/workers/indexer.js 36 this.da = {}; +var console = { + log: function (msg, obj) { + postMessage({debug: true, msg: msg, obj: obj}); + } +}; + //#require "libs/vendor/mootools-1.2.4-core-server.js" //#require "libs/vendor/mootools-1.2.4-request.js" //#require hunk ./contrib/musicplayer/src/workers/indexer.js 64 }; function getTags(cap) { - if(!cap) return false; + if(!cap) + return false; + var parser = new ID3({ url: "/uri/" + encodeURIComponent(cap), hunk ./contrib/musicplayer/src/workers/indexer.js 84 }, onFailure: function (calledBy) { + console.log("Failed to parse tags for", cap); parser.destroy(); delete parser; hunk ./contrib/musicplayer/src/workers/indexer.js 107 } function finish () { - postMessage("**FINISHED**"); + if(!queue.length) + postMessage("**FINISHED**"); + else + checkQueue(); } hunk ./contrib/musicplayer/tests/initialize.js 5 windmill.jsTest.register([ 'test_Goal', + 'test_BinaryFile', 'test_ID3', 'test_ID3v1', hunk ./contrib/musicplayer/tests/initialize.js 10 'test_ID3v2', + 'test_ID3v22', + 'test_ID3v23', + 'test_ID3v24', + 'test_BrowserCouchDict', 'test_BrowserCouch', 'test_BrowserCouch_tempView', hunk ./contrib/musicplayer/tests/initialize.js 20 'test_BrowserCouch_liveView', 'test_DocumentTemplate', - 'test_CollectionScannerController', 'test_Menu', hunk ./contrib/musicplayer/tests/initialize.js 21 - 'test_NavigationController', 'test_Dialog', hunk ./contrib/musicplayer/tests/initialize.js 22 - 'test_SettingsController', - 'test_ProgressBar', 'test_SegmentedProgressBar', hunk ./contrib/musicplayer/tests/initialize.js 24 - 'test_PlayerController' + + 'test_CollectionScannerController', + 'test_NavigationController', + 'test_SettingsController', + 'test_PlayerController', + 'test_SongContextController' ]); hunk ./contrib/musicplayer/tests/test_PlayerController.js 23 !Player.nowPlaying() ); - var play_event_fired = false; - function test_playEvent (song) { - jum.assertEquals("first argument to callback function should be playing song", - self.songs[0].id, - song.id - ); - - Player.removeEvent("play", test_PlayEvent); - delete test_playEvent; - } + self.play_event_fired = false; + Player.addEvent("play", function () { + self.play_event_fired = true; + self.play_event_args = arguments; + }); hunk ./contrib/musicplayer/tests/test_PlayerController.js 29 - Player.addEvent("play", test_playEvent); Player.play(self.songs[0]); hunk ./contrib/musicplayer/tests/test_PlayerController.js 30 - - jum.assertTrue("'play' event should have been fired", - play_event_fired + }; + + this.test_playEvent = { + method: "waits.forJS", + params: { + js: function () { return !!self.play_event_fired } + } + }; + + this.test_verifyPlayEventArgs = function () { + jum.assertEquals("first argument to callback function should be playing song", + self.songs[0].id, + self.play_event_args[0].id ); }; hunk ./contrib/musicplayer/tests/test_PlayerController.js 46 + this.test_verifySongInfo = [ + {method: "asserts.assertText", params: {id: "song_title", validator: "Persona"}}, + {method: "asserts.assertText", params: {id: "song_album_title", validator: "Urgency"}}, + {method: "asserts.assertText", params: {id: "song_artist_name", validator: "Superhumanoids"}} + ]; + this.test_getNext = function () { jum.assertTrue("there shouldn't be a next song, since there is no playlist", !Player.getNext() hunk ./contrib/musicplayer/tests/test_PlayerController.js 67 ); }; + this.test_prevNextButtons = [ + {method: "asserts.assertText", params: {id: "prev_song", validator: "Persona"}}, + {method: "asserts.assertText", params: {id: "next_song", validator: "Maps"}} + ]; + this.test_getPrev = function () { jum.assertEquals("previous song in playlist should have been returned", self.songs[0].id, hunk ./contrib/musicplayer/tests/test_PlayerController.js 96 self.songs[0].id, Player.getNext() ); + + Player.setPlaylist([self.songs[2].id]); + jum.assertEquals("next song should be from queue, ignoring the playlist", + self.songs[0].id, + Player.getNext() + ); }; return this; hunk ./contrib/musicplayer/tests/test_ProgressBar.js 35 self.pb.options.foreground = "rgba(255, 0, 0, 255)"; self.pb.setProgress(0.7); - jun.assertEqualArrays(BLACK, getPixel( 1)); - jun.assertEqualArrays(RED, getPixel( 52)); - jun.assertEqualArrays(RED, getPixel( 69)); - jun.assertEqualArrays(TRANSPARENT, getPixel(100)); + jum.assertEqualArrays(BLACK, getPixel( 1)); + jum.assertEqualArrays(RED, getPixel( 52)); + jum.assertEqualArrays(RED, getPixel( 69)); + jum.assertEqualArrays(TRANSPARENT, getPixel(100)); }; this.test_decrementation = function () { addfile ./contrib/musicplayer/tests/test_SongContextController.js hunk ./contrib/musicplayer/tests/test_SongContextController.js 1 +var test_SongContextController = new function () { + var SongContext = da.controller.SongContext, + self = this; + + this.setup = function () { + self.called = { + a_init: 0, + a_show: 0, + a_hide: 0, + a_update: 0, + + b_init: 0, + b_show: 0, + b_hide: 0, + b_update: 0 + }; + + SongContext.register({ + id: "__a__", + title: "Click meh!", + + initialize: function (container) { + self.called.a_init++; + self.passed_container = $type(container) === "element"; + self.a_container = container; + container.grab(new Element("div", { + id: "__a__test_el", + html: "Hai!" + })); + }, + + show: function () { + self.called.a_show++; + }, + + hide: function () { + self.called.a_hide++; + }, + + update: function (song) { + self.called.a_update++; + self.passed_song = song; + } + }); + + SongContext.register({ + id: "__b__", + title: "Click meh too!", + initialize: function () { self.called.b_init++; }, + show: function () { self.called.b_show++; }, + hide: function () { self.called.b_hide++; }, + update: function () { self.called.b_update++; } + }); + }, + + this.test_show = function () { + SongContext.show("__a__"); + jum.assertEquals("initialize function should have been called once", + 1, self.called.a_init + ); + jum.assertTrue("dom nodes should have been inserted into the document", + !!$("__a__test_el") + ); + jum.assertEquals("show function should have been called once", + 1, self.called.a_show + ); + jum.assertEquals("context should be visible", + "block", $("__a__test_el").getParent().style.display + ); + jum.assertEquals("update function should have been called once", + 1, self.called.a_update + ); + jum.assertEquals("loading screen should be visible", + "block", $("song_context_loading").style.display + ); + }; + + this.test_updateArguments = function () { + jum.assertTrue("song should be passed to the update function", + self.passed_song instanceof da.db.DocumentTemplate.Song + ); + }; + + this.test_contextSwitch = function () { + SongContext.show("__b__"); + jum.assertEquals("A's hide function should have been called", + 1, self.called.a_hide + ); + jum.assertEquals("B's hide function shouldn't have been called", + 0, self.called.b_hide + ); + jum.assertEquals("B's show function should have been called", + 1, self.called.b_show + ); + }; +} hunk ./src/allmydata/test/test_musicplayer.py 70 self.settings['JAVASCRIPT_TEST_DIR'] = '../contrib/musicplayer/tests' self.settings['SCRIPT_APPEND_ONLY'] = True + self.browser_debugging = True self.test_url = 'static/musicplayer/index_devel.html' hunk ./src/allmydata/test/test_musicplayer.py 72 + shutil.copytree('../contrib/musicplayer/src', self.public_html_path + '/musicplayer') hunk ./src/allmydata/test/test_musicplayer.py 74 - #os.makedirs(self.public_html_path + '/musicplayer/js/workers') shutil.copytree('../contrib/musicplayer/build/js/workers', self.public_html_path + '/musicplayer/js/workers') d = self.client.create_dirnode() hunk ./src/allmydata/test/test_musicplayer.py 113 class FirefoxTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Firefox): pass + hunk ./src/allmydata/test/tilting.py 34 return d def _set_up_windmill(self): - self.browser_debugging = True + self.browser_debugging = False self.browser_name = 'firefox' self.test_url = '/' self._js_test_details = [] } Context: [add-music-players-full-deps josip.lisec@gmail.com**20100710190714 Ignore-this: 59d7b3890a1f9349bc8ac511af05b2b8 ] [add-music-players-core-deps josip.lisec@gmail.com**20100710185908 Ignore-this: 58f5546ff75501f77d6b346ae99eebec ] [add-music-player josip.lisec@gmail.com**20100710185704 Ignore-this: c29dc0709640abd2e33cfd119b2681f ] [misc/build_helpers/run-with-pythonpath.py: fix stale comment, and remove 'trial' example that is not the right way to run trial. david-sarah@jacaranda.org**20100726225729 Ignore-this: a61f55557ad69a1633bfb2b8172cce97 ] [docs/specifications/dirnodes.txt: 'mesh'->'grid'. david-sarah@jacaranda.org**20100723061616 Ignore-this: 887bcf921ef00afba8e05e9239035bca ] [docs/specifications/dirnodes.txt: bring layer terminology up-to-date with architecture.txt, and a few other updates (e.g. note that the MAC is no longer verified, and that URIs can be unknown). Also 'Tahoe'->'Tahoe-LAFS'. david-sarah@jacaranda.org**20100723054703 Ignore-this: f3b98183e7d0a0f391225b8b93ac6c37 ] [docs: use current cap to Zooko's wiki page in example text zooko@zooko.com**20100721010543 Ignore-this: 4f36f36758f9fdbaf9eb73eac23b6652 fixes #1134 ] [__init__.py: silence DeprecationWarning about BaseException.message globally. fixes #1129 david-sarah@jacaranda.org**20100720011939 Ignore-this: 38808986ba79cb2786b010504a22f89 ] [test_runner: test that 'tahoe --version' outputs no noise (e.g. DeprecationWarnings). david-sarah@jacaranda.org**20100720011345 Ignore-this: dd358b7b2e5d57282cbe133e8069702e ] [TAG allmydata-tahoe-1.7.1 zooko@zooko.com**20100719131352 Ignore-this: 6942056548433dc653a746703819ad8c ] Patch bundle hash: 22835c80e300cdec9f7c0f92ba86a06703c4fb7c