├── .gitignore ├── Makefile ├── README.md ├── app ├── actions │ └── actions.js ├── assets │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── icon.svg │ ├── icon │ │ ├── add.svg │ │ ├── addall.svg │ │ ├── back.svg │ │ ├── card.svg │ │ ├── close.svg │ │ ├── loop.svg │ │ ├── max.svg │ │ ├── min.svg │ │ ├── music.svg │ │ ├── next.svg │ │ ├── one.svg │ │ ├── pause.svg │ │ ├── play.svg │ │ ├── playlist.svg │ │ ├── previous.svg │ │ ├── remove.svg │ │ ├── removeall.svg │ │ ├── search.svg │ │ ├── shuffle.svg │ │ ├── star.svg │ │ ├── unmax.svg │ │ ├── volume.svg │ │ ├── volume_max.svg │ │ ├── volume_min.svg │ │ └── volume_mute.svg │ ├── img │ │ └── up.svg │ ├── logo.svg │ └── tray.png ├── components │ ├── AlbumCard.jsx │ ├── App.jsx │ ├── Content.jsx │ ├── Header.jsx │ ├── HomeContent.jsx │ ├── LoginForm.jsx │ ├── MiniAlbumCard.jsx │ ├── MusicContent.jsx │ ├── PlayContentCard.jsx │ ├── PlayList.jsx │ ├── PlayListControl.jsx │ ├── Player.jsx │ ├── SearchBar.jsx │ ├── SearchContent.jsx │ ├── SideBar.jsx │ ├── SongCard.jsx │ ├── SongList.jsx │ ├── SongListContent.jsx │ ├── Spinner.jsx │ ├── Toast.jsx │ ├── UserState.jsx │ └── Volume.jsx ├── containers │ └── CloudMusic.jsx ├── libs │ └── lrcparse.js ├── main.js ├── postcss │ ├── _albumcard.css │ ├── _button.css │ ├── _card.css │ ├── _colors.css │ ├── _content.css │ ├── _header.css │ ├── _home-content.css │ ├── _loginform.css │ ├── _minialbumcard.css │ ├── _playcontentcard.css │ ├── _player.css │ ├── _playlist.css │ ├── _playlistcontrol.css │ ├── _reset.css │ ├── _scrollbar.css │ ├── _search-content.css │ ├── _searchbar.css │ ├── _sidebar.css │ ├── _songcard.css │ ├── _songlist-content.css │ ├── _songlist.css │ ├── _spinner.css │ ├── _toast.css │ ├── _user-state.css │ ├── _volume.css │ └── index.css ├── reducers │ ├── alert.js │ ├── index.js │ ├── playcontent.js │ ├── player.js │ ├── router.js │ ├── search.js │ ├── song.js │ ├── songlist.js │ ├── toast.js │ ├── user.js │ └── usersong.js └── server │ └── index.js ├── index.html ├── main.js ├── package.json ├── server ├── crypto.js └── server.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # node.js 2 | # 3 | node_modules/ 4 | npm-debug.log 5 | 6 | # Vim 7 | # swap 8 | [._]*.s[a-w][a-z] 9 | [._]s[a-w][a-z] 10 | # session 11 | Session.vim 12 | # temporary 13 | .netrwhist 14 | *~ 15 | # auto-generated tag files 16 | tags 17 | 18 | dist/ 19 | 20 | release/ 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile 3 | # disoul, 2016-11-04 14:57 4 | # 5 | ELECTRON_PACKAGER = ./node_modules/.bin/electron-packager 6 | 7 | UNAME_S := $(shell uname -s) 8 | ifeq ($(UNAME_S), Darwin) 9 | PLATFORM = darwin 10 | else 11 | ifeq ($(UNAME_S),Linux) 12 | PLATFORM = linux 13 | else 14 | PLATFORM = unknow 15 | endif 16 | endif 17 | 18 | UNAME_M := $(shell uname -m) 19 | ifeq ($(UNAME_M), x86_64) 20 | ARCH = x64 21 | else 22 | ifeq ($(UNAME_M), x86) 23 | ARCH = ia32 24 | else 25 | ARCH = unknow 26 | endif 27 | endif 28 | 29 | install: 30 | @npm install 31 | 32 | #TODO: icon and ignore files 33 | release: 34 | ./node_modules/.bin/webpack -p 35 | $(ELECTRON_PACKAGER) . CloudMusic --platform=$(PLATFORM) --arch=$(ARCH) --out=release --overwrite --version=1.4.5 --ignore --icon="./app/assets/icon.icns" --prune 36 | 37 | .PHONY: release install 38 | # vim:ft=make 39 | # 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NeteaseCloudMusic Electron 2 | 3 | [![devDependency Status](https://david-dm.org/disoul/electron-cloud-music/dev-status.svg)](https://david-dm.org/disoul/electron-cloud-music#info=devDependencies) 4 | 网易云音乐Electron版 5 | 6 | ## 进度 7 | * 搜索歌曲+播放(版权歌曲无法播放 8 | * 播放列表 9 | * 手机登陆 10 | * 个人歌单(创建,收藏 11 | * 歌曲界面(滚动歌词 12 | * 主页推荐 13 | * 喜欢歌曲 && 自动向网易提交听歌记录 14 | * [TODO] 私人FM 15 | 16 | ![预览截图](http://7xn38i.com1.z0.glb.clouddn.com/QQ20161106-1@2x.png) 17 | ![预览截图](http://7xn38i.com1.z0.glb.clouddn.com/QQ20161106-0@2x.png) 18 | ![预览截图](http://7xn38i.com1.z0.glb.clouddn.com/QQ20161106-2@2x.png) 19 | 20 | ## 试用 21 | 打包了64位的linux和mac,见[release](https://github.com/disoul/electron-cloud-music/releases/tag/0.0.2) 22 | 23 | ## Build 24 | 25 | ```bash 26 | git clone https://github.com/disoul/electron-cloud-music && cd electron-cloud-music 27 | npm install 28 | 29 | # Dev 30 | 31 | # Start dev server 32 | npm run dev 33 | 34 | # run cloudmusic in proj root path 35 | # electron will load from 127.0.0.1:8080(webpack-dev-server 36 | npm start 37 | 38 | # Release 39 | 40 | vim main.js 41 | # edit main.js like this 42 | //mainWindow.loadURL('http://127.0.0.1:8080'); 43 | mainWindow.loadURL('file://' + __dirname + '/index.html'); 44 | 45 | # build 46 | make release 47 | ``` 48 | -------------------------------------------------------------------------------- /app/actions/actions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import { Search, Login, getPlayList, SonglistDetail, getLyric,playlistTracks } from '../server'; 3 | import lryicParser from '../libs/lrcparse'; 4 | export function play() { 5 | return { type: 'PLAYER', state: 'PLAYER_PLAY' }; 6 | } 7 | 8 | export function pause() { 9 | return { type: 'PLAYER', state: 'PLAYER_PAUSE' }; 10 | } 11 | 12 | export function startSearch(keywords) { 13 | return { type: 'SEARCH', state: 'START', payload: { keywords: keywords }} 14 | } 15 | 16 | export function errorSearch(e) { 17 | return { type: 'SEARCH', state: 'ERROR', payload: e } 18 | } 19 | 20 | export function finishSearch(res) { 21 | return { type: 'SEARCH', state: 'FINISH', payload: res } 22 | } 23 | 24 | export function closeSearch() { 25 | return { type: 'SEARCH', state: 'CLOSE' } 26 | } 27 | 28 | export function search(keywords) { 29 | return dispatch => { 30 | dispatch(startSearch(keywords)); 31 | Search(keywords).then( res => { 32 | dispatch(finishSearch(res)); 33 | } ) 34 | .catch( e => { 35 | dispatch(errorSearch(e)); 36 | } ) 37 | }; 38 | } 39 | 40 | export function changeSong(song) { 41 | return { type: 'SONG', state: 'CHANGE', payload: song} 42 | } 43 | 44 | export function playFromList(index) { 45 | return { type: 'SONG', state: 'PLAYFROMLIST', payload: index} 46 | } 47 | 48 | export function addSong(song) { 49 | return { type: 'SONG', state: 'ADD', payload: song} 50 | } 51 | 52 | export function addSongList(songlist, isplay) { 53 | return { type: 'SONG', state: 'ADDLIST', payload: { 54 | songlist: songlist, 55 | play: isplay, 56 | } 57 | } 58 | } 59 | 60 | export function nextSong() { 61 | return { type: 'SONG', state: 'NEXT' } 62 | } 63 | 64 | export function previousSong() { 65 | return { type: 'SONG', state: 'PREVIOUS' } 66 | } 67 | 68 | export function changeRule() { 69 | return { type: 'SONG', state: 'CHANGERULE' } 70 | } 71 | 72 | export function removesongfromlist(index) { 73 | return {type: 'SONG', state: 'REMOVEFROMLIST', payload: index} 74 | } 75 | 76 | export function removesonglist() { 77 | return {type: 'SONG', state: 'REMOVELIST' } 78 | } 79 | 80 | export function showPlayList() { 81 | return { type: 'SONG', state: 'SHOWPLAYLIST' } 82 | } 83 | 84 | export function closePlayList() { 85 | return { type: 'SONG', state: 'CLOSEPLAYLIST' } 86 | } 87 | 88 | export function logging_in(form) { 89 | return { type: 'USER', state: 'LOGIN_STATE_LOGGING_IN', payload: form } 90 | }; 91 | 92 | export function logged_in(res) { 93 | return { type: 'USER', state: 'LOGIN_STATE_LOGGED_IN', payload: res } 94 | }; 95 | 96 | export function logged_failed(errorinfo) { 97 | return { type: 'USER', state: 'LOGIN_STATE_LOGGED_FAILED', payload: errorinfo } 98 | } 99 | 100 | export function loginform(flag) { 101 | return { type: 'USER', state: 'LOGINFORM', payload: flag } 102 | } 103 | 104 | export function toguest() { 105 | return { type: 'USER', state: 'GUEST' } 106 | } 107 | 108 | export function login(form) { 109 | return dispatch => { 110 | dispatch(logging_in(form)); 111 | Login(form.phone, form.password) 112 | .then(res => { 113 | localStorage.setItem('user', JSON.stringify(res)); 114 | dispatch(logged_in(res)); 115 | dispatch(fetchusersong(res.profile.userId)); 116 | }) 117 | .catch(error => { 118 | dispatch(logged_failed(error.toString())); 119 | }); 120 | } 121 | } 122 | 123 | export function fetchingusersong(id) { 124 | return { type: 'USERSONG', state: 'FETCHING', payload: id } 125 | } 126 | 127 | export function getusersong(res) { 128 | return { type: 'USERSONG', state: 'GET', payload: res } 129 | } 130 | 131 | export function fetchusersongerror(err) { 132 | return { type: 'USERSONG', state: 'ERROR', payload: err } 133 | } 134 | 135 | export function fetchusersong(uid) { 136 | return dispatch => { 137 | dispatch(fetchingusersong(uid)); 138 | getPlayList(uid) 139 | .then(res => { 140 | dispatch(getusersong(res.playlist)); 141 | }) 142 | .catch(err => { 143 | dispatch(fetchusersongerror(err)); 144 | }); 145 | } 146 | } 147 | 148 | // push content to routerstack 149 | export function push(content) { 150 | return { type: 'ROUTER', state: 'PUSH', payload: content } 151 | } 152 | 153 | export function pop() { 154 | return { type: 'ROUTER', state: 'POP' } 155 | } 156 | 157 | // 获取歌单内容 158 | export function fetchsonglistdetail(id) { 159 | return dispatch => { 160 | dispatch(fetchingsonglistdetail(id)); 161 | SonglistDetail(id) 162 | .then( res => { 163 | dispatch(getsonglistdetail(res)); 164 | }) 165 | .catch(error => { 166 | dispatch(fetchsonglistdetailerror(error)); 167 | }); 168 | }; 169 | } 170 | 171 | export function fetchingsonglistdetail(id) { 172 | return { type: 'SONGLIST', state: 'FETCHING', payload: id } 173 | } 174 | 175 | export function getsonglistdetail(res) { 176 | return { type: 'SONGLIST', state: 'GET', payload: res } 177 | } 178 | 179 | export function fetchsonglistdetailerror(err) { 180 | return { type: 'SONGLIST', state: 'ERROR', payload: err } 181 | } 182 | 183 | export function showplaycontentmini() { 184 | return { type: 'PLAYCONTENT', state: 'SHOWMINI' } 185 | } 186 | 187 | export function hiddenplaycontentmini() { 188 | return { type: 'PLAYCONTENT', state: 'HIDDENMINI' } 189 | } 190 | 191 | export function showplaycontentmax() { 192 | return { type: 'PLAYCONTENT', state: 'SHOWMAX' } 193 | } 194 | 195 | export function hiddenplaycontentmax() { 196 | return { type: 'PLAYCONTENT', state: 'HIDDENMAX' } 197 | } 198 | 199 | function fetchinglyric() { 200 | return { type: 'PLAYCONTENT', state: 'LRCFETCH' } 201 | } 202 | 203 | function getlyric(res) { 204 | return { type: 'PLAYCONTENT', state: 'LRCGET', payload: res } 205 | } 206 | 207 | function errorlyric(err) { 208 | return { type: 'PLAYCONTENT', state: 'LRCERROR', payload: err } 209 | } 210 | 211 | export function lyric(id) { 212 | return dispatch => { 213 | dispatch(fetchinglyric()); 214 | getLyric(id).then( res => { 215 | dispatch(getlyric(lryicParser(res))); 216 | }) 217 | .catch( err => { 218 | dispatch(errorlyric(err)); 219 | }); 220 | }; 221 | } 222 | 223 | export function setlyric(index) { 224 | return { type: 'PLAYCONTENT', state: 'LRCSET', payload: index } 225 | } 226 | 227 | function addToast(content) { 228 | return { type: 'TOAST', state: 'ADD', payload: content } 229 | } 230 | 231 | function removeToast() { 232 | return { type: 'TOAST', state: 'FINISH' } 233 | } 234 | 235 | export function toast(content) { 236 | return dispatch => { 237 | dispatch(addToast(content)); 238 | window.setTimeout(dispatch, 5000, removeToast()); 239 | } 240 | } 241 | 242 | export function changeclientmode(mode) { 243 | return { type: 'PLAYCONTENT', state: 'CLIENT_MODE', payload: mode }; 244 | } 245 | -------------------------------------------------------------------------------- /app/assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disoul/electron-cloud-music/0336c509f44cdc88882a32f2f15938ff3f5b0161/app/assets/icon.icns -------------------------------------------------------------------------------- /app/assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disoul/electron-cloud-music/0336c509f44cdc88882a32f2f15938ff3f5b0161/app/assets/icon.ico -------------------------------------------------------------------------------- /app/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disoul/electron-cloud-music/0336c509f44cdc88882a32f2f15938ff3f5b0161/app/assets/icon.png -------------------------------------------------------------------------------- /app/assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 50 | 52 | 55 | 59 | 60 | 61 | 65 | 69 | 76 | 77 | 87 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /app/assets/icon/add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/icon/addall.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/icon/back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/assets/icon/card.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/icon/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/icon/loop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/icon/max.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/assets/icon/min.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/icon/music.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/icon/next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/icon/one.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/icon/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/icon/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/icon/playlist.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/icon/previous.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/icon/remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/icon/removeall.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/icon/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/icon/shuffle.svg: -------------------------------------------------------------------------------- 1 | 17 | 20 | 21 | -------------------------------------------------------------------------------- /app/assets/icon/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/icon/unmax.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/assets/icon/volume.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 22 | 23 | -------------------------------------------------------------------------------- /app/assets/icon/volume_max.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/icon/volume_min.svg: -------------------------------------------------------------------------------- 1 | 17 | 21 | 22 | -------------------------------------------------------------------------------- /app/assets/icon/volume_mute.svg: -------------------------------------------------------------------------------- 1 | 17 | 21 | 22 | -------------------------------------------------------------------------------- /app/assets/img/up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/assets/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disoul/electron-cloud-music/0336c509f44cdc88882a32f2f15938ff3f5b0161/app/assets/tray.png -------------------------------------------------------------------------------- /app/components/AlbumCard.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class AlbumCard extends Component { 4 | constructor(props: any) { 5 | super(props); 6 | } 7 | 8 | _addsonglist(e, isplay) { 9 | this.props.addSongList(this.props.songs, isplay); 10 | } 11 | 12 | render() { 13 | return ( 14 |
17 |
18 | 19 |
20 |

{this.props.data.playCount}

21 |
22 |
23 |
24 |
25 |

26 | {this.props.data.name} 27 |

28 |

29 | 来自:{this.props.data.creator.nickname} 30 |

31 |
32 |
33 | {this.props.data.tags.length > 0 ?

TAGS:

: ''} 34 | {this.props.data.tags.map(tag => { 35 | return ( 36 |
37 | {tag} 38 |
39 | ); 40 | })} 41 |
42 |
43 | 46 | 49 |
50 |
51 |
52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/components/App.jsx: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import React, { Component } from 'react'; 3 | import Header from './Header.jsx'; 4 | import Content from './Content.jsx'; 5 | import Player from './Player.jsx'; 6 | import LoginForm from './LoginForm.jsx'; 7 | import PlayContentCard from './PlayContentCard.jsx'; 8 | import Toast from './Toast.jsx'; 9 | 10 | import { connect } from 'react-redux'; 11 | import { bindActionCreators } from 'redux'; 12 | import * as Actions from '../actions/actions'; 13 | 14 | const mapStateToProps = state => ({ 15 | player: state.player, 16 | search: state.search, 17 | song: state.song, 18 | user: state.user, 19 | usersong: state.usersong, 20 | router: state.router, 21 | songlist: state.songlist, 22 | playcontent: state.playcontent, 23 | toast: state.toast 24 | }); 25 | 26 | const mapDispatchToProps = (dispatch) => { 27 | let actions = {}; 28 | for (let key in Actions) { 29 | actions[key] = bindActionCreators(Actions[key], dispatch); 30 | } 31 | return { 32 | actions: actions, 33 | }; 34 | }; 35 | 36 | class App extends Component { 37 | loginForm() { 38 | if (this.props.user.showForm) { 39 | return ( 40 | 44 | ); 45 | } else { 46 | return; 47 | } 48 | } 49 | 50 | toast() { 51 | if (this.props.toast.toastQuery[0]) { 52 | return 53 | } 54 | } 55 | 56 | render() { 57 | const { song } = this.props; 58 | return ( 59 |
60 |
61 | {this.loginForm()} 62 | {this.toast()} 63 | 64 | 68 |
69 | ); 70 | } 71 | } 72 | 73 | 74 | export default connect(mapStateToProps, mapDispatchToProps)(App); 75 | -------------------------------------------------------------------------------- /app/components/Content.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import SearchContent from './SearchContent.jsx'; 3 | import HomeContent from './HomeContent.jsx'; 4 | import SideBar from './SideBar.jsx'; 5 | import Player from './Player.jsx'; 6 | 7 | export default class Content extends Component { 8 | constructor(props: any) { 9 | super(props); 10 | } 11 | 12 | renderContent() { 13 | const { router } = this.props; 14 | return ( 15 |
16 | { 17 | router.routerStack.map( (component, index) => { 18 | let Component = component; 19 | if (index == router.routerStack.length - 1) { 20 | return () 21 | } else { 22 | return () 23 | } 24 | }) 25 | } 26 |
27 | ) 28 | } 29 | 30 | render() { 31 | return ( 32 |
33 | 34 |
35 | {this.renderContent()} 36 |
37 | 38 |
39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import SearchBar from './SearchBar.jsx'; 3 | import UserState from './UserState.jsx'; 4 | 5 | export default class Header extends Component { 6 | _hideApp(e) { 7 | Electron.ipcRenderer.send('hideapp'); 8 | } 9 | 10 | _max(e) { 11 | Electron.ipcRenderer.send('maximize'); 12 | } 13 | 14 | _min(e) { 15 | Electron.ipcRenderer.send('minimize'); 16 | } 17 | 18 | _back(e) { 19 | this.props.actions.pop(); 20 | } 21 | 22 | _clientmini() { 23 | this.props.actions.changeclientmode('mini'); 24 | } 25 | 26 | render() { 27 | let Logo=require('../assets/logo.svg'); 28 | let CloseIcon = require('../assets/icon/close.svg'); 29 | let MaxIcon = require('../assets/icon/max.svg'); 30 | let MinIcon = require('../assets/icon/min.svg'); 31 | let BackIcon = require('../assets/icon/back.svg'); 32 | let CardIcon = require('../assets/icon/card.svg'); 33 | return ( 34 |
40 |
41 | 42 |
43 |
44 | this._back(e) } 50 | /> 51 |
52 |
53 |
54 | 59 | 67 |
73 | this._clientmini() } 76 | /> 77 | this._min(e) } 79 | /> 80 | this._max(e) } 82 | /> 83 | this._hideApp(e) } 85 | /> 86 |
87 |
88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/components/HomeContent.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { recommendResource,recommendSongs } from '../server'; 3 | import Spinner from './Spinner.jsx'; 4 | import MiniAlbumCard from './MiniAlbumCard.jsx'; 5 | import SongList from './SongList.jsx'; 6 | 7 | export default class HomeContent extends Component { 8 | constructor(props: any) { 9 | super(props); 10 | this.state = { 11 | recommendState: 'nouser', 12 | recommend: null, 13 | songState: 'nouser', 14 | songs: null, 15 | } 16 | } 17 | 18 | componentWillReceiveProps(props) { 19 | if (props.user == this.props.user) { 20 | return; 21 | } 22 | if (props.user.loginState == 'logged_in') { 23 | this.setState({ 24 | recommendState: 'fetching', 25 | songState: 'fetching', 26 | }); 27 | recommendResource().then(res => { 28 | this.setState({ 29 | recommendState: 'get', 30 | recommend: res.recommend, 31 | }); 32 | console.logg('REEE', res); 33 | }); 34 | recommendSongs().then(res => { 35 | this.setState({ 36 | songState: 'get', 37 | songs: res.recommend, 38 | }); 39 | console.logg('REEE', res); 40 | }); 41 | } else { 42 | this.setState({ 43 | recommendState: 'nouser', 44 | }); 45 | } 46 | } 47 | 48 | render() { 49 | return ( 50 |
55 |
56 |
57 |
58 |

个性化推荐

59 |
60 | { 61 | this.state.recommendState=='fetching' ? : 62 | this.state.recommendState=='get' ? 63 | (
64 | { 65 | this.state.recommend.map( (songlist, index) => 66 | 72 | ) 73 | } 74 |
) : 75 | (
76 |
) 77 | } 78 |
79 |
80 | { 81 | this.state.songState=='get' ? 82 | () : null 87 | } 88 |
89 |
90 |
91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/components/LoginForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class LoginForm extends Component { 4 | constructor(props: any) { 5 | super(props); 6 | this.state = { 7 | phoneValid: true, 8 | passwordValid: true, 9 | } 10 | } 11 | 12 | _onSubmit(e) { 13 | e.preventDefault(); 14 | if (this.refs.phone.value === '') { 15 | this.setState({ 16 | phoneValid: false 17 | }); 18 | return; 19 | }; 20 | if (this.refs.password.value === '') { 21 | this.setState({ 22 | passwordValid: false 23 | }); 24 | return; 25 | }; 26 | this.props.login({ 27 | phone: this.refs.phone.value, 28 | password: this.refs.password.value, 29 | }); 30 | this.props.loginform(false); 31 | } 32 | 33 | _closeForm(e) { 34 | this.props.loginform(false); 35 | } 36 | 37 | _onChange(e, target) { 38 | if (target === 'phone') { 39 | if (!this.state.phoneValid) { 40 | this.setState({ 41 | phoneValid: true, 42 | }); 43 | } 44 | } else { 45 | if (!this.state.passwordValid) { 46 | this.setState({ 47 | passwordValid: true, 48 | }); 49 | } 50 | } 51 | } 52 | 53 | render() { 54 | let Close = require('../assets/icon/close.svg'); 55 | return ( 56 |
57 |
58 |

登陆

59 | this._closeForm(e) } 62 | /> 63 |
64 |
this._onSubmit(e)} 68 | > 69 | this._onChange(e, 'phone')} 73 | /> 74 | this._onChange(e, 'pw')} 77 | type="password" ref="password" placeholder="输入密码" /> 78 | 79 |
80 | 84 |
85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/components/MiniAlbumCard.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import SongListContent from './SongListContent.jsx'; 3 | 4 | export default class MiniAlbumCard extends Component { 5 | constructor(props: any) { 6 | super(props); 7 | } 8 | 9 | _songlistdetail(e) { 10 | this.props.push(SongListContent); 11 | this.props.fetchsonglistdetail(this.props.data.id); 12 | } 13 | 14 | render() { 15 | return ( 16 |
this._songlistdetail(e) } 19 | > 20 |
21 | 22 |
23 |

{this.props.data.playcount}

24 |
25 |
26 |

{this.props.data.name}

27 |
28 |
29 |
30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/components/MusicContent.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class MusicContent extends Component { 4 | render() { 5 | return ( 6 |
7 | ); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/components/PlayContentCard.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { playlistTracks } from '../server'; 3 | 4 | export default class PlayContentCard extends Component { 5 | constructor(props: any) { 6 | super(props); 7 | this.state = { 8 | cardMode: 'mini', 9 | height: '0px', 10 | width: '0px', 11 | left: '10px', 12 | lyricTranslate: 90, 13 | }; 14 | } 15 | 16 | componentWillReceiveProps(props) { 17 | if (props.data && this.props.data == undefined) { 18 | this.setState({ 19 | height: '100px', 20 | width: '300px', 21 | }); 22 | } 23 | if ((this.props.playcontent.mode != props.playcontent.mode) && props.playcontent.mode == 'max') { 24 | this.setState({ 25 | height: '100%', 26 | width: '100%', 27 | left: '0', 28 | }); 29 | } 30 | if (this.props.playcontent.mode && props.playcontent.mode == 'mini' && this.props.data != undefined) { 31 | this.setState({ 32 | height: '100px', 33 | width: '300px', 34 | left: '10px', 35 | }); 36 | } 37 | if (props.data != this.props.data) { 38 | if (this.props.playcontent.state == 'hidden') { 39 | this.props.actions.showplaycontentmini(); 40 | } 41 | this.setState({ 42 | lyricTranslate: 90, 43 | }); 44 | if (props.data == undefined) { 45 | this.setState({ 46 | height: '0px', 47 | width: '0px', 48 | }); 49 | } else { 50 | this.props.actions.lyric(props.data.id); 51 | } 52 | } 53 | } 54 | 55 | componentDidUpdate(props, state) { 56 | if (this.props.playcontent.currentLyric != props.playcontent.currentLyric) { 57 | console.logg('change'); 58 | let target = this.refs.current; 59 | let container = this.refs.lyric; 60 | this.setState({ 61 | lyricTranslate: this.state.lyricTranslate + container.getBoundingClientRect().top - target.getBoundingClientRect().top + 90, 62 | }); 63 | } 64 | } 65 | 66 | _showmaxormini(e) { 67 | if (this.props.playcontent.mode == 'mini') { 68 | this.props.actions.showplaycontentmax(); 69 | } else { 70 | this.props.actions.hiddenplaycontentmax(); 71 | } 72 | } 73 | 74 | _starSong(e, id) { 75 | let pid = this.props.usersong.create[0].id; 76 | playlistTracks('add', pid, id).then(res => { 77 | if (res.code == 502) { 78 | this.props.actions.toast('收藏失败!歌曲已经存在'); 79 | } else if (res.code == 200) { 80 | this.props.actions.toast('收藏成功!'); 81 | } else { 82 | this.props.actions.toast('收藏失败 Code:'+res.code); 83 | } 84 | }).catch(err => { 85 | this.props.actions.toast('收藏失败!' + err); 86 | }); 87 | e.stopPropagation(); 88 | } 89 | 90 | renderLyric() { 91 | const { playcontent } = this.props; 92 | return playcontent.lyric.lyric.map((lrcobj, index) => { 93 | return ( 94 |
99 |

{lrcobj.content}

100 |
101 | ); 102 | }); 103 | } 104 | 105 | renderMain() { 106 | let Star = require('../assets/icon/star.svg'); 107 | return ( 108 |
this._showmaxormini(e)} 120 | > 121 |
126 |
127 | 128 |
129 |
130 |

131 | {this.props.data.name} 132 |

133 |

134 | {this.props.data.artists[0].name} 135 |

136 |
137 |
138 |
143 |
148 |
149 |
150 |
151 |
152 | 153 |
154 |
155 |

156 | {this.props.data.name} 157 |

158 |

159 | {this.props.data.artists[0].name} 160 |

161 |

162 | {this.props.data.album.name} 163 |

164 |
this._starSong(e, this.props.data.id)} 166 | className="maxplaycontent__info__control"> 167 | 170 | 喜欢 171 |
172 |
173 |
174 |
175 |
182 | {this.renderLyric()} 183 |
184 |
185 |
186 |
187 |
188 | ); 189 | } 190 | 191 | renderDefault() { 192 | return ( 193 |
200 |
201 | ); 202 | } 203 | 204 | render() { 205 | if (this.props.data == undefined) { 206 | return this.renderDefault(); 207 | } else { 208 | return this.renderMain(); 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /app/components/PlayList.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class PlayList extends Component { 4 | constructor(props: any) { 5 | super(props); 6 | 7 | this.state = { 8 | show: false, 9 | } 10 | } 11 | 12 | secToTime(sec) { 13 | sec = sec / 1000; 14 | let min = parseInt(sec / 60); 15 | if (min < 10) { 16 | min = '0' + min; 17 | } 18 | let second = parseInt(sec % 60); 19 | if (second < 10) { 20 | second = '0' + second; 21 | } 22 | 23 | return min + ':' + second; 24 | } 25 | 26 | getSongClassName(index) { 27 | if (index == this.props.song.currentSongIndex) { 28 | return "playlist__content__list__song current"; 29 | } else { 30 | return "playlist__content__list__song"; 31 | } 32 | } 33 | 34 | componentWillReceiveProps(props) { 35 | if (props.showplaylist && !this.state.show) { 36 | this.setState({ 37 | show: true, 38 | }); 39 | } 40 | if (!props.showplaylist && this.state.show) { 41 | this.setState({ 42 | show: false, 43 | }); 44 | } 45 | } 46 | 47 | componentDidUpdate(props, state) { 48 | if (this.props.song.currentSongIndex != props.song.currentSongIndex) { 49 | this.autoScroll(); 50 | } 51 | } 52 | 53 | autoScroll() { 54 | let target = this.refs.current; 55 | let container = this.refs.container; 56 | container.scrollTop = 0; 57 | container.scrollTop = target.getBoundingClientRect().top - container.getBoundingClientRect().top - 150; 58 | } 59 | 60 | _closeplaylist(e) { 61 | this.props.closePlayList(); 62 | } 63 | 64 | _removefromlist(e, index) { 65 | e.stopPropagation(); 66 | this.props.removesongfromlist(index); 67 | } 68 | 69 | _removeall(e) { 70 | this.props.removesonglist(); 71 | } 72 | 73 | _playfromlist(e, index) { 74 | this.props.playFromList(index); 75 | } 76 | 77 | render() { 78 | let Close = require('../assets/icon/close.svg'); 79 | let Remove = require('../assets/icon/remove.svg'); 80 | let RemoveAll = require('../assets/icon/removeall.svg'); 81 | return ( 82 |
83 |
84 |

播放列表

85 |
86 | this._removeall(e)} 89 | /> 90 | this._closeplaylist(e)} 93 | /> 94 |
95 |
96 |
    97 | {this.props.song.songlist.map( 98 | (song, index) => { 99 | return ( 100 |
  • this._playfromlist(e, index)} 105 | > 106 |
    107 |

    {song.name}

    108 |
    109 |
    110 |

    {song.artists[0].name}

    111 |
    112 |
    113 |

    {this.secToTime(song.duration)}

    114 |
    115 |
    116 | this._removefromlist(e, index)} 118 | /> 119 |
    120 |
  • 121 | ); 122 | }) 123 | } 124 |
125 |
126 |
127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /app/components/PlayListControl.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class PlayListControl extends Component { 4 | constructor(props: any) { 5 | super(props); 6 | } 7 | 8 | _changeRule(e) { 9 | this.props.changeRule(); 10 | } 11 | 12 | _showorhidePlaylist(e) { 13 | if (this.props.song.showplaylist) { 14 | this.props.closePlayList(); 15 | } else { 16 | this.props.showPlayList(); 17 | } 18 | } 19 | 20 | getClassName() { 21 | if (this.props.song.showplaylist) { 22 | return "player__playlistcontrol__playlist active"; 23 | } else { 24 | return "player__playlistcontrol__playlist"; 25 | } 26 | } 27 | 28 | getPlayRule() { 29 | //FIXME: svg-loader only accept string args 30 | if (this.props.song.rules[this.props.song.playRule] == 'one') { 31 | return require('../assets/icon/one.svg'); 32 | } 33 | if (this.props.song.rules[this.props.song.playRule] == 'loop') { 34 | return require('../assets/icon/loop.svg'); 35 | } 36 | if (this.props.song.rules[this.props.song.playRule] == 'shuffle') { 37 | return require('../assets/icon/shuffle.svg'); 38 | } 39 | } 40 | 41 | render() { 42 | let PlayRule = this.getPlayRule(); 43 | let PlayListIcon = require('../assets/icon/playlist.svg'); 44 | return ( 45 |
46 |
47 | this._changeRule(e)} 50 | /> 51 |
52 |
this._showorhidePlaylist(e)} 54 | className={this.getClassName()}> 55 | 58 |
59 |

{this.props.song.songlist.length}

60 |
61 |
62 |
63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/components/Player.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Volume from './Volume.jsx'; 3 | import PlayListControl from './PlayListControl.jsx'; 4 | import PlayerList from './PlayList.jsx'; 5 | import { getSongUrl, logWeb } from '../server'; 6 | 7 | export default class Player extends Component { 8 | constructor(props: any) { 9 | super(props); 10 | this.mouseState = { 11 | press: false, 12 | }; 13 | 14 | this.autoplay = true; 15 | 16 | this.state = { 17 | playbuttonIcon: 'play', 18 | currentTime: 0, 19 | duration: 1, 20 | buffered: 0, 21 | source: '', 22 | state: 'get', 23 | } 24 | } 25 | 26 | componentDidMount() { 27 | let self = this; 28 | this.refs.audio.addEventListener("progress", (e) => { 29 | this.setState({ 30 | buffered: e.target.buffered.end(e.target.buffered.length - 1) 31 | }); 32 | }, true); 33 | 34 | this.refs.audio.addEventListener("durationchange", e => { 35 | this.setState({ 36 | duration: e.target.duration 37 | }); 38 | }, true) 39 | 40 | this.refs.audio.addEventListener("timeupdate", e => { 41 | if (self.mouseState.press || self.state.playbuttonIcon == 'play') { 42 | return; 43 | } 44 | this.setState({ 45 | currentTime: e.target.currentTime 46 | }); 47 | 48 | const { playcontent } = this.props; 49 | 50 | let i = playcontent.currentLyric + 1; 51 | if (i < playcontent.lyric.lyric.length && playcontent.lyric.lyric[i].time < e.target.currentTime) { 52 | this.props.actions.setlyric(i); 53 | } 54 | }, true) 55 | 56 | this.refs.audio.addEventListener("canplay", e => { 57 | if (this.autoplay) { 58 | self.props.actions.play(); 59 | this.autoplay = false; 60 | this.setState({ 61 | state: 'get', 62 | }); 63 | } 64 | }, true) 65 | 66 | this.refs.audio.addEventListener("ended", e => { 67 | self.props.actions.nextSong(); 68 | }, true) 69 | 70 | this.refs.audio.addEventListener("seeked", e => { 71 | const { playcontent } = this.props; 72 | console.logg('seekset', e.target.currentTime, this.getCurrentLyric( 73 | 0, 74 | playcontent.lyric.lyric.length - 1, 75 | e.target.currentTime, 76 | playcontent.lyric.lyric 77 | )); 78 | self.props.actions.setlyric(this.getCurrentLyric( 79 | 0, 80 | playcontent.lyric.lyric.length - 1, 81 | e.target.currentTime, 82 | playcontent.lyric.lyric 83 | )); 84 | }, true); 85 | 86 | Electron.ipcRenderer.on('playorpause', event => { 87 | this._playorpause(); 88 | }); 89 | 90 | Electron.ipcRenderer.on('previous', event => { 91 | this._previous(); 92 | }); 93 | 94 | Electron.ipcRenderer.on('next', event => { 95 | this._next(); 96 | }); 97 | } 98 | 99 | componentWillReceiveProps(props) { 100 | let self = this; 101 | const { song } = this.props; 102 | if (props.player.isplay) { 103 | this.refs.audio.play(); 104 | this.setState({ 105 | playbuttonIcon: 'pause', 106 | }); 107 | } else { 108 | this.refs.audio.pause(); 109 | this.setState({ 110 | playbuttonIcon: 'play', 111 | }); 112 | } 113 | 114 | if ( props.song.songlist.length > 0 && 115 | !_.isEqual(props.song.songlist[props.song.currentSongIndex], 116 | song.songlist[song.currentSongIndex]) 117 | ) { 118 | // 向网易发送听歌数据 119 | if ( song.songlist.length > 0 ) { 120 | logWeb( 121 | 'play', 122 | song.songlist[song.currentSongIndex].id, 123 | Math.floor(self.refs.audio.currentTime), 124 | 'ui' 125 | ).then(res => { 126 | }); 127 | } 128 | 129 | self.props.actions.pause(); 130 | self.setState({ 131 | state: 'loading', 132 | currentTime: 0, 133 | }); 134 | 135 | getSongUrl(props.song.songlist[props.song.currentSongIndex], data => { 136 | if (!data.url) { 137 | self.props.actions.nextSong(); 138 | } 139 | if (data.id == props.song.songlist[props.song.currentSongIndex].id) { 140 | self.setState({ 141 | source: data.url, 142 | }); 143 | } 144 | }); 145 | } 146 | } 147 | 148 | componentDidUpdate(props, state) { 149 | // update audio 150 | if (state.source !== this.state.source) { 151 | this.autoplay = true; 152 | } 153 | } 154 | 155 | getCurrentLyric(start, end, currentTime, lyric) { 156 | if (lyric[start].time >= currentTime) { 157 | return start; 158 | } 159 | if (lyric[end].time <= currentTime) { 160 | return end; 161 | } 162 | let mid = Math.floor((end + start)/2); 163 | if (mid == start) { 164 | return start; 165 | } 166 | if (lyric[mid].time < currentTime) { 167 | return this.getCurrentLyric(mid, end, currentTime, lyric); 168 | } else { 169 | return this.getCurrentLyric(start, mid, currentTime, lyric); 170 | } 171 | } 172 | 173 | secToTime(sec) { 174 | let min = parseInt(sec / 60); 175 | if (min < 10) { 176 | min = '0' + min; 177 | } 178 | let second = parseInt(sec % 60); 179 | if (second < 10) { 180 | second = '0' + second; 181 | } 182 | 183 | return min + ':' + second; 184 | } 185 | 186 | updateVolume(volume, ismute) { 187 | console.logg('mute', ismute); 188 | if (ismute) { 189 | this.refs.audio.volume = 0; 190 | } else { 191 | this.refs.audio.volume = volume; 192 | } 193 | } 194 | 195 | _playorpause() { 196 | if (!this.props.song.currentSongIndex && this.props.song.songlist.length > 0) { 197 | this.props.actions.playFromList(0); 198 | } 199 | if (this.props.player.isplay) { 200 | this.props.actions.pause(); 201 | } else { 202 | this.props.actions.play(); 203 | } 204 | } 205 | 206 | _previous(e) { 207 | this.props.actions.previousSong(); 208 | } 209 | 210 | _next(e) { 211 | this.props.actions.nextSong(); 212 | } 213 | 214 | _handleMouseUp(e) { 215 | if (!this.mouseState.press) { 216 | return; 217 | } 218 | this.mouseState.press = false; 219 | let pgbarWidth = this.refs.pgbar.clientWidth; 220 | this.setState({ 221 | currentTime: this.refs.audio.duration * (e.pageX - this.refs.pgbar.getBoundingClientRect().left) / pgbarWidth 222 | }); 223 | this.refs.audio.currentTime = this.state.currentTime; 224 | } 225 | 226 | _handleMouseMove(e) { 227 | if (!this.mouseState.press) { 228 | return; 229 | } 230 | let pgbarWidth = this.refs.pgbar.clientWidth; 231 | this.setState({ 232 | currentTime: this.refs.audio.duration * (e.pageX - this.refs.pgbar.getBoundingClientRect().left) / pgbarWidth 233 | }); 234 | } 235 | 236 | _seek(e) { 237 | if (this.state.source) { 238 | this.mouseState.press = true; 239 | window.addEventListener("mouseup", this._handleMouseUp.bind(this)); 240 | window.addEventListener("mousemove", this._handleMouseMove.bind(this)); 241 | } 242 | } 243 | 244 | render() { 245 | let Previous = require('../assets/icon/previous.svg'); 246 | let Next = require('../assets/icon/next.svg'); 247 | let Play = require('../assets/icon/' + this.state.playbuttonIcon + '.svg'); 248 | return ( 249 |
250 | 255 |
256 | 261 | 269 | 274 |
275 |
276 |

277 | {this.secToTime(this.state.currentTime)} 278 |

279 |
{ this._seek(e) }} 281 | > 282 |
283 |
{ this._seek(e) }} 286 | style={{ 287 | width: String(this.state.currentTime / this.state.duration * 100) + '%' 288 | }} 289 | > 290 |
291 |
292 |
298 |
299 |

300 | {this.secToTime(this.state.duration)} 301 |

302 |
303 | 304 | 310 | 318 |
319 | ); 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /app/components/SearchBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { search } from '../server'; 3 | import SeachContent from './SearchContent.jsx'; 4 | 5 | export default class SearchBar extends Component { 6 | constructor(props: any) { 7 | super(props); 8 | } 9 | 10 | _onSubmit(e) { 11 | e.preventDefault(); 12 | this.props.push(SeachContent); 13 | this.props.search(this.refs.search.value); 14 | } 15 | 16 | render() { 17 | let SearchIcon = require("../assets/icon/search.svg"); 18 | return ( 19 |
25 |
this._onSubmit(e)} 26 | > 27 | 30 | 31 | 32 |
33 |
34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/components/SearchContent.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Spinner from './Spinner.jsx'; 3 | import SongCard from './SongCard.jsx'; 4 | import SongList from './SongList.jsx'; 5 | 6 | export default class SearchContent extends Component { 7 | constructor(props: any) { 8 | super(props); 9 | } 10 | 11 | renderResult() { 12 | if (this.props.search.searchResponse.songCount === 0) { 13 | return (

无结果

); 14 | } else { 15 | return ( 16 |
17 |
18 |

最佳匹配

19 | 23 |
24 |
25 | 30 |
31 |
32 | ); 33 | } 34 | } 35 | 36 | render() { 37 | if (this.props.search.searchState == 'START') { 38 | return this.renderSearching(); 39 | } 40 | if (this.props.search.searchState == 'FINISH') { 41 | return this.renderFinish(); 42 | } 43 | if (this.props.search.searchState == 'ERROR') { 44 | return this.renderError(); 45 | } 46 | } 47 | 48 | renderSearching() { 49 | return ( 50 |
55 |
56 |

57 | {this.props.search.searchInfo.keywords} 58 | 搜索中... 59 |

60 |
61 |
62 | 63 |
64 |
65 | ); 66 | } 67 | 68 | renderFinish() { 69 | return ( 70 |
75 |
76 |

77 | {this.props.search.searchInfo.keywords} 78 | 搜索到{this.props.search.searchResponse.songCount}首歌曲 79 |

80 |
81 | { this.renderResult() } 82 |
83 | ); 84 | } 85 | 86 | renderError() { 87 | return ( 88 |
Error
93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/components/SideBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Spinner from './Spinner.jsx'; 3 | import SongListContent from './SongListContent.jsx'; 4 | 5 | export default class SideBar extends Component { 6 | constructor(props: any) { 7 | super(props); 8 | this.state = { 9 | showCreate: true, 10 | showCollect: true, 11 | createHeight: null, 12 | collectHeight: null, 13 | }; 14 | this.scroll = { 15 | lastScrollTop: 0, 16 | }; 17 | } 18 | 19 | componentDidUpdate(props, state) { 20 | // 获取歌单块高度进行transition 21 | if (this.props.usersong.state == 'get' && props.usersong.state == 'fetching'){ 22 | this.setState({ 23 | createHeight: this.refs.create.getBoundingClientRect().height, 24 | collectHeight: this.refs.collect.getBoundingClientRect().height, 25 | }); 26 | } 27 | } 28 | 29 | _songlistdetail(id) { 30 | this.props.actions.push(SongListContent); 31 | this.props.actions.fetchsonglistdetail(id); 32 | } 33 | 34 | getCreate() { 35 | const { usersong } = this.props; 36 | switch (usersong.state){ 37 | case 'nouser': 38 | return

无用户

39 | case 'fetching': 40 | return 41 | case 'error': 42 | return

获取歌单出错{usersong.errorinfo}

43 | case 'get': 44 | let PlayListIcon = require('../assets/icon/playlist.svg') 45 | let self = this; 46 | return ( 47 |
    54 | {usersong.create.map((songlist, index) => { 55 | return ( 56 |
  • self._songlistdetail(songlist.id)} 58 | key={index} 59 | className="sidebar__mylist__content__list"> 60 | 61 |

    {songlist.name}

    62 |
  • 63 | ) 64 | })} 65 |
66 | ); 67 | } 68 | } 69 | 70 | getCollect() { 71 | const { usersong } = this.props; 72 | let self = this; 73 | switch (usersong.state){ 74 | case 'nouser': 75 | return

无用户

76 | case 'fetching': 77 | return 78 | case 'error': 79 | return

获取歌单出错{usersong.errorinfo}

80 | case 'get': 81 | let PlayListIcon = require('../assets/icon/playlist.svg') 82 | return ( 83 |
    90 | {usersong.collect.map((songlist, index) => { 91 | return ( 92 |
  • self._songlistdetail(songlist.id)} 94 | key={index} 95 | className="sidebar__mylist__content__list"> 96 | 97 |

    {songlist.name}

    98 |
  • 99 | ) 100 | })} 101 |
102 | ); 103 | } 104 | } 105 | 106 | _showorhide(target) { 107 | if (target === 'showCreate') { 108 | this.setState({ 109 | showCreate: !this.state.showCreate, 110 | }); 111 | } 112 | if (target === 'showCollect') { 113 | this.setState({ 114 | showCollect: !this.state.showCollect, 115 | }); 116 | } 117 | } 118 | 119 | _onscroll(e) { 120 | if (this.props.playcontent.state == 'show' && e.target.scrollTop > this.scroll.lastScrollTop) { 121 | this.props.actions.hiddenplaycontentmini(); 122 | } 123 | if (this.props.playcontent.state == 'hidden' && e.target.scrollTop < this.scroll.lastScrollTop) { 124 | this.props.actions.showplaycontentmini(); 125 | } 126 | this.scroll.lastScrollTop = e.target.scrollTop; 127 | } 128 | 129 | render() { 130 | return ( 131 |
this._onscroll(e)} 133 | className="sidebar"> 134 |
135 |

我创建的歌单

136 | this._showorhide('showCreate')} 140 | src={require('url!../assets/img/up.svg')} 141 | /> 142 | {this.getCreate()} 143 |
144 |
145 |

我收藏的歌单

146 | this._showorhide('showCollect')} 151 | /> 152 | {this.getCollect()} 153 |
154 |
155 | ); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /app/components/SongCard.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class SongCard extends Component { 4 | constructor(props: any) { 5 | super(props); 6 | } 7 | 8 | _playsong(e, song) { 9 | this.props.changeSong(song); 10 | } 11 | 12 | render() { 13 | return ( 14 |
this._playsong(e, this.props.data)} 17 | > 18 | 19 |
20 |

21 | {this.props.data.name} 22 |

23 |

24 | 专辑:{this.props.data.album.name} 25 | 歌手:{this.props.data.artists[0].name} 26 |

27 |
28 |
29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/components/SongList.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class SongList extends Component { 4 | constructor(props: any) { 5 | super(props); 6 | } 7 | 8 | secToTime(sec) { 9 | sec = sec / 1000; 10 | let min = parseInt(sec / 60); 11 | if (min < 10) { 12 | min = '0' + min; 13 | } 14 | let second = parseInt(sec % 60); 15 | if (second < 10) { 16 | second = '0' + second; 17 | } 18 | 19 | return min + ':' + second; 20 | } 21 | 22 | getShortName(name, limit) { 23 | if (name.length > limit) { 24 | return name.slice(0, limit) + '...'; 25 | } else { 26 | return name; 27 | } 28 | } 29 | 30 | _playsong(e, song) { 31 | this.props.changeSong(song); 32 | } 33 | 34 | _addsong(e, song) { 35 | this.props.addSong(song); 36 | } 37 | 38 | render() { 39 | let Add = require('../assets/icon/add.svg'); 40 | return ( 41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | {this.props.data.map( (song, index) => { 56 | return ( 57 | 60 | 61 | 67 | 73 | 76 | 79 | 82 | 90 | 91 | ); 92 | })} 93 | 94 |
编号音乐标题歌手专辑时长热度
{index + 1} 62 | this._addsong(e, song)} 65 | /> 66 | this._playsong(e, song)} 70 | > 71 | {this.getShortName(song.name, 30)} 72 | 74 | {this.getShortName(song.artists[0].name, 15)} 75 | 77 | {this.getShortName(song.album.name, 18)} 78 | 80 | {this.secToTime(song.duration)} 81 | 83 |
84 |
88 |
89 |
95 |
96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/components/SongListContent.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // 歌单内容 3 | 4 | import React, { Component } from 'react'; 5 | import Spinner from './Spinner.jsx'; 6 | import AlbumCard from './AlbumCard.jsx'; 7 | import SongList from './SongList.jsx'; 8 | 9 | export default class SongListContent extends Component { 10 | constructor(props: any) { 11 | super(props); 12 | } 13 | 14 | renderResult() { 15 | if (this.props.search.searchResponse.songCount === 0) { 16 | return (

无结果

); 17 | } else { 18 | return ( 19 |
20 |
21 |

最佳匹配

22 | 26 |
27 |
28 | 33 |
34 |
35 | ); 36 | } 37 | } 38 | 39 | render() { 40 | if (this.props.songlist.state == 'fetching') { 41 | return this.renderFetching(); 42 | } 43 | if (this.props.songlist.state == 'get') { 44 | return this.renderFinish(); 45 | } 46 | if (this.props.songlist.state == 'error') { 47 | return this.renderFetching(); 48 | } 49 | } 50 | 51 | renderFetching() { 52 | return ( 53 |
58 |
59 |

歌单详情

60 |
61 |
62 | 63 |
64 |
65 | ); 66 | } 67 | 68 | renderFinish() { 69 | let songs = this.props.songlist.content.tracks; 70 | songs.map(song => { 71 | song.artists = song.ar; 72 | song.album = song.al; 73 | song.duration = song.dt; 74 | song.score = song.pop; 75 | if (song.h) { 76 | song.hMusic = song.h; 77 | song.hMusic.bitrate = song.h.br; 78 | } 79 | if (song.m) { 80 | song.mMusic = song.m; 81 | song.mMusic.bitrate = song.m.br; 82 | } 83 | if (song.l) { 84 | song.lMusic = song.l; 85 | song.lMusic.bitrate = song.l.br; 86 | } 87 | }); 88 | return ( 89 |
94 |
95 |

歌单详情

96 |
97 |
98 |
99 | 104 |
105 |
106 | 111 |
112 |
113 |
114 | ); 115 | } 116 | 117 | renderError() { 118 | return ( 119 |
Error
124 | ); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /app/components/Spinner.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class Spinner extends Component { 4 | render() { 5 | return ( 6 |
7 |
8 | ); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/components/Toast.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class Toast extends Component { 4 | constructor(props: any) { 5 | super(props); 6 | } 7 | 8 | render() { 9 | return ( 10 |
11 |

{this.props.content}

12 |
13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/components/UserState.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { search } from '../server'; 3 | import Spinner from './Spinner.jsx'; 4 | 5 | export default class UserState extends Component { 6 | constructor(props: any) { 7 | super(props); 8 | this.state = { 9 | showMenu: false, 10 | } 11 | } 12 | 13 | _login(e) { 14 | this.props.loginform(true); 15 | } 16 | 17 | _showMenu(e) { 18 | this.setState({ 19 | showMenu: !this.state.showMenu, 20 | }); 21 | } 22 | 23 | _logout(e) { 24 | this.props.toguest(); 25 | } 26 | 27 | componentDidMount() { 28 | let self = this; 29 | // 根据cookie判断是否自动登陆 30 | Electron.ipcRenderer.on('cookie', (e, cookies) => { 31 | console.logg(cookies); 32 | let flag = 0; 33 | cookies.map(cookie => { 34 | if (cookie.name === 'MUSIC_U') { 35 | flag++; 36 | } 37 | if ((cookie.name === '__remember_me') && (cookie.value === 'true')) { 38 | flag++; 39 | } 40 | }); 41 | if (flag > 1) { 42 | if (localStorage.user) { 43 | self.props.logged_in(JSON.parse(localStorage.getItem('user'))); 44 | self.props.fetchusersong(self.props.user.profile.userId); 45 | } 46 | } 47 | }); 48 | } 49 | 50 | render() { 51 | if (this.props.user.loginState == 'logged_in') { 52 | return this.renderUser(); 53 | } else if (this.props.user.loginState == 'logging_in') { 54 | return this.renderLogging(); 55 | } else { 56 | return this.renderGuest(); 57 | } 58 | } 59 | 60 | renderGuest() { 61 | if (this.props.user.loginState == 'logged_failed') { 62 | alert(this.props.user.loginError, "登录失败"); 63 | this.props.toguest(); 64 | } 65 | return ( 66 |
72 |
73 |
74 |
this._login(e) }> 75 | 登录 76 |
77 |
78 | ); 79 | } 80 | 81 | renderLogging() { 82 | return ( 83 |
89 |
90 | 91 |
92 |
this._login(e) }> 93 | 登录中.. 94 |
95 |
96 | ); 97 | } 98 | 99 | renderUser() { 100 | return ( 101 |
107 |
this._showMenu(e)} 110 | > 111 | 112 | { this.state.showMenu ? (
113 |
    114 |
  • this._logout(e) } 116 | >退出登录
  • 117 |
118 |
) : ''} 119 |
120 |
121 | {this.props.user.profile.nickname} 122 |
123 |
124 | ); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /app/components/Volume.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class Volume extends Component { 4 | constructor(props: any) { 5 | super(props); 6 | this.mouseState = { 7 | press: false, 8 | }; 9 | this.state = { 10 | volume: 1, 11 | mute: false, 12 | } 13 | } 14 | 15 | _handleMouseUp(e) { 16 | if (!this.mouseState.press) { 17 | return; 18 | } 19 | this.mouseState.press = false; 20 | let volumeWidth = this.refs.volume.clientWidth; 21 | let volume = (e.pageX - this.refs.volume.getBoundingClientRect().left) / volumeWidth; 22 | volume = volume > 1 ? 1 : volume; 23 | volume = volume < 0 ? 0 : volume; 24 | 25 | this.setState({ 26 | "volume": volume 27 | }); 28 | this.props.updateVolume(this.state.volume, this.state.mute); 29 | } 30 | 31 | _handleMouseMove(e) { 32 | if (!this.mouseState.press) { 33 | return; 34 | } 35 | let volumeWidth = this.refs.volume.clientWidth; 36 | let volume = (e.pageX - this.refs.volume.getBoundingClientRect().left) / volumeWidth; 37 | volume = volume > 1 ? 1 : volume; 38 | volume = volume < 0 ? 0 : volume; 39 | 40 | this.setState({ 41 | "volume": volume 42 | }); 43 | this.props.updateVolume(this.state.volume, this.state.mute); 44 | } 45 | 46 | _mouseDown(e) { 47 | this.mouseState.press = true; 48 | window.addEventListener("mouseup", this._handleMouseUp.bind(this)); 49 | window.addEventListener("mousemove", this._handleMouseMove.bind(this)); 50 | } 51 | 52 | _mute(e) { 53 | this.setState({ 54 | mute: !this.state.mute 55 | }, () => { 56 | this.props.updateVolume(this.state.volume, this.state.mute); 57 | }); 58 | } 59 | 60 | getVolumeIcon() { 61 | if (this.state.mute) { 62 | return require('../assets/icon/volume_mute.svg'); 63 | } 64 | 65 | if (this.state.volume > 0.7 ) { 66 | return require('../assets/icon/volume_max.svg'); 67 | } else if (this.state.volume > 0.3) { 68 | return require('../assets/icon/volume_min.svg'); 69 | } else { 70 | return require('../assets/icon/volume.svg'); 71 | } 72 | } 73 | 74 | render() { 75 | var VolumeIcon = this.getVolumeIcon(); 76 | return ( 77 |
78 | this._mute(e) } 80 | className="i" /> 81 |
this._mouseDown(e) } 85 | > 86 |
90 |
91 |
92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/containers/CloudMusic.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import App from '../components/App.jsx'; 4 | 5 | import thunkMiddleware from 'redux-thunk'; 6 | import createLogger from 'redux-logger'; 7 | import { Provider } from 'react-redux'; 8 | import { createStore, applyMiddleware } from 'redux'; 9 | 10 | import cloudMusic from '../reducers'; 11 | 12 | const loggerMiddleware = createLogger(); 13 | const createStoreWithMiddleware = applyMiddleware( 14 | thunkMiddleware, 15 | loggerMiddleware 16 | )(createStore); 17 | 18 | var store = createStoreWithMiddleware(cloudMusic); 19 | class CloudMusic extends Component { 20 | render() { 21 | return ( 22 | 23 | 24 | 25 | ); 26 | } 27 | } 28 | 29 | render( 30 | , 31 | document.body 32 | ); 33 | -------------------------------------------------------------------------------- /app/libs/lrcparse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | export default function lyricParser(lrc) { 3 | return { 4 | 'lyric': parseLrc(lrc.lrc.lyric), 5 | 'tlyric': parseLrc(lrc.tlyric.lyric), 6 | 'lyricuser': lrc.lyricUser, 7 | 'transuser': lrc.transUser, 8 | } 9 | } 10 | 11 | function parseLrc(lrc) { 12 | let _lrc = lrc.split('\n'); 13 | let parsedLrc = [{ 14 | time: 0, 15 | content: '', 16 | }]; 17 | for (let i = 0;i < _lrc.length;i++) { 18 | let timeReg = /^\[([0-9][0-9])\:([0-9][0-9].*)](.*)$/i; 19 | let parsed = timeReg.exec(_lrc[i]); 20 | if (parsed == null) { 21 | continue; 22 | } 23 | let min = parseInt(parsed[1]); 24 | let sec = parseFloat(parsed[2]); 25 | 26 | parsedLrc.push({ 27 | 'time': sec + min * 60, 28 | 'content': parsed[3], 29 | }); 30 | } 31 | 32 | return parsedLrc; 33 | } 34 | -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | require('./postcss/index.css'); 2 | var _ = require('lodash') 3 | require('./containers/CloudMusic.jsx'); 4 | 5 | window.LOG = true; 6 | 7 | console.__proto__.constructor.prototype.logg = function() { 8 | if (window.LOG) 9 | console.log.apply(console, arguments); 10 | } 11 | -------------------------------------------------------------------------------- /app/postcss/_albumcard.css: -------------------------------------------------------------------------------- 1 | .albumcard { 2 | height: 200px; 3 | min-width: 500px; 4 | max-width: 800px; 5 | display: flex; 6 | align-items: stretch; 7 | } 8 | 9 | .albumcard__cover { 10 | position: relative; 11 | } 12 | 13 | .albumcard__cover__playcount { 14 | position: absolute; 15 | bottom: 0; 16 | width: 100%; 17 | height: 30px; 18 | line-height: 30px; 19 | padding-left: 10px; 20 | background-color: rgba(0, 0, 0, 0.5); 21 | color: #fff; 22 | } 23 | 24 | .albumcard-left { 25 | flex: 1; 26 | display: flex; 27 | justify-content: space-between; 28 | padding-right: 16px; 29 | padding-bottom: 16px; 30 | flex-direction: column; 31 | } 32 | 33 | .albumcard__info { 34 | padding: 20px 20px; 35 | } 36 | 37 | .albumcard__info__name { 38 | font-size: 30px; 39 | color: $primarytext; 40 | } 41 | 42 | .albumcard__info__creator { 43 | font-size: 20px; 44 | margin-top: 20px; 45 | color: $secondarytext; 46 | } 47 | 48 | .albumcard__buttons { 49 | display: flex; 50 | justify-content: flex-end; 51 | } 52 | 53 | .albumcard__buttons__button { 54 | margin-right: 5px; 55 | border-radius: 2px; 56 | &:first-child { 57 | color: $red-500; 58 | } 59 | &:last-child { 60 | color: $indigo-500; 61 | } 62 | } 63 | 64 | .albumcard__tags { 65 | display: flex; 66 | font-size: 18px; 67 | padding-left: 20px; 68 | p { 69 | color: $primarytext; 70 | display: block; 71 | } 72 | } 73 | 74 | .albumcard__tags__tag{ 75 | color: $secondarytext; 76 | margin-right: 10px; 77 | } 78 | -------------------------------------------------------------------------------- /app/postcss/_button.css: -------------------------------------------------------------------------------- 1 | @define-mixin btn { 2 | border: 0; 3 | background-color: $indigo-200.0; 4 | height: 36px; 5 | font-size: 20px; 6 | line-height: 36px; 7 | text-align: center; 8 | cursor: pointer; 9 | padding: 0 16px; 10 | transition: background-color ease 0.5s; 11 | font-weight: bold; 12 | &:hover { 13 | background-color: $indigo-200; 14 | } 15 | } 16 | 17 | .btn-normal { 18 | @mixin btn; 19 | } 20 | 21 | .btn-bg { 22 | @mixin btn; 23 | color: #fff; 24 | background-color: $indigo-700; 25 | &:hover { 26 | background-color: $indigo-500; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/postcss/_card.css: -------------------------------------------------------------------------------- 1 | .card { 2 | background-color: #fff; 3 | cursor: pointer; 4 | box-shadow: 0 2px 2px rgba(0, 0, 0, .18); 5 | transition: all ease 0.5s; 6 | transform: translateY(0); 7 | border-radius: 2px; 8 | * { 9 | cursor: pointer; 10 | } 11 | &:hover { 12 | transform: translateY(-4px); 13 | box-shadow: 0 8px 8px rgba(0, 0, 0, .18); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/postcss/_colors.css: -------------------------------------------------------------------------------- 1 | $red-200: #ef9a9a; 2 | $red-300: #e57373; 3 | $red-500: #f44336; 4 | $red-700: #d32f2f; 5 | $red-300: #e57373; 6 | $red-900: #b0120a; 7 | $red-100: #f9bdbb; 8 | 9 | $indigo-50: #e8eaf6; 10 | $indigo-200: #9fa8da; 11 | $indigo-500: #3f51b5; 12 | $indigo-700: #303f9f; 13 | $indigo-900: #1a237e; 14 | 15 | $primarytext: rgba(0, 0, 0, .87); 16 | $secondarytext: rgba(0, 0, 0, .54); 17 | $disabletext: rgba(0, 0, 0, .38); 18 | 19 | $content: #fafafa; 20 | $content2: #f7f7f7; 21 | $content3: #eeeeee; 22 | $dividers: #e0e0e0; 23 | 24 | $grey-200: #eeeeee; 25 | $grey-300: #e0e0e0; 26 | $grey-400: #bdbdbd; 27 | $grey-500: #9e9e9e; 28 | $grey-600: #757575; 29 | 30 | $icon: rgba(0, 0, 0, .38); 31 | -------------------------------------------------------------------------------- /app/postcss/_content.css: -------------------------------------------------------------------------------- 1 | @import "home-content"; 2 | @import "search-content"; 3 | @import "songlist-content"; 4 | @import "sidebar"; 5 | 6 | #content { 7 | flex: 1; 8 | display: flex; 9 | position: relative; 10 | align-items: strench; 11 | } 12 | 13 | .main-content { 14 | flex: 1; 15 | position: relative; 16 | display: flex; 17 | } 18 | 19 | .content { 20 | flex: 1; 21 | color: $primarytext; 22 | background-color: $content3; 23 | z-index: 10; 24 | display: flex; 25 | flex-direction: column; 26 | overflow-y: auto; 27 | @mixin scrollbar; 28 | } 29 | 30 | .content__headinfo { 31 | width: 100%; 32 | padding: 35px 30px 5px 30px; 33 | border-bottom: 2px solid #c70c0c; 34 | background-color: $content; 35 | p { 36 | font-size: 25px; 37 | } 38 | } 39 | 40 | /* FIXME */ 41 | .content__main { 42 | height: 0; 43 | } 44 | 45 | .content__main__list { 46 | padding-bottom: $PlayerHeight; 47 | margin-top: 30px; 48 | } 49 | 50 | .content__card { 51 | padding: 20px 20px; 52 | } 53 | -------------------------------------------------------------------------------- /app/postcss/_header.css: -------------------------------------------------------------------------------- 1 | @import "searchbar"; 2 | @import "user-state"; 3 | 4 | 5 | .header { 6 | display: flex; 7 | align-items: center; 8 | height: 70px; 9 | border-top: 0.8px solid black; 10 | border-bottom: 3px solid #c70c0c; 11 | padding: 0 5px 0 30px; 12 | justify-content: space-between; 13 | background-color: $red-500; 14 | box-shadow: 0 4px 4px rgba(0, 0, 0, .18); 15 | z-index: 11; 16 | } 17 | 18 | .header__logo { 19 | svg g{ 20 | height: 30px; 21 | } 22 | } 23 | 24 | .header__back { 25 | fill: #fff; 26 | margin-left: 10px; 27 | cursor: pointer; 28 | transition: all ease 0.5s; 29 | width: 30px; 30 | height: 30px; 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | border-radius: 50%; 35 | * { 36 | cursor: pointer; 37 | } 38 | &:hover { 39 | background-color: rgba(255, 255, 255, 0.4); 40 | } 41 | } 42 | 43 | .header__space { 44 | flex: 1; 45 | } 46 | 47 | .header__windowcontrol { 48 | margin-left: 20px; 49 | svg { 50 | fill: #fff; 51 | cursor: pointer; 52 | * { 53 | cursor: pointer; 54 | } 55 | } 56 | } 57 | 58 | .header__windowcontrol__clientmini { 59 | margin-right: 10px; 60 | width: 20px; 61 | } 62 | -------------------------------------------------------------------------------- /app/postcss/_home-content.css: -------------------------------------------------------------------------------- 1 | @import 'minialbumcard'; 2 | 3 | #home-content { 4 | .recommend__main { 5 | display: flex; 6 | justify-content: space-around; 7 | flex-wrap: wrap; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/postcss/_loginform.css: -------------------------------------------------------------------------------- 1 | .loginform { 2 | position: absolute; 3 | height: 270px; 4 | width: 500px; 5 | z-index: 100; 6 | top: calc(50% - 100px); 7 | left: calc(50% - 250px); 8 | background-color: $content; 9 | overflow: hidden; 10 | box-shadow: 0 24px 24px rgba(0, 0, 0, .18); 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | } 15 | 16 | .loginform__header { 17 | width: 100%; 18 | height: 56px; 19 | font-size: 20px; 20 | position: relative; 21 | background-color: $indigo-500; 22 | color: white; 23 | display: flex; 24 | align-items: center; 25 | justify-content: space-between; 26 | h2 { 27 | height: 100%; 28 | line-height: 56px; 29 | padding-left: 20px; 30 | } 31 | } 32 | 33 | .loginform__header__close { 34 | cursor: pointer; 35 | margin-right: 10px; 36 | fill: #fff; 37 | } 38 | 39 | .loginform__form { 40 | width: 100%; 41 | display: flex; 42 | flex-direction: column; 43 | align-items: center; 44 | position: relative; 45 | input { 46 | font-size: 18px; 47 | width: 450px; 48 | height: 50px; 49 | border: 0; 50 | border-bottom: 2px solid $grey-300; 51 | background-color: transparent; 52 | margin-top: 20px; 53 | outline: none; 54 | transition: all ease 0.5s; 55 | &:focus { 56 | border-bottom: 3px solid $indigo-500; 57 | 58 | } 59 | } 60 | } 61 | 62 | .loginform__submit { 63 | width: 100px; 64 | height: 40px; 65 | font-size: 20px; 66 | position: absolute; 67 | bottom: 16px; 68 | right: 25px; 69 | } 70 | -------------------------------------------------------------------------------- /app/postcss/_minialbumcard.css: -------------------------------------------------------------------------------- 1 | .minialbumcard { 2 | width: 200px; 3 | height: 200px; 4 | overflow: hidden; 5 | border-radius: 10px !important; 6 | position: relative; 7 | margin-top: 15px; 8 | } 9 | 10 | .minialbumcard__cover__playcount { 11 | position: absolute; 12 | bottom: 0; 13 | width: 100%; 14 | height: 30px; 15 | line-height: 30px; 16 | padding-left: 10px; 17 | background-color: rgba(0, 0, 0, 0.5); 18 | color: #fff; 19 | } 20 | 21 | .minialbumcard__cover { 22 | img { 23 | width: 100%; 24 | height: 100%; 25 | } 26 | } 27 | 28 | .minialbumcard__cover__name { 29 | position: absolute; 30 | color: #fff; 31 | width: 100%; 32 | padding: 10px; 33 | font-size: 17px; 34 | line-height: 20px; 35 | background-color: rgba(0, 0, 0, .5); 36 | top: 0; 37 | } 38 | -------------------------------------------------------------------------------- /app/postcss/_playcontentcard.css: -------------------------------------------------------------------------------- 1 | @define-mixin cover $size { 2 | height: $size; 3 | width: $size; 4 | overflow: hidden; 5 | img { 6 | width: 100%; 7 | } 8 | } 9 | 10 | .playcontent { 11 | bottom: 70px; 12 | left: 10px; 13 | overflow: hidden; 14 | position: absolute; 15 | z-index: 10; 16 | transition: all ease 0.5s; 17 | background-color: $indigo-50; 18 | } 19 | 20 | .playcontent.max { 21 | bottom: calc(50% - 300px); 22 | left: calc(50% - 250px); 23 | box-shadow: 0px 24px 24px rgba(0, 0, 0, .18); 24 | padding-top: 70px; 25 | } 26 | 27 | .miniplaycontent-wrapper { 28 | display: flex; 29 | padding: 10px 10px; 30 | position: absolute; 31 | transition: all ease 0.5s; 32 | width: 300px; 33 | height: 100px; 34 | right: 0; 35 | top: 0; 36 | } 37 | 38 | .maxplaycontent-wrapper { 39 | display: flex; 40 | position: absolute; 41 | transition: all ease 0.5s; 42 | width: 100%; 43 | height: 100%; 44 | } 45 | 46 | .miniplaycontent__cover { 47 | @mixin cover 80px; 48 | } 49 | 50 | .miniplaycontent__info { 51 | margin-left: 20px; 52 | display: flex; 53 | flex: 1; 54 | flex-direction: column; 55 | font-size: 20px; 56 | justify-content: space-around; 57 | } 58 | 59 | .miniplaycontent__info__name { 60 | color: $primarytext; 61 | } 62 | 63 | .miniplaycontent__info__artist { 64 | color: $secondarytext; 65 | } 66 | 67 | .maxplaycontent-bg { 68 | background-size: cover; 69 | filter: blur(20px); 70 | position: absolute; 71 | width: 100%; 72 | height: 100%; 73 | } 74 | 75 | .maxplaycontent-main { 76 | background-color: rgba(0, 0, 0, .5); 77 | width: 100%; 78 | height: 100%; 79 | z-index: 1; 80 | display: flex; 81 | justify-content: space-around; 82 | color: #fff; 83 | padding-top: 40px; 84 | } 85 | 86 | .maxplaycontent-song { 87 | display: flex; 88 | width: 500px; 89 | } 90 | 91 | .maxplaycontent__cover { 92 | @mixin cover 200px; 93 | border-radius: 20px; 94 | box-shadow: 0 8px 8px rgba(0, 0, 0, .18); 95 | } 96 | 97 | .maxplaycontent__info { 98 | margin-left: 20px; 99 | padding-top: 20px; 100 | flex: 1; 101 | } 102 | 103 | .maxplaycontent__info__name { 104 | font-size: 30px; 105 | line-height: 40px; 106 | } 107 | 108 | .maxplaycontent__info__artist { 109 | margin-top: 10px; 110 | font-size: 20px; 111 | line-height: 25px; 112 | color: rgba(255, 255, 255, .54); 113 | } 114 | 115 | .maxplaycontent__info__album { 116 | font-size: 20px; 117 | line-height: 25px; 118 | color: rgba(255, 255, 255, .54); 119 | } 120 | 121 | .maxplaycontent-lyric { 122 | height: 200px; 123 | width: 500px; 124 | overflow: hidden; 125 | } 126 | 127 | .maxplaycontent__info__control { 128 | margin-top: 10px; 129 | cursor: pointer; 130 | font-size: 17px; 131 | align-items: center; 132 | display: flex; 133 | width: 100px; 134 | &:hover { 135 | .i { 136 | fill: #fff; 137 | } 138 | span { 139 | color: #fff; 140 | } 141 | } 142 | * { 143 | cursor: pointer; 144 | } 145 | .i { 146 | fill: #fff.54; 147 | width: 30px; 148 | height: 30px; 149 | transition: all ease 0.5s; 150 | } 151 | span { 152 | color: #fff.54; 153 | transition: all ease 0.5s; 154 | } 155 | } 156 | 157 | .maxplaycontent-lyric__wrapper { 158 | transition: all ease 0.5s; 159 | width: 100%; 160 | } 161 | 162 | .lyric { 163 | font-size: 16px; 164 | line-height: 20px; 165 | height: 20px; 166 | width: 100%; 167 | text-align: center; 168 | color: rgba(255, 255, 255, .54); 169 | transition: all ease 0.5s; 170 | margin: 8px 0; 171 | } 172 | 173 | .lyric.current { 174 | font-size: 20px; 175 | line-height: 25px; 176 | height: 25px; 177 | color: #fff; 178 | } 179 | -------------------------------------------------------------------------------- /app/postcss/_player.css: -------------------------------------------------------------------------------- 1 | $PlayerHeight: 53px; 2 | @import "volume"; 3 | @import "playlist"; 4 | @import "playlistcontrol"; 5 | 6 | 7 | @define-mixin player-btn $size { 8 | height: $size; 9 | width: $size; 10 | .i { 11 | fill: $red-500; 12 | } 13 | } 14 | 15 | @keyframes loading { 16 | from { 17 | background-color: $indigo-500; 18 | } 19 | 20 | to { 21 | background-color: $red-500; 22 | } 23 | } 24 | 25 | .player { 26 | height: $PlayerHeight; 27 | background-color: #fff.9; 28 | padding: 0 30px; 29 | display: flex; 30 | align-items: center; 31 | position: fixed; 32 | z-index: 20; 33 | box-shadow: 0 -4px 4px rgba(0, 0, 0, .18); 34 | bottom: 0; 35 | left: 0; 36 | width: 100%; 37 | } 38 | 39 | .player__btns { 40 | display: flex; 41 | align-items: center; 42 | } 43 | 44 | .player__btns__backward, .player__btns__forward { 45 | @mixin player-btn 30px; 46 | } 47 | 48 | .player__btns__play { 49 | @mixin player-btn 40px; 50 | } 51 | 52 | .player__btns-btn { 53 | padding: 0; 54 | border: 2px solid $red-500; 55 | border-radius: 50%; 56 | margin: 0 15px; 57 | cursor: pointer; 58 | background-color: transparent; 59 | * { 60 | cursor: pointer; 61 | } 62 | &:focus { 63 | outline: none; 64 | } 65 | } 66 | 67 | .player__pg { 68 | display: flex; 69 | height: 100%; 70 | flex: 1; 71 | margin: 0 20px; 72 | align-items: center; 73 | p { 74 | color: $primarytext; 75 | } 76 | } 77 | 78 | .player__pg__bar { 79 | flex: 1; 80 | height: 7px; 81 | margin: 0 15px; 82 | border-radius: 5px; 83 | background-color: $red-200; 84 | position: relative; 85 | } 86 | 87 | .player__pg__bar-ready { 88 | position: absolute; 89 | top: 0; 90 | height: 100%; 91 | background-color: $red-300; 92 | border-radius: 5px; 93 | } 94 | 95 | .player__pg__bar-cur-wrapper { 96 | position: absolute; 97 | top: 0; 98 | height: 100%; 99 | width: 100%; 100 | background-color: transparent; 101 | z-index: 10; 102 | } 103 | 104 | .player__pg__bar-cur { 105 | height: 100%; 106 | background-color: #c70c0c; 107 | border-top: 1px solid #f41616; 108 | border-radius: 5px; 109 | position: relative; 110 | &::after { 111 | display: block; 112 | content: ''; 113 | width: 20px; 114 | height: 20px; 115 | background-color: $indigo-500; 116 | position: absolute; 117 | right: -10px; 118 | top: -7px; 119 | border-radius: 50%; 120 | box-sizing: border-box; 121 | cursor: pointer; 122 | } 123 | } 124 | 125 | .player__pg__bar-cur.loading { 126 | &::after { 127 | animation-duration: 0.6s; 128 | animation-direction: alternate; 129 | animation-iteration-count: infinite; 130 | animation-name: loading; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /app/postcss/_playlist.css: -------------------------------------------------------------------------------- 1 | .playlist { 2 | position: absolute; 3 | width: 600px; 4 | height: 400px; 5 | bottom: $PlayerHeight; 6 | background-color: $content; 7 | display: flex; 8 | flex-direction: column; 9 | /* chrome absolute zindex bug */ 10 | box-shadow: 0 -4px 4px rgba(0, 0, 0, .18) inset; 11 | overflow: hidden; 12 | border-radius: 2px; 13 | z-index: 13; 14 | transition: all ease 0.3s; 15 | } 16 | 17 | .playlist__header { 18 | height: 60px; 19 | width: 100%; 20 | background-color: $indigo-500; 21 | color: white; 22 | font-szie: 30px; 23 | padding: 0 20px; 24 | box-shadow: 0 4px 4px rgba(0, 0, 0, .18); 25 | display: flex; 26 | align-items: center; 27 | justify-content: space-between; 28 | .i { 29 | cursor: pointer; 30 | fill: #fff; 31 | margin: 0 5px; 32 | } 33 | .space { 34 | flex: 1; 35 | } 36 | } 37 | 38 | .playlist__content { 39 | flex: 1; 40 | color: $primarytext; 41 | @mixin scrollbar; 42 | overflow-y: auto; 43 | } 44 | 45 | .playlist__content__list__song { 46 | display: flex; 47 | padding: 5px 20px; 48 | height: 40px; 49 | align-items: center; 50 | border-bottom: 1px solid $dividers; 51 | &:hover { 52 | background-color: $grey-300; 53 | } 54 | cursor: pointer; 55 | * { 56 | cursor: pointer; 57 | } 58 | } 59 | 60 | .playlist__content__list__song.current { 61 | background-color: $grey-300; 62 | } 63 | 64 | .playlist__content__list__song-name { 65 | flex: 9; 66 | } 67 | .playlist__content__list__song-artist { 68 | flex: 3; 69 | } 70 | .playlist__content__list__song-duration { 71 | flex: 1; 72 | } 73 | -------------------------------------------------------------------------------- /app/postcss/_playlistcontrol.css: -------------------------------------------------------------------------------- 1 | .player__playlistcontrol { 2 | margin: 0 20px; 3 | height: 24px; 4 | display: flex; 5 | align-items: center; 6 | .i { 7 | height: 100%; 8 | cursor: pointer; 9 | fill: $icon; 10 | } 11 | } 12 | 13 | .player__playlistcontrol__playlistrule { 14 | height: 100%; 15 | } 16 | 17 | .player__playlistcontrol__playlist { 18 | padding: 0 10px; 19 | height: 30px; 20 | margin-left: 10px; 21 | display: flex; 22 | align-items: center; 23 | color: #535353; 24 | background-color: rgba(255, 255, 255, 0); 25 | border-radius: 15px; 26 | transition: background-color ease 0.5s; 27 | cursor: pointer; 28 | * { 29 | cursor: pointer; 30 | } 31 | &:hover { 32 | background-color: rgba(0, 0, 0, 0.3); 33 | } 34 | } 35 | 36 | .player__playlistcontrol__playlist.active { 37 | background-color: rgba(0, 0, 0, 0.3); 38 | } 39 | 40 | .player__playlistcontrol__playlist__count { 41 | margin-left: 5px; 42 | } 43 | -------------------------------------------------------------------------------- /app/postcss/_reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | -------------------------------------------------------------------------------- /app/postcss/_scrollbar.css: -------------------------------------------------------------------------------- 1 | @define-mixin scrollbar { 2 | &::-webkit-scrollbar { 3 | width: 10px; 4 | } 5 | 6 | &::-webkit-scrollbar-track { 7 | border-radius: 10px; 8 | background-color: white; 9 | } 10 | 11 | &::-webkit-scrollbar-thumb { 12 | border-radius: 10px; 13 | background-color: rgba(0, 0, 0, 0.2); 14 | &:hover { 15 | background-color: rgba(0, 0, 0, 0.3); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/postcss/_search-content.css: -------------------------------------------------------------------------------- 1 | #search-content .content__headinfo { 2 | .keywords { 3 | color: $secondarytext; 4 | margin-right: 10px; 5 | } 6 | } 7 | 8 | #search-content .content__main__bestmarch { 9 | h2 { 10 | padding:16px 30px; 11 | font-size: 21px; 12 | } 13 | .songcard { 14 | margin-left: 30px; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/postcss/_searchbar.css: -------------------------------------------------------------------------------- 1 | .header__searchbar { 2 | height: 30px; 3 | width: 250px; 4 | border-radius: 15px; 5 | background-color: white; 6 | display: flex; 7 | align-items: center; 8 | padding: 0 12px; 9 | label { 10 | width: 20px; 11 | height: 20px; 12 | transform: translateY(2px); 13 | display: inline-block; 14 | } 15 | .i { 16 | color: #535353; 17 | width: 100%; 18 | height: 100%; 19 | } 20 | input { 21 | width: 200px; 22 | height: 100%; 23 | background-color: transparent; 24 | border: 0; 25 | margin-left: 5px; 26 | outline: 0; 27 | } 28 | } 29 | 30 | #search-form { 31 | display: flex; 32 | align-items: center; 33 | } 34 | -------------------------------------------------------------------------------- /app/postcss/_sidebar.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | min-height: 100%; 3 | width: 300px; 4 | background-color: #fff; 5 | border-right: 1px solid $dividers; 6 | overflow-y: auto; 7 | overflow-x: hidden; 8 | @mixin scrollbar; 9 | padding-bottom: $PlayerHeight; 10 | } 11 | 12 | .sidebar__mylist { 13 | position: relative; 14 | color: $secondarytext; 15 | h3 { 16 | padding-left: 5px; 17 | margin-top: 15px; 18 | color: $primarytext; 19 | height: 30px; 20 | line-height: 30px; 21 | border-bottom: 1px solid $dividers; 22 | } 23 | } 24 | 25 | .sidebar__mylist__control { 26 | position: absolute; 27 | right: 10px; 28 | top: 2px; 29 | transition: transform ease 0.5s; 30 | cursor: pointer; 31 | @mixin scrollbar; 32 | } 33 | 34 | .sidebar__mylist__content { 35 | overflow: hidden; 36 | transition: height ease 0.5s; 37 | } 38 | 39 | .sidebar__mylist__content__list { 40 | padding-left: 15px; 41 | height: 30px; 42 | line-height: 30px; 43 | display: flex; 44 | flex-wrap: nowrap; 45 | align-items: center; 46 | cursor: pointer; 47 | * { 48 | cursor: pointer; 49 | } 50 | .i { 51 | fill: $secondarytext; 52 | margin-right: 5px; 53 | } 54 | p { 55 | white-space: nowrap; 56 | width: 1px; 57 | } 58 | &:hover { 59 | color: $primarytext; 60 | background-color: $grey-300; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/postcss/_songcard.css: -------------------------------------------------------------------------------- 1 | .songcard { 2 | height: 100px; 3 | min-width: 400px; 4 | max-width: 600px; 5 | display: flex; 6 | padding: 10px 10px; 7 | align-items: center; 8 | img { 9 | height: 100%; 10 | } 11 | } 12 | 13 | .songcard__info { 14 | margin-left: 20px; 15 | line-height: 30px; 16 | font-size: 20px; 17 | cursor: pointer; 18 | } 19 | -------------------------------------------------------------------------------- /app/postcss/_songlist-content.css: -------------------------------------------------------------------------------- 1 | #songlist-content .content__main__card { 2 | padding: 20px 20px; 3 | } 4 | -------------------------------------------------------------------------------- /app/postcss/_songlist.css: -------------------------------------------------------------------------------- 1 | .songlist { 2 | width: 100%; 3 | font-size: 16px; 4 | background-color: $content2; 5 | color: $primarytext; 6 | tr { 7 | height: 30px; 8 | line-height: 30px; 9 | border-top: 1px solid $dividers; 10 | } 11 | 12 | th,td { 13 | padding-left: 5px; 14 | } 15 | 16 | th.th-center { 17 | text-align: center; 18 | } 19 | 20 | thead tr { 21 | padding: 5px 0; 22 | color: rgba(0, 0, 0, .54); 23 | } 24 | 25 | tbody { 26 | tr { 27 | transition: all ease 0.5s; 28 | } 29 | tr:hover { 30 | background-color: $grey-300; 31 | } 32 | } 33 | } 34 | .songlist-table__index { 35 | width: 5%; 36 | padding-right: 10px; 37 | text-align: right; 38 | } 39 | 40 | .songlist-table__name { 41 | width: 30%; 42 | cursor: pointer; 43 | &:hover { 44 | color: black; 45 | } 46 | } 47 | 48 | .songlist-table__button { 49 | height: 100%; 50 | display: flex; 51 | align-items: center; 52 | justify-content: center; 53 | .i { 54 | fill: $icon; 55 | height: 26px; 56 | width: 26px; 57 | /* FIXME: parent can't height 100% */ 58 | transform: translateY(6px); 59 | } 60 | * { 61 | cursor: pointer; 62 | } 63 | } 64 | 65 | .songlist-table__artists { 66 | width: 20%; 67 | } 68 | 69 | .songlist-table__album { 70 | width: 20%; 71 | } 72 | 73 | .songlist-table__duration { 74 | width: 5%; 75 | } 76 | 77 | .songlist-table__hot { 78 | width: 15%; 79 | } 80 | 81 | .songlist-table__hotbar-wrapper { 82 | height: 10px; 83 | width: 80%; 84 | margin: auto; 85 | border-radius: 5px; 86 | background-color: $red-300; 87 | overflow: hidden; 88 | } 89 | 90 | .songlist-table__hotbar { 91 | background-color: $red-700; 92 | border-radius: 5px; 93 | height: 100%; 94 | } 95 | 96 | .songlist-table { 97 | text-align: left; 98 | width: 100%; 99 | } 100 | -------------------------------------------------------------------------------- /app/postcss/_spinner.css: -------------------------------------------------------------------------------- 1 | /* https://github.com/lukehaas/css-loaders */ 2 | 3 | .loader, 4 | .loader:before, 5 | .loader:after { 6 | background: #ffffff; 7 | -webkit-animation: load1 1s infinite ease-in-out; 8 | animation: load1 1s infinite ease-in-out; 9 | width: 1em; 10 | height: 4em; 11 | } 12 | .loader:before, 13 | .loader:after { 14 | position: absolute; 15 | top: 0; 16 | content: ''; 17 | } 18 | .loader:before { 19 | left: -1.5em; 20 | -webkit-animation-delay: -0.32s; 21 | animation-delay: -0.32s; 22 | } 23 | .loader { 24 | color: #ffffff; 25 | text-indent: -9999em; 26 | margin: 88px auto; 27 | position: relative; 28 | font-size: 11px; 29 | -webkit-transform: translateZ(0); 30 | -ms-transform: translateZ(0); 31 | transform: translateZ(0); 32 | -webkit-animation-delay: -0.16s; 33 | animation-delay: -0.16s; 34 | } 35 | .loader:after { 36 | left: 1.5em; 37 | } 38 | 39 | @-webkit-keyframes load1 { 40 | 0%, 41 | 80%, 42 | 100% { 43 | box-shadow: 0 0; 44 | height: 4em; 45 | } 46 | 40% { 47 | box-shadow: 0 -2em; 48 | height: 5em; 49 | } 50 | } 51 | 52 | @keyframes load1 { 53 | 0%, 54 | 80%, 55 | 100% { 56 | box-shadow: 0 0; 57 | height: 4em; 58 | } 59 | 40% { 60 | box-shadow: 0 -2em; 61 | height: 5em; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/postcss/_toast.css: -------------------------------------------------------------------------------- 1 | .toast { 2 | position: absolute; 3 | height: 48px; 4 | min-width: 300px; 5 | padding: 0 24px; 6 | border-radius: 20px; 7 | color: #fff; 8 | background-color: rgba(0, 0, 0, .8); 9 | line-height: 48px; 10 | right: 20px; 11 | bottom: 40px; 12 | z-index: 10000; 13 | } 14 | -------------------------------------------------------------------------------- /app/postcss/_user-state.css: -------------------------------------------------------------------------------- 1 | .header__user { 2 | height: 50px; 3 | display: flex; 4 | align-items: center; 5 | color: #D6D6D6; 6 | } 7 | 8 | .header__user__avatar { 9 | width: 50px; 10 | margin-right: 20px; 11 | margin-left: 20px; 12 | position: relative; 13 | cursor: pointer; 14 | * { 15 | cursor: pointer; 16 | } 17 | img { 18 | width: 100%; 19 | height: 100%; 20 | border-radius: 50%; 21 | } 22 | 23 | .loader { 24 | font-size: 7px; 25 | } 26 | } 27 | 28 | .header__user__menu { 29 | position: absolute; 30 | top: 70px; 31 | width: 150px; 32 | background-color: $content; 33 | box-shadow: 0px 0px 14px rgba(0, 0, 0, .18); 34 | color: $primarytext; 35 | padding: 10px 0; 36 | font-size: 17px; 37 | &::before { 38 | content: ''; 39 | position: absolute; 40 | width: 0; 41 | height: 0; 42 | border-left: 10px solid transparent; 43 | border-right: 10px solid transparent; 44 | border-bottom: 10px solid $content; 45 | top: -10px; 46 | left: 13px; 47 | } 48 | } 49 | 50 | .header__user__menu__list { 51 | li { 52 | padding-left: 10px; 53 | height: 25px; 54 | line-height: 25px; 55 | transition: background-color ease 0.5s; 56 | cursor: pointer; 57 | &:hover { 58 | background-color: $grey-300; 59 | } 60 | } 61 | } 62 | 63 | .header__user__login, .header__user__name { 64 | height: 30px; 65 | line-height: 30px; 66 | cursor: pointer; 67 | } 68 | -------------------------------------------------------------------------------- /app/postcss/_volume.css: -------------------------------------------------------------------------------- 1 | .player__volume { 2 | display: flex; 3 | align-items: center; 4 | .i { 5 | fill: $icon; 6 | } 7 | } 8 | 9 | .player__volume__bar-wrapper { 10 | width: 120px; 11 | height: 10px; 12 | margin-left: 10px; 13 | background-color: $red-200; 14 | border-radius: 10px; 15 | overflow: hidden; 16 | } 17 | 18 | .player__volume__bar { 19 | background-color: $red-500; 20 | height: 100%; 21 | border-top-right-radius: 10px; 22 | border-bottom-right-radius: 10px; 23 | } 24 | -------------------------------------------------------------------------------- /app/postcss/index.css: -------------------------------------------------------------------------------- 1 | @import "reset"; 2 | @import "colors"; 3 | @import "scrollbar"; 4 | @import "header"; 5 | @import "player"; 6 | @import "toast"; 7 | @import "content"; 8 | @import "button"; 9 | @import "spinner"; 10 | @import "songcard"; 11 | @import "albumcard"; 12 | @import "songlist"; 13 | @import "loginform"; 14 | @import "card"; 15 | @import "playcontentcard"; 16 | 17 | * { 18 | box-sizing: border-box; 19 | cursor: default; 20 | user-select: none; 21 | } 22 | 23 | .app { 24 | display: flex; 25 | flex-direction: column; 26 | height: 100vh; 27 | position: relative; 28 | } 29 | 30 | body { 31 | overflow: hidden; 32 | } 33 | -------------------------------------------------------------------------------- /app/reducers/alert.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | export default function alert(state, action) { 3 | if (action.type !== 'USER') { 4 | if (state) { 5 | return state; 6 | } else { 7 | return { 8 | showAlert: false, 9 | }; 10 | } 11 | } 12 | let newState = Object.assign({}, state); 13 | switch (action.state) { 14 | case 'NEWALERT': 15 | newState.showAlert = true; 16 | newState.body = action.payload; 17 | return newState; 18 | case 'CLOSEALERT': 19 | newState.showAlert = false; 20 | return newState; 21 | default: 22 | return newState; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import player from './player'; 3 | import search from './search'; 4 | import song from './song'; 5 | import user from './user'; 6 | import usersong from './usersong'; 7 | import router from './router'; 8 | import songlist from './songlist'; 9 | import playcontent from './playcontent'; 10 | import toast from './toast'; 11 | 12 | const cloudMusic = combineReducers({ 13 | player, 14 | search, 15 | song, 16 | user, 17 | usersong, 18 | router, 19 | songlist, 20 | playcontent, 21 | toast, 22 | }); 23 | 24 | export default cloudMusic; 25 | -------------------------------------------------------------------------------- /app/reducers/playcontent.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | export default function playcontent(state, action) { 3 | if (action.type !== 'PLAYCONTENT') { 4 | if (state) { 5 | return state; 6 | } else { 7 | return { 8 | clientmode: 'normal', 9 | mode: 'mini', 10 | state: 'show', 11 | lyricState: 'fetching', 12 | lyric: { lyric: [{content: '无歌词', time: '0'}]}, 13 | currentLyric: 0, 14 | lyricError: null, 15 | }; 16 | } 17 | } 18 | let newState = Object.assign({}, state); 19 | switch (action.state) { 20 | case 'SHOWMINI': 21 | newState.mode = 'mini'; 22 | newState.state = 'show'; 23 | return newState; 24 | case 'HIDDENMINI': 25 | newState.mode = 'mini'; 26 | newState.state = 'hidden'; 27 | return newState; 28 | case 'SHOWMAX': 29 | newState.mode = 'max'; 30 | return newState; 31 | case 'HIDDENMAX': 32 | newState.mode = 'mini'; 33 | return newState; 34 | case 'LRCFETCH': 35 | newState.lyricState = 'fetching'; 36 | return newState; 37 | case 'LRCGET': 38 | newState.lyricState = 'get'; 39 | newState.lyric = action.payload; 40 | newState.currentLyric = 0; 41 | return newState; 42 | case 'LRCERROR': 43 | newState.lyricState = 'error'; 44 | newState.lyricError = action.payload; 45 | newState.lyric = { lyric: [{content: '无歌词', time: '0'}]}; 46 | return newState; 47 | case 'LRCSET': 48 | newState.currentLyric = action.payload; 49 | return newState; 50 | case 'CLIENT_MODE': 51 | newState.clientmode = action.payload; 52 | return newState; 53 | default: 54 | return newState; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/reducers/player.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | export default function player(state, action) { 3 | switch (action.type) { 4 | case 'PLAYER': 5 | if (action.state == 'PLAYER_PLAY') { 6 | return { isplay: true } 7 | } else { 8 | return { isplay: false } 9 | } 10 | 11 | default: 12 | if (state) { 13 | return state; 14 | } else { 15 | return { isplay: false } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/reducers/router.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import HomeContent from '../components/HomeContent.jsx'; 3 | 4 | export default function router(state, action) { 5 | if (action.type !== 'ROUTER') { 6 | if (state) { 7 | return state; 8 | } else { 9 | return { 10 | // default content 11 | routerStack: [HomeContent], 12 | canPop: false, 13 | }; 14 | } 15 | } 16 | let newState = Object.assign({}, state); 17 | switch (action.state) { 18 | case 'PUSH': 19 | for (let i = 0;i < newState.routerStack.length;i++) { 20 | if (newState.routerStack[i] == action.payload) { 21 | let t = newState.routerStack[i]; 22 | newState.routerStack[i] = newState.routerStack[newState.routerStack.length - 1]; 23 | newState.routerStack[newState.routerStack.length - 1] = t; 24 | return newState; 25 | } 26 | } 27 | newState.routerStack.push(action.payload); 28 | if (newState.routerStack.length > 1) { 29 | newState.canPop = true; 30 | } 31 | return newState; 32 | case 'POP': 33 | if (newState.routerStack.length > 1) { 34 | newState.routerStack.pop(); 35 | } 36 | if (newState.routerStack.length > 1) { 37 | newState.canPop = true; 38 | } else { 39 | newState.canPop = false; 40 | } 41 | return newState; 42 | default: 43 | return newState; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/reducers/search.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | export default function search(state, action) { 3 | if (action.type !== 'SEARCH') { 4 | if (state) { 5 | return state; 6 | } else { 7 | return { 8 | searchState: 'FINISH', 9 | searchResponse: null, 10 | errorInfo: null, 11 | } 12 | } 13 | } 14 | let newState = Object.assign({}, state); 15 | newState.searchState = action.state; 16 | switch (action.state) { 17 | case 'START': 18 | newState.searchInfo = action.payload; 19 | return newState; 20 | case 'CLOSE': 21 | return newState; 22 | case 'FINISH': 23 | newState.searchResponse = action.payload; 24 | return newState; 25 | case 'ERROR': 26 | newState.errorInfo = action.payload; 27 | return newState; 28 | default: 29 | return newState; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/reducers/song.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | export default function song(state, action) { 3 | let rules = ['loop', ] 4 | if (action.type !== 'SONG') { 5 | if (state) { 6 | return state; 7 | } else { 8 | return { 9 | songlist: [], 10 | playRule: 0, // loop one shuffle 11 | rules: ['loop', 'one', 'shuffle'], 12 | showplaylist: false, 13 | currentSongIndex: 0, 14 | }; 15 | } 16 | } 17 | let newState = _.clone(state, true); 18 | switch (action.state) { 19 | case 'CHANGE': 20 | let index = isExist(action.payload, newState.songlist); 21 | if (index) { 22 | index--; 23 | newState.currentSongIndex = index; 24 | } else { 25 | var songlist = _.clone(newState.songlist, true); 26 | songlist.push(action.payload); 27 | newState.songlist = songlist; 28 | newState.currentSongIndex = newState.songlist.length - 1; 29 | } 30 | return newState; 31 | case 'REMOVEFROMLIST': 32 | if ((newState.currentSongIndex) == action.payload && (action.payload == newState.songlist.length - 1)) { 33 | newState.currentSongIndex--; 34 | } 35 | if (newState.currentSongIndex > action.payload) { 36 | newState.currentSongIndex--; 37 | } 38 | let songlist = _.clone(newState.songlist, true); 39 | songlist.splice(action.payload, 1); 40 | newState.songlist = songlist; 41 | return newState; 42 | case 'REMOVELIST': 43 | newState.songlist = []; 44 | return newState; 45 | case 'SHOWPLAYLIST': 46 | newState.showplaylist = true; 47 | return newState; 48 | case 'CLOSEPLAYLIST': 49 | newState.showplaylist = false; 50 | return newState; 51 | case 'PLAYFROMLIST': 52 | newState.currentSongIndex = action.payload; 53 | // FIXME 54 | if (newState.playRule == 2) { 55 | let toShuffle = []; 56 | for (let i = 0;i < newState.songlist.length;i++) { 57 | if (i == 0) { 58 | toShuffle[i] = newState.currentSongIndex; 59 | } else if (i == newState.currentSongIndex) { 60 | toShuffle[i] = 0; 61 | } else { 62 | toShuffle[i] = i; 63 | } 64 | } 65 | newState.shuffleList = getShuffle(toShuffle, 1); 66 | newState.shuffleIndex = 0; 67 | } 68 | return newState; 69 | case 'ADD': 70 | if (isExist(action.payload, state.songlist)) { 71 | return state; 72 | } 73 | var songlist = _.clone(newState.songlist, true); 74 | songlist.push(action.payload); 75 | newState.songlist = songlist; 76 | // if shuffle 77 | if (newState.playRule == 2) { 78 | newState.shuffleList.push(newState.songlist.length - 1); 79 | newState.shuffleList = getShuffle( 80 | newState.shuffleList, 81 | newState.shuffleIndex + 1 82 | ); 83 | } 84 | if (newState.songlist.length == 1) { 85 | newState.currentSongIndex = 0; 86 | } 87 | return newState; 88 | case 'ADDLIST': 89 | if (action.payload.play) { 90 | var playIndex = newState.songlist.length; 91 | } 92 | var songlist = _.clone(newState.songlist, true); 93 | action.payload.songlist.map(song => { 94 | if (isExist(song, newState.songlist)) { 95 | return; 96 | } 97 | songlist.push(song); 98 | if (newState.playRule == 2) { 99 | newState.shuffleList.push(songlist.length - 1); 100 | } 101 | }); 102 | if (newState.playRule == 2) { 103 | newState.shuffleList = getShuffle( 104 | newState.shuffleList, 105 | newState.shuffleIndex + 1 106 | ); 107 | } 108 | if (action.payload.play && newState.songlist.length > playIndex) { 109 | newState.currentSongIndex = playIndex; 110 | } 111 | if (songlist.length == 1) { 112 | newState.currentSongIndex = 0; 113 | } 114 | newState.songlist = songlist; 115 | return newState; 116 | case 'CHANGERULE': 117 | if (newState.playRule == 2) { 118 | newState.playRule = 0; 119 | } else { 120 | newState.playRule++; 121 | } 122 | 123 | // if rule is shuffle 124 | if (newState.playRule == 2) { 125 | let toShuffle = []; 126 | for (let i = 0;i < newState.songlist.length;i++) { 127 | if (i == 0) { 128 | toShuffle[i] = newState.currentSongIndex; 129 | } else if (i == newState.currentSongIndex) { 130 | toShuffle[i] = 0; 131 | } else { 132 | toShuffle[i] = i; 133 | } 134 | } 135 | newState.shuffleList = getShuffle(toShuffle, 1); 136 | newState.shuffleIndex = 0; 137 | } 138 | 139 | return newState; 140 | case 'NEXT': 141 | if (newState.songlist.length == 0) { 142 | return newState; 143 | } 144 | if ((newState.playRule == 0) || (newState.playRule == 1)) { 145 | if (newState.currentSongIndex === newState.songlist.length - 1){ 146 | newState.currentSongIndex = 0; 147 | } else { 148 | newState.currentSongIndex++; 149 | } 150 | return newState; 151 | } else if (newState.playRule == 2) { // shuffle 152 | if (newState.shuffleIndex === newState.shuffleList.length - 1){ 153 | newState.shuffleIndex = 0; 154 | } else { 155 | newState.shuffleIndex++; 156 | } 157 | newState.currentSongIndex = newState.shuffleList[newState.shuffleIndex]; 158 | return newState; 159 | } 160 | //TODO: shuffle 161 | case 'PREVIOUS': 162 | if (newState.songlist.length == 0) { 163 | return newState; 164 | } 165 | if ((newState.playRule == 0) || (newState.playRule == 1)) { 166 | if (newState.currentSongIndex === 0){ 167 | newState.currentSongIndex = newState.songlist.length - 1; 168 | } else { 169 | newState.currentSongIndex--; 170 | } 171 | return newState; 172 | } else if (newState.playRule == 2) { // shuffle 173 | if (newState.shuffleIndex === 0){ 174 | newState.shuffleIndex = newState.shuffleList.length - 1; 175 | } else { 176 | newState.shuffleIndex--; 177 | } 178 | newState.currentSongIndex = newState.shuffleList[newState.shuffleIndex]; 179 | return newState; 180 | } 181 | default: 182 | return newState; 183 | } 184 | } 185 | 186 | function getShuffle(lastshuffle, index) { 187 | let toShuffle = lastshuffle.slice(index, lastshuffle.length); 188 | lastshuffle = lastshuffle.slice(0, index); 189 | doShuffle(toShuffle).map(value => { 190 | lastshuffle.push(value); 191 | }); 192 | 193 | return lastshuffle; 194 | } 195 | 196 | function doShuffle(list) { 197 | for (let i = list.length;i > 0;i--) { 198 | let j = Math.floor(Math.random() * i); 199 | let x = list[i - 1]; 200 | list[i - 1] = list[j]; 201 | list[j] = x; 202 | } 203 | 204 | return list; 205 | } 206 | 207 | function isExist(newsong, list) { 208 | for (let i = 0;i < list.length;i++) { 209 | if (list[i].id == newsong.id) { 210 | return i + 1; 211 | } 212 | } 213 | return false; 214 | } 215 | -------------------------------------------------------------------------------- /app/reducers/songlist.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | export default function songlist(state, action) { 3 | if (action.type !== 'SONGLIST') { 4 | if (state) { 5 | return state; 6 | } else { 7 | return { 8 | state: 'fetching', 9 | }; 10 | } 11 | } 12 | let newState = Object.assign({}, state); 13 | switch (action.state) { 14 | case 'FETCHING': 15 | newState.state = 'fetching'; 16 | return newState; 17 | case 'GET': 18 | newState.content = action.payload; 19 | newState.state = 'get'; 20 | return newState; 21 | case 'ERROR': 22 | newState.errorinfo = action.payload; 23 | newState.state = 'error'; 24 | return newState; 25 | default: 26 | return newState; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/reducers/toast.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | export default function toast(state, action) { 3 | if (action.type !== 'TOAST') { 4 | if (state) { 5 | return state; 6 | } else { 7 | return { 8 | toastQuery: [], 9 | } 10 | } 11 | } 12 | let newState = _.clone(state, true); 13 | switch (action.state) { 14 | case 'ADD': 15 | var query = _.clone(newState.toastQuery, true); 16 | query.push(action.payload); 17 | newState.toastQuery = query; 18 | return newState; 19 | case 'FINISH': 20 | var query = _.clone(newState.toastQuery, true); 21 | query.splice(0, 1); 22 | newState.toastQuery = query; 23 | return newState; 24 | default: 25 | return newState; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/reducers/user.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | export default function user(state, action) { 3 | if (action.type !== 'USER') { 4 | if (state) { 5 | return state; 6 | } else { 7 | return { 8 | loginState: 'guest', 9 | showForm: false, 10 | }; 11 | } 12 | } 13 | let newState = Object.assign({}, state); 14 | switch (action.state) { 15 | case 'LOGIN_STATE_LOGGING_IN': 16 | newState.loginState = 'logging_in'; 17 | return newState; 18 | case 'LOGIN_STATE_LOGGED_IN': 19 | newState.loginState = 'logged_in'; 20 | newState.account = action.payload.account; 21 | newState.profile = action.payload.profile; 22 | return newState; 23 | case 'LOGIN_STATE_LOGGED_FAILED': 24 | newState.loginState = 'logged_failed'; 25 | newState.loginError = action.payload; 26 | return newState; 27 | case 'LOGINFORM': 28 | newState.showForm = action.payload; 29 | return newState; 30 | case 'GUEST': 31 | newState.loginState = 'guest'; 32 | removeCookie(); 33 | return newState; 34 | default: 35 | return newState; 36 | } 37 | } 38 | 39 | function removeCookie() { 40 | Electron.ipcRenderer.sendSync('removecookie', 'http://localhost:11015', 'MUSIC_U'); 41 | Electron.ipcRenderer.sendSync('removecookie', 'http://loaclhost:11015', 'NETEASE_WDA_UID'); 42 | Electron.ipcRenderer.sendSync('removecookie', 'http://localhost:11015', '__csrf'); 43 | Electron.ipcRenderer.sendSync('removecookie', 'http://localhost:11015', '__remember_me'); 44 | } 45 | -------------------------------------------------------------------------------- /app/reducers/usersong.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | export default function user(state, action) { 3 | if (action.type !== 'USERSONG') { 4 | if (state) { 5 | return state; 6 | } else { 7 | return { 8 | create: [], 9 | collect: [], 10 | state: 'nouser', 11 | uid: null, 12 | }; 13 | } 14 | } 15 | let newState = Object.assign({}, state); 16 | switch (action.state) { 17 | case 'FETCHING': 18 | newState.state = 'fetching'; 19 | newState.uid = action.payload; 20 | return newState; 21 | case 'GET': 22 | [newState.create, newState.collect] = separatePlayList(newState.uid, action.payload); 23 | newState.state = 'get'; 24 | return newState; 25 | case 'ERROR': 26 | newState.state = 'error'; 27 | newState.errorinfo = action.payload; 28 | return newState; 29 | default: 30 | return newState; 31 | } 32 | } 33 | 34 | function separatePlayList(id, list) { 35 | let create = [], collect = []; 36 | list.map(songlist => { 37 | if (songlist.userId === id) { 38 | create.push(songlist); 39 | } else { 40 | collect.push(songlist); 41 | } 42 | }); 43 | 44 | return [create, collect]; 45 | } 46 | -------------------------------------------------------------------------------- /app/server/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import lyricParser from '../libs/lrcparse'; 3 | 4 | function requestPromise(path, res, rej) { 5 | return new Promise((resolve, reject) => { 6 | if (rej) { 7 | reject(rej); 8 | } 9 | fetch('http://localhost:11015/' + path, { 10 | credentials: 'include', 11 | }) 12 | .then( res => { 13 | return res.json(); 14 | }).then(json => { 15 | let [flag, response] = res(json); 16 | if (flag) { 17 | resolve(response); 18 | } else { 19 | reject(response); 20 | } 21 | }).catch(e => { 22 | reject(e); 23 | }); 24 | }) 25 | } 26 | 27 | // id --> mp3url 28 | export function getSongUrl(song, callback) { 29 | var id = song.id, br; 30 | if (song.hMusic) { 31 | br = song.hMusic.bitrate; 32 | } else if (song.mMusic) { 33 | br = song.mMusic.bitrate; 34 | } else if (song.lMusic) { 35 | br = song.lMusic.bitrate; 36 | } 37 | fetch('http://localhost:11015/music/url?id=' + id + '&br=' + br, { 38 | credentials: 'include', 39 | }) 40 | .then( res => { 41 | return res.json(); 42 | }).then( json => { 43 | callback(json.data[0]); 44 | } ) 45 | } 46 | 47 | // 搜索歌曲 48 | export function Search(keywords) { 49 | return requestPromise( 50 | 'search/?keywords=' + keywords + '&type=1&limit=40', 51 | json => { return [true, json.result] }); 52 | } 53 | 54 | // 登录 55 | export function Login(username, pw) { 56 | const emailReg = /^[-a-z0-9~!$%^&*_=+}{\'?]+(\.[-a-z0-9~!$%^&*_=+}{\'?]+)*@([a-z0-9_][-a-z0-9_]*(\.[-a-z0-9_]+)*\.(aero|arpa|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org|pro|travel|mobi|[a-z][a-z])|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,5})?$/i; 57 | const phoneReg = /^[0-9]{11}$/i 58 | let fetchUrl= '' 59 | if (phoneReg.test(username)) { 60 | fetchUrl = 'login/cellphone?phone=' + username + '&password=' + pw; 61 | } else if (emailReg.test(username)) { 62 | fetchUrl = 'login?email=' + username + '&password=' + pw; 63 | } else { 64 | var rej = '用户名格式错误'; 65 | } 66 | 67 | return requestPromise( 68 | fetchUrl, 69 | json => { 70 | if (json.code != 200) { 71 | return [false, 'Error:' + JSON.stringify(json)]; 72 | } else { 73 | console.logg('resolve', json); 74 | return [true, json]; 75 | } 76 | }, rej) 77 | } 78 | 79 | export function getPlayList(uid) { 80 | return requestPromise( 81 | 'user/playlist?uid=' + uid, 82 | json => { 83 | return [true, json] 84 | }); 85 | } 86 | 87 | // 获取歌单详情 88 | export function SonglistDetail(id) { 89 | return requestPromise( 90 | 'playlist/detail?id=' + id, 91 | json => { 92 | return [true, json.playlist] 93 | }); 94 | } 95 | 96 | export function recommendResource() { 97 | return requestPromise( 98 | 'recommend/resource', 99 | json => { 100 | return [true, json] 101 | }); 102 | } 103 | 104 | export function recommendSongs() { 105 | return requestPromise( 106 | 'recommend/songs', 107 | json => { 108 | return [true, json] 109 | }); 110 | } 111 | 112 | export function playlistTracks(op, pid, tracks) { 113 | return requestPromise( 114 | 'playlist/tracks?op='+op+'&pid='+pid+'&tracks='+tracks, 115 | json => { 116 | return [true, json] 117 | }); 118 | } 119 | 120 | // 获取歌词 121 | export function getLyric(id) { 122 | return requestPromise( 123 | 'lyric?id=' + id, 124 | json => { 125 | return [true, json] 126 | }); 127 | } 128 | 129 | export function logWeb(action, id, time, end) { 130 | var json = { 131 | 'id': id, 132 | 'type': 'song', 133 | 'wifi': 0, 134 | 'download': 0, 135 | 'time': time, 136 | 'end': end, 137 | }; 138 | json = JSON.stringify(json); 139 | return requestPromise( 140 | 'log/web?action=' + action + '&json=' + json, 141 | json => { 142 | return [true, json] 143 | }); 144 | } 145 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const electron = require('electron'); 4 | const app = electron.app; 5 | const BrowserWindow = electron.BrowserWindow; 6 | const ipcMain = electron.ipcMain; 7 | const Tray = electron.Tray; 8 | const Menu = electron.Menu; 9 | 10 | const Childprocess = require('child_process'); 11 | const path = require('path'); 12 | const http = require('http'); 13 | var server = require('./server/server'); 14 | 15 | let mainWindow; 16 | var appIcon = null; 17 | 18 | function createWindow () { 19 | mainWindow = new BrowserWindow({ 20 | width: 1200, 21 | height: 800, 22 | webPreferences: { 23 | nodeIntegration: 'iframe', 24 | webSecurity: false, 25 | }, 26 | title: 'CloudMusic', 27 | frame: false, 28 | icon: 'app/assets/icon.png', 29 | }); 30 | 31 | mainWindow.loadURL('http://127.0.0.1:8080'); 32 | //mainWindow.loadURL('file://' + __dirname + '/index.html'); 33 | console.log('loadURL'); 34 | 35 | mainWindow.webContents.on('did-finish-load', function() { 36 | var session = electron.session.defaultSession; 37 | session.cookies.get({}, function(error, cookies) { 38 | mainWindow.webContents.send('cookie', cookies); 39 | }); 40 | }); 41 | 42 | //mainWindow.webContents.openDevTools(); 43 | 44 | mainWindow.on('closed', function() { 45 | mainWindow = null; 46 | }); 47 | 48 | server.listen(11015, function() { 49 | console.log('cloud music server listening on port 11015...') 50 | }); 51 | 52 | // Tray 53 | appIcon = new Tray('./app/assets/tray.png'); 54 | const contextMenu = Menu.buildFromTemplate([ 55 | { label: '播放/暂停', type: 'normal', click: 56 | function(menuitem, window) { 57 | mainWindow.webContents.send('playorpause'); 58 | } 59 | }, 60 | { label: '上一首', type: 'normal', click: 61 | function(menuitem, window) { 62 | mainWindow.webContents.send('previous'); 63 | } 64 | }, 65 | { label: '下一首', type: 'normal', click: 66 | function(menuitem, window) { 67 | mainWindow.webContents.send('next'); 68 | } 69 | }, 70 | { label: '隐藏/显示', type: 'normal', click: 71 | function(menuitem, window) { 72 | if (mainWindow.isVisible()) { 73 | mainWindow.hide(); 74 | } else { 75 | mainWindow.show(); 76 | } 77 | } 78 | }, 79 | { label: '退出', type: 'normal', click: 80 | function(menuitem, window) { 81 | app.quit(); 82 | } 83 | }, 84 | ]); 85 | 86 | appIcon.setToolTip('CloudMusic'); 87 | appIcon.setContextMenu(contextMenu); 88 | 89 | ipcMain.on('hideapp', function(e) { 90 | mainWindow.hide(); 91 | e.sender.send('hided'); 92 | }); 93 | 94 | ipcMain.on('minimize', function(e) { 95 | mainWindow.minimize(); 96 | e.sender.send('minimize'); 97 | }); 98 | 99 | ipcMain.on('maximize', function(e) { 100 | if (mainWindow.isMaximized()) { 101 | mainWindow.unmaximize(); 102 | } else { 103 | mainWindow.maximize(); 104 | } 105 | e.sender.send('maximize'); 106 | }); 107 | } 108 | 109 | app.on('ready', createWindow); 110 | 111 | app.on('window-all-closed', function () { 112 | if (process.platform !== 'darwin') { 113 | app.quit(); 114 | } 115 | }); 116 | 117 | app.on('activate', function () { 118 | if (mainWindow === null) { 119 | createWindow(); 120 | } 121 | }); 122 | 123 | ipcMain.on('removecookie', function(e, url, name) { 124 | var session = electron.session.fromPartition(); 125 | session.cookies.remove(url, name, function() { 126 | console.log('remove', url, name); 127 | e.returnValue = 'OK'; 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-cloud-music", 3 | "version": "0.0.2", 4 | "description": "", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "webpack-dev-server --inline --compress --content-base=./", 8 | "start": "electron .", 9 | "build": "webpack -p", 10 | "release": "echo $0" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/disoul/electron-cloud-music.git" 15 | }, 16 | "author": "disoul", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/disoul/electron-cloud-music/issues" 20 | }, 21 | "homepage": "https://github.com/disoul/electron-cloud-music#readme", 22 | "devDependencies": { 23 | "autoprefixer": "^6.3.6", 24 | "babel": "^6.5.2", 25 | "babel-core": "^6.7.4", 26 | "babel-loader": "^6.2.4", 27 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 28 | "babel-plugin-transform-runtime": "^6.15.0", 29 | "babel-preset-es2015": "^6.18.0", 30 | "babel-preset-react": "^6.16.0", 31 | "babel-runtime": "^6.18.0", 32 | "browserslist": "^1.3.1", 33 | "css-loader": "^0.23.1", 34 | "electron-packager": "^8.2.0", 35 | "electron-prebuilt": "^1.4.5", 36 | "eslint-loader": "^1.3.0", 37 | "exports-loader": "^0.6.3", 38 | "file-loader": "^0.8.5", 39 | "imports-loader": "^0.6.5", 40 | "less": "^2.6.1", 41 | "less-loader": "^2.2.3", 42 | "lodash": "^4.11.2", 43 | "portfinder": "^1.0.3", 44 | "postcss-color-alpha": "^1.0.3", 45 | "postcss-loader": "^0.9.1", 46 | "precss": "^1.4.0", 47 | "react": "^0.14.8", 48 | "react-dom": "^0.14.8", 49 | "react-hot-loader": "^1.3.0", 50 | "react-redux": "^4.4.1", 51 | "react-transform": "0.0.3", 52 | "redux": "^3.3.1", 53 | "redux-logger": "^2.6.1", 54 | "redux-thunk": "^2.0.1", 55 | "style-loader": "^0.13.1", 56 | "svg-react-loader": "^0.3.7", 57 | "tough-cookie": "^2.2.2", 58 | "url-loader": "^0.5.7", 59 | "webpack": "^1.13.3", 60 | "webpack-dev-middleware": "^1.6.1", 61 | "webpack-hot-middleware": "^2.10.0" 62 | }, 63 | "dependencies": { 64 | "querystring": "^0.2.0", 65 | "express": "^4.13.4", 66 | "big-integer": "^1.6.15", 67 | "lodash": "^4.13.1", 68 | "react": "^15.1.0", 69 | "react-dom": "^15.1.0", 70 | "whatwg-fetch": "^0.11.0" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /server/crypto.js: -------------------------------------------------------------------------------- 1 | // 参考 https://github.com/darknessomi/musicbox/wiki/ 2 | 'use strict' 3 | const crypto = require('crypto'); 4 | const bigInt = require('big-integer'); 5 | const modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' 6 | const nonce = '0CoJUm6Qyw8W8jud' 7 | const pubKey = '010001' 8 | 9 | String.prototype.hexEncode = function(){ 10 | var hex, i; 11 | 12 | var result = ""; 13 | for (i=0; i ('babel-preset-' + e)).map(require.resolve) 25 | } 26 | }, 27 | { test: /\.css?$/, loader: "style-loader!css-loader!postcss-loader" }, 28 | { test: /\.svg?$/, loader: "babel?presets[]=es2015,presets[]=react!svg-react?reactDOM=react", 29 | exclude: /img/, 30 | }, 31 | { test: /\.(png|jpg)?$/, loader: "url?name=[path]" }, 32 | 33 | ] 34 | }, 35 | postcss: function() { 36 | return [precss, autoprefixer({ browsers: browserslist('last 2 Chrome versions') }), postcsscoloralpha] 37 | }, 38 | plugins: [ 39 | new webpack.HotModuleReplacementPlugin(), 40 | new webpack.ProvidePlugin({ 41 | 'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch' 42 | }), 43 | ], 44 | }; 45 | --------------------------------------------------------------------------------