Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
5
assets/webapps/music-oga.gaiamobile.org/js/ui/tab_bar.js
Normal file
5
assets/webapps/music-oga.gaiamobile.org/js/ui/tab_bar.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
;'use strict';var TabBar={playlistArray:[{metadata:{l10nId:'playlists-shuffle-all'},option:'shuffleAll'},{metadata:{l10nId:'playlists-highest-rated'},option:'rated'},{metadata:{l10nId:'playlists-recently-added'},option:'date'},{metadata:{l10nId:'playlists-most-played'},option:'played'},{metadata:{l10nId:'playlists-least-played'},option:'played'},null],get view(){return document.getElementById('tabs');},get tabs(){return this.view.querySelectorAll('[role="tab"]');},set option(choice){var map={'mix':'tabs-mix','playlist':'tabs-playlists','artist':'tabs-artists','album':'tabs-albums','title':'tabs-songs'};var tab=document.getElementById(map[choice]);AccessibilityHelper.setAriaSelected(tab,this.tabs);this._option=choice;},get option(){return this._option;},init:function tab_init(){this.option='mix';this.view.addEventListener('touchend',this);},setDisabled:function tab_setDisabled(option){this.disabled=option;},handleEvent:function tab_handleEvent(evt){if(this.disabled){return;}
|
||||
switch(evt.type){case'touchend':var target=evt.target;if(!target){return;}
|
||||
if(this.option===target.dataset.option){return;}else{this.option=target.dataset.option;}
|
||||
switch(target.id){case'tabs-mix':ListView.cancelEnumeration();ModeManager.start(MODE_TILES);TilesView.hideSearch();break;case'tabs-playlists':ModeManager.start(MODE_LIST,function(){ListView.activate();this.playlistArray.forEach(function(playlist){ListView.update(this.option,playlist);}.bind(this));}.bind(this));break;case'tabs-artists':case'tabs-albums':case'tabs-songs':var info={key:'metadata.'+this.option,range:null,direction:(this.option==='title')?'next':'nextunique',option:this.option};ModeManager.start(MODE_LIST,function(){ListView.activate(info);});break;}
|
||||
break;default:return;}}};
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
;'use strict';var TitleBar={get view(){return document.getElementById('title');},get titleText(){return document.querySelector('#title-text bdi');},get playerIcon(){return document.getElementById('title-player');},get doneButton(){return document.getElementById('title-done');},get scanProgress(){return document.getElementById('scan-progress');},get scanSpinner(){return document.getElementById('scan-spinner');},get scanCount(){return document.getElementById('scan-count');},get scanArtist(){return document.getElementById('scan-artist');},get scanTitle(){return document.getElementById('scan-title');},init:function tb_init(){this.view.addEventListener('click',this);this.view.addEventListener('action',this.onActionBack);},changeTitleText:function tb_changeTitleText(content){this.titleText.textContent=content;},showBackArrow:function(show){if(show){this.view.setAttribute('action','back');}
|
||||
else{this.view.removeAttribute('action');}},onActionBack:function(){if(App.pendingPick){if(ModeManager.currentMode===MODE_PICKER){App.pendingPick.postError('pick cancelled');return;}
|
||||
PlayerView.stop();}
|
||||
ModeManager.pop();},showScanProgress:function(info){this.scanProgress.classList.remove('hidden');this.scanSpinner.classList.remove('hidden');this.scanCount.textContent=info.count;this.scanArtist.textContent=info.artist||'';this.scanTitle.textContent=info.title||'';},hideScanProgress:function(){this.scanSpinner.classList.add('hidden');setTimeout(function(){this.scanProgress.classList.add('hidden');}.bind(this),100);},handleEvent:function tb_handleEvent(evt){var target=evt.target;function cleanupPick(){PlayerView.stop();}
|
||||
switch(evt.type){case'click':if(!target){return;}
|
||||
switch(target.id){case'title-player':if(PlayerView.isQueued){ModeManager.push(MODE_PLAYER);}
|
||||
break;case'title-done':var currentFileInfo=PlayerView.currentFileInfo;LazyLoader.load('js/metadata/album_art_cache.js').then(()=>{return AlbumArtCache.getThumbnailBlob(currentFileInfo);}).then((picture)=>{var currentMetadata=currentFileInfo.metadata;var playingBlob=PlayerView.playingBlob;App.pendingPick.postResult({type:playingBlob.type,blob:playingBlob,name:currentMetadata.title||'',metadata:{title:currentMetadata.title,artist:currentMetadata.artist,album:currentMetadata.album,picture:picture}});cleanupPick();});break;}
|
||||
break;default:return;}}};
|
||||
430
assets/webapps/music-oga.gaiamobile.org/js/ui/views/list_view.js
Normal file
430
assets/webapps/music-oga.gaiamobile.org/js/ui/views/list_view.js
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
/* exported ListView */
|
||||
/* global App, createListElement, Database, IDBKeyRange, ModeManager,
|
||||
MODE_PLAYER, MODE_SUBLIST, MODE_SEARCH_FROM_LIST, PlaybackQueue,
|
||||
PlayerView, SearchView, SubListView, TabBar */
|
||||
'use strict';
|
||||
|
||||
// Assuming the ListView will prepare 5 pages for batch loading.
|
||||
// Each page contains 7 list elements.
|
||||
var LIST_BATCH_SIZE = 7 * 5;
|
||||
// View of List
|
||||
var ListView = {
|
||||
get view() {
|
||||
return document.getElementById('views-list');
|
||||
},
|
||||
|
||||
get anchor() {
|
||||
return document.getElementById('views-list-anchor');
|
||||
},
|
||||
|
||||
get searchBox() {
|
||||
return document.getElementById('views-list-search');
|
||||
},
|
||||
|
||||
get searchInput() {
|
||||
return document.getElementById('views-list-search-input');
|
||||
},
|
||||
|
||||
get dataSource() {
|
||||
return this._dataSource;
|
||||
},
|
||||
|
||||
set dataSource(source) {
|
||||
this._dataSource = source;
|
||||
},
|
||||
|
||||
init: function lv_init() {
|
||||
this.clean();
|
||||
|
||||
this.view.addEventListener('click', this);
|
||||
this.view.addEventListener('touchmove', this);
|
||||
this.view.addEventListener('scroll', this);
|
||||
this.searchInput.addEventListener('focus', this);
|
||||
},
|
||||
|
||||
clean: function lv_clean() {
|
||||
this.cancelEnumeration();
|
||||
|
||||
this.info = null;
|
||||
this.handle = null;
|
||||
this.dataSource = [];
|
||||
|
||||
this.index = 0;
|
||||
this.lastDataIndex = 0;
|
||||
this.firstLetters = [];
|
||||
this.lastFirstLetter = null;
|
||||
this.anchor.innerHTML = '';
|
||||
this.anchor.style.height = 0;
|
||||
this.view.scrollTop = 0;
|
||||
this.hideSearch();
|
||||
this.moveTimer = null;
|
||||
this.scrollTimer = null;
|
||||
},
|
||||
|
||||
cancelEnumeration: function lv_cancelEnumeration() {
|
||||
// Cancel a pending enumeration before start a new one
|
||||
if (this.handle) {
|
||||
Database.cancelEnumeration(this.handle);
|
||||
}
|
||||
},
|
||||
|
||||
hideSearch: function lv_hideSearch() {
|
||||
this.searchInput.value = '';
|
||||
// XXX: we probably want to animate this...
|
||||
if (this.view.scrollTop < this.searchBox.offsetHeight) {
|
||||
this.view.scrollTop = this.searchBox.offsetHeight;
|
||||
}
|
||||
},
|
||||
|
||||
// This function basically create the section header of the list elements.
|
||||
// When we hit a different first letter, this function will use it to
|
||||
// create a new header then keep it, until to hit another different one,
|
||||
// it will create the next header with the new first letter.
|
||||
createHeader: function lv_createHeader(option, result) {
|
||||
var firstLetter = result.metadata[option].charAt(0);
|
||||
var headerLi;
|
||||
|
||||
if (this.lastFirstLetter !== firstLetter) {
|
||||
this.lastFirstLetter = firstLetter;
|
||||
|
||||
headerLi = document.createElement('li');
|
||||
headerLi.className = 'list-header';
|
||||
headerLi.textContent = this.lastFirstLetter || '?';
|
||||
headerLi.setAttribute('role', 'heading');
|
||||
headerLi.setAttribute('aria-level', '2');
|
||||
}
|
||||
|
||||
return headerLi;
|
||||
},
|
||||
|
||||
activate: function lv_activate(info) {
|
||||
// If info is not provided, then we should be displaying playlists,
|
||||
// so it does not need to enumerate from MediaDB.
|
||||
if (!info) {
|
||||
this.clean();
|
||||
return;
|
||||
}
|
||||
|
||||
// Choose one of the indexes to get the count and it should be the
|
||||
// correct count because failed records don't contain metadata, so
|
||||
// here we just pick the album, artist or title as indexes.
|
||||
Database.count('metadata.' + info.option, null, function(count) {
|
||||
this.clean();
|
||||
this.info = info;
|
||||
// Keep the count with the info for later use in PlayerView.
|
||||
this.info.count = count;
|
||||
|
||||
this.handle = Database.enumerate(info.key, info.range, info.direction,
|
||||
function(record) {
|
||||
if (record) {
|
||||
// Check if music is in picker mode because we don't to allow the
|
||||
// user to pick locked music.
|
||||
if (!App.pendingPick || !record.metadata.locked) {
|
||||
this.dataSource.push(record);
|
||||
}
|
||||
|
||||
// Save the current length of the dataSource to lastDataIndex
|
||||
// because we might expand the length to the total count of
|
||||
// the records, since we cannot retrieve all of them in a short
|
||||
// time and the enumeration might be cancelled.
|
||||
// It will also be used to judge the enumeration is end of not.
|
||||
this.lastDataIndex = this.dataSource.length;
|
||||
}
|
||||
|
||||
// When we got the first batch size of the records,
|
||||
// or the total count is less than the batch size,
|
||||
// display it so that users are able to see the first paint
|
||||
// very quickly.
|
||||
if (this.dataSource.length === LIST_BATCH_SIZE || !record) {
|
||||
this.batchUpdate(info.option, LIST_BATCH_SIZE);
|
||||
// If record is null then the enumeration is finished,
|
||||
// so ListView has all the records and is able to adjust
|
||||
// the height.
|
||||
count = record ? count : null;
|
||||
this.adjustHeight(info.option, count);
|
||||
// In picker mode we have to use the ListView's dataSource to
|
||||
// display the correct overlay.
|
||||
if (App.pendingPick) {
|
||||
App.knownSongs = this.dataSource;
|
||||
App.showCorrectOverlay();
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
update: function lv_update(option, result) {
|
||||
if (result === null) {
|
||||
App.showCorrectOverlay();
|
||||
return;
|
||||
}
|
||||
|
||||
this.dataSource.push(result);
|
||||
|
||||
if (option !== 'playlist') {
|
||||
var header = this.createHeader(option, result);
|
||||
if (header) {
|
||||
this.anchor.appendChild(header);
|
||||
}
|
||||
}
|
||||
|
||||
this.anchor.appendChild(createListElement(option, result, this.index));
|
||||
|
||||
this.index++;
|
||||
},
|
||||
|
||||
// This function is used for judging if the ListView should update and the
|
||||
// range it should update, it sees where the bottom element is and calculates
|
||||
// its position to know how many to update.
|
||||
judgeAndUpdate: function lv_judgeAndUpdate() {
|
||||
// If there is no lastChild then the first paint is not drawn yet.
|
||||
// Also if the info is not provide, we don't have to judge for updating.
|
||||
if (!this.anchor.lastChild || !this.info) {
|
||||
return;
|
||||
}
|
||||
|
||||
var itemHeight = this.anchor.lastChild.offsetHeight;
|
||||
var scrolledHeight = this.view.scrollTop + this.view.offsetHeight;
|
||||
var position = Math.round(scrolledHeight / itemHeight);
|
||||
var last = this.anchor.children.length;
|
||||
var range = position + this.firstLetters.length - last;
|
||||
|
||||
if (range > 0) {
|
||||
this.batchUpdate(TabBar.option, range + LIST_BATCH_SIZE);
|
||||
|
||||
// If list handle is cancelled and the lastDataIndex is not -1,
|
||||
// it means the enumeration was cancelled and the dataSource is incomplete
|
||||
// so that we have to resume it from the last data index we have.
|
||||
if (this.handle.state === 'cancelled' && this.lastDataIndex > -1) {
|
||||
var info = this.info;
|
||||
var index = this.lastDataIndex + 1;
|
||||
|
||||
this.handle = Database.advancedEnumerate(
|
||||
info.key, info.range, info.direction, index, function(record) {
|
||||
if (record) {
|
||||
this.dataSource[index] = record;
|
||||
this.lastDataIndex = index;
|
||||
index++;
|
||||
} else {
|
||||
this.lastDataIndex = -1;
|
||||
}
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// See where is the last index we have for the existing children, and start
|
||||
// to create the rest elements from it, note that here we use fragment to
|
||||
// update all the new elements at once, this is for reducing the amount of
|
||||
// appending child to the DOM tree.
|
||||
batchUpdate: function lv_batchUpdate(option, range) {
|
||||
var start = this.index;
|
||||
var end = start + range;
|
||||
var fragment = document.createDocumentFragment();
|
||||
|
||||
if (end > this.dataSource.length) {
|
||||
end = this.dataSource.length;
|
||||
}
|
||||
|
||||
for (var i = start; i < end; i++) {
|
||||
var data = this.dataSource[i];
|
||||
if (data) {
|
||||
var header = this.createHeader(option, data);
|
||||
|
||||
if (header) {
|
||||
fragment.appendChild(header);
|
||||
}
|
||||
|
||||
fragment.appendChild(createListElement(option, data, this.index));
|
||||
this.index++;
|
||||
}
|
||||
}
|
||||
|
||||
this.anchor.appendChild(fragment);
|
||||
},
|
||||
|
||||
// Because the correct height of ListView depends on how many records and
|
||||
// how many section headers it got, also we don't want to cause too many
|
||||
// repaints by appending children to the DOM tree and changes the height,
|
||||
// when we got the first count from the MediaDB or the enumeration is end,
|
||||
// we can fake the the height by the first adjustment(count), then fix the
|
||||
// height to correct by the second adjustment(record is null).
|
||||
adjustHeight: function lv_adjustHeight(option, count) {
|
||||
// If it's the first launch, then dataSource will be empty and we don't
|
||||
// need to adjust the height.
|
||||
if (this.dataSource.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!count) {
|
||||
count = this.dataSource.length;
|
||||
this.firstLetters.length = 0;
|
||||
var previousFirstLetter;
|
||||
for (var i = 0; i < this.dataSource.length; i++) {
|
||||
var metadata = this.dataSource[i].metadata;
|
||||
var firstLetter = metadata[option].charAt(0);
|
||||
if (previousFirstLetter !== firstLetter) {
|
||||
this.firstLetters.push(firstLetter);
|
||||
previousFirstLetter = firstLetter;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Assuming we have all the letters from A to Z.
|
||||
this.firstLetters.length = 26;
|
||||
}
|
||||
|
||||
var headerHeight = this.anchor.firstChild.offsetHeight;
|
||||
var itemHeight = this.anchor.lastChild.offsetHeight;
|
||||
var bottomHeight = parseInt(getComputedStyle(this.anchor.lastChild, null).
|
||||
getPropertyValue('margin-bottom'), 10);
|
||||
|
||||
this.anchor.style.height = (
|
||||
headerHeight * this.firstLetters.length +
|
||||
itemHeight * count +
|
||||
bottomHeight
|
||||
) + 'px';
|
||||
},
|
||||
|
||||
playWithShuffleAll: function lv_playWithShuffleAll() {
|
||||
ModeManager.push(MODE_PLAYER, () => {
|
||||
PlayerView.clean();
|
||||
|
||||
Database.count('metadata.title', null, (count) => {
|
||||
var info = {
|
||||
key: 'metadata.title',
|
||||
range: null,
|
||||
direction: 'next',
|
||||
option: 'title',
|
||||
count: count
|
||||
};
|
||||
|
||||
PlaybackQueue.shuffle = true;
|
||||
PlayerView.activate(new PlaybackQueue.DynamicQueue(info));
|
||||
PlayerView.start();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
playWithIndex: function lv_playWithIndex(index) {
|
||||
ModeManager.push(MODE_PLAYER, () => {
|
||||
// Because the ListView might still retrieving the records, and
|
||||
// we are assigning the dataSource to the PlayerView, since
|
||||
// setDBInfo will expand the dataSource length to the total
|
||||
// count we will be retrieved, we must cancel the enumeration
|
||||
// or the length will be expanded to a wrong number.
|
||||
this.cancelEnumeration();
|
||||
|
||||
PlayerView.clean();
|
||||
PlayerView.activate(new PlaybackQueue.DynamicQueue(this.info, index));
|
||||
PlayerView.start();
|
||||
});
|
||||
},
|
||||
|
||||
activateSubListView: function lv_activateSubListView(target) {
|
||||
var option = target.dataset.option;
|
||||
var index = target.dataset.index;
|
||||
var data = this.dataSource[index];
|
||||
var keyRange = (target.dataset.keyRange != 'all') ?
|
||||
IDBKeyRange.only(target.dataset.keyRange) : null;
|
||||
var l10nId = data.metadata.l10nId;
|
||||
var direction =
|
||||
(l10nId === 'playlists-most-played' ||
|
||||
l10nId === 'playlists-recently-added' ||
|
||||
l10nId === 'playlists-highest-rated') ?
|
||||
'prev' : 'next';
|
||||
|
||||
// SubListView needs to prepare the songs data before entering it,
|
||||
// So here we initialize the SubListView before push the view.
|
||||
ModeManager.waitForView(MODE_SUBLIST, () => {
|
||||
SubListView.activate(option, data, index, keyRange, direction, () => {
|
||||
ModeManager.push(MODE_SUBLIST);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
handleEvent: function lv_handleEvent(evt) {
|
||||
var target = evt.target;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (evt.type) {
|
||||
case 'click':
|
||||
if (target.id === 'views-list-search-close') {
|
||||
if (ModeManager.currentMode === MODE_SEARCH_FROM_LIST) {
|
||||
ModeManager.pop();
|
||||
}
|
||||
this.hideSearch();
|
||||
evt.preventDefault();
|
||||
} else {
|
||||
var option = target.dataset.option;
|
||||
// When an user select "Shuffle all"
|
||||
// We just play all songs with shuffle order
|
||||
// or change mode to subList view and list songs
|
||||
if (option === 'shuffleAll') {
|
||||
this.playWithShuffleAll();
|
||||
} else if (option === 'title') {
|
||||
this.playWithIndex(parseInt(target.dataset.index, 10));
|
||||
} else if (option) {
|
||||
this.activateSubListView(target);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'focus':
|
||||
if (target.id === 'views-list-search-input') {
|
||||
if (ModeManager.currentMode !== MODE_SEARCH_FROM_LIST) {
|
||||
ModeManager.start(MODE_SEARCH_FROM_LIST, function() {
|
||||
// Let the search view gets the focus.
|
||||
SearchView.searchInput.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'touchmove':
|
||||
// Start the rest batch updating after the first paint
|
||||
if (this.anchor.children.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.moveTimer) {
|
||||
clearTimeout(this.moveTimer);
|
||||
}
|
||||
|
||||
// If the move timer is not cancelled, it should be a suitable time
|
||||
// to update the ui because we don't want to render elements while
|
||||
// the list is scrolling.
|
||||
this.moveTimer = setTimeout(function() {
|
||||
this.judgeAndUpdate();
|
||||
this.moveTimer = null;
|
||||
}.bind(this), 50);
|
||||
break;
|
||||
|
||||
case 'scroll':
|
||||
// Start the rest batch updating after the first paint
|
||||
if (this.anchor.children.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.scrollTimer) {
|
||||
clearTimeout(this.scrollTimer);
|
||||
}
|
||||
|
||||
// If the user try to scroll as possible as it can, after the scrolling
|
||||
// stops, we can see where the position is and try to render the rest
|
||||
// elements that should be displayed on the screen.
|
||||
this.scrollTimer = setTimeout(function() {
|
||||
this.judgeAndUpdate();
|
||||
this.scrollTimer = null;
|
||||
}.bind(this), 500);
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
;'use strict';var MODE_TILES=1;var MODE_LIST=2;var MODE_SUBLIST=3;var MODE_PLAYER=4;var MODE_SEARCH_FROM_TILES=5;var MODE_SEARCH_FROM_LIST=6;var MODE_PICKER=7;var ModeManager={views:{1:{id:'views-tiles',paths:['js/ui/views/tiles_view.js']},2:{id:'views-list',paths:['js/ui/views/list_view.js']},3:{id:'views-sublist',paths:['js/ui/views/subList_view.js']},4:{id:'views-player',paths:['js/ui/views/player_view.js','js/queue.js']},5:{id:'views-search',paths:['js/ui/views/search_view.js','shared/js/text_normalizer.js']},6:{id:'views-search',paths:['js/ui/views/search_view.js','shared/js/text_normalizer.js']},7:{id:'views-list',paths:['js/ui/views/list_view.js']}},_modeStack:[],playerTitle:null,get currentMode(){return this._modeStack[this._modeStack.length-1];},get previousMode(){return this._modeStack[this._modeStack.length-2];},start:function(mode,callback){this._modeStack=[];this._resetViews();this._pushMode(mode,false,function(){this._modeStack=[mode];this._updateMode(callback);}.bind(this));},push:function(mode,callback){this._pushMode(mode,true,function(){this._modeStack.push(mode);this._updateMode(callback);}.bind(this));},pop:function(){if(this._modeStack.length<=1){return;}
|
||||
this._popMode();this._modeStack.pop();this._updateMode();},_resetViews:function(){for(var mode in this.views){var sheet=document.getElementById(this.views[mode].id);sheet.classList.remove('animated');sheet.classList.remove('current');sheet.classList.add('next');}},waitForView:function(mode,callback){var view=this.views[mode];if(view.isLoaded){if(callback){callback(view);return;}}
|
||||
LazyLoader.load(view.paths).then(()=>{if(view.isLoaded){return;}
|
||||
var sheet=document.getElementById(view.id);sheet.classList.remove('hidden');switch(view.id){case'views-tiles':TilesView.init();break;case'views-list':ListView.init();break;case'views-sublist':SubListView.init();break;case'views-player':if(App.pendingPick){PlayerView.init(TYPE_SINGLE,false);}else{PlayerView.init(TYPE_LIST);}
|
||||
break;case'views-search':SearchView.init();break;}
|
||||
if(view.id==='views-player'){return PlaybackQueue.loadSettings();}}).then(()=>{view.isLoaded=true;if(callback){callback(view);}});},_pushMode:function(mode,animated,callback){this.waitForView(mode,(view)=>{var nextId=view.id;var currentId=this.currentMode?this.views[this.currentMode].id:null;var nextSheet=document.getElementById(nextId);var currentSheet=document.getElementById(currentId);if(nextSheet){nextSheet.classList.toggle('animated',animated);nextSheet.classList.remove('previous');nextSheet.classList.remove('next');nextSheet.classList.add('current');}
|
||||
if(currentSheet){currentSheet.classList.toggle('animated',animated);currentSheet.classList.remove('current');currentSheet.classList.add('previous');}
|
||||
if(callback){callback();}});},_popMode:function(callback){var currentId=this.views[this.currentMode].id;var previousId=this.views[this.previousMode].id;var currentSheet=document.getElementById(currentId);var previousSheet=document.getElementById(previousId);currentSheet.classList.remove('current');currentSheet.classList.add('next');previousSheet.classList.remove('previous');previousSheet.classList.add('current');if(callback){callback();}},_updateBackArrow:function(){var noBackArrow=[MODE_TILES,MODE_LIST,MODE_SEARCH_FROM_TILES,MODE_SEARCH_FROM_LIST];var hide=noBackArrow.indexOf(this.currentMode)>-1;TitleBar.showBackArrow(!hide);},updateTitle:function(){var title;switch(this.currentMode){case MODE_TILES:title=this.playerTitle||navigator.mozL10n.get('music');break;case MODE_LIST:case MODE_SUBLIST:switch(TabBar.option){case'playlist':title=navigator.mozL10n.get('playlists');break;case'artist':title=navigator.mozL10n.get('artists');break;case'album':title=navigator.mozL10n.get('albums');break;case'title':title=navigator.mozL10n.get('songs');break;}
|
||||
break;case MODE_PLAYER:title=this.playerTitle||navigator.mozL10n.get('unknownTitle');break;case MODE_PICKER:title=navigator.mozL10n.get('picker-title');break;}
|
||||
if(title){TitleBar.changeTitleText(title);}
|
||||
TitleBar.view.hidden=this.currentMode===MODE_SEARCH_FROM_TILES||this.currentMode===MODE_SEARCH_FROM_LIST;},updatePlayerIcon:function(){var isPlayerMode=(this.currentMode===MODE_PLAYER);var isPlayerQueued=(this.views[MODE_PLAYER].isLoaded&&PlayerView.isQueued);TitleBar.playerIcon.hidden=isPlayerMode||!isPlayerQueued;if(App.pendingPick){TitleBar.doneButton.hidden=!isPlayerMode;}},_updateTabs:function(){var hideTabs=this.currentMode===MODE_PLAYER||this.currentMode===MODE_SEARCH_FROM_TILES||this.currentMode===MODE_SEARCH_FROM_LIST;TabBar.view.classList.toggle('away',hideTabs);},_updateMode:function(callback){this._updateBackArrow();this.updateTitle();this.updatePlayerIcon();this._updateTabs();this._enableNFCSharing((this.currentMode===MODE_PLAYER));if(callback){callback();}},_enableNFCSharing:function(enabled){if(!navigator.mozNfc){return;}
|
||||
if(enabled&&!App.pendingPick){navigator.mozNfc.onpeerready=function(event){var peer=event.peer;if(peer){peer.sendFile(PlayerView.playingBlob);}};}else{navigator.mozNfc.onpeerready=null;}}};
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
;'use strict';var TYPE_LIST='list';var TYPE_SINGLE='single';var PLAYSTATUS_STOPPED='STOPPED';var PLAYSTATUS_PLAYING='PLAYING';var PLAYSTATUS_PAUSED='PAUSED';var PLAYSTATUS_FWD_SEEK='FWD_SEEK';var PLAYSTATUS_REV_SEEK='REV_SEEK';var INTERRUPT_BEGIN='mozinterruptbegin';var PlayerView={_repeatModes:['off','list','song'],get view(){return document.getElementById('views-player');},get audio(){return document.getElementById('player-audio');},get foreCoverImage(){return document.querySelector('.cover-image.visible');},get backCoverImage(){return document.querySelector('.cover-image:not(.visible)');},get playStatus(){return this._playStatus;},set playStatus(val){this._playStatus=val;},get isQueued(){return Boolean(this.queue&&this.queue.length);},init:function pv_init(type=TYPE_LIST,autoplay=true){this.artist=document.getElementById('player-cover-artist');this.album=document.getElementById('player-cover-album');this.artistText=document.querySelector('#player-cover-artist bdi');this.albumText=document.querySelector('#player-cover-album bdi');this.timeoutID;this.cover=document.getElementById('player-cover');this.coverImageURL=null;this.shareButton=document.getElementById('player-cover-share');this.repeatButton=document.getElementById('player-album-repeat');this.shuffleButton=document.getElementById('player-album-shuffle');this.ratings=document.getElementById('player-album-rating').children;this.seekSlider=document.getElementById('player-seek');this.seekRegion=document.getElementById('player-seek-bar');this.seekBar=document.getElementById('player-seek-bar-progress');this.seekIndicator=document.getElementById('player-seek-bar-indicator');this.seekElapsed=document.getElementById('player-seek-elapsed');this.seekRemaining=document.getElementById('player-seek-remaining');this.playControl=document.getElementById('player-controls-play');this.previousControl=document.getElementById('player-controls-previous');this.nextControl=document.getElementById('player-controls-next');this.banner=document.getElementById('info-banner');this.isTouching=false;this.isFastSeeking=false;this.playStatus=PLAYSTATUS_STOPPED;this.pausedPosition=null;this.handle=null;this.currentFileInfo=null;this.playingBlob=null;this.setSeekBar(0,0);this.intervalID=null;this.view.addEventListener('click',this);this.view.addEventListener('contextmenu',this);this.seekRegion.addEventListener('touchstart',this);this.seekRegion.addEventListener('touchmove',this);this.seekRegion.addEventListener('touchend',this);this.seekSlider.addEventListener('keypress',this);this.previousControl.addEventListener('touchend',this);this.nextControl.addEventListener('touchend',this);this.audio.addEventListener('play',this);this.audio.addEventListener('pause',this);this.audio.addEventListener('playing',this);this.audio.addEventListener('durationchange',this);this.audio.addEventListener('timeupdate',this);this.audio.addEventListener('ended',this);this.audio.addEventListener('mozinterruptbegin',this);this.audio.addEventListener('mozinterruptend',this);window.addEventListener('visibilitychange',this);window.addEventListener('storage',this._handleInterpageMessage.bind(this));navigator.mozL10n.ready(this.updateL10n.bind(this));this.type=type;this.autoplay=autoplay;if(this.type===TYPE_SINGLE){this.shuffleButton.disabled=true;this.repeatButton.disabled=true;this.previousControl.disabled=true;this.nextControl.disabled=true;}},checkSCOStatus:function pv_checkSCOStatus(){if(typeof MusicComms!=='undefined'&&MusicComms.enabled){var SCOStatus=MusicComms.isSCOEnabled;this.playControl.disabled=this.previousControl.disabled=this.nextControl.disabled=SCOStatus;this.seekRegion.parentNode.classList.toggle('disabled',SCOStatus);this.banner.classList.toggle('visible',SCOStatus);}},clean:function pv_clean(){if(this.handle){Database.cancelEnumeration(this.handle);}
|
||||
this.queue=null;this.playingBlob=null;this.coverImageURL=null;this.foreCoverImage.style.backgroundImage='';},activate:function pv_activate(queue){this.queue=queue;this.shuffleButton.disabled=(this.queue.length<2);this.setRepeat(PlaybackQueue.repeat);this.setShuffle(PlaybackQueue.shuffle);},showInfo:function pv_showInfo(){this.cover.classList.add('slideOut');if(this.timeoutID){window.clearTimeout(this.timeoutID);}
|
||||
this.timeoutID=window.setTimeout(function pv_hideInfo(){this.cover.classList.remove('slideOut');}.bind(this),5000);},setInfo:function pv_setInfo(fileinfo){this.currentFileInfo=fileinfo;var metadata=fileinfo.metadata;if(typeof ModeManager!=='undefined'){ModeManager.playerTitle=metadata.title;ModeManager.updateTitle();if(metadata.locked||this.type===TYPE_SINGLE){this.shareButton.classList.add('hidden');this.artist.classList.add('hidden-cover-share');this.album.classList.add('hidden-cover-share');}else{this.shareButton.classList.remove('hidden');this.artist.classList.remove('hidden-cover-share');this.album.classList.remove('hidden-cover-share');}}else{var titleBar=document.querySelector('#title-text bdi');titleBar.textContent=metadata.title||navigator.mozL10n.get('unknownTitle');titleBar.dataset.l10nId=metadata.title?'':'unknownTitle';}
|
||||
this.artistText.textContent=metadata.artist||navigator.mozL10n.get('unknownArtist');this.artistText.dataset.l10nId=metadata.artist?'':'unknownArtist';this.albumText.textContent=metadata.album||navigator.mozL10n.get('unknownAlbum');this.albumText.dataset.l10nId=metadata.album?'':'unknownAlbum';this.setCoverImage(fileinfo);},setCoverImage:function pv_setCoverImage(fileinfo){LazyLoader.load('js/metadata/album_art_cache.js').then(()=>{return AlbumArtCache.getFullSizeURL(fileinfo);}).then((url)=>{if(this.coverImageURL!==url){this.coverImageURL=url;var back=this.backCoverImage,fore=this.foreCoverImage;back.style.backgroundImage='url('+url+')';back.classList.add('visible');fore.classList.remove('visible');}});},getRepeat:function pv_getRepeat(){return this._repeatModes.indexOf(this.repeatButton.value);},setRepeat:function pv_setRepeat(value){this.repeatButton.value=this._repeatModes[value];this.repeatButton.setAttribute('data-l10n-id','repeat-'+this._repeatModes[value]);},setShuffle:function pv_setShuffle(value){this.shuffleButton.classList.toggle('shuffle-on',value);this.shuffleButton.setAttribute('aria-pressed',value);},setRatings:function pv_setRatings(rated){for(var i=0;i<5;i++){var rating=this.ratings[i];if(i===rated-1){rating.setAttribute('aria-checked',true);}else{rating.setAttribute('aria-checked',false);}
|
||||
if(i<rated){rating.classList.add('star-on');}else{rating.classList.remove('star-on');}}},setAudioSrc:function pv_setAudioSrc(file){var url=URL.createObjectURL(file);this.playingBlob=file;this.audio.removeAttribute('src');this.audio.load();this.audio.mozAudioChannelType='content';this.audio.src=url;this.audio.load();this.audio.play();this.audio.onloadeddata=function(evt){URL.revokeObjectURL(url);};this.audio.onerror=(function(evt){if(this.onerror){this.onerror(evt);}}).bind(this);this.setSeekBar(0,0);},updateRemoteMetadata:function pv_updateRemoteMetadata(){if(typeof MusicComms==='undefined'||this.queue.length===0){return;}
|
||||
var metadata=this.currentFileInfo.metadata;var notifyMetadata={title:metadata.title||navigator.mozL10n.get('unknownTitle'),artist:metadata.artist||navigator.mozL10n.get('unknownArtist'),album:metadata.album||navigator.mozL10n.get('unknownAlbum'),duration:this.audio.duration*1000,mediaNumber:this.rawIndex+1,totalMediaCount:this.queue.length};if(this.audio.currentTime===0){LazyLoader.load('js/metadata/album_art_cache.js').then(()=>{return AlbumArtCache.getThumbnailBlob(this.currentFileInfo);}).then((blob)=>{notifyMetadata.picture=blob;MusicComms.notifyMetadataChanged(notifyMetadata);});}else{MusicComms.notifyMetadataChanged(notifyMetadata);}},updateRemotePlayStatus:function pv_updateRemotePlayStatus(){if(typeof MusicComms==='undefined'){return;}
|
||||
var position=this.pausedPosition?this.pausedPosition:this.audio.currentTime;var info={playStatus:this.playStatus,duration:this.audio.duration*1000,position:position*1000};this.pausedPosition=(this.playStatus===PLAYSTATUS_PLAYING)?null:this.audio.currentTime;MusicComms.notifyStatusChanged(info);},PLAYER_IS_OCCUPIED_BY:'music-player-is-occupied-by',_handleInterpageMessage:function(evt){if(evt.key===this.PLAYER_IS_OCCUPIED_BY){if(evt.newValue&&evt.newValue!==location.href){this.pause();}}},_sendInterpageMessage:function(){window.localStorage.setItem(this.PLAYER_IS_OCCUPIED_BY,location.href);},_clearInterpageMessage:function(){var whoIsPlaying=window.localStorage.getItem(this.PLAYER_IS_OCCUPIED_BY);if(whoIsPlaying&&whoIsPlaying===window.location.href){window.localStorage.removeItem(this.PLAYER_IS_OCCUPIED_BY);}},start:function pv_start(autoplay=true){this.checkSCOStatus();this._sendInterpageMessage();this.showInfo();this.queue.current().then((songData)=>{this.setInfo(songData);if(songData.blob){return songData.blob;}
|
||||
this.setRatings(songData.metadata.rated);Database.incrementPlayCount(songData);return Database.getFile(songData,true);}).then((file)=>{this.setAudioSrc(file);if(!autoplay||!this.autoplay||MusicComms.isSCOEnabled){this.pause();}}).catch(function(msg){console.error(msg);});},play:function pv_play(targetIndex){this.checkSCOStatus();this._sendInterpageMessage();this.showInfo();if(this.audio.duration>20&&this.audio.duration-this.audio.currentTime<1){this.next(true);}else{this.audio.play();}},pause:function pv_pause(){this.checkSCOStatus();this._clearInterpageMessage();this.audio.pause();},stop:function pv_stop(){this.pause();this.audio.removeAttribute('src');this.audio.load();this.clean();if(typeof ModeManager!=='undefined'){ModeManager.playerTitle=null;if(ModeManager.currentMode===MODE_PLAYER){ModeManager.pop();}else{ModeManager.updateTitle();ModeManager.updatePlayerIcon();}}
|
||||
this.playStatus=PLAYSTATUS_STOPPED;this.updateRemotePlayStatus();},next:function pv_next(isAutomatic){if(this.type===TYPE_SINGLE){this.start(false);}else if(this.queue.next(isAutomatic)){this.start();}else{this.stop();}},previous:function pv_previous(){if(this.audio.currentTime<=3){this.queue.previous();}
|
||||
this.start();},startFastSeeking:function pv_startFastSeeking(direction){this.isTouching=this.isFastSeeking=true;var offset=direction*2;this.prevPlayStatus=this.playStatus;this.playStatus=direction?PLAYSTATUS_FWD_SEEK:PLAYSTATUS_REV_SEEK;this.updateRemotePlayStatus();this.intervalID=window.setInterval(function(){this.seekAudio(this.audio.currentTime+offset);}.bind(this),15);},stopFastSeeking:function pv_stopFastSeeking(){this.isTouching=this.isFastSeeking=false;if(this.intervalID){window.clearInterval(this.intervalID);}
|
||||
this.playStatus=this.prevPlayStatus;this.prevPlayStatus=null;this.updateRemotePlayStatus();},updateSeekBar:function pv_updateSeekBar(){if(this.isTouching){return;}
|
||||
if(typeof ModeManager==='undefined'||ModeManager.currentMode===MODE_PLAYER){this.seekAudio();}},seekAudio:function pv_seekAudio(seekTime){if(seekTime!==undefined){this.audio.currentTime=Math.floor(seekTime);}
|
||||
this.setSeekBar(this.audio.duration,this.audio.currentTime);},setSeekBar:function pv_setSeekBar(endTime,currentTime){if(this.seekBar.max!=endTime){navigator.mozL10n.setAttributes(this.seekSlider,'playbackSeekBar',{'duration':formatTime(endTime)});}
|
||||
this.seekBar.max=isFinite(endTime)?endTime:0;this.seekBar.value=currentTime;var formattedCurrentTime=formatTime(currentTime);this.seekSlider.setAttribute('aria-valuetext',formattedCurrentTime);this.seekSlider.setAttribute('aria-valuemax',this.seekBar.max);this.seekSlider.setAttribute('aria-valuenow',currentTime);var ratio=(isFinite(endTime)&&endTime!==0)?(currentTime/endTime):0;var x=(ratio*this.seekBar.offsetWidth-
|
||||
this.seekIndicator.offsetWidth/2);if(this.isLTR){x=x+'px';}else{if(x<0){x=Math.abs(x)+'px';}else{x='-'+x+'px';}}
|
||||
this.seekIndicator.style.transform='translateX('+x+')';this.seekElapsed.textContent=formattedCurrentTime;var remainingTime=endTime-currentTime;this.seekRemaining.textContent=(remainingTime>0)?'-'+formatTime(remainingTime):'---:--';},share:function pv_shareFile(){var songData=this.currentFileInfo;if(songData.metadata.locked){return;}
|
||||
LazyLoader.load('js/metadata/album_art_cache.js').then(()=>{return Promise.all([Database.getFile(songData),AlbumArtCache.getThumbnailBlob(songData)]);}).then(function([file,pictureBlob]){var filename=songData.name,name=filename.substring(filename.lastIndexOf('/')+1);var activityData={type:'audio/*',number:1,blobs:[file],filenames:[name],filepaths:[filename],metadata:[{title:songData.metadata.title,artist:songData.metadata.artist,album:songData.metadata.album,picture:pictureBlob}]};if(PlayerView.playStatus!==PLAYSTATUS_PLAYING){var a=new MozActivity({name:'share',data:activityData});a.onerror=function(e){console.warn('share activity error:',a.error.name);};}
|
||||
else{(function(){var hack_activity_property='_hack_hack_shut_up';var hack_setting_property='music._hack.pause_please';navigator.mozSettings.addObserver(hack_setting_property,observer);activityData[hack_activity_property]=hack_setting_property;var a=new MozActivity({name:'share',data:activityData});a.onerror=a.onsuccess=cleanup;function observer(e){PlayerView.pause();}
|
||||
function cleanup(){navigator.mozSettings.removeObserver(hack_setting_property,observer);if(PlayerView.playStatus===PLAYSTATUS_PAUSED){PlayerView.play();}}}());}});},handleEvent:function pv_handleEvent(evt){var target=evt.target;if(!target){return;}
|
||||
switch(evt.type){case'click':switch(target.id){case'player-cover':case'player-cover-image-1':case'player-cover-image-2':this.showInfo();break;case'player-controls-play':if(this.playControl.classList.contains('is-pause')){this.play();}else{this.pause();}
|
||||
break;case'player-album-repeat':this.showInfo();var repeat=PlaybackQueue.Repeat.next(this.getRepeat());PlaybackQueue.repeat=repeat;this.setRepeat(repeat);break;case'player-album-shuffle':this.showInfo();var shuffle=!target.classList.contains('shuffle-on');PlaybackQueue.shuffle=shuffle;this.setShuffle(shuffle);break;case'player-cover-share':this.share();break;}
|
||||
if(target.dataset.rating){this.showInfo();var songData=this.currentFileInfo;var targetRating=parseInt(target.dataset.rating,10);var newRating=(targetRating===songData.metadata.rated)?targetRating-1:targetRating;Database.setSongRating(songData,newRating);this.setRatings(newRating);}
|
||||
break;case'play':this.playControl.classList.remove('is-pause');this.playStatus=PLAYSTATUS_PLAYING;this.playControl.setAttribute('data-l10n-id','playbackPause');this.updateRemotePlayStatus();break;case'pause':this.playControl.classList.add('is-pause');this.playStatus=PLAYSTATUS_PAUSED;this.playControl.setAttribute('data-l10n-id','playbackPlay');this.updateRemotePlayStatus();break;case'touchstart':case'touchmove':if(evt.type==='touchstart'){this.isTouching=true;this.seekIndicator.classList.add('highlight');}
|
||||
if(this.isTouching&&this.audio.duration>0){var touch=evt.touches[0];var x=(touch.clientX-target.offsetLeft)/target.offsetWidth;if(x<0){x=0;}
|
||||
if(x>1){x=1;}
|
||||
if(this.isLTR){this.seekTime=x*this.seekBar.max;}else{this.seekTime=this.audio.duration-x*this.seekBar.max;}
|
||||
this.setSeekBar(this.audio.duration,this.seekTime);}
|
||||
break;case'touchend':if(this.isFastSeeking){this.stopFastSeeking();}else if(target.id==='player-seek-bar'){this.seekIndicator.classList.remove('highlight');if(this.audio.duration>0&&this.isTouching){this.seekAudio(this.seekTime);this.seekTime=0;}
|
||||
this.isTouching=false;}else if(target.id==='player-controls-previous'){this.previous();}else if(target.id==='player-controls-next'){this.next();}
|
||||
break;case'keypress':var step=Math.max(this.audio.duration/20,2);if(evt.keyCode==evt.DOM_VK_DOWN){this.seekAudio(this.audio.currentTime-step);}else if(evt.keyCode==evt.DOM_VK_UP){this.seekAudio(this.audio.currentTime+step);}
|
||||
break;case'contextmenu':if(target.id==='player-controls-next'){this.startFastSeeking(1);}else if(target.id==='player-controls-previous'){this.startFastSeeking(-1);}
|
||||
break;case'durationchange':case'timeupdate':this.updateSeekBar();if(evt.type==='durationchange'||this.audio.currentTime===0){this.updateRemoteMetadata();}
|
||||
break;case'ended':this.next(true);break;case'visibilitychange':if(document.hidden){this.audio.removeEventListener('timeupdate',this);}else{this.audio.addEventListener('timeupdate',this);this.updateSeekBar();}
|
||||
break;case'mozinterruptbegin':this.playStatus=INTERRUPT_BEGIN;this.updateRemotePlayStatus();break;case'mozinterruptend':this.playStatus=PLAYSTATUS_PLAYING;this.updateRemotePlayStatus();break;default:return;}},updateL10n:function pv_updateL10n(){this.isLTR=navigator.mozL10n.language.direction==='ltr'?true:false;}};
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
/* exported SearchView */
|
||||
/* global App, createListElement, Database, ListView, ModeManager, MODE_LIST,
|
||||
MODE_PICKER, MODE_PLAYER, MODE_SEARCH_FROM_TILES, MODE_SUBLIST,
|
||||
MODE_TILES, PlaybackQueue, PlayerView, SubListView, TabBar,
|
||||
TilesView */
|
||||
'use strict';
|
||||
|
||||
var SearchView = {
|
||||
context: {
|
||||
ALL: 'ALL',
|
||||
ARTISTS: 'ARTISTS',
|
||||
ALBUMS: 'ALBUMS',
|
||||
SONGS: 'SONGS',
|
||||
},
|
||||
|
||||
searchContext: 'ALL',
|
||||
|
||||
get view() {
|
||||
return document.getElementById('views-search');
|
||||
},
|
||||
|
||||
get searchInput() {
|
||||
return document.getElementById('views-search-input');
|
||||
},
|
||||
|
||||
get searchArtistsView() {
|
||||
return document.getElementById('views-search-artists');
|
||||
},
|
||||
|
||||
get searchAlbumsView() {
|
||||
return document.getElementById('views-search-albums');
|
||||
},
|
||||
|
||||
get searchTitlesView() {
|
||||
return document.getElementById('views-search-titles');
|
||||
},
|
||||
|
||||
showNoResult: function sv_showNoResult(show) {
|
||||
var view = document.getElementById('views-search-no-result');
|
||||
view.classList.toggle('hidden', !show);
|
||||
},
|
||||
|
||||
init: function sv_init() {
|
||||
this.dataSource = [];
|
||||
this.searchHandles = { artist: null, album: null, title: null };
|
||||
|
||||
this.view.addEventListener('click', this);
|
||||
this.view.addEventListener('touchend', this);
|
||||
this.searchInput.addEventListener('focus', this);
|
||||
this.searchInput.addEventListener('input', this);
|
||||
this.searchInput.addEventListener('keypress', this);
|
||||
},
|
||||
|
||||
switchContext: function sv_switchContext() {
|
||||
switch (TabBar.option) {
|
||||
case 'mix':
|
||||
case 'playlist':
|
||||
this.searchContext = SearchView.context.ALL;
|
||||
break;
|
||||
case 'artist':
|
||||
this.searchContext = SearchView.context.ARTISTS;
|
||||
break;
|
||||
case 'album':
|
||||
this.searchContext = SearchView.context.ALBUMS;
|
||||
break;
|
||||
case 'title':
|
||||
this.searchContext = SearchView.context.SONGS;
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
search: function sv_search(query) {
|
||||
this.clear();
|
||||
if (!query) {
|
||||
return;
|
||||
}
|
||||
|
||||
var lists = { artist: this.searchArtistsView,
|
||||
album: this.searchAlbumsView,
|
||||
title: this.searchTitlesView };
|
||||
var numResults = { artist: 0, album: 0, title: 0 };
|
||||
|
||||
function sv_showResult(option, result) {
|
||||
/* jshint validthis:true */
|
||||
if (result === null) {
|
||||
this.searchHandles[option] = null;
|
||||
var totalFound = numResults.artist + numResults.album +
|
||||
numResults.title;
|
||||
this.showNoResult(totalFound === 0);
|
||||
return;
|
||||
}
|
||||
|
||||
this.dataSource.push(result);
|
||||
numResults[option]++;
|
||||
lists[option].classList.remove('hidden');
|
||||
lists[option].getElementsByClassName('search-result-count')[0]
|
||||
.textContent = numResults[option];
|
||||
lists[option].getElementsByClassName('search-results')[0].appendChild(
|
||||
createListElement(option, result, this.dataSource.length - 1, query)
|
||||
);
|
||||
this.showNoResult(false);
|
||||
}
|
||||
|
||||
// Only shows the search results of tracks when it's in picker mode
|
||||
if (!App.pendingPick) {
|
||||
if (this.searchContext === this.context.ALL ||
|
||||
this.searchContext === this.context.ARTISTS) {
|
||||
this.searchHandles.artist = Database.search(
|
||||
'artist', query, sv_showResult.bind(this, 'artist')
|
||||
);
|
||||
}
|
||||
if (this.searchContext === this.context.ALL ||
|
||||
this.searchContext === this.context.ALBUMS) {
|
||||
this.searchHandles.album = Database.search(
|
||||
'album', query, sv_showResult.bind(this, 'album')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.searchContext === this.context.ALL ||
|
||||
this.searchContext === this.context.SONGS) {
|
||||
this.searchHandles.title = Database.search(
|
||||
'title', query, sv_showResult.bind(this, 'title')
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
clear: function sv_clear() {
|
||||
for (var option in this.searchHandles) {
|
||||
var handle = this.searchHandles[option];
|
||||
if (handle) {
|
||||
Database.cancelEnumeration(handle);
|
||||
}
|
||||
}
|
||||
|
||||
var views = [this.searchArtistsView, this.searchAlbumsView,
|
||||
this.searchTitlesView];
|
||||
views.forEach(function(view) {
|
||||
view.getElementsByClassName('search-results')[0].innerHTML = '';
|
||||
view.classList.add('hidden');
|
||||
});
|
||||
this.showNoResult(false);
|
||||
this.dataSource = [];
|
||||
},
|
||||
|
||||
hide: function sv_hide() {
|
||||
this.searchInput.value = '';
|
||||
this.clear();
|
||||
|
||||
if (ModeManager.currentMode === MODE_SEARCH_FROM_TILES) {
|
||||
ModeManager.start(MODE_TILES, () => {
|
||||
TilesView.hideSearch();
|
||||
});
|
||||
} else {
|
||||
ModeManager.start(App.pendingPick ? MODE_PICKER : MODE_LIST, () => {
|
||||
ListView.hideSearch();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
openResult: function sv_openResult(target) {
|
||||
var index = target.dataset.index;
|
||||
|
||||
var option = target.dataset.option;
|
||||
var keyRange = target.dataset.keyRange;
|
||||
var data = this.dataSource[index];
|
||||
|
||||
if (option === 'title') {
|
||||
ModeManager.push(MODE_PLAYER, () => {
|
||||
PlayerView.activate(new PlaybackQueue.StaticQueue([data]));
|
||||
PlayerView.start();
|
||||
});
|
||||
} else {
|
||||
// SubListView needs to prepare the songs data before entering it,
|
||||
// So here we initialize the SubListView before push the view.
|
||||
ModeManager.waitForView(MODE_SUBLIST, () => {
|
||||
SubListView.activate(option, data, index, keyRange, 'next', () => {
|
||||
ModeManager.push(MODE_SUBLIST);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleEvent: function sv_handleEvent(evt) {
|
||||
var target = evt.target;
|
||||
switch (evt.type) {
|
||||
case 'touchend':
|
||||
if (target.id === 'views-search-clear') {
|
||||
evt.preventDefault();
|
||||
this.searchInput.value = '';
|
||||
this.clear();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'click':
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.id === 'views-search-close') {
|
||||
evt.preventDefault();
|
||||
this.hide();
|
||||
}
|
||||
|
||||
if (target.dataset.index) {
|
||||
this.openResult(target);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'focus':
|
||||
this.switchContext();
|
||||
break;
|
||||
|
||||
case 'input':
|
||||
if (target.id === 'views-search-input') {
|
||||
this.search(target.value);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'keypress':
|
||||
if (target.id === 'views-search-input') {
|
||||
if (evt.keyCode === evt.DOM_VK_RETURN) {
|
||||
evt.preventDefault();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
/* exported SubListView */
|
||||
/* global AlbumArtCache, createListElement, Database, LazyLoader, ModeManager,
|
||||
MODE_PLAYER, PlaybackQueue, PlayerView, showImage, TabBar */
|
||||
'use strict';
|
||||
|
||||
var SubListView = {
|
||||
unknownNameL10nIds: {
|
||||
'artist': 'unknownArtist',
|
||||
'album': 'unknownAlbum',
|
||||
'title': 'unknownTitle'
|
||||
},
|
||||
|
||||
get view() {
|
||||
return document.getElementById('views-sublist');
|
||||
},
|
||||
|
||||
get dataSource() {
|
||||
return this._dataSource;
|
||||
},
|
||||
|
||||
get anchor() {
|
||||
return document.getElementById('views-sublist-anchor');
|
||||
},
|
||||
|
||||
get headerImage() {
|
||||
return document.getElementById('views-sublist-header-image');
|
||||
},
|
||||
|
||||
get headerName() {
|
||||
return document.getElementById('views-sublist-header-name');
|
||||
},
|
||||
|
||||
get playAllButton() {
|
||||
return document.getElementById('views-sublist-controls-play');
|
||||
},
|
||||
|
||||
get shuffleButton() {
|
||||
return document.getElementById('views-sublist-controls-shuffle');
|
||||
},
|
||||
|
||||
set dataSource(source) {
|
||||
this._dataSource = source;
|
||||
|
||||
// At the same time we also check how many songs in an album
|
||||
// Shuffle button is not necessary when an album only contains one song
|
||||
this.shuffleButton.disabled = (this._dataSource.length < 2);
|
||||
},
|
||||
|
||||
init: function slv_init() {
|
||||
this.handle = null;
|
||||
this.dataSource = [];
|
||||
this.index = 0;
|
||||
|
||||
this.view.addEventListener('click', this);
|
||||
},
|
||||
|
||||
clean: function slv_clean() {
|
||||
// Cancel a pending enumeration before start a new one
|
||||
if (this.handle) {
|
||||
Database.cancelEnumeration(this.handle);
|
||||
}
|
||||
|
||||
this.dataSource = [];
|
||||
this.index = 0;
|
||||
this.anchor.innerHTML = '';
|
||||
this.view.scrollTop = 0;
|
||||
},
|
||||
|
||||
setHeaderImage: function slv_setHeaderImage(fileinfo) {
|
||||
// See if we are viewing the predefined playlists, if so, then replace the
|
||||
// fileinfo with the first record in the dataSource to display the first
|
||||
// album art for every predefined playlist.
|
||||
if (TabBar.playlistArray.indexOf(fileinfo) !== -1) {
|
||||
fileinfo = this.dataSource[0];
|
||||
}
|
||||
|
||||
LazyLoader.load('js/metadata/album_art_cache.js').then(() => {
|
||||
return AlbumArtCache.getThumbnailURL(fileinfo);
|
||||
}).then((url) => {
|
||||
showImage(this.albumImage, url);
|
||||
});
|
||||
},
|
||||
|
||||
setHeaderName: function slv_setHeaderName(name, l10nId) {
|
||||
this.headerName.textContent = name;
|
||||
this.headerName.dataset.l10nId = l10nId;
|
||||
},
|
||||
|
||||
activate: function(option, data, index, keyRange, direction, callback) {
|
||||
var targetOption = (option === 'date') ? option : 'metadata.' + option;
|
||||
this.clean();
|
||||
|
||||
this.handle = Database.enumerateAll(targetOption, keyRange, direction,
|
||||
(dataArray) => {
|
||||
var headerName;
|
||||
var headerNameL10nId = '';
|
||||
var maxDiscNum = 1;
|
||||
|
||||
if (option === 'album') {
|
||||
dataArray.sort(function(e1, e2) {
|
||||
return (e1.metadata.discnum - e2.metadata.discnum) ||
|
||||
(e1.metadata.tracknum - e2.metadata.tracknum);
|
||||
});
|
||||
|
||||
maxDiscNum = Math.max(
|
||||
dataArray[dataArray.length - 1].metadata.disccount,
|
||||
dataArray[dataArray.length - 1].metadata.discnum
|
||||
);
|
||||
}
|
||||
|
||||
var inPlaylist = (option !== 'artist' &&
|
||||
option !== 'album' &&
|
||||
option !== 'title');
|
||||
|
||||
headerName = data.metadata[inPlaylist ? 'title' : option];
|
||||
if (!headerName) {
|
||||
headerNameL10nId = this.unknownNameL10nIds[option];
|
||||
headerName = navigator.mozL10n.get(headerNameL10nId);
|
||||
}
|
||||
|
||||
// Overrides l10nId.
|
||||
if (data.metadata.l10nId) {
|
||||
headerNameL10nId = data.metadata.l10nId;
|
||||
}
|
||||
|
||||
this.dataSource = dataArray;
|
||||
this.setHeaderName(headerName, headerNameL10nId);
|
||||
this.setHeaderImage(data);
|
||||
|
||||
dataArray.forEach((songData) => {
|
||||
songData.multidisc = (maxDiscNum > 1);
|
||||
this.update(songData, inPlaylist);
|
||||
});
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Set inPlaylist to true if you want the index instead of the track #
|
||||
// By default it is the track #
|
||||
update: function slv_update(result, useIndexNumber) {
|
||||
if (result === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var option = useIndexNumber ? 'song-index' : 'song';
|
||||
this.anchor.appendChild(createListElement(option, result, this.index));
|
||||
|
||||
this.index++;
|
||||
},
|
||||
|
||||
handleEvent: function slv_handleEvent(evt) {
|
||||
var target = evt.target;
|
||||
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (evt.type) {
|
||||
case 'click':
|
||||
if (target === this.shuffleButton || target === this.playAllButton) {
|
||||
ModeManager.push(MODE_PLAYER, () => {
|
||||
PlayerView.clean();
|
||||
PlaybackQueue.shuffle = (target === this.shuffleButton);
|
||||
PlayerView.activate(new PlaybackQueue.StaticQueue(this.dataSource));
|
||||
PlayerView.start();
|
||||
});
|
||||
} else if (target.dataset.index) {
|
||||
ModeManager.push(MODE_PLAYER, () => {
|
||||
PlayerView.clean();
|
||||
var targetIndex = parseInt(target.dataset.index, 10);
|
||||
PlayerView.activate(new PlaybackQueue.StaticQueue(
|
||||
this.dataSource, targetIndex
|
||||
));
|
||||
PlayerView.start();
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
/* exported TilesView */
|
||||
/* global AlbumArtCache, Database, IDBKeyRange, LazyLoader, ModeManager,
|
||||
MODE_PLAYER, MODE_SEARCH_FROM_TILES, PlaybackQueue, PlayerView,
|
||||
SearchView, showImage */
|
||||
'use strict';
|
||||
|
||||
var TilesView = {
|
||||
get view() {
|
||||
return document.getElementById('views-tiles');
|
||||
},
|
||||
|
||||
get anchor() {
|
||||
return document.getElementById('views-tiles-anchor');
|
||||
},
|
||||
|
||||
get searchBox() {
|
||||
return document.getElementById('views-tiles-search');
|
||||
},
|
||||
|
||||
get searchInput() {
|
||||
return document.getElementById('views-tiles-search-input');
|
||||
},
|
||||
|
||||
get dataSource() {
|
||||
return this._dataSource;
|
||||
},
|
||||
|
||||
set dataSource(source) {
|
||||
this._dataSource = source;
|
||||
},
|
||||
|
||||
init: function tv_init() {
|
||||
this.handle = null;
|
||||
this.dataSource = [];
|
||||
this.index = 0;
|
||||
|
||||
this.view.addEventListener('click', this);
|
||||
this.searchInput.addEventListener('focus', this);
|
||||
},
|
||||
|
||||
clean: function tv_clean() {
|
||||
this.dataSource = [];
|
||||
this.index = 0;
|
||||
this.anchor.innerHTML = '';
|
||||
},
|
||||
|
||||
hideSearch: function tv_hideSearch() {
|
||||
this.searchInput.value = '';
|
||||
// XXX: we probably want to animate this...
|
||||
if (this.view.scrollTop < this.searchBox.offsetHeight) {
|
||||
this.view.scrollTop = this.searchBox.offsetHeight;
|
||||
}
|
||||
},
|
||||
|
||||
activate: function tv_activate(callback) {
|
||||
// Enumerate existing song entries in the database. List them all, and
|
||||
// sort them in ascending order by album. Use enumerateAll() here so that
|
||||
// we get all the results we want and then pass them synchronously to the
|
||||
// _addItem() function. If we did it asynchronously, then we'd get one
|
||||
// redraw for every song.
|
||||
|
||||
// Cancel a pending enumeration before starting a new one.
|
||||
if (this.handle) {
|
||||
Database.cancelEnumeration(this.handle);
|
||||
}
|
||||
|
||||
this.handle = Database.enumerateAll('metadata.album', null, 'nextunique',
|
||||
function(songs) {
|
||||
TilesView.clean();
|
||||
songs.forEach(function(song) {
|
||||
TilesView._addItem(song);
|
||||
});
|
||||
|
||||
// Display the TilesView once the UI has been populated.
|
||||
document.getElementById('views-tiles').classList.remove('hidden');
|
||||
|
||||
if (callback) {
|
||||
callback(songs);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_addItem: function tv_addItem(result) {
|
||||
this.dataSource.push(result);
|
||||
|
||||
var tile = document.createElement('div');
|
||||
tile.className = 'tile';
|
||||
|
||||
var container = document.createElement('div');
|
||||
container.className = 'tile-container';
|
||||
container.setAttribute('role', 'button');
|
||||
|
||||
var albumArt = document.createElement('img');
|
||||
albumArt.className = 'tile-album-art';
|
||||
container.appendChild(albumArt);
|
||||
|
||||
var titleBar = document.createElement('div');
|
||||
titleBar.className = 'tile-title-bar';
|
||||
var artistName = document.createElement('div');
|
||||
artistName.className = 'tile-title-artist';
|
||||
var artistNameText = document.createElement('bdi');
|
||||
artistName.appendChild(artistNameText);
|
||||
var albumName = document.createElement('div');
|
||||
albumName.className = 'tile-title-album';
|
||||
var albumNameText = document.createElement('bdi');
|
||||
albumName.appendChild(albumNameText);
|
||||
artistNameText.textContent =
|
||||
result.metadata.artist || navigator.mozL10n.get('unknownArtist');
|
||||
artistNameText.dataset.l10nId =
|
||||
result.metadata.artist ? '' : 'unknownArtist';
|
||||
albumNameText.textContent =
|
||||
result.metadata.album || navigator.mozL10n.get('unknownAlbum');
|
||||
albumNameText.dataset.l10nId = result.metadata.album ? '' : 'unknownAlbum';
|
||||
titleBar.appendChild(artistName);
|
||||
|
||||
// There are 6 tiles in one group
|
||||
// and the first tile is the main-tile
|
||||
// so we mod 6 to find out who is the main-tile
|
||||
if (this.index % 6 === 0) {
|
||||
tile.classList.add('main-tile');
|
||||
artistName.classList.add('main-tile-title');
|
||||
titleBar.appendChild(albumName);
|
||||
} else {
|
||||
tile.classList.add('sub-tile');
|
||||
artistName.classList.add('sub-tile-title');
|
||||
}
|
||||
|
||||
var index = this.index;
|
||||
LazyLoader.load('js/metadata/album_art_cache.js').then(() => {
|
||||
var NUM_INITIALLY_VISIBLE_TILES = 8;
|
||||
var INITIALLY_HIDDEN_TILE_WAIT_TIME_MS = 1000;
|
||||
|
||||
var setTileBackgroundClosure = function(url) {
|
||||
showImage(albumArt, url);
|
||||
};
|
||||
|
||||
if (index <= NUM_INITIALLY_VISIBLE_TILES) {
|
||||
// Load this tile's background now, because it's visible.
|
||||
AlbumArtCache.getThumbnailURL(result).then(setTileBackgroundClosure);
|
||||
} else {
|
||||
// Defer loading hidden tiles until the visible ones are done.
|
||||
setTimeout(function() {
|
||||
AlbumArtCache.getThumbnailURL(result).then(setTileBackgroundClosure);
|
||||
}, INITIALLY_HIDDEN_TILE_WAIT_TIME_MS);
|
||||
}
|
||||
});
|
||||
|
||||
container.dataset.index = this.index;
|
||||
|
||||
// The tile info(album/artist) shows only when the cover does not exist
|
||||
if (!result.metadata.picture) {
|
||||
container.appendChild(titleBar);
|
||||
} else {
|
||||
container.setAttribute('aria-label', artistName.textContent + ' ' +
|
||||
albumName.textContent);
|
||||
}
|
||||
|
||||
tile.appendChild(container);
|
||||
this.anchor.appendChild(tile);
|
||||
|
||||
this.index++;
|
||||
},
|
||||
|
||||
handleEvent: function tv_handleEvent(evt) {
|
||||
var target = evt.target;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (evt.type) {
|
||||
case 'click':
|
||||
if (target.id === 'views-tiles-search-close') {
|
||||
if (ModeManager.currentMode === MODE_SEARCH_FROM_TILES) {
|
||||
ModeManager.pop();
|
||||
}
|
||||
this.hideSearch();
|
||||
evt.preventDefault();
|
||||
} else if (target.dataset.index) {
|
||||
tv_playAlbum(this.dataSource[target.dataset.index],
|
||||
target.dataset.index);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'focus':
|
||||
if (target.id === 'views-tiles-search-input') {
|
||||
if (ModeManager.currentMode !== MODE_SEARCH_FROM_TILES) {
|
||||
ModeManager.start(MODE_SEARCH_FROM_TILES, function() {
|
||||
// Let the search view gets the focus.
|
||||
SearchView.searchInput.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
function tv_playAlbum(data, index) {
|
||||
var key = 'metadata.album';
|
||||
var range = IDBKeyRange.only(data.metadata.album);
|
||||
var direction = 'next';
|
||||
|
||||
ModeManager.push(MODE_PLAYER, function() {
|
||||
PlayerView.clean();
|
||||
|
||||
// When an user tap an album on the tilesView
|
||||
// we have to get all the song data first
|
||||
// because the shuffle option might be ON
|
||||
// and we have create shuffled list and play in shuffle order
|
||||
Database.enumerateAll(key, range, direction, (dataArray) => {
|
||||
PlayerView.activate(new PlaybackQueue.StaticQueue(dataArray));
|
||||
PlayerView.start();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
Loading…
Reference in a new issue