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
430
gaia/dev_apps/music-oga/js/ui/views/list_view.js
Executable file
430
gaia/dev_apps/music-oga/js/ui/views/list_view.js
Executable 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
295
gaia/dev_apps/music-oga/js/ui/views/mode_manager.js
Executable file
295
gaia/dev_apps/music-oga/js/ui/views/mode_manager.js
Executable file
|
|
@ -0,0 +1,295 @@
|
|||
/* exported ModeManager */
|
||||
/* global App, LazyLoader, ListView, PlaybackQueue, PlayerView, SearchView,
|
||||
SubListView, TabBar, TilesView, TitleBar, TYPE_LIST, TYPE_SINGLE */
|
||||
'use strict';
|
||||
|
||||
// This Application has five modes: TILES, SEARCH, LIST, SUBLIST, and PLAYER
|
||||
// Search has two "modes", depending on whether we came from TILES or LIST.
|
||||
//
|
||||
// Before the Music app is launched we use display: none to hide the modes so
|
||||
// that Gecko will not try to apply CSS styles on those elements which seems are
|
||||
// actions that slows down the startup time we will remove display: none on
|
||||
// elements when we need to display them.
|
||||
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(() => {
|
||||
// Our view might have been loaded while we were waiting so check again.
|
||||
if (view.isLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the view's hidden style before pushing it.
|
||||
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) {
|
||||
// Don't autoplay in the pick activity.
|
||||
PlayerView.init(TYPE_SINGLE, false);
|
||||
} else {
|
||||
PlayerView.init(TYPE_LIST);
|
||||
}
|
||||
break;
|
||||
case 'views-search':
|
||||
SearchView.init();
|
||||
break;
|
||||
}
|
||||
|
||||
// The PlaybackQueue needs to load the settings from asyncStorage before
|
||||
// we can use it.
|
||||
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 doesn't exist, that should be the first time launch
|
||||
// so we can just ignore changeTitleText()
|
||||
// because the title is already localized in HTML
|
||||
// And if title does exist, it should be the localized "Music"
|
||||
// so it will be just fine to update changeTitleText() again
|
||||
if (title) {
|
||||
TitleBar.changeTitleText(title);
|
||||
}
|
||||
|
||||
// Hide the title bar when music is in search modes.
|
||||
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;
|
||||
// We have to show the done button when we are in picker mode
|
||||
// and previewing the selecting song
|
||||
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();
|
||||
|
||||
// Music only share the playing file when it's in player mode.
|
||||
// Disable the NFC sharing when it's in the other modes.
|
||||
this._enableNFCSharing((this.currentMode === MODE_PLAYER));
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
|
||||
_enableNFCSharing: function(enabled) {
|
||||
if (!navigator.mozNfc) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (enabled && !App.pendingPick) {
|
||||
// Assign the sharing function to onpeerready so that it will trigger
|
||||
// the shrinking ui to share the playing file.
|
||||
navigator.mozNfc.onpeerready = function(event) {
|
||||
var peer = event.peer;
|
||||
if (peer) {
|
||||
peer.sendFile(PlayerView.playingBlob);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// The mozNfc api will check onpeerready, if it's null, then it will not
|
||||
// trigger the shrinking ui so it won't be able to share the file.
|
||||
navigator.mozNfc.onpeerready = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
894
gaia/dev_apps/music-oga/js/ui/views/player_view.js
Executable file
894
gaia/dev_apps/music-oga/js/ui/views/player_view.js
Executable file
|
|
@ -0,0 +1,894 @@
|
|||
/* exported PlayerView */
|
||||
/* global AlbumArtCache, Database, formatTime, LazyLoader, MusicComms,
|
||||
ModeManager, MODE_PLAYER, MozActivity, PlaybackQueue */
|
||||
'use strict';
|
||||
|
||||
// We have two types of the playing sources
|
||||
// These are for player to know which source type is playing
|
||||
var TYPE_LIST = 'list';
|
||||
var TYPE_SINGLE = 'single';
|
||||
|
||||
// AVRCP spec defined the statuses in capitalized and to be simple,
|
||||
// our player just use them instead of defining new constant strings.
|
||||
var PLAYSTATUS_STOPPED = 'STOPPED';
|
||||
var PLAYSTATUS_PLAYING = 'PLAYING';
|
||||
var PLAYSTATUS_PAUSED = 'PAUSED';
|
||||
var PLAYSTATUS_FWD_SEEK = 'FWD_SEEK';
|
||||
var PLAYSTATUS_REV_SEEK = 'REV_SEEK';
|
||||
// Interrupt begin and end are the statuses for audio channel,
|
||||
// they will be used when music app is interrupt by some other channels.
|
||||
var INTERRUPT_BEGIN = 'mozinterruptbegin';
|
||||
|
||||
// View of Player
|
||||
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); // Set 0 to default seek position
|
||||
this.intervalID = null;
|
||||
|
||||
this.view.addEventListener('click', this);
|
||||
this.view.addEventListener('contextmenu', this);
|
||||
|
||||
// Seeking audio too frequently causes the Desktop build hangs
|
||||
// A related Bug 739094 in Bugzilla
|
||||
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);
|
||||
// Listen to mozinterruptbegin and mozinterruptend for notifying the system
|
||||
// media playback widget to reflect the playing status.
|
||||
this.audio.addEventListener('mozinterruptbegin', this);
|
||||
this.audio.addEventListener('mozinterruptend', this);
|
||||
|
||||
// Listen to visiblitychange to know when to stop listening to the
|
||||
// 'timeupdate' event.
|
||||
window.addEventListener('visibilitychange', this);
|
||||
|
||||
// XXX: We use localstorage event as a workaround solution in music app
|
||||
// to resolve audio channel competetion between regular mode and pick mode.
|
||||
// This shouldn't have handled by music app itself. Remove the patch of
|
||||
// bug 894744 once we have better solution.
|
||||
window.addEventListener('storage', this._handleInterpageMessage.bind(this));
|
||||
|
||||
// Listen to language changes to update the language direction accordingly
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
||||
// When SCO is connected, music is unable to play sounds even it's in the
|
||||
// foreground, this is a limitation for 1.3, see bug 946556. To adapt this,
|
||||
// we regulate the controls to restrict some actions and hope it can give
|
||||
// better ux to the specific scenario.
|
||||
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() {
|
||||
// Cancel a pending enumeration before start a new one
|
||||
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);
|
||||
},
|
||||
|
||||
// This function is for the animation on the album art (cover).
|
||||
// The info (album, artist) will initially show up when a song being played,
|
||||
// if users does not tap the album art (cover) again,
|
||||
// then it will be disappeared after 5 seconds
|
||||
// however, if a user taps before 5 seconds ends,
|
||||
// then the timeout will be cleared to keep the info on screen.
|
||||
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;
|
||||
|
||||
// Handle the title bar and the share button when the player is not launched
|
||||
// by open activity.
|
||||
if (typeof ModeManager !== 'undefined') {
|
||||
ModeManager.playerTitle = metadata.title;
|
||||
ModeManager.updateTitle();
|
||||
|
||||
// If it is a locked music file, or if we are handling a Pick activity
|
||||
// then we should not give the user the option of sharing the file.
|
||||
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 {
|
||||
// we can't use TitleBar here as if we are in the activity
|
||||
// it will not be initialised.
|
||||
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;
|
||||
// Reset src before we set a new source to the audio element
|
||||
this.audio.removeAttribute('src');
|
||||
this.audio.load();
|
||||
// Add mozAudioChannelType to the player
|
||||
this.audio.mozAudioChannelType = 'content';
|
||||
this.audio.src = url;
|
||||
this.audio.load();
|
||||
|
||||
this.audio.play();
|
||||
// An object URL must be released by calling URL.revokeObjectURL()
|
||||
// when we no longer need them
|
||||
this.audio.onloadeddata = function(evt) { URL.revokeObjectURL(url); };
|
||||
this.audio.onerror = (function(evt) {
|
||||
if (this.onerror) {
|
||||
this.onerror(evt);
|
||||
}
|
||||
}).bind(this);
|
||||
// when play a new song, reset the seekBar first
|
||||
// this can prevent showing wrong duration
|
||||
// due to b2g cannot get some mp3's duration
|
||||
// and the seekBar can still show 00:00 to -00:00
|
||||
this.setSeekBar(0, 0);
|
||||
},
|
||||
|
||||
updateRemoteMetadata: function pv_updateRemoteMetadata() {
|
||||
// If MusicComms does not exist or queue is empty, we don't have to update
|
||||
// the metadata.
|
||||
if (typeof MusicComms === 'undefined' || this.queue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the playing information to AVRCP devices.
|
||||
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'),
|
||||
|
||||
// AVRCP expects the duration in ms, so we convert from s to ms.
|
||||
duration: this.audio.duration * 1000,
|
||||
mediaNumber: this.rawIndex + 1,
|
||||
totalMediaCount: this.queue.length
|
||||
};
|
||||
|
||||
// Grab the album art if this is a new song; otherwise, don't bother, since
|
||||
// listeners should already have the album art. Note: if no .picture
|
||||
// attribute is in the metadata, then listeners should reuse the previous
|
||||
// picture. If .picture is null, something went wrong and listeners should
|
||||
// probably use a blank picture (or their own placeholder).
|
||||
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 MusicComms does not exist then no need to update the play status.
|
||||
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
|
||||
};
|
||||
|
||||
// Before we resume the player, we need to keep the paused position
|
||||
// because once the connected A2DP device receives different positions
|
||||
// on AFTER paused and BEFORE playing, it will break the play/pause states
|
||||
// that the A2DP device kept.
|
||||
this.pausedPosition = (this.playStatus === PLAYSTATUS_PLAYING) ?
|
||||
null : this.audio.currentTime;
|
||||
|
||||
// Notify the remote device that status is changed.
|
||||
MusicComms.notifyStatusChanged(info);
|
||||
},
|
||||
|
||||
PLAYER_IS_OCCUPIED_BY: 'music-player-is-occupied-by',
|
||||
|
||||
_handleInterpageMessage: function(evt) {
|
||||
if (evt.key === this.PLAYER_IS_OCCUPIED_BY) {
|
||||
// if there is another page (different from the page we are at now)
|
||||
// going to play, stop the current one
|
||||
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 playing the current song in the queue.
|
||||
*
|
||||
* @param {Boolean} autoplay True if the song should immediately start playing
|
||||
* (unless we can't for some reason), false otherwise.
|
||||
*/
|
||||
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);
|
||||
|
||||
// We pause at the beginning when SCO is enabled. The user can still
|
||||
// select songs to play, but it won't start until the SCO is disconnected.
|
||||
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();
|
||||
|
||||
// The player must be paused, so let's resume it. However, if we're very
|
||||
// close to the end of the song (and if the song is not really short) then
|
||||
// just skip to the next song rather than finishing this one. (This works
|
||||
// around Bug 1157118 where if we're within a fraction of a second of the
|
||||
// end of a .m4a file, it takes a long time to get an ended event and move
|
||||
// to the next song.)
|
||||
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();
|
||||
// Player in open activity does not have ModeManager.
|
||||
if (typeof ModeManager !== 'undefined') {
|
||||
ModeManager.playerTitle = null;
|
||||
// To leave player mode and set the correct title to the TitleBar
|
||||
// we have to decide which mode we should back to when the player stops
|
||||
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 a song starts more than 3 (seconds),
|
||||
// when users click skip backward, it will restart the current song
|
||||
// otherwise just skip to the previous song
|
||||
if (this.audio.currentTime <= 3) {
|
||||
this.queue.previous();
|
||||
}
|
||||
|
||||
this.start();
|
||||
},
|
||||
|
||||
startFastSeeking: function pv_startFastSeeking(direction) {
|
||||
// direction can be 1 or -1, 1 means forward and -1 means rewind.
|
||||
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() {
|
||||
// Don't update the seekbar when the user is seeking.
|
||||
if (this.isTouching) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If ModeManager is undefined, then the music app is launched by the open
|
||||
// activity. Otherwise, only seek the audio when the mode is PLAYER because
|
||||
// updating the UI will slow down the other pages, such as the scrolling in
|
||||
// ListView.
|
||||
if (typeof ModeManager === 'undefined' ||
|
||||
ModeManager.currentMode === MODE_PLAYER) {
|
||||
this.seekAudio();
|
||||
}
|
||||
},
|
||||
|
||||
seekAudio: function pv_seekAudio(seekTime) {
|
||||
if (seekTime !== undefined) {
|
||||
// Because of bug 1119186, setting the currentTime to a value as same as
|
||||
// the audio.duration seems corrupts the audio element, so here we floor
|
||||
// the seek time to prevent it.
|
||||
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) {
|
||||
// Duration changed, update accessibility label.
|
||||
navigator.mozL10n.setAttributes(this.seekSlider,
|
||||
'playbackSeekBar', {'duration': formatTime(endTime)});
|
||||
}
|
||||
|
||||
this.seekBar.max = isFinite(endTime) ? endTime : 0;
|
||||
this.seekBar.value = currentTime;
|
||||
|
||||
var formattedCurrentTime = formatTime(currentTime);
|
||||
// Adjust values for accessibility
|
||||
this.seekSlider.setAttribute('aria-valuetext', formattedCurrentTime);
|
||||
this.seekSlider.setAttribute('aria-valuemax', this.seekBar.max);
|
||||
this.seekSlider.setAttribute('aria-valuenow', currentTime);
|
||||
|
||||
// if endTime is 0, that's a reset of seekBar
|
||||
var ratio = (isFinite(endTime) && endTime !== 0) ?
|
||||
(currentTime / endTime) : 0;
|
||||
// The width of the seek indicator must be also considered
|
||||
// so we divide the width of seek indicator by 2 to find the center point
|
||||
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;
|
||||
// Check if there is remaining time to show, avoiding to display "-00:00"
|
||||
// while song is loading (Bug 833710)
|
||||
this.seekRemaining.textContent =
|
||||
(remainingTime > 0) ? '-' + formatTime(remainingTime) : '---:--';
|
||||
},
|
||||
|
||||
share: function pv_shareFile() {
|
||||
// We try to fix Bug 814323 by using current workaround of bluetooth
|
||||
// transfer so we will pass both filenames and filepaths. The filepaths can
|
||||
// be removed after Bug 811615 is fixed.
|
||||
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],
|
||||
// We only pass some metadata attributes so we don't share personal
|
||||
// details like # of times played and ratings
|
||||
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 {
|
||||
// HACK HACK HACK
|
||||
//
|
||||
// Bug 956811: If we are currently playing music and share the
|
||||
// music with an inline activity handler (like the set
|
||||
// ringtone app) that wants to play music itself, we have a
|
||||
// problem because we have two foreground apps playing music
|
||||
// and neither one takes priority over the other. This is an
|
||||
// underlying bug in the way that inline activities are
|
||||
// handled and in our "audio competing policy". See bug
|
||||
// 892371.
|
||||
//
|
||||
// To work around this problem, if the music app is currently
|
||||
// playing anything, then before we launch the activity we start
|
||||
// listening for changes on a property in the settings database.
|
||||
// If the setting changes, we pause our playback and don't resume
|
||||
// until the activity returns. Then we pass the name of this magic
|
||||
// setting as a secret undocumented property of the activity so that
|
||||
// the ringtones app can use it.
|
||||
//
|
||||
// This done as much as possible in a self-invoking function to make
|
||||
// it easier to remove the hack when we have a real bug fix.
|
||||
//
|
||||
// See also the corresponding code in apps/ringtones/js/share.js
|
||||
//
|
||||
// HACK HACK HACK
|
||||
(function() {
|
||||
// This are the magic names we'll use for this hack
|
||||
var hack_activity_property = '_hack_hack_shut_up';
|
||||
var hack_setting_property = 'music._hack.pause_please';
|
||||
|
||||
// Listen for changes to the magic setting
|
||||
navigator.mozSettings.addObserver(hack_setting_property,
|
||||
observer);
|
||||
|
||||
// Pass the magic setting name as part of the activity request
|
||||
activityData[hack_activity_property] = hack_setting_property;
|
||||
|
||||
// Now initiate the activity. This code is the same as the
|
||||
// normal non-hack code in the if clause above.
|
||||
var a = new MozActivity({
|
||||
name: 'share',
|
||||
data: activityData
|
||||
});
|
||||
|
||||
a.onerror = a.onsuccess = cleanup;
|
||||
|
||||
// This is the function that pauses the music if the activity
|
||||
// handler sets the magic settings property.
|
||||
function observer(e) {
|
||||
// If the value of the setting has changed, then we pause the
|
||||
// music. Note that we don't care what the new value of the
|
||||
// setting is. We only care whether it has changed. The
|
||||
// ringtones app will just toggle it back and forth between
|
||||
// true and false.
|
||||
PlayerView.pause();
|
||||
}
|
||||
|
||||
// When the activity is done, we stop observing the setting.
|
||||
// And if we have been paused, then we resume playing.
|
||||
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');
|
||||
// The play event is fired when audio playback has begun.
|
||||
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) {
|
||||
// target is the seek bar
|
||||
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 isFastSeeking is true then the event is trigger by the long press
|
||||
// of the previous or next buttons, so stop the fast seeking.
|
||||
// Otherwise, check the target id then do the corresponding actions.
|
||||
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':
|
||||
// The standard accessible control for sliders is arrow up/down keys.
|
||||
// Our screenreader synthesizes those events on swipe up/down gestures.
|
||||
// Currently, we only allow screen reader users to adjust sliders with a
|
||||
// constant step size (there is no page up/down equivalent). In the case
|
||||
// of music, we make sure that the maximum amount of steps for the
|
||||
// entire duration is 20, or 2 second increments if the duration is less
|
||||
// then 40 seconds.
|
||||
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();
|
||||
|
||||
// Update the metadata when the new track is really loaded
|
||||
// when it just started to play, or the duration will be 0 then it will
|
||||
// break the duration that the connected A2DP has.
|
||||
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);
|
||||
// Ensure that the scrubber is synced up. It can get out of sync if we
|
||||
// paused while the music app was in the background.
|
||||
this.updateSeekBar();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'mozinterruptbegin':
|
||||
this.playStatus = INTERRUPT_BEGIN;
|
||||
this.updateRemotePlayStatus();
|
||||
break;
|
||||
|
||||
case 'mozinterruptend':
|
||||
// After received the mozinterruptend event the player should recover
|
||||
// its status to the status before mozinterruptbegin, it should be
|
||||
// PLAYING because mozinterruptbegin only fires when an audio element
|
||||
// is playing.
|
||||
this.playStatus = PLAYSTATUS_PLAYING;
|
||||
this.updateRemotePlayStatus();
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
updateL10n: function pv_updateL10n() {
|
||||
this.isLTR = navigator.mozL10n.language.direction === 'ltr' ? true : false;
|
||||
}
|
||||
};
|
||||
235
gaia/dev_apps/music-oga/js/ui/views/search_view.js
Executable file
235
gaia/dev_apps/music-oga/js/ui/views/search_view.js
Executable file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
186
gaia/dev_apps/music-oga/js/ui/views/subList_view.js
Executable file
186
gaia/dev_apps/music-oga/js/ui/views/subList_view.js
Executable file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
220
gaia/dev_apps/music-oga/js/ui/views/tiles_view.js
Executable file
220
gaia/dev_apps/music-oga/js/ui/views/tiles_view.js
Executable file
|
|
@ -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