add keyborad shortcuts, connect to lastfm, rearrange vendor files, sync page title for playing song, fix bug for click cancel button on favorite dialog show new song dialog

This commit is contained in:
listen1 2016-05-27 17:21:27 +08:00
parent 72e9cfdab2
commit 03f49562c4
14 changed files with 2631 additions and 26 deletions

View File

@ -1,4 +1,4 @@
Listen 1 (Chrome Extension) 最后更新于5月21日)
Listen 1 (Chrome Extension) 最后更新于5月27日)
==========
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE)
@ -17,8 +17,6 @@ Listen 1 (Chrome Extension) 最后更新于5月21日
Chrome安装
----
不能直接用chrome打开安装不能直接用chrome打开安装不能直接用chrome打开安装。重要的话说三遍。
1. 下载项目的zip文件在右上方有个 `Download ZIP`, 解压到本地
2. chrome右上角的设置按钮下找到更多工具打开`扩展程序`
@ -37,6 +35,15 @@ Firefox打包安装
更新日志
-------
`2016-05-27`
* 增加快捷键功能(输入?查看快捷键设置)
* 支持同步播放记录到last.fm
* 增加搜索loading时的图标(感谢@richdho的提交
* 页面标题增加显示当前播放信息
* 修复了在收藏对话框点击取消出现新建歌单的bug
* 重新组织代码文件夹结构
`2016-05-21`
* 增加歌单分页加载功能(感谢@wild-flame的提交)

110
css/hotkeys.css Normal file
View File

@ -0,0 +1,110 @@
/*!
* angular-hotkeys v1.7.0
* https://chieffancypants.github.io/angular-hotkeys
* Copyright (c) 2016 Wes Cruver
* License: MIT
*/
.cfp-hotkeys-container {
display: table !important;
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
color: #333;
font-size: 1em;
background-color: rgba(255,255,255,0.9);
}
.cfp-hotkeys-container.fade {
z-index: -1024;
visibility: hidden;
opacity: 0;
-webkit-transition: opacity 0.15s linear;
-moz-transition: opacity 0.15s linear;
-o-transition: opacity 0.15s linear;
transition: opacity 0.15s linear;
}
.cfp-hotkeys-container.fade.in {
z-index: 10002;
visibility: visible;
opacity: 1;
}
.cfp-hotkeys-title {
font-weight: bold;
text-align: center;
font-size: 1.2em;
}
.cfp-hotkeys {
width: 100%;
height: 100%;
display: table-cell;
vertical-align: middle;
}
.cfp-hotkeys table {
margin: auto;
color: #333;
}
.cfp-content {
display: table-cell;
vertical-align: middle;
}
.cfp-hotkeys-keys {
padding: 5px;
text-align: right;
}
.cfp-hotkeys-key {
display: inline-block;
color: #fff;
background-color: #333;
border: 1px solid #333;
border-radius: 5px;
text-align: center;
margin-right: 5px;
box-shadow: inset 0 1px 0 #666, 0 1px 0 #bbb;
padding: 5px 9px;
font-size: 1em;
}
.cfp-hotkeys-text {
padding-left: 10px;
font-size: 1em;
}
.cfp-hotkeys-close {
position: fixed;
top: 20px;
right: 20px;
font-size: 2em;
font-weight: bold;
padding: 5px 10px;
border: 1px solid #ddd;
border-radius: 5px;
min-height: 45px;
min-width: 45px;
text-align: center;
}
.cfp-hotkeys-close:hover {
background-color: #fff;
cursor: pointer;
}
@media all and (max-width: 500px) {
.cfp-hotkeys {
font-size: 0.8em;
}
}
@media all and (min-width: 750px) {
.cfp-hotkeys {
font-size: 1.2em;
}
}

View File

@ -975,6 +975,15 @@ li {
margin-left: 93px;
}
.dialog-connect-lastfm .buttons {
margin-top: 30px;
}
.dialog-connect-lastfm .confirm-button{
margin-left: 40px;
margin-right: 48px;
}
.source-list {
position: absolute;
right: -32px;

174
js/app.js
View File

@ -10,7 +10,7 @@
return value && JSON.parse(value);
}
var app = angular.module('listenone', ['angularSoundManager', 'ui-notification', 'loWebManager'])
var app = angular.module('listenone', ['angularSoundManager', 'ui-notification', 'loWebManager', 'cfp.hotkeys', 'lastfmClient'])
.config( [
'$compileProvider',
function( $compileProvider )
@ -31,6 +31,18 @@
});
});
app.config(function(hotkeysProvider) {
hotkeysProvider.templateTitle = '快捷键列表';
hotkeysProvider.cheatSheetDescription = '显示/隐藏快捷键列表';
});
app.config(function(lastfmProvider) {
lastfmProvider.setOptions({
apiKey: '6790c00a181128dc7c4ce06cd99d17c8',
apiSecret: 'd68f1dfc6ff43044c96a79ae7dfb5c27'
});
});
app.run(['angularPlayer', 'Notification', 'loWeb', function(angularPlayer, Notification, loWeb) {
angularPlayer.setBootstrapTrack(
loWeb.bootstrapTrack(
@ -63,8 +75,12 @@
app.controller('NavigationController', ['$scope', '$http',
'$httpParamSerializerJQLike', '$timeout',
'angularPlayer', 'Notification', '$rootScope', 'loWeb',
'hotkeys', 'lastfm',
function($scope, $http, $httpParamSerializerJQLike,
$timeout, angularPlayer, Notification, $rootScope, loWeb){
$timeout, angularPlayer, Notification, $rootScope,
loWeb, hotkeys, lastfm) {
$rootScope.page_title = "Listen 1";
$scope.window_url_stack = [];
$scope.current_tag = 2;
$scope.is_window_hidden = 1;
@ -78,6 +94,8 @@
$scope.dialog_title = '';
$scope.isDoubanLogin = false;
$scope.lastfm = lastfm;
$scope.$on('isdoubanlogin:update', function(event, data) {
$scope.isDoubanLogin = data;
@ -201,6 +219,10 @@
$scope.dialog_cover_img_url = data.cover_img_url;
$scope.dialog_playlist_title = data.playlist_title;
}
if (dialog_type == 4) {
$scope.dialog_title = '连接到Last.fm';
$scope.dialog_type = 4;
}
};
$scope.chooseDialogOption = function(option_id) {
@ -302,6 +324,10 @@
$scope.closeDialog = function() {
$scope.is_dialog_hidden = 1;
$scope.dialog_type = 0;
// update lastfm status if not authorized
if (lastfm.isAuthRequested()) {
lastfm.updateStatus();
}
};
$scope.setCurrentList = function(list_id) {
@ -430,6 +456,22 @@
};
reader.readAsText(fileObject);
}
$scope.showShortcuts = function() {
hotkeys.toggleCheatSheet();
}
hotkeys.add({
combo: 'f',
description: '快速搜索',
callback: function() {
$scope.showTag(3);
$timeout(function(){$("#search-input").focus();}, 0);
}
});
}]);
app.directive('customOnChange', function() {
@ -444,9 +486,12 @@
app.controller('PlayController', ['$scope', '$timeout','$log',
'$anchorScroll', '$location', 'angularPlayer', '$http',
'$httpParamSerializerJQLike','$rootScope', 'Notification','loWeb',
function($scope, $timeout, $log, $anchorScroll, $location, angularPlayer,
$http, $httpParamSerializerJQLike, $rootScope, Notification, loWeb){
'$httpParamSerializerJQLike','$rootScope', 'Notification',
'loWeb', 'hotkeys', 'lastfm',
function($scope, $timeout, $log, $anchorScroll, $location,
angularPlayer, $http, $httpParamSerializerJQLike,
$rootScope, Notification, loWeb, hotkeys, lastfm){
$scope.menuHidden = true;
$scope.volume = angularPlayer.getVolume();
$scope.mute = angularPlayer.getMuteStatus();
@ -455,6 +500,9 @@
$scope.lyricLineNumber = -1;
$scope.lastTrackId = null;
$scope.scrobbleTrackId = null;
$scope.scrobbleTimer = new Timer();
$scope.loadLocalSettings = function() {
var defaultSettings = {"playmode": 0, "nowplaying_track_id": -1, "volume": 90};
var localSettings = localStorage.getObject('player-settings');
@ -599,6 +647,45 @@
localStorage.setObject('current-playing', data);
});
$scope.$on('currentTrack:duration', function(event, data) {
if (!lastfm.isAuthorized()) {
return;
}
if (data == 0) {
return;
}
if ($scope.scrobbleTrackId == angularPlayer.getCurrentTrack()) {
return;
}
// new song arrives
$scope.scrobbleTrackId = angularPlayer.getCurrentTrack();
var track = angularPlayer.getTrack($scope.scrobbleTrackId);
var startTimestamp = Math.round((new Date()).valueOf() / 1000);
$scope.scrobbleTimer.start(function(){
lastfm.scrobble(startTimestamp, track.title, track.artist, track.album, function(){});
});
// according to scrobble rule
// http://www.last.fm/api/scrobbling
var secondsToScrobble = Math.min(data/1000/2, 60*4);
$scope.scrobbleTimer.update(secondsToScrobble);
});
$scope.$on('music:isPlaying', function(event, data) {
if (!lastfm.isAuthorized()) {
return;
}
if ($scope.scrobbleTrackId == null) {
return;
}
if (data) {
$scope.scrobbleTimer.resume();
}
else {
$scope.scrobbleTimer.pause();
};
});
function parseLyric(lyric) {
var lines = lyric.split('\n');
var result = [];
@ -679,6 +766,12 @@
$(".lyric").animate({ scrollTop: "0px" }, 500);
var url = '/lyric?track_id=' + data;
var track = angularPlayer.getTrack(data);
$rootScope.page_title = '▶ ' + track.title + ' - ' + track.artist;
if (lastfm.isAuthorized()) {
lastfm.sendNowPlaying(track.title, track.artist, function(){});
}
if (track.lyric_url != null) {
url = url + '&lyric_url=' + track.lyric_url;
}
@ -713,6 +806,77 @@
}
});
// define keybind
hotkeys.add({
combo: 'p',
description: '播放/暂停',
callback: function() {
if(angularPlayer.isPlayingStatus()) {
//if playing then pause
angularPlayer.pause();
} else {
//else play if not playing
angularPlayer.play();
}
}
});
hotkeys.add({
combo: '[',
description: '上一首',
callback: function() {
angularPlayer.prevTrack();
}
});
hotkeys.add({
combo: ']',
description: '下一首',
callback: function() {
angularPlayer.nextTrack();
}
});
hotkeys.add({
combo: 'm',
description: '静音/取消静音',
callback: function() {
// mute indeed toggle mute status
angularPlayer.mute();
}
});
hotkeys.add({
combo: 'l',
description: '打开/关闭播放列表',
callback: function() {
$scope.togglePlaylist();
}
});
hotkeys.add({
combo: 's',
description: '切换播放模式(顺序/随机)',
callback: function() {
$scope.changePlaymode();
}
});
hotkeys.add({
combo: 'u',
description: '音量增加',
callback: function() {
$timeout(function(){angularPlayer.adjustVolume(true);});
}
});
hotkeys.add({
combo: 'd',
description: '音量减少',
callback: function() {
$timeout(function(){angularPlayer.adjustVolume(false);});
}
});
}]);
app.controller('InstantSearchController', ['$scope', '$http', '$timeout', 'angularPlayer', 'loWeb',

258
js/lastfm.js Normal file
View File

@ -0,0 +1,258 @@
(function() {
'use strict';
Storage.prototype.setObject = function(key, value) {
this.setItem(key, JSON.stringify(value));
}
Storage.prototype.getObject = function(key) {
var value = this.getItem(key);
return value && JSON.parse(value);
}
angular.module('lastfmClient', []).provider('lastfm', function() {
this.options = {
apiKey: 'unknown',
apiSecret: 'unknown'
};
this.setOptions = function(options) {
if (!angular.isObject(options)) throw new Error("Options should be an object!");
this.options = angular.extend({}, this.options, options);
};
this.apiUrl = 'https://ws.audioscrobbler.com/2.0/';
this.$get = ['$http', '$window', function($http, $window) {
var options = this.options;
var apiUrl = this.apiUrl;
var status = 0;
/**
* Computes string for signing request
*
* See http://www.last.fm/api/authspec#8
*/
function generateSign(params) {
var keys = [];
var o = '';
for (var x in params) {
if (params.hasOwnProperty(x)) {
keys.push(x);
}
}
// params has to be ordered alphabetically
keys.sort();
for (var i = 0; i < keys.length; i++) {
if (keys[i] == 'format' || keys[i] == 'callback') {
continue;
}
o = o + keys[i] + params[keys[i]];
}
// append secret
return MD5(o + options.apiSecret);
}
/**
* Creates query string from object properties
*/
function createQueryString(params) {
var parts = [];
for (var x in params) {
if (params.hasOwnProperty(x)) {
parts.push( x + '=' + encodeURIComponent(params[x]));
}
}
return parts.join('&');
}
function getAuth(callback){
var url = apiUrl + '?method=auth.gettoken&api_key=' + options.apiKey + '&format=json';
$http.get(url).success(function(data) {
var token = data.token;
localStorage.setObject('lastfmtoken', token);
var grant_url = 'http://www.last.fm/api/auth/?api_key=' + options.apiKey + '&token=' + token;
$window.open(grant_url, '_blank');
status = 1;
if (callback != null) {
callback();
}
});
};
function cancelAuth(){
localStorage.removeItem('lastfmsession');
localStorage.removeItem('lastfmtoken');
updateStatus();
};
function _isAuthRequested() {
var token = localStorage.getObject('lastfmtoken');
return (token != null);
}
function updateStatus() {
// auth status
// 0: never request for auth
// 1: request but fail to success
// 2: success auth
if (!_isAuthRequested()) {
status = 0;
return;
}
getUserInfo(function(data){
if (data == null) {
status = 1;
}
else {
status = 2;
}
});
}
function getSession(callback) {
// load session info from localStorage
var mySession = localStorage.getObject('lastfmsession');
if (mySession != null) {
return callback(mySession);
}
// trade session with token
var token = localStorage.getObject('lastfmtoken');
if (token == null){
return callback(null);
}
// token exists
var params = {
method: 'auth.getsession',
api_key: options.apiKey,
token: token
};
var apiSig = generateSign(params);
var url = apiUrl + '?' + createQueryString(params) + '&api_sig=' + apiSig + '&format=json';
$http.get(url).success(function(data){
mySession = data.session;
localStorage.setObject('lastfmsession', mySession);
callback(mySession);
}).error(function (errResponse, status) {
if(status == 403){
callback(null);
}
});
};
function sendNowPlaying(track, artist, callback) {
getSession(function(session){
var params = {
method: 'track.updatenowplaying',
track: track,
artist: artist,
api_key: options.apiKey,
sk: session.key
};
params.api_sig = generateSign(params);
var url = apiUrl + '?' + createQueryString(params) + '&format=json';
$http.post(url).success(function(data){
if (callback != null) {
callback(data);
}
});
});
};
function scrobble(timestamp, track, artist, album, callback) {
getSession(function(session){
var params = {
method: 'track.scrobble',
'timestamp[0]': timestamp,
'track[0]': track,
'artist[0]': artist,
api_key: options.apiKey,
sk: session.key
};
if ((album !='') && (album != null)) {
params['album[0]'] = album;
}
params.api_sig = generateSign(params);
var url = apiUrl + '?' + createQueryString(params) + '&format=json';
$http.post(url).success(function(data){
if (callback != null) {
callback(data);
}
});
});
};
function getUserInfo(callback) {
getSession(function(session){
if (session == null) {
callback(null);
return;
}
var params = {
method: 'user.getinfo',
api_key: options.apiKey,
sk: session.key
};
params.api_sig = generateSign(params);
var url = apiUrl + '?' + createQueryString(params) + '&format=json';
$http.post(url).success(function(data){
if (callback != null) {
callback(data);
}
});
});
};
function isAuthorized() {
return (status == 2);
}
function isAuthRequested() {
return !(status == 0);
}
function getStatusText() {
if(status == 0) {
return '未连接';
}
if(status == 1) {
return '连接中';
}
if(status == 2) {
return '已连接';
}
}
var publicApi = {
getAuth : getAuth,
cancelAuth : cancelAuth,
getSession : getSession,
sendNowPlaying : sendNowPlaying,
scrobble : scrobble,
getUserInfo : getUserInfo,
getStatusText : getStatusText,
updateStatus : updateStatus,
isAuthorized : isAuthorized,
isAuthRequested : isAuthRequested
};
return publicApi;
}];
});
}) ();

View File

@ -47,6 +47,9 @@ var qq = (function() {
}
function qq_get_image_url(qqimgid, img_type) {
if (qqimgid == null) {
return '';
}
var category = '';
if(img_type == 'artist') {
category = 'mid_singer_300'
@ -54,6 +57,7 @@ var qq = (function() {
if(img_type == 'album') {
category = 'mid_album_300';
}
var s = [category, qqimgid[qqimgid.length - 2], qqimgid[qqimgid.length - 1], qqimgid].join('/');
var url = 'http://imgcache.qq.com/music/photo/' + s + '.jpg';
return url;

View File

View File

1661
js/vendor/hotkeys.js vendored Normal file

File diff suppressed because it is too large Load Diff

207
js/vendor/md5.js vendored Executable file
View File

@ -0,0 +1,207 @@
/**
*
* MD5 (Message-Digest Algorithm)
* http://www.webtoolkit.info/
* refer to: https://github.com/david-sabata/web-scrobbler/blob/master/vendor/md5.js
**/
function MD5(string) {
function RotateLeft(lValue, iShiftBits) {
return (lValue<<iShiftBits) | (lValue>>>(32-iShiftBits));
}
function AddUnsigned(lX,lY) {
var lX4,lY4,lX8,lY8,lResult;
lX8 = (lX & 0x80000000);
lY8 = (lY & 0x80000000);
lX4 = (lX & 0x40000000);
lY4 = (lY & 0x40000000);
lResult = (lX & 0x3FFFFFFF)+(lY & 0x3FFFFFFF);
if (lX4 & lY4) {
return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
}
if (lX4 | lY4) {
if (lResult & 0x40000000) {
return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
} else {
return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
}
} else {
return (lResult ^ lX8 ^ lY8);
}
}
function F(x,y,z) { return (x & y) | ((~x) & z); }
function G(x,y,z) { return (x & z) | (y & (~z)); }
function H(x,y,z) { return (x ^ y ^ z); }
function I(x,y,z) { return (y ^ (x | (~z))); }
function FF(a,b,c,d,x,s,ac) {
a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac));
return AddUnsigned(RotateLeft(a, s), b);
};
function GG(a,b,c,d,x,s,ac) {
a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac));
return AddUnsigned(RotateLeft(a, s), b);
};
function HH(a,b,c,d,x,s,ac) {
a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac));
return AddUnsigned(RotateLeft(a, s), b);
};
function II(a,b,c,d,x,s,ac) {
a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac));
return AddUnsigned(RotateLeft(a, s), b);
};
function ConvertToWordArray(string) {
var lWordCount;
var lMessageLength = string.length;
var lNumberOfWords_temp1=lMessageLength + 8;
var lNumberOfWords_temp2=(lNumberOfWords_temp1-(lNumberOfWords_temp1 % 64))/64;
var lNumberOfWords = (lNumberOfWords_temp2+1)*16;
var lWordArray=Array(lNumberOfWords-1);
var lBytePosition = 0;
var lByteCount = 0;
while ( lByteCount < lMessageLength ) {
lWordCount = (lByteCount-(lByteCount % 4))/4;
lBytePosition = (lByteCount % 4)*8;
lWordArray[lWordCount] = (lWordArray[lWordCount] | (string.charCodeAt(lByteCount)<<lBytePosition));
lByteCount++;
}
lWordCount = (lByteCount-(lByteCount % 4))/4;
lBytePosition = (lByteCount % 4)*8;
lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80<<lBytePosition);
lWordArray[lNumberOfWords-2] = lMessageLength<<3;
lWordArray[lNumberOfWords-1] = lMessageLength>>>29;
return lWordArray;
};
function WordToHex(lValue) {
var WordToHexValue="",WordToHexValue_temp="",lByte,lCount;
for (lCount = 0;lCount<=3;lCount++) {
lByte = (lValue>>>(lCount*8)) & 255;
WordToHexValue_temp = "0" + lByte.toString(16);
WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length-2,2);
}
return WordToHexValue;
};
function Utf8Encode(string) {
string = string.replace(/\r\n/g,"\n");
var utftext = "";
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
}
else if((c > 127) && (c < 2048)) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
}
else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
}
return utftext;
};
var x=Array();
var k,AA,BB,CC,DD,a,b,c,d;
var S11=7, S12=12, S13=17, S14=22;
var S21=5, S22=9 , S23=14, S24=20;
var S31=4, S32=11, S33=16, S34=23;
var S41=6, S42=10, S43=15, S44=21;
string = Utf8Encode(string);
x = ConvertToWordArray(string);
a = 0x67452301; b = 0xEFCDAB89; c = 0x98BADCFE; d = 0x10325476;
for (k=0;k<x.length;k+=16) {
AA=a; BB=b; CC=c; DD=d;
a=FF(a,b,c,d,x[k+0], S11,0xD76AA478);
d=FF(d,a,b,c,x[k+1], S12,0xE8C7B756);
c=FF(c,d,a,b,x[k+2], S13,0x242070DB);
b=FF(b,c,d,a,x[k+3], S14,0xC1BDCEEE);
a=FF(a,b,c,d,x[k+4], S11,0xF57C0FAF);
d=FF(d,a,b,c,x[k+5], S12,0x4787C62A);
c=FF(c,d,a,b,x[k+6], S13,0xA8304613);
b=FF(b,c,d,a,x[k+7], S14,0xFD469501);
a=FF(a,b,c,d,x[k+8], S11,0x698098D8);
d=FF(d,a,b,c,x[k+9], S12,0x8B44F7AF);
c=FF(c,d,a,b,x[k+10],S13,0xFFFF5BB1);
b=FF(b,c,d,a,x[k+11],S14,0x895CD7BE);
a=FF(a,b,c,d,x[k+12],S11,0x6B901122);
d=FF(d,a,b,c,x[k+13],S12,0xFD987193);
c=FF(c,d,a,b,x[k+14],S13,0xA679438E);
b=FF(b,c,d,a,x[k+15],S14,0x49B40821);
a=GG(a,b,c,d,x[k+1], S21,0xF61E2562);
d=GG(d,a,b,c,x[k+6], S22,0xC040B340);
c=GG(c,d,a,b,x[k+11],S23,0x265E5A51);
b=GG(b,c,d,a,x[k+0], S24,0xE9B6C7AA);
a=GG(a,b,c,d,x[k+5], S21,0xD62F105D);
d=GG(d,a,b,c,x[k+10],S22,0x2441453);
c=GG(c,d,a,b,x[k+15],S23,0xD8A1E681);
b=GG(b,c,d,a,x[k+4], S24,0xE7D3FBC8);
a=GG(a,b,c,d,x[k+9], S21,0x21E1CDE6);
d=GG(d,a,b,c,x[k+14],S22,0xC33707D6);
c=GG(c,d,a,b,x[k+3], S23,0xF4D50D87);
b=GG(b,c,d,a,x[k+8], S24,0x455A14ED);
a=GG(a,b,c,d,x[k+13],S21,0xA9E3E905);
d=GG(d,a,b,c,x[k+2], S22,0xFCEFA3F8);
c=GG(c,d,a,b,x[k+7], S23,0x676F02D9);
b=GG(b,c,d,a,x[k+12],S24,0x8D2A4C8A);
a=HH(a,b,c,d,x[k+5], S31,0xFFFA3942);
d=HH(d,a,b,c,x[k+8], S32,0x8771F681);
c=HH(c,d,a,b,x[k+11],S33,0x6D9D6122);
b=HH(b,c,d,a,x[k+14],S34,0xFDE5380C);
a=HH(a,b,c,d,x[k+1], S31,0xA4BEEA44);
d=HH(d,a,b,c,x[k+4], S32,0x4BDECFA9);
c=HH(c,d,a,b,x[k+7], S33,0xF6BB4B60);
b=HH(b,c,d,a,x[k+10],S34,0xBEBFBC70);
a=HH(a,b,c,d,x[k+13],S31,0x289B7EC6);
d=HH(d,a,b,c,x[k+0], S32,0xEAA127FA);
c=HH(c,d,a,b,x[k+3], S33,0xD4EF3085);
b=HH(b,c,d,a,x[k+6], S34,0x4881D05);
a=HH(a,b,c,d,x[k+9], S31,0xD9D4D039);
d=HH(d,a,b,c,x[k+12],S32,0xE6DB99E5);
c=HH(c,d,a,b,x[k+15],S33,0x1FA27CF8);
b=HH(b,c,d,a,x[k+2], S34,0xC4AC5665);
a=II(a,b,c,d,x[k+0], S41,0xF4292244);
d=II(d,a,b,c,x[k+7], S42,0x432AFF97);
c=II(c,d,a,b,x[k+14],S43,0xAB9423A7);
b=II(b,c,d,a,x[k+5], S44,0xFC93A039);
a=II(a,b,c,d,x[k+12],S41,0x655B59C3);
d=II(d,a,b,c,x[k+3], S42,0x8F0CCC92);
c=II(c,d,a,b,x[k+10],S43,0xFFEFF47D);
b=II(b,c,d,a,x[k+1], S44,0x85845DD1);
a=II(a,b,c,d,x[k+8], S41,0x6FA87E4F);
d=II(d,a,b,c,x[k+15],S42,0xFE2CE6E0);
c=II(c,d,a,b,x[k+6], S43,0xA3014314);
b=II(b,c,d,a,x[k+13],S44,0x4E0811A1);
a=II(a,b,c,d,x[k+4], S41,0xF7537E82);
d=II(d,a,b,c,x[k+11],S42,0xBD3AF235);
c=II(c,d,a,b,x[k+2], S43,0x2AD7D2BB);
b=II(b,c,d,a,x[k+9], S44,0xEB86D391);
a=AddUnsigned(a,AA);
b=AddUnsigned(b,BB);
c=AddUnsigned(c,CC);
d=AddUnsigned(d,DD);
}
var temp = WordToHex(a)+WordToHex(b)+WordToHex(c)+WordToHex(d);
return temp.toLowerCase();
}

155
js/vendor/timer.js vendored Normal file
View File

@ -0,0 +1,155 @@
'use strict';
/**
* Timer
* https://github.com/david-sabata/web-scrobbler/blob/master/core/background/timer.js
*/
function Timer() {
var callback = null,
timeoutId = null,
target = null, // target seconds
pausedOn = null, // marks pause time in seconds
startedOn = null, // marks start time in seconds
spentPaused = 0, // sum of paused time in seconds
hasTriggered = false; // already triggered callback?
/**
* Returns current time in seconds
*/
function now() {
return Math.round((new Date()).valueOf() / 1000);
}
function setTrigger(seconds) {
clearTrigger();
timeoutId = setTimeout(function() {
callback();
hasTriggered = true;
}, seconds * 1000);
}
/**
* Clears internal timeout
*/
function clearTrigger() {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = null;
}
/**
* Set timer and define trigger callback.
* Use update function to define time to trigger.
*/
this.start = function(cb) {
this.reset();
startedOn = now();
callback = cb;
};
/**
* Pause timer
*/
this.pause = function() {
// only if timer was started and was running
if (startedOn !== null && pausedOn === null) {
pausedOn = now();
clearTrigger();
}
};
/**
* Unpause timer
*/
this.resume = function() {
// only if timer was started and was paused
if (startedOn !== null && pausedOn !== null) {
spentPaused += now() - pausedOn;
pausedOn = null;
if (!hasTriggered && target !== null) {
setTrigger(target - this.getElapsed());
}
}
};
/**
* Update time for this timer before callback is triggered.
* Already elapsed time is not modified and callback
* will be triggered immediately if the new time is less than elapsed.
*
* Pass null to set destination time to 'never' - this prevents the timer from
* triggering but still keeps it counting time.
*
* Intentionally does not check if the callback was already triggered.
* This allows to update the timer after it went out once and still
* be able to properly trigger the callback for the new timeout.
*/
this.update = function(seconds) {
// only if timer was started
if (startedOn !== null) {
target = seconds;
if (seconds !== null) {
if (pausedOn === null) {
setTrigger(target - this.getElapsed());
}
} else {
clearTrigger();
}
}
};
/**
* Returns seconds passed from the timer was started.
* Time spent paused does not count
*/
this.getElapsed = function() {
var val = now() - startedOn - spentPaused;
if (pausedOn !== null) {
val -= (now() - pausedOn);
}
return val;
};
/**
* Checks if current timer has already triggered its callback
*/
this.hasTriggered = function() {
return hasTriggered;
};
/**
* Returns remaining (unpaused) seconds or null if no destination time is set
*/
this.getRemainingSeconds = function() {
if (target === null) {
return null;
}
return target - this.getElapsed();
};
/**
* Reset timer
*/
this.reset = function() {
target = null;
startedOn = null;
pausedOn = null;
spentPaused = 0;
callback = null;
hasTriggered = false;
clearTrigger();
};
};

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" ng-app="listenone">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
@ -8,33 +8,39 @@
<meta name="description" content="">
<meta name="author" content="">
<title>Listen 1</title>
<title ng-bind="page_title">Listen 1</title>
<link href="css/bootstrap.min.css" rel="stylesheet">
<link href="css/angular-ui-notification.css" rel="stylesheet">
<link href="css/cover.css" rel="stylesheet">
<link href="css/player.css" rel="stylesheet">
<link href="css/hotkeys.css" rel="stylesheet">
<script type="text/javascript" src="js/vendor/jquery-1.12.2.js"></script>
<script type="text/javascript" src="js/vendor/angular.min.js"></script>
<script type="text/javascript" src="js/vendor/angular-soundmanager2.js"></script>
<script type="text/javascript" src="js/vendor/angular-ui-notification.js"></script>
<!-- require jQuery as dependency -->
<script type="text/javascript" src="js/vendor/hotkeys.js"></script>
<script type="text/javascript" src="js/vendor/md5.js"></script>
<script type="text/javascript" src="js/vendor/aes.js"></script>
<script type="text/javascript" src="js/vendor/bigint.js"></script>
<script type="text/javascript" src="js/vendor/timer.js"></script>
<script type="text/javascript" src="js/lastfm.js"></script>
<script type="text/javascript" src="js/aes.js"></script>
<script type="text/javascript" src="js/bigint.js"></script>
<script type="text/javascript" src="js/lowebutil.js"></script>
<script type="text/javascript" src="js/xiami.js"></script>
<script type="text/javascript" src="js/qq.js"></script>
<script type="text/javascript" src="js/netease.js"></script>
<script type="text/javascript" src="js/provider/xiami.js"></script>
<script type="text/javascript" src="js/provider/qq.js"></script>
<script type="text/javascript" src="js/provider/netease.js"></script>
<script type="text/javascript" src="js/myplaylist.js"></script>
<script type="text/javascript" src="js/loweb.js"></script>
<script type="text/javascript" src="js/app.js"></script>
</head>
<body ng-app="listenone" ng-controller="NavigationController">
<body ng-controller="NavigationController">
<!-- dialog -->
<div class="shadow" ng-hide="is_dialog_hidden==1"></div>
@ -91,12 +97,21 @@
</div>
<button class="btn btn-primary confirm-button" ng-click="editMyPlaylist(list_id)">修改歌单</button>
<button class="btn btn-default" ng-click="cancelNewDialog()">取消</button>
<button class="btn btn-default" ng-click="closeDialog()">取消</button>
<div class='dialog-footer'>
<button class="btn btn-danger remove-button" ng-click="removeMyPlaylist(list_id)">删除歌单</button>
</div>
</div>
<div ng-show="dialog_type==4" class="dialog-connect-lastfm">
<p>正在打开Last.fm页面...</p>
<p>请在打开的页面点击"Yes, all access", 允许Listen 1访问你的账户。</p>
<div class="buttons">
<button class="btn btn-primary confirm-button" ng-click="lastfm.updateStatus();closeDialog();">已经完成授权</button>
<button class="btn btn-warning warning-button" ng-click="lastfm.getAuth();">遇到问题,再次打开授权页</button>
</div>
</div>
</div>
</div>
@ -186,7 +201,7 @@
<!-- Initialize a new AngularJS app and associate it with a module named "instantSearch"-->
<div class="searchbox" ng-controller="InstantSearchController" >
<!-- Create a binding between the searchString model and the text field -->
<input type="text" class="form-control" ng-model="keywords" placeholder="输入歌曲名,歌手或专辑" ng-model-options="{debounce: 500}" />
<input type="text" id="search-input" class="form-control" ng-model="keywords" placeholder="输入歌曲名,歌手或专辑" ng-model-options="{debounce: 500}" />
<ul class="nav nav-tabs">
@ -219,7 +234,7 @@
<!-- content page: 设置 -->
<div class="site-wrapper" ng-show="current_tag==4">
<div class="site-wrapper" ng-show="current_tag==4" ng-init="lastfm.updateStatus()">
<div class="site-wrapper-innerd" resize>
<div class="cover-container">
<!-- <div class="settings-title"><span>第三方登录<span></div>
@ -241,6 +256,21 @@
<input id="my-file-selector" type="file" style="display:none;" ng-model="myuploadfiles" custom-on-change="importMySettings">上传备份文件
</label>
</div>
<div class="settings-title"><span>快捷键<span></div>
<div class="settings-content">
<div>
<button class="btn btn-primary confirm-button" ng-click="showShortcuts()">查看快捷键列表</button>
</div>
</div>
<div class="settings-title"><span>连接到 Last.fm<span></div>
<div class="settings-content">
<div>
<p> 状态:{{ lastfm.getStatusText() }} </p>
<button class="btn btn-primary confirm-button" ng-show="!lastfm.isAuthRequested()" ng-click="lastfm.getAuth(); showDialog(4);">连接到 Last.fm</button>
<button class="btn btn-warning confirm-button" ng-show="lastfm.isAuthRequested() && !lastfm.isAuthorized()" ng-click="lastfm.getAuth(); showDialog(4);">重新连接</button>
<button class="btn btn-primary confirm-button" ng-show="lastfm.isAuthRequested()" ng-click="lastfm.cancelAuth();">取消连接</button>
</div>
</div>
<div class="settings-title"><span>关于<span></div>
<div class="settings-content">
<p> Listen 1 主页: <a href="http://listen1.github.io/listen1/" target="_blank"> http://listen1.github.io/listen1/ </a> </p>
@ -302,9 +332,9 @@
<div class="mastfoot" ng-controller="PlayController as playCtrl" ng-init="loadLocalSettings()">
<div class="m-playbar" >
<div class="btns">
<a class="previous" title="上一首" prev-track >上一首</a>
<a class="play" ng-class="{pas: isPlaying}" title="播放/暂停" play-pause-toggle>播放/暂停</a>
<a class="next" title="下一首" next-track>下一首</a>
<a class="previous" title="上一首([)" prev-track >上一首([)</a>
<a class="play" ng-class="{pas: isPlaying}" title="播放/暂停(p)" play-pause-toggle>播放/暂停(p)</a>
<a class="next" title="下一首(])" next-track>下一首(])</a>
</div>
<div class="head">
@ -341,12 +371,12 @@
<div class="ctrl">
<a class="icn icn-add" ng-click="showDialog(0, currentPlaying)" title="添加到歌单">添加到歌单</a>
<a class="icn" ng-class="{ 'icn-shuffle': settings.playmode == 1, 'icn-loop': settings.playmode == 0 }" title="{{ settings.playmode | playmode_title }}" ng-click="changePlaymode()"></a>
<a class="icn icn-list" title="列表" ng-click="togglePlaylist()"></a>
<a class="icn" ng-class="{ 'icn-shuffle': settings.playmode == 1, 'icn-loop': settings.playmode == 0 }" title="{{ settings.playmode | playmode_title }}(s)" ng-click="changePlaymode()"></a>
<a class="icn icn-list" title="列表(l)" ng-click="togglePlaylist()"></a>
</div>
<div class="volume-ctrl">
<a class="icn" ng-class="{ 'icn-vol-mute': mute, 'icn-vol': mute == false }" title="音量" ng-click="toggleMuteStatus()"></a>
<a class="icn" ng-class="{ 'icn-vol-mute': mute, 'icn-vol': mute == false }" title="静音(m) 增大(u) 减少(d)" ng-click="toggleMuteStatus()"></a>
<div class="m-pbar volume" >
<div class="barbg" id="volumebar" mode="volume" draggable>
<div class="cur" ng-style="{width : volume + '%' }">