diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..683b18b
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": ['react', 'es2015-webpack']
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..389c71a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+# node.js
+#
+node_modules/
+npm-debug.log
+
+# Vim
+# swap
+[._]*.s[a-w][a-z]
+[._]s[a-w][a-z]
+# session
+Session.vim
+# temporary
+.netrwhist
+*~
+# auto-generated tag files
+tags
+
+dist/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c7803db
--- /dev/null
+++ b/README.md
@@ -0,0 +1,31 @@
+# NeteaseCloudMusic Electron
+
+网易云音乐Electron版
+`` 开发中 ``
+
+## 进度
+目前只有基本功能
+* 搜索歌曲+播放(版权歌曲无法播放
+* 播放列表
+* 手机登陆
+* 个人歌单(创建,收藏
+* [TODO] 歌曲界面
+* [TODO] 主页推荐
+* [TODO] 私人FM
+![预览截图](http://7xn38i.com1.z0.glb.clouddn.com/snapshot5.png)
+
+## Build
+目前还没有打包,如果想预览开发效果可以按以下步骤自行构建
+
+```bash
+git clone https://github.com/disoul/electron-cloud-music && cd electron-cloud-music
+npm install
+npm install webpack@2.1.0-beta.5 -g
+npm install webpack-dev-server@2.0.0-beta -g
+npm install electron-prebuilt -g
+webpack-dev-server --inline --compress --content-base=./
+
+// run cloudmusic in proj root path
+// electron will load from 127.0.0.1:8080(webpack-dev-server
+electron ./
+```
diff --git a/app/actions/actions.js b/app/actions/actions.js
new file mode 100644
index 0000000..d839776
--- /dev/null
+++ b/app/actions/actions.js
@@ -0,0 +1,180 @@
+'use strict'
+import { Search, Login, getPlayList, SonglistDetail } from '../server';
+export function play() {
+ return { type: 'PLAYER', state: 'PLAYER_PLAY' };
+}
+
+export function pause() {
+ return { type: 'PLAYER', state: 'PLAYER_PAUSE' };
+}
+
+export function startSearch(keywords) {
+ return { type: 'SEARCH', state: 'START', payload: { keywords: keywords }}
+}
+
+export function errorSearch(e) {
+ return { type: 'SEARCH', state: 'ERROR', payload: e }
+}
+
+export function finishSearch(res) {
+ return { type: 'SEARCH', state: 'FINISH', payload: res }
+}
+
+export function closeSearch() {
+ return { type: 'SEARCH', state: 'CLOSE' }
+}
+
+export function search(keywords) {
+ return dispatch => {
+ dispatch(startSearch(keywords));
+ Search(keywords).then( res => {
+ dispatch(finishSearch(res));
+ } )
+ .catch( e => {
+ dispatch(errorSearch(e));
+ } )
+ };
+}
+
+export function changeSong(song) {
+ return { type: 'SONG', state: 'CHANGE', payload: song}
+}
+
+export function playFromList(index) {
+ return { type: 'SONG', state: 'PLAYFROMLIST', payload: index}
+}
+
+export function addSong(song) {
+ return { type: 'SONG', state: 'ADD', payload: song}
+}
+
+export function addSongList(songlist, isplay) {
+ return { type: 'SONG', state: 'ADDLIST', payload: {
+ songlist: songlist,
+ play: isplay,
+ }
+ }
+}
+
+export function nextSong() {
+ return { type: 'SONG', state: 'NEXT' }
+}
+
+export function previousSong() {
+ return { type: 'SONG', state: 'PREVIOUS' }
+}
+
+export function changeRule() {
+ return { type: 'SONG', state: 'CHANGERULE' }
+}
+
+export function showPlayList() {
+ return { type: 'SONG', state: 'SHOWPLAYLIST' }
+}
+
+export function closePlayList() {
+ return { type: 'SONG', state: 'CLOSEPLAYLIST' }
+}
+
+export function logging_in(form) {
+ return { type: 'USER', state: 'LOGIN_STATE_LOGGING_IN', payload: form }
+};
+
+export function logged_in(res) {
+ return { type: 'USER', state: 'LOGIN_STATE_LOGGED_IN', payload: res }
+};
+
+export function logged_failed(errorinfo) {
+ return { type: 'USER', state: 'LOGIN_STATE_LOGGED_FAILED', payload: errorinfo }
+}
+
+export function loginform(flag) {
+ return { type: 'USER', state: 'LOGINFORM', payload: flag }
+}
+
+export function toguest() {
+ return { type: 'USER', state: 'GUEST' }
+}
+
+export function login(form) {
+ return dispatch => {
+ dispatch(logging_in(form));
+ Login(form.phone, form.password)
+ .then(res => {
+ localStorage.setItem('user', JSON.stringify(res));
+ dispatch(logged_in(res));
+ dispatch(fetchusersong(res.profile.userId));
+ })
+ .catch(error => {
+ dispatch(logged_failed(error.toString()));
+ });
+ }
+}
+
+export function fetchingusersong(id) {
+ return { type: 'USERSONG', state: 'FETCHING', payload: id }
+}
+
+export function getusersong(res) {
+ return { type: 'USERSONG', state: 'GET', payload: res }
+}
+
+export function fetchusersongerror(err) {
+ return { type: 'USERSONG', state: 'ERROR', payload: err }
+}
+
+export function fetchusersong(uid) {
+ return dispatch => {
+ dispatch(fetchingusersong(uid));
+ getPlayList(uid)
+ .then(res => {
+ dispatch(getusersong(res.playlist));
+ })
+ .catch(err => {
+ dispatch(fetchusersongerror(err));
+ });
+ }
+}
+
+// push content to routerstack
+export function push(content) {
+ return { type: 'ROUTER', state: 'PUSH', payload: content }
+}
+
+export function pop() {
+ return { type: 'ROUTER', state: 'PUSH' }
+}
+
+// 获取歌单内容
+export function fetchsonglistdetail(id) {
+ return dispatch => {
+ dispatch(fetchingsonglistdetail(id));
+ SonglistDetail(id)
+ .then( res => {
+ dispatch(getsonglistdetail(res));
+ })
+ .catch(error => {
+ dispatch(fetchsonglistdetailerror(error));
+ });
+ };
+}
+
+export function fetchingsonglistdetail(id) {
+ return { type: 'SONGLIST', state: 'FETCHING', payload: id }
+}
+
+export function getsonglistdetail(res) {
+ return { type: 'SONGLIST', state: 'GET', payload: res }
+}
+
+export function fetchsonglistdetailerror(err) {
+ return { type: 'SONGLIST', state: 'ERROR', payload: err }
+}
+
+export function showplaycontentmini() {
+ return { type: 'PLAYCONTENT', state: 'SHOWMINI' }
+}
+
+export function hiddenplaycontentmini() {
+ return { type: 'PLAYCONTENT', state: 'HIDDENMINI' }
+}
diff --git a/app/assets/icon.png b/app/assets/icon.png
new file mode 100644
index 0000000..e81305d
Binary files /dev/null and b/app/assets/icon.png differ
diff --git a/app/assets/icon.svg b/app/assets/icon.svg
new file mode 100644
index 0000000..3179d92
--- /dev/null
+++ b/app/assets/icon.svg
@@ -0,0 +1,95 @@
+
+
+
+
diff --git a/app/assets/icon/add.svg b/app/assets/icon/add.svg
new file mode 100644
index 0000000..9a3b1b0
--- /dev/null
+++ b/app/assets/icon/add.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icon/close.svg b/app/assets/icon/close.svg
new file mode 100644
index 0000000..fef83a5
--- /dev/null
+++ b/app/assets/icon/close.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icon/loop.svg b/app/assets/icon/loop.svg
new file mode 100644
index 0000000..5147343
--- /dev/null
+++ b/app/assets/icon/loop.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icon/max.svg b/app/assets/icon/max.svg
new file mode 100644
index 0000000..8c3bf0e
--- /dev/null
+++ b/app/assets/icon/max.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/assets/icon/min.svg b/app/assets/icon/min.svg
new file mode 100644
index 0000000..fa26c46
--- /dev/null
+++ b/app/assets/icon/min.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/assets/icon/music.svg b/app/assets/icon/music.svg
new file mode 100644
index 0000000..6e498ac
--- /dev/null
+++ b/app/assets/icon/music.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/app/assets/icon/next.svg b/app/assets/icon/next.svg
new file mode 100644
index 0000000..e90c34b
--- /dev/null
+++ b/app/assets/icon/next.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icon/one.svg b/app/assets/icon/one.svg
new file mode 100644
index 0000000..1cfd6b9
--- /dev/null
+++ b/app/assets/icon/one.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icon/pause.svg b/app/assets/icon/pause.svg
new file mode 100644
index 0000000..4c1b11e
--- /dev/null
+++ b/app/assets/icon/pause.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icon/play.svg b/app/assets/icon/play.svg
new file mode 100644
index 0000000..2b52124
--- /dev/null
+++ b/app/assets/icon/play.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icon/playlist.svg b/app/assets/icon/playlist.svg
new file mode 100644
index 0000000..61f4d63
--- /dev/null
+++ b/app/assets/icon/playlist.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icon/previous.svg b/app/assets/icon/previous.svg
new file mode 100644
index 0000000..7147e7b
--- /dev/null
+++ b/app/assets/icon/previous.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icon/search.svg b/app/assets/icon/search.svg
new file mode 100644
index 0000000..232112c
--- /dev/null
+++ b/app/assets/icon/search.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icon/shuffle.svg b/app/assets/icon/shuffle.svg
new file mode 100644
index 0000000..e1ee0fa
--- /dev/null
+++ b/app/assets/icon/shuffle.svg
@@ -0,0 +1,20 @@
+
diff --git a/app/assets/icon/volume.svg b/app/assets/icon/volume.svg
new file mode 100644
index 0000000..e39cf2b
--- /dev/null
+++ b/app/assets/icon/volume.svg
@@ -0,0 +1,22 @@
+
+
diff --git a/app/assets/icon/volume_max.svg b/app/assets/icon/volume_max.svg
new file mode 100644
index 0000000..e0caf08
--- /dev/null
+++ b/app/assets/icon/volume_max.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icon/volume_min.svg b/app/assets/icon/volume_min.svg
new file mode 100644
index 0000000..1250e8f
--- /dev/null
+++ b/app/assets/icon/volume_min.svg
@@ -0,0 +1,21 @@
+
diff --git a/app/assets/icon/volume_mute.svg b/app/assets/icon/volume_mute.svg
new file mode 100644
index 0000000..2df9820
--- /dev/null
+++ b/app/assets/icon/volume_mute.svg
@@ -0,0 +1,21 @@
+
diff --git a/app/assets/img/up.svg b/app/assets/img/up.svg
new file mode 100644
index 0000000..27b3020
--- /dev/null
+++ b/app/assets/img/up.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/assets/logo.svg b/app/assets/logo.svg
new file mode 100644
index 0000000..d24e7b0
--- /dev/null
+++ b/app/assets/logo.svg
@@ -0,0 +1,51 @@
+
+
+
+
diff --git a/app/components/AlbumCard.jsx b/app/components/AlbumCard.jsx
new file mode 100644
index 0000000..ef379b1
--- /dev/null
+++ b/app/components/AlbumCard.jsx
@@ -0,0 +1,54 @@
+import React, { Component } from 'react';
+
+export default class AlbumCard extends Component {
+ constructor(props: any) {
+ super(props);
+ }
+
+ _addsonglist(e, isplay) {
+ this.props.addSongList(this.props.songs, isplay);
+ }
+
+ render() {
+ return (
+
+
+
+
+
{this.props.data.playCount}
+
+
+
+
+
+ {this.props.data.name}
+
+
+ 来自:{this.props.data.creator.nickname}
+
+
+
+ {this.props.data.tags.length > 0 ?
TAGS:
: ''}
+ {this.props.data.tags.map(tag => {
+ return (
+
+ {tag}
+
+ );
+ })}
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/app/components/App.jsx b/app/components/App.jsx
new file mode 100644
index 0000000..2726ad0
--- /dev/null
+++ b/app/components/App.jsx
@@ -0,0 +1,84 @@
+'use strict'
+import React, { Component } from 'react';
+import Header from './Header.jsx';
+import Content from './Content.jsx';
+import Player from './Player.jsx';
+import LoginForm from './LoginForm.jsx';
+import PlayContentCard from './PlayContentCard.jsx';
+
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+import * as Actions from '../actions/actions';
+
+const mapStateToProps = state => ({
+ player: state.player,
+ search: state.search,
+ song: state.song,
+ user: state.user,
+ usersong: state.usersong,
+ router: state.router,
+ songlist: state.songlist,
+ playcontent: state.playcontent,
+});
+
+const mapDispatchToProps = (dispatch) => ({
+ actions: {
+ play: bindActionCreators(Actions.play, dispatch),
+ pause: bindActionCreators(Actions.pause, dispatch),
+ search: bindActionCreators(Actions.search, dispatch),
+ closeSearch: bindActionCreators(Actions.closeSearch, dispatch),
+ changeSong: bindActionCreators(Actions.changeSong, dispatch),
+ changeRule: bindActionCreators(Actions.changeRule, dispatch),
+ addSong: bindActionCreators(Actions.addSong, dispatch),
+ addSongList: bindActionCreators(Actions.addSongList, dispatch),
+ nextSong: bindActionCreators(Actions.nextSong, dispatch),
+ previousSong: bindActionCreators(Actions.previousSong, dispatch),
+ showPlayList: bindActionCreators(Actions.showPlayList, dispatch),
+ closePlayList: bindActionCreators(Actions.closePlayList, dispatch),
+ playFromList: bindActionCreators(Actions.playFromList, dispatch),
+ login: bindActionCreators(Actions.login, dispatch),
+ logged_in: bindActionCreators(Actions.logged_in, dispatch),
+ toguest: bindActionCreators(Actions.toguest, dispatch),
+ loginform: bindActionCreators(Actions.loginform, dispatch),
+ fetchusersong: bindActionCreators(Actions.fetchusersong, dispatch),
+ push: bindActionCreators(Actions.push, dispatch),
+ pop: bindActionCreators(Actions.pop, dispatch),
+ fetchsonglistdetail: bindActionCreators(Actions.fetchsonglistdetail, dispatch),
+ showplaycontentmini: bindActionCreators(Actions.showplaycontentmini, dispatch),
+ hiddenplaycontentmini: bindActionCreators(Actions.hiddenplaycontentmini, dispatch),
+ }
+});
+
+class App extends Component {
+ loginForm() {
+ if (this.props.user.showForm) {
+ return (
+
+ );
+ } else {
+ return;
+ }
+ }
+
+ render() {
+ const { song } = this.props;
+ return (
+
+
+ {this.loginForm()}
+
+
+
+ );
+ }
+}
+
+
+export default connect(mapStateToProps, mapDispatchToProps)(App);
diff --git a/app/components/Content.jsx b/app/components/Content.jsx
new file mode 100644
index 0000000..9955f74
--- /dev/null
+++ b/app/components/Content.jsx
@@ -0,0 +1,42 @@
+import React, { Component } from 'react';
+import SearchContent from './SearchContent.jsx';
+import HomeContent from './HomeContent.jsx';
+import SideBar from './SideBar.jsx';
+import Player from './Player.jsx';
+
+export default class Content extends Component {
+ constructor(props: any) {
+ super(props);
+ }
+
+ renderSearchContent() {
+ console.log(this);
+ if (this.props.search.hidden) {
+ return;
+ } else {
+ return
+ }
+ }
+
+ renderContent() {
+ const { router } = this.props;
+ let Component = router.routerStack[router.routerStack.length - 1];
+ return
+ }
+
+ render() {
+ return (
+
+
+
+ {this.renderContent()}
+
+
+
+ );
+ }
+}
diff --git a/app/components/Header.jsx b/app/components/Header.jsx
new file mode 100644
index 0000000..b891dd4
--- /dev/null
+++ b/app/components/Header.jsx
@@ -0,0 +1,67 @@
+import React, { Component } from 'react';
+import SearchBar from './SearchBar.jsx';
+import UserState from './UserState.jsx';
+
+export default class Header extends Component {
+ _closeApp(e) {
+ Electron.ipcRenderer.send('closeapp');
+ }
+
+ _max(e) {
+ Electron.ipcRenderer.send('maximize');
+ }
+
+ _min(e) {
+ Electron.ipcRenderer.send('minimize');
+ }
+
+ render() {
+ let Logo=require('../assets/logo.svg?name=Logo');
+ let CloseIcon = require('../assets/icon/close.svg?name=CloseIcon');
+ let MaxIcon = require('../assets/icon/max.svg?name=MaxIcon');
+ let MinIcon = require('../assets/icon/min.svg?name=MinIcon');
+ return (
+
+
+
+
+
+
+
+
+
+ this._min(e) }
+ />
+ this._max(e) }
+ />
+ this._closeApp(e) }
+ />
+
+
+ );
+ }
+}
diff --git a/app/components/HomeContent.jsx b/app/components/HomeContent.jsx
new file mode 100644
index 0000000..dcbf1e4
--- /dev/null
+++ b/app/components/HomeContent.jsx
@@ -0,0 +1,17 @@
+import React, { Component } from 'react';
+
+export default class HomeContent extends Component {
+ constructor(props: any) {
+ super(props);
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/app/components/LoginForm.jsx b/app/components/LoginForm.jsx
new file mode 100644
index 0000000..7bc5642
--- /dev/null
+++ b/app/components/LoginForm.jsx
@@ -0,0 +1,87 @@
+import React, { Component } from 'react';
+
+export default class LoginForm extends Component {
+ constructor(props: any) {
+ super(props);
+ this.state = {
+ phoneValid: true,
+ passwordValid: true,
+ }
+ }
+
+ _onSubmit(e) {
+ e.preventDefault();
+ if (this.refs.phone.value === '') {
+ this.setState({
+ phoneValid: false
+ });
+ return;
+ };
+ if (this.refs.password.value === '') {
+ this.setState({
+ passwordValid: false
+ });
+ return;
+ };
+ this.props.login({
+ phone: this.refs.phone.value,
+ password: this.refs.password.value,
+ });
+ this.props.loginform(false);
+ }
+
+ _closeForm(e) {
+ this.props.loginform(false);
+ }
+
+ _onChange(e, target) {
+ if (target === 'phone') {
+ if (!this.state.phoneValid) {
+ this.setState({
+ phoneValid: true,
+ });
+ }
+ } else {
+ if (!this.state.passwordValid) {
+ this.setState({
+ passwordValid: true,
+ });
+ }
+ }
+ }
+
+ render() {
+ let Close = require('../assets/icon/close.svg?name=Close');
+ return (
+
+
+
登陆
+ this._closeForm(e) }
+ />
+
+
+
+
+ );
+ }
+}
diff --git a/app/components/MusicContent.jsx b/app/components/MusicContent.jsx
new file mode 100644
index 0000000..4ada90e
--- /dev/null
+++ b/app/components/MusicContent.jsx
@@ -0,0 +1,9 @@
+import React, { Component } from 'react';
+
+export default class MusicContent extends Component {
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/app/components/PlayContentCard.jsx b/app/components/PlayContentCard.jsx
new file mode 100644
index 0000000..905f02d
--- /dev/null
+++ b/app/components/PlayContentCard.jsx
@@ -0,0 +1,73 @@
+import React, { Component } from 'react';
+
+export default class PlayContentCard extends Component {
+ constructor(props: any) {
+ super(props);
+ this.state = {
+ cardMode: 'mini',
+ height: '0px',
+ width: '0px',
+ };
+ }
+
+ componentWillReceiveProps(props) {
+ if (props.data && this.props.data == undefined) {
+ this.setState({
+ height: '100px',
+ width: '300px',
+ });
+ }
+ if (props.data != this.props.data) {
+ if (this.props.playcontent.state == 'hidden') {
+ this.props.showplaycontentmini();
+ }
+ }
+ }
+
+ renderMini() {
+ return (
+
+
+
+
+
+
+
+ {this.props.data.name}
+
+
+ {this.props.data.artists[0].name}
+
+
+
+
+ );
+ }
+
+ renderDefault() {
+ return (
+
+
+ );
+ }
+
+ render() {
+ if (this.props.data == undefined)
+ return this.renderDefault();
+ if (this.state.cardMode === 'mini')
+ return this.renderMini();
+ }
+}
diff --git a/app/components/PlayList.jsx b/app/components/PlayList.jsx
new file mode 100644
index 0000000..34dfe4c
--- /dev/null
+++ b/app/components/PlayList.jsx
@@ -0,0 +1,107 @@
+import React, { Component } from 'react';
+
+export default class PlayList extends Component {
+ constructor(props: any) {
+ super(props);
+
+ this.state = {
+ show: false,
+ }
+ }
+
+ secToTime(sec) {
+ sec = sec / 1000;
+ let min = parseInt(sec / 60);
+ if (min < 10) {
+ min = '0' + min;
+ }
+ let second = parseInt(sec % 60);
+ if (second < 10) {
+ second = '0' + second;
+ }
+
+ return min + ':' + second;
+ }
+
+ getSongClassName(index) {
+ if (index == this.props.song.currentSongIndex) {
+ return "playlist__content__list__song current";
+ } else {
+ return "playlist__content__list__song";
+ }
+ }
+
+ componentWillReceiveProps(props) {
+ if (props.showplaylist && !this.state.show) {
+ this.setState({
+ show: true,
+ });
+ }
+ if (!props.showplaylist && this.state.show) {
+ this.setState({
+ show: false,
+ });
+ }
+ }
+
+ componentDidUpdate(props, state) {
+ if (this.props.song.currentSongIndex != props.song.currentSongIndex) {
+ this.autoScroll();
+ }
+ }
+
+ autoScroll() {
+ let target = this.refs.current;
+ let container = this.refs.container;
+ container.scrollTop = 0;
+ container.scrollTop = target.getBoundingClientRect().top - container.getBoundingClientRect().top - 150;
+ }
+
+ _closeplaylist(e) {
+ this.props.closePlayList();
+ }
+
+ _playfromlist(e, index) {
+ this.props.playFromList(index);
+ }
+
+ render() {
+ let Close = require('../assets/icon/close.svg?name=Close');
+ return (
+
+
+
播放列表
+ this._closeplaylist(e)}
+ />
+
+
+
+ {this.props.song.songlist.map(
+ (song, index) => {
+ return (
+ - this._playfromlist(e, index)}
+ >
+
+
+
{song.artists[0].name}
+
+
+
{this.secToTime(song.duration)}
+
+
+ );
+ })
+ }
+
+
+
+ );
+ }
+}
diff --git a/app/components/PlayListControl.jsx b/app/components/PlayListControl.jsx
new file mode 100644
index 0000000..ad92f4a
--- /dev/null
+++ b/app/components/PlayListControl.jsx
@@ -0,0 +1,65 @@
+import React, { Component } from 'react';
+
+export default class PlayListControl extends Component {
+ constructor(props: any) {
+ super(props);
+ }
+
+ _changeRule(e) {
+ this.props.changeRule();
+ }
+
+ _showorhidePlaylist(e) {
+ if (this.props.song.showplaylist) {
+ this.props.closePlayList();
+ } else {
+ this.props.showPlayList();
+ }
+ }
+
+ getClassName() {
+ if (this.props.song.showplaylist) {
+ return "player__playlistcontrol__playlist active";
+ } else {
+ return "player__playlistcontrol__playlist";
+ }
+ }
+
+ getPlayRule() {
+ //FIXME: svg-loader only accept string args
+ if (this.props.song.rules[this.props.song.playRule] == 'one') {
+ return require('../assets/icon/one.svg');
+ }
+ if (this.props.song.rules[this.props.song.playRule] == 'loop') {
+ return require('../assets/icon/loop.svg');
+ }
+ if (this.props.song.rules[this.props.song.playRule] == 'shuffle') {
+ return require('../assets/icon/shuffle.svg');
+ }
+ }
+
+ render() {
+ let PlayRule = this.getPlayRule();
+ let PlayListIcon = require('../assets/icon/playlist.svg');
+ return (
+
+
+
this._changeRule(e)}
+ />
+
+
this._showorhidePlaylist(e)}
+ className={this.getClassName()}>
+
+
+
{this.props.song.songlist.length}
+
+
+
+ );
+ }
+}
diff --git a/app/components/Player.jsx b/app/components/Player.jsx
new file mode 100644
index 0000000..b16c4cc
--- /dev/null
+++ b/app/components/Player.jsx
@@ -0,0 +1,257 @@
+import React, { Component } from 'react';
+import Volume from './Volume.jsx';
+import PlayListControl from './PlayListControl.jsx';
+import PlayerList from './PlayList.jsx';
+import { getSongUrl } from '../server';
+
+export default class Player extends Component {
+ constructor(props: any) {
+ super(props);
+ this.mouseState = {
+ press: false,
+ };
+
+ this.autoplay = true;
+
+ this.state = {
+ playbuttonIcon: 'play',
+ currentTime: 0,
+ duration: 1,
+ buffered: 0,
+ source: '',
+ state: 'ready',
+ }
+ }
+
+ componentDidMount() {
+ let self = this;
+ this.refs.audio.addEventListener("progress", (e) => {
+ this.setState({
+ buffered: e.target.buffered.end(e.target.buffered.length - 1)
+ });
+ }, true);
+
+ this.refs.audio.addEventListener("durationchange", e => {
+ this.setState({
+ duration: e.target.duration
+ });
+ }, true)
+
+ this.refs.audio.addEventListener("timeupdate", e => {
+ if (self.mouseState.press) {
+ return;
+ }
+ this.setState({
+ currentTime: e.target.currentTime
+ });
+ }, true)
+
+ this.refs.audio.addEventListener("canplay", e => {
+ if (this.autoplay) {
+ self.props.actions.play();
+ this.autoplay = false;
+ }
+ }, true)
+
+ this.refs.audio.addEventListener("ended", e => {
+ self.props.actions.nextSong();
+ }, true)
+ }
+
+ componentWillReceiveProps(props) {
+ let self = this;
+ if (props.player.isplay) {
+ this.refs.audio.play();
+ this.setState({
+ playbuttonIcon: 'pause',
+ });
+ } else {
+ this.refs.audio.pause();
+ this.setState({
+ playbuttonIcon: 'play',
+ });
+ }
+
+ if (
+ props.song.songlist[props.song.currentSongIndex] !==
+ this.props.song.songlist[this.props.song.currentSongIndex]
+ ) {
+ this.setState({
+ state: 'fetch',
+ });
+ getSongUrl(props.song.songlist[props.song.currentSongIndex], data => {
+ if (!data.url) {
+ self.setState({
+ state: 'ready',
+ });
+ self.props.actions.nextSong();
+ return;
+ }
+
+ self.setState({
+ source: data.url,
+ currentTime: 0,
+ state: 'ready',
+ }, () => {
+ self.props.actions.pause();
+ });
+ });
+ }
+ }
+
+ componentDidUpdate(props, state) {
+ // update audio
+ if (state.source !== this.state.source) {
+ this.autoplay = true;
+ }
+ }
+
+ secToTime(sec) {
+ let min = parseInt(sec / 60);
+ if (min < 10) {
+ min = '0' + min;
+ }
+ let second = parseInt(sec % 60);
+ if (second < 10) {
+ second = '0' + second;
+ }
+
+ return min + ':' + second;
+ }
+
+ updateVolume(volume, ismute) {
+ console.log('mute', ismute);
+ if (ismute) {
+ this.refs.audio.volume = 0;
+ } else {
+ this.refs.audio.volume = volume;
+ }
+ }
+
+ _playorpause() {
+ const { song } = this.props;
+ if (this.props.player.isplay) {
+ this.props.actions.pause();
+ } else {
+ this.props.actions.play();
+ if (song.songlist.length > 0 && song.currentSongIndex == undefined) {
+ this.props.actions.playFromList(0);
+ }
+ }
+ }
+
+ _previous(e) {
+ this.props.actions.previousSong();
+ }
+
+ _next(e) {
+ this.props.actions.nextSong();
+ }
+
+ _handleMouseUp(e) {
+ if (!this.mouseState.press) {
+ return;
+ }
+ this.mouseState.press = false;
+ let pgbarWidth = this.refs.pgbar.clientWidth;
+ this.setState({
+ currentTime: this.refs.audio.duration * (e.pageX - this.refs.pgbar.getBoundingClientRect().left) / pgbarWidth
+ });
+ this.refs.audio.currentTime = this.state.currentTime;
+ }
+
+ _handleMouseMove(e) {
+ if (!this.mouseState.press) {
+ return;
+ }
+ let pgbarWidth = this.refs.pgbar.clientWidth;
+ this.setState({
+ currentTime: this.refs.audio.duration * (e.pageX - this.refs.pgbar.getBoundingClientRect().left) / pgbarWidth
+ });
+ }
+
+ _seek(e) {
+ if (!this.state.source) {
+ return;
+ }
+ this.mouseState.press = true;
+ window.addEventListener("mouseup", this._handleMouseUp.bind(this));
+ window.addEventListener("mousemove", this._handleMouseMove.bind(this));
+ }
+
+ render() {
+ let Previous = require('../assets/icon/previous.svg');
+ let Next = require('../assets/icon/next.svg');
+ let Play = require('../assets/icon/' + this.state.playbuttonIcon + '.svg');
+ return (
+
+
+
+
+
+
+
+
+
+ {this.secToTime(this.state.currentTime)}
+
+
{ this._seek(e) }}
+ >
+
+
{ this._seek(e) }}
+ style={{
+ width: String(this.state.currentTime / this.state.duration * 100) + '%'
+ }}
+ >
+
+
+
+
+
+ {this.secToTime(this.state.duration)}
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/app/components/SearchBar.jsx b/app/components/SearchBar.jsx
new file mode 100644
index 0000000..690f1ea
--- /dev/null
+++ b/app/components/SearchBar.jsx
@@ -0,0 +1,36 @@
+import React, { Component } from 'react';
+import { search } from '../server';
+import SeachContent from './SearchContent.jsx';
+
+export default class SearchBar extends Component {
+ constructor(props: any) {
+ super(props);
+ }
+
+ _onSubmit(e) {
+ e.preventDefault();
+ this.props.push(SeachContent);
+ this.props.search(this.refs.search.value);
+ }
+
+ render() {
+ let SearchIcon = require("../assets/icon/search.svg?name=SearchIcon");
+ return (
+
+ );
+ }
+}
diff --git a/app/components/SearchContent.jsx b/app/components/SearchContent.jsx
new file mode 100644
index 0000000..b38c6b2
--- /dev/null
+++ b/app/components/SearchContent.jsx
@@ -0,0 +1,83 @@
+import React, { Component } from 'react';
+import Spinner from './Spinner.jsx';
+import SongCard from './SongCard.jsx';
+import SongList from './SongList.jsx';
+
+export default class SearchContent extends Component {
+ constructor(props: any) {
+ super(props);
+ }
+
+ renderResult() {
+ if (this.props.search.searchResponse.songCount === 0) {
+ return ( 无结果
);
+ } else {
+ return (
+
+ );
+ }
+ }
+
+ render() {
+ if (this.props.search.searchState == 'START') {
+ return this.renderSearching();
+ }
+ if (this.props.search.searchState == 'FINISH') {
+ return this.renderFinish();
+ }
+ if (this.props.search.searchState == 'ERROR') {
+ return this.renderError();
+ }
+ }
+
+ renderSearching() {
+ return (
+
+
+
+ {this.props.search.searchInfo.keywords}
+ 搜索中...
+
+
+
+
+
+
+ );
+ }
+
+ renderFinish() {
+ return (
+
+
+
+ {this.props.search.searchInfo.keywords}
+ 搜索到{this.props.search.searchResponse.songCount}首歌曲
+
+
+ { this.renderResult() }
+
+ );
+ }
+
+ renderError() {
+ return (
+ Error
+ );
+ }
+}
diff --git a/app/components/SideBar.jsx b/app/components/SideBar.jsx
new file mode 100644
index 0000000..b1d6a1d
--- /dev/null
+++ b/app/components/SideBar.jsx
@@ -0,0 +1,155 @@
+import React, { Component } from 'react';
+import Spinner from './Spinner.jsx';
+import SongListContent from './SongListContent.jsx';
+
+export default class SideBar extends Component {
+ constructor(props: any) {
+ super(props);
+ this.state = {
+ showCreate: true,
+ showCollect: true,
+ createHeight: null,
+ collectHeight: null,
+ };
+ this.scroll = {
+ lastScrollTop: 0,
+ };
+ }
+
+ componentDidUpdate(props, state) {
+ // 获取歌单块高度进行transition
+ if (this.props.usersong.state == 'get' && props.usersong.state == 'fetching'){
+ this.setState({
+ createHeight: this.refs.create.getBoundingClientRect().height,
+ collectHeight: this.refs.collect.getBoundingClientRect().height,
+ });
+ }
+ }
+
+ _songlistdetail(id) {
+ this.props.actions.push(SongListContent);
+ this.props.actions.fetchsonglistdetail(id);
+ }
+
+ getCreate() {
+ const { usersong } = this.props;
+ switch (usersong.state){
+ case 'nouser':
+ return 无用户
+ case 'fetching':
+ return
+ case 'error':
+ return 获取歌单出错{usersong.errorinfo}
+ case 'get':
+ let PlayListIcon = require('../assets/icon/playlist.svg?name=PlayListIcon')
+ let self = this;
+ return (
+
+ {usersong.create.map(songlist => {
+ return (
+ - self._songlistdetail(songlist.id)}
+ className="sidebar__mylist__content__list">
+
+
{songlist.name}
+
+ )
+ })}
+
+ );
+ }
+ }
+
+ getCollect() {
+ const { usersong } = this.props;
+ let self = this;
+ switch (usersong.state){
+ case 'nouser':
+ return 无用户
+ case 'fetching':
+ return
+ case 'error':
+ return 获取歌单出错{usersong.errorinfo}
+ case 'get':
+ let PlayListIcon = require('../assets/icon/playlist.svg?name=PlayListIcon')
+ return (
+
+ {usersong.collect.map(songlist => {
+ return (
+ - self._songlistdetail(songlist.id)}
+ className="sidebar__mylist__content__list">
+
+
{songlist.name}
+
+ )
+ })}
+
+ );
+ }
+ }
+
+ _showorhide(target) {
+ if (target === 'showCreate') {
+ this.setState({
+ showCreate: !this.state.showCreate,
+ });
+ }
+ if (target === 'showCollect') {
+ this.setState({
+ showCollect: !this.state.showCollect,
+ });
+ }
+ }
+
+ _onscroll(e) {
+ if (this.props.playcontent.state == 'show' && e.target.scrollTop > this.scroll.lastScrollTop) {
+ this.props.actions.hiddenplaycontentmini();
+ }
+ if (this.props.playcontent.state == 'hidden' && e.target.scrollTop < this.scroll.lastScrollTop) {
+ this.props.actions.showplaycontentmini();
+ }
+ this.scroll.lastScrollTop = e.target.scrollTop;
+ }
+
+ render() {
+ return (
+ this._onscroll(e)}
+ className="sidebar">
+
+
我创建的歌单
+
this._showorhide('showCreate')}
+ src={require('url!../assets/img/up.svg')}
+ />
+ {this.getCreate()}
+
+
+
我收藏的歌单
+
this._showorhide('showCollect')}
+ />
+ {this.getCollect()}
+
+
+ );
+ }
+}
diff --git a/app/components/SongCard.jsx b/app/components/SongCard.jsx
new file mode 100644
index 0000000..8f7b18b
--- /dev/null
+++ b/app/components/SongCard.jsx
@@ -0,0 +1,31 @@
+import React, { Component } from 'react';
+
+export default class SongCard extends Component {
+ constructor(props: any) {
+ super(props);
+ }
+
+ _playsong(e, song) {
+ this.props.changeSong(song);
+ }
+
+ render() {
+ return (
+ this._playsong(e, this.props.data)}
+ >
+
+
+
+ {this.props.data.name}
+
+
+ 专辑:{this.props.data.album.name}
+ 歌手:{this.props.data.artists[0].name}
+
+
+
+ );
+ }
+}
diff --git a/app/components/SongList.jsx b/app/components/SongList.jsx
new file mode 100644
index 0000000..707a1aa
--- /dev/null
+++ b/app/components/SongList.jsx
@@ -0,0 +1,96 @@
+import React, { Component } from 'react';
+
+export default class SongList extends Component {
+ constructor(props: any) {
+ super(props);
+ }
+
+ secToTime(sec) {
+ sec = sec / 1000;
+ let min = parseInt(sec / 60);
+ if (min < 10) {
+ min = '0' + min;
+ }
+ let second = parseInt(sec % 60);
+ if (second < 10) {
+ second = '0' + second;
+ }
+
+ return min + ':' + second;
+ }
+
+ getShortName(name, limit) {
+ if (name.length > limit) {
+ return name.slice(0, limit) + '...';
+ } else {
+ return name;
+ }
+ }
+
+ _playsong(e, song) {
+ this.props.changeSong(song);
+ }
+
+ _addsong(e, song) {
+ this.props.addSong(song);
+ }
+
+ render() {
+ let Add = require('../assets/icon/add.svg?name=Add');
+ return (
+
+
+
+
+ 编号 |
+ |
+ 音乐标题 |
+ 歌手 |
+ 专辑 |
+ 时长 |
+ 热度 |
+
+
+
+ {this.props.data.map( (song, index) => {
+ return (
+
+ {index + 1} |
+
+ this._addsong(e, song)}
+ />
+ |
+ this._playsong(e, song)}
+ >
+ {this.getShortName(song.name, 30)}
+ |
+
+ {this.getShortName(song.artists[0].name, 15)}
+ |
+
+ {this.getShortName(song.album.name, 18)}
+ |
+
+ {this.secToTime(song.duration)}
+ |
+
+
+ |
+
+ );
+ })}
+
+
+
+ );
+ }
+}
diff --git a/app/components/SongListContent.jsx b/app/components/SongListContent.jsx
new file mode 100644
index 0000000..c3a1125
--- /dev/null
+++ b/app/components/SongListContent.jsx
@@ -0,0 +1,114 @@
+'use strict';
+// 歌单内容
+
+import React, { Component } from 'react';
+import Spinner from './Spinner.jsx';
+import AlbumCard from './AlbumCard.jsx';
+import SongList from './SongList.jsx';
+
+export default class SongListContent extends Component {
+ constructor(props: any) {
+ super(props);
+ }
+
+ renderResult() {
+ if (this.props.search.searchResponse.songCount === 0) {
+ return ( 无结果
);
+ } else {
+ return (
+
+ );
+ }
+ }
+
+ render() {
+ if (this.props.songlist.state == 'fetching') {
+ return this.renderFetching();
+ }
+ if (this.props.songlist.state == 'get') {
+ return this.renderFinish();
+ }
+ if (this.props.songlist.state == 'error') {
+ return this.renderFetching();
+ }
+ }
+
+ renderFetching() {
+ return (
+
+ );
+ }
+
+ renderFinish() {
+ let songs = this.props.songlist.content.tracks;
+ songs.map(song => {
+ song.artists = song.ar;
+ song.album = song.al;
+ song.duration = song.dt;
+ song.score = song.pop;
+ if (song.h) {
+ song.hMusic = song.h;
+ song.hMusic.bitrate = song.h.br;
+ }
+ if (song.m) {
+ song.mMusic = song.m;
+ song.mMusic.bitrate = song.m.br;
+ }
+ if (song.l) {
+ song.lMusic = song.l;
+ song.lMusic.bitrate = song.l.br;
+ }
+ });
+ return (
+
+ );
+ }
+
+ renderError() {
+ return (
+ Error
+ );
+ }
+}
diff --git a/app/components/Spinner.jsx b/app/components/Spinner.jsx
new file mode 100644
index 0000000..f27cf59
--- /dev/null
+++ b/app/components/Spinner.jsx
@@ -0,0 +1,10 @@
+import React, { Component } from 'react';
+
+export default class Spinner extends Component {
+ render() {
+ return (
+
+
+ );
+ }
+}
diff --git a/app/components/UserState.jsx b/app/components/UserState.jsx
new file mode 100644
index 0000000..74be8ff
--- /dev/null
+++ b/app/components/UserState.jsx
@@ -0,0 +1,126 @@
+import React, { Component } from 'react';
+import { search } from '../server';
+import Spinner from './Spinner.jsx';
+
+export default class UserState extends Component {
+ constructor(props: any) {
+ super(props);
+ this.state = {
+ showMenu: false,
+ }
+ }
+
+ _login(e) {
+ this.props.loginform(true);
+ }
+
+ _showMenu(e) {
+ this.setState({
+ showMenu: !this.state.showMenu,
+ });
+ }
+
+ _logout(e) {
+ this.props.toguest();
+ }
+
+ componentDidMount() {
+ let self = this;
+ // 根据cookie判断是否自动登陆
+ Electron.ipcRenderer.on('cookie', (e, cookies) => {
+ console.log(cookies);
+ let flag = 0;
+ cookies.map(cookie => {
+ if (cookie.name === 'MUSIC_U') {
+ flag++;
+ }
+ if ((cookie.name === '__remember_me') && (cookie.value === 'true')) {
+ flag++;
+ }
+ });
+ if (flag > 1) {
+ if (localStorage.user) {
+ self.props.logged_in(JSON.parse(localStorage.getItem('user')));
+ self.props.fetchusersong(self.props.user.profile.userId);
+ }
+ }
+ });
+ }
+
+ render() {
+ if (this.props.user.loginState == 'logged_in') {
+ return this.renderUser();
+ } else if (this.props.user.loginState == 'logging_in') {
+ return this.renderLogging();
+ } else {
+ return this.renderGuest();
+ }
+ }
+
+ renderGuest() {
+ if (this.props.user.loginState == 'logged_failed') {
+ alert(this.props.user.loginError, "登录失败");
+ this.props.toguest();
+ }
+ return (
+
+
+
+
this._login(e) }>
+ 登录
+
+
+ );
+ }
+
+ renderLogging() {
+ return (
+
+
+
+
+
this._login(e) }>
+ 登录中..
+
+
+ );
+ }
+
+ renderUser() {
+ return (
+
+
this._showMenu(e)}
+ >
+
+ { this.state.showMenu ? (
+
+ - this._logout(e) }
+ >退出登录
+
+
) : ''}
+
+
+ {this.props.user.profile.nickname}
+
+
+ );
+ }
+}
diff --git a/app/components/Volume.jsx b/app/components/Volume.jsx
new file mode 100644
index 0000000..c56bb2a
--- /dev/null
+++ b/app/components/Volume.jsx
@@ -0,0 +1,94 @@
+import React, { Component } from 'react';
+
+export default class Volume extends Component {
+ constructor(props: any) {
+ super(props);
+ this.mouseState = {
+ press: false,
+ };
+ this.state = {
+ volume: 1,
+ mute: false,
+ }
+ }
+
+ _handleMouseUp(e) {
+ if (!this.mouseState.press) {
+ return;
+ }
+ this.mouseState.press = false;
+ let volumeWidth = this.refs.volume.clientWidth;
+ let volume = (e.pageX - this.refs.volume.getBoundingClientRect().left) / volumeWidth;
+ volume = volume > 1 ? 1 : volume;
+ volume = volume < 0 ? 0 : volume;
+
+ this.setState({
+ "volume": volume
+ });
+ this.props.updateVolume(this.state.volume, this.state.mute);
+ }
+
+ _handleMouseMove(e) {
+ if (!this.mouseState.press) {
+ return;
+ }
+ let volumeWidth = this.refs.volume.clientWidth;
+ let volume = (e.pageX - this.refs.volume.getBoundingClientRect().left) / volumeWidth;
+ volume = volume > 1 ? 1 : volume;
+ volume = volume < 0 ? 0 : volume;
+
+ this.setState({
+ "volume": volume
+ });
+ this.props.updateVolume(this.state.volume, this.state.mute);
+ }
+
+ _mouseDown(e) {
+ this.mouseState.press = true;
+ window.addEventListener("mouseup", this._handleMouseUp.bind(this));
+ window.addEventListener("mousemove", this._handleMouseMove.bind(this));
+ }
+
+ _mute(e) {
+ this.setState({
+ mute: !this.state.mute
+ }, () => {
+ this.props.updateVolume(this.state.volume, this.state.mute);
+ });
+ }
+
+ getVolumeIcon() {
+ if (this.state.mute) {
+ return require('../assets/icon/volume_mute.svg');
+ }
+
+ if (this.state.volume > 0.7 ) {
+ return require('../assets/icon/volume_max.svg');
+ } else if (this.state.volume > 0.3) {
+ return require('../assets/icon/volume_min.svg');
+ } else {
+ return require('../assets/icon/volume.svg');
+ }
+ }
+
+ render() {
+ var VolumeIcon = this.getVolumeIcon();
+ return (
+
+
this._mute(e) }
+ className="i" />
+ this._mouseDown(e) }
+ >
+
+
+
+ );
+ }
+}
diff --git a/app/containers/CloudMusic.jsx b/app/containers/CloudMusic.jsx
new file mode 100644
index 0000000..69d518e
--- /dev/null
+++ b/app/containers/CloudMusic.jsx
@@ -0,0 +1,31 @@
+import React, { Component } from 'react';
+import { render } from 'react-dom';
+import App from '../components/App.jsx';
+
+import thunkMiddleware from 'redux-thunk';
+import createLogger from 'redux-logger';
+import { Provider } from 'react-redux';
+import { createStore, applyMiddleware } from 'redux';
+
+import cloudMusic from '../reducers';
+
+const loggerMiddleware = createLogger();
+const createStoreWithMiddleware = applyMiddleware(
+ thunkMiddleware,
+ loggerMiddleware
+)(createStore);
+
+var store = createStoreWithMiddleware(cloudMusic);
+class CloudMusic extends Component {
+ render() {
+ return (
+
+
+
+ );
+ }
+}
+React.render(
+ ,
+ document.body
+);
diff --git a/app/main.js b/app/main.js
new file mode 100644
index 0000000..4fe089e
--- /dev/null
+++ b/app/main.js
@@ -0,0 +1,2 @@
+require('./postcss/index.css');
+require('./containers/CloudMusic.jsx');
diff --git a/app/postcss/_albumcard.css b/app/postcss/_albumcard.css
new file mode 100644
index 0000000..824757d
--- /dev/null
+++ b/app/postcss/_albumcard.css
@@ -0,0 +1,77 @@
+.albumcard {
+ height: 200px;
+ min-width: 500px;
+ max-width: 800px;
+ display: flex;
+ align-items: stretch;
+}
+
+.albumcard__cover {
+ position: relative;
+}
+
+.albumcard__cover__playcount {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ height: 30px;
+ line-height: 30px;
+ padding-left: 10px;
+ background-color: rgba(0, 0, 0, 0.5);
+ color: #fff;
+}
+
+.albumcard-left {
+ flex: 1;
+ display: flex;
+ justify-content: space-between;
+ padding-right: 16px;
+ padding-bottom: 16px;
+ flex-direction: column;
+}
+
+.albumcard__info {
+ padding: 20px 20px;
+}
+
+.albumcard__info__name {
+ font-size: 30px;
+ color: $primarytext;
+}
+
+.albumcard__info__creator {
+ font-size: 20px;
+ margin-top: 20px;
+ color: $secondarytext;
+}
+
+.albumcard__buttons {
+ display: flex;
+ justify-content: flex-end;
+}
+
+.albumcard__buttons__button {
+ margin-right: 5px;
+ border-radius: 2px;
+ &:first-child {
+ color: $red-500;
+ }
+ &:last-child {
+ color: $indigo-500;
+ }
+}
+
+.albumcard__tags {
+ display: flex;
+ font-size: 18px;
+ padding-left: 20px;
+ p {
+ color: $primarytext;
+ display: block;
+ }
+}
+
+.albumcard__tags__tag{
+ color: $secondarytext;
+ margin-right: 10px;
+}
diff --git a/app/postcss/_button.css b/app/postcss/_button.css
new file mode 100644
index 0000000..6a2e372
--- /dev/null
+++ b/app/postcss/_button.css
@@ -0,0 +1,28 @@
+@define-mixin btn {
+ border: 0;
+ background-color: $indigo-200.0;
+ height: 36px;
+ font-size: 20px;
+ line-height: 36px;
+ text-align: center;
+ cursor: pointer;
+ padding: 0 16px;
+ transition: background-color ease 0.5s;
+ font-weight: bold;
+ &:hover {
+ background-color: $indigo-200;
+ }
+}
+
+.btn-normal {
+ @mixin btn;
+}
+
+.btn-bg {
+ @mixin btn;
+ color: #fff;
+ background-color: $indigo-700;
+ &:hover {
+ background-color: $indigo-500;
+ }
+}
diff --git a/app/postcss/_card.css b/app/postcss/_card.css
new file mode 100644
index 0000000..0254472
--- /dev/null
+++ b/app/postcss/_card.css
@@ -0,0 +1,15 @@
+.card {
+ background-color: #fff;
+ cursor: pointer;
+ box-shadow: 0 2px 2px rgba(0, 0, 0, .18);
+ transition: all ease 0.5s;
+ transform: translateY(0);
+ border-radius: 2px;
+ * {
+ cursor: pointer;
+ }
+ &:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 8px 8px rgba(0, 0, 0, .18);
+ }
+}
diff --git a/app/postcss/_colors.css b/app/postcss/_colors.css
new file mode 100644
index 0000000..dd52e82
--- /dev/null
+++ b/app/postcss/_colors.css
@@ -0,0 +1,30 @@
+$red-200: #ef9a9a;
+$red-300: #e57373;
+$red-500: #f44336;
+$red-700: #d32f2f;
+$red-300: #e57373;
+$red-900: #b0120a;
+$red-100: #f9bdbb;
+
+$indigo-50: #e8eaf6;
+$indigo-200: #9fa8da;
+$indigo-500: #3f51b5;
+$indigo-700: #303f9f;
+$indigo-900: #1a237e;
+
+$primarytext: rgba(0, 0, 0, .87);
+$secondarytext: rgba(0, 0, 0, .54);
+$disabletext: rgba(0, 0, 0, .38);
+
+$content: #fafafa;
+$content2: #f7f7f7;
+$content3: #eeeeee;
+$dividers: #e0e0e0;
+
+$grey-200: #eeeeee;
+$grey-300: #e0e0e0;
+$grey-400: #bdbdbd;
+$grey-500: #9e9e9e;
+$grey-600: #757575;
+
+$icon: rgba(0, 0, 0, .38);
diff --git a/app/postcss/_content.css b/app/postcss/_content.css
new file mode 100644
index 0000000..1fe1a93
--- /dev/null
+++ b/app/postcss/_content.css
@@ -0,0 +1,51 @@
+@import "home-content";
+@import "search-content";
+@import "songlist-content";
+@import "sidebar";
+
+#content {
+ flex: 1;
+ display: flex;
+ position: relative;
+ align-items: strench;
+}
+
+.main-content {
+ flex: 1;
+ position: relative;
+ display: flex;
+}
+
+.content {
+ flex: 1;
+ color: $primarytext;
+ background-color: $content3;
+ z-index: 10;
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+ @mixin scrollbar;
+}
+
+.content__headinfo {
+ width: 100%;
+ padding: 35px 30px 5px 30px;
+ border-bottom: 2px solid #c70c0c;
+ background-color: $content;
+ p {
+ font-size: 25px;
+ }
+}
+
+/* FIXME */
+.content__main {
+ height: 0;
+}
+
+.content__main__list {
+ margin-top: 30px;
+}
+
+.content__card {
+ padding: 20px 20px;
+}
diff --git a/app/postcss/_header.css b/app/postcss/_header.css
new file mode 100644
index 0000000..98fe1a7
--- /dev/null
+++ b/app/postcss/_header.css
@@ -0,0 +1,37 @@
+@import "searchbar";
+@import "user-state";
+
+
+.header {
+ display: flex;
+ align-items: center;
+ height: 70px;
+ border-top: 0.8px solid black;
+ border-bottom: 3px solid #c70c0c;
+ padding: 0 5px 0 30px;
+ justify-content: space-between;
+ background-color: $red-500;
+ box-shadow: 0 4px 4px rgba(0, 0, 0, .18);
+ z-index: 11;
+}
+
+.header__logo {
+ img {
+ height: 55px;
+ }
+}
+
+.header__space {
+ flex: 1;
+}
+
+.header__windowcontrol {
+ margin-left: 20px;
+ svg {
+ fill: #fff;
+ cursor: pointer;
+ * {
+ cursor: pointer;
+ }
+ }
+}
diff --git a/app/postcss/_home-content.css b/app/postcss/_home-content.css
new file mode 100644
index 0000000..217c912
--- /dev/null
+++ b/app/postcss/_home-content.css
@@ -0,0 +1,12 @@
+.home-content {
+ flex: 1;
+ background-color: $content;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.home-content__main {
+ font-size: 200px;
+ color: rgba(255, 255, 255, 0.1);
+}
diff --git a/app/postcss/_loginform.css b/app/postcss/_loginform.css
new file mode 100644
index 0000000..897c6a3
--- /dev/null
+++ b/app/postcss/_loginform.css
@@ -0,0 +1,69 @@
+.loginform {
+ position: absolute;
+ height: 270px;
+ width: 500px;
+ z-index: 100;
+ top: calc(50% - 100px);
+ left: calc(50% - 250px);
+ background-color: $content;
+ overflow: hidden;
+ box-shadow: 0 24px 24px rgba(0, 0, 0, .18);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.loginform__header {
+ width: 100%;
+ height: 56px;
+ font-size: 20px;
+ position: relative;
+ background-color: $indigo-500;
+ color: white;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ h2 {
+ height: 100%;
+ line-height: 56px;
+ padding-left: 20px;
+ }
+}
+
+.loginform__header__close {
+ cursor: pointer;
+ margin-right: 10px;
+ fill: #fff;
+}
+
+.loginform__form {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ position: relative;
+ input {
+ font-size: 18px;
+ width: 450px;
+ height: 50px;
+ border: 0;
+ border-bottom: 2px solid $grey-300;
+ background-color: transparent;
+ margin-top: 20px;
+ outline: none;
+ transition: all ease 0.5s;
+ &:focus {
+ border-bottom: 3px solid $indigo-500;
+
+ }
+ }
+}
+
+.loginform__submit {
+ width: 100px;
+ height: 40px;
+ font-size: 20px;
+ position: absolute;
+ bottom: 16px;
+ right: 25px;
+}
diff --git a/app/postcss/_playcontentcard.css b/app/postcss/_playcontentcard.css
new file mode 100644
index 0000000..f599930
--- /dev/null
+++ b/app/postcss/_playcontentcard.css
@@ -0,0 +1,40 @@
+.miniplaycontent {
+ bottom: 70px;
+ left: 10px;
+ overflow: hidden;
+ position: absolute;
+ z-index: 10;
+ background-color: $indigo-50;
+}
+
+.miniplaycontent-wrapper {
+ display: flex;
+ padding: 10px 10px;
+ width: 300px;
+ height: 100px;
+}
+
+.miniplaycontent__cover {
+ height: 80px;
+ width: 80px;
+ overflow: hidden;
+ img {
+ width: 100%;
+ }
+}
+
+.miniplaycontent__info {
+ margin-left: 20px;
+ display: flex;
+ flex-direction: column;
+ font-size: 20px;
+ justify-content: space-around;
+}
+
+.miniplaycontent__info__name {
+ color: $primarytext;
+}
+
+.miniplaycontent__info__artist {
+ color: $secondarytext;
+}
diff --git a/app/postcss/_player.css b/app/postcss/_player.css
new file mode 100644
index 0000000..92bb2e3
--- /dev/null
+++ b/app/postcss/_player.css
@@ -0,0 +1,132 @@
+$PlayerHeight: 53px;
+@import "volume";
+@import "playlist";
+@import "playlistcontrol";
+
+
+@define-mixin player-btn $size {
+ height: $size;
+ width: $size;
+ .i {
+ fill: $red-500;
+ }
+}
+
+@keyframes loading {
+ from {
+ background-color: $indigo-500;
+ }
+
+ to {
+ background-color: $red-500;
+ }
+}
+
+.player {
+ height: $PlayerHeight;
+ background-color: #fff.9;
+ padding: 0 30px;
+ display: flex;
+ align-items: center;
+ position: fixed;
+ z-index: 20;
+ box-shadow: 0 -4px 4px rgba(0, 0, 0, .18);
+ bottom: 0;
+ left: 0;
+ width: 100%;
+}
+
+.player__btns {
+ display: flex;
+ align-items: center;
+}
+
+.player__btns__backward, .player__btns__forward {
+ @mixin player-btn 30px;
+}
+
+.player__btns__play {
+ @mixin player-btn 40px;
+}
+
+.player__btns-btn {
+ padding: 0;
+ border: 2px solid $red-500;
+ border-radius: 50%;
+ margin: 0 15px;
+ cursor: pointer;
+ background-color: transparent;
+ * {
+ cursor: pointer;
+ }
+ &:focus {
+ outline: none;
+ }
+}
+
+.player__pg {
+ display: flex;
+ height: 100%;
+ flex: 1;
+ margin: 0 20px;
+ align-items: center;
+ p {
+ color: $primarytext;
+ }
+}
+
+.player__pg__bar {
+ flex: 1;
+ height: 7px;
+ margin: 0 15px;
+ border-radius: 5px;
+ background-color: $red-200;
+ position: relative;
+}
+
+.player__pg__bar-ready {
+ position: absolute;
+ top: 0;
+ height: 100%;
+ background-color: $red-300;
+ border-radius: 5px;
+}
+
+.player__pg__bar-cur-wrapper {
+ position: absolute;
+ top: 0;
+ height: 100%;
+ width: 100%;
+ background-color: transparent;
+ z-index: 10;
+}
+
+.player__pg__bar-cur {
+ height: 100%;
+ background-color: #c70c0c;
+ border-top: 1px solid #f41616;
+ border-radius: 5px;
+ position: relative;
+ &::after {
+ display: block;
+ content: '';
+ width: 20px;
+ height: 20px;
+ background-color: $indigo-500;
+ position: absolute;
+ right: -10px;
+ top: -7px;
+ border-radius: 50%;
+ box-sizing: border-box;
+ cursor: pointer;
+ }
+}
+
+.player__pg__bar-cur.loading {
+ &::after {
+ animation-duration: 0.6s;
+ animation-direction: alternate;
+ animation-iteration-count: infinite;
+ animation-name: loading;
+ }
+}
diff --git a/app/postcss/_playlist.css b/app/postcss/_playlist.css
new file mode 100644
index 0000000..da6da47
--- /dev/null
+++ b/app/postcss/_playlist.css
@@ -0,0 +1,68 @@
+.playlist {
+ position: absolute;
+ width: 600px;
+ height: 400px;
+ bottom: $PlayerHeight;
+ background-color: $content;
+ display: flex;
+ flex-direction: column;
+ /* chrome absolute zindex bug */
+ box-shadow: 0 -4px 4px rgba(0, 0, 0, .18) inset;
+ overflow: hidden;
+ border-radius: 2px;
+ z-index: 13;
+ transition: all ease 0.3s;
+}
+
+.playlist__header {
+ height: 60px;
+ width: 100%;
+ background-color: $indigo-500;
+ color: white;
+ font-szie: 30px;
+ padding: 0 20px;
+ box-shadow: 0 4px 4px rgba(0, 0, 0, .18);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ .i {
+ cursor: pointer;
+ fill: #fff;
+ }
+}
+
+.playlist__content {
+ flex: 1;
+ color: $primarytext;
+ @mixin scrollbar;
+ overflow-y: auto;
+}
+
+.playlist__content__list__song {
+ display: flex;
+ padding: 5px 20px;
+ height: 40px;
+ align-items: center;
+ border-bottom: 1px solid $dividers;
+ &:hover {
+ background-color: $grey-300;
+ }
+ cursor: pointer;
+ * {
+ cursor: pointer;
+ }
+}
+
+.playlist__content__list__song.current {
+ background-color: $grey-300;
+}
+
+.playlist__content__list__song-name {
+ flex: 9;
+}
+.playlist__content__list__song-artist {
+ flex: 3;
+}
+.playlist__content__list__song-duration {
+ flex: 1;
+}
diff --git a/app/postcss/_playlistcontrol.css b/app/postcss/_playlistcontrol.css
new file mode 100644
index 0000000..6b6482f
--- /dev/null
+++ b/app/postcss/_playlistcontrol.css
@@ -0,0 +1,42 @@
+.player__playlistcontrol {
+ margin: 0 20px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ .i {
+ height: 100%;
+ cursor: pointer;
+ fill: $icon;
+ }
+}
+
+.player__playlistcontrol__playlistrule {
+ height: 100%;
+}
+
+.player__playlistcontrol__playlist {
+ padding: 0 10px;
+ height: 30px;
+ margin-left: 10px;
+ display: flex;
+ align-items: center;
+ color: #535353;
+ background-color: rgba(255, 255, 255, 0);
+ border-radius: 15px;
+ transition: background-color ease 0.5s;
+ cursor: pointer;
+ * {
+ cursor: pointer;
+ }
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.3);
+ }
+}
+
+.player__playlistcontrol__playlist.active {
+ background-color: rgba(0, 0, 0, 0.3);
+}
+
+.player__playlistcontrol__playlist__count {
+ margin-left: 5px;
+}
diff --git a/app/postcss/_reset.css b/app/postcss/_reset.css
new file mode 100644
index 0000000..e515647
--- /dev/null
+++ b/app/postcss/_reset.css
@@ -0,0 +1,48 @@
+/* http://meyerweb.com/eric/tools/css/reset/
+ v2.0 | 20110126
+ License: none (public domain)
+*/
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section {
+ display: block;
+}
+body {
+ line-height: 1;
+}
+ol, ul {
+ list-style: none;
+}
+blockquote, q {
+ quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+ content: '';
+ content: none;
+}
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
diff --git a/app/postcss/_scrollbar.css b/app/postcss/_scrollbar.css
new file mode 100644
index 0000000..848939e
--- /dev/null
+++ b/app/postcss/_scrollbar.css
@@ -0,0 +1,18 @@
+@define-mixin scrollbar {
+ &::-webkit-scrollbar {
+ width: 10px;
+ }
+
+ &::-webkit-scrollbar-track {
+ border-radius: 10px;
+ background-color: white;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ border-radius: 10px;
+ background-color: rgba(0, 0, 0, 0.2);
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.3);
+ }
+ }
+}
diff --git a/app/postcss/_search-content.css b/app/postcss/_search-content.css
new file mode 100644
index 0000000..f46967c
--- /dev/null
+++ b/app/postcss/_search-content.css
@@ -0,0 +1,16 @@
+#search-content .content__headinfo {
+ .keywords {
+ color: $secondarytext;
+ margin-right: 10px;
+ }
+}
+
+#search-content .content__main__bestmarch {
+ h2 {
+ padding:16px 30px;
+ font-size: 21px;
+ }
+ .songcard {
+ margin-left: 30px;
+ }
+}
diff --git a/app/postcss/_searchbar.css b/app/postcss/_searchbar.css
new file mode 100644
index 0000000..028b8c3
--- /dev/null
+++ b/app/postcss/_searchbar.css
@@ -0,0 +1,33 @@
+.header__searchbar {
+ height: 30px;
+ width: 250px;
+ border-radius: 15px;
+ background-color: white;
+ display: flex;
+ align-items: center;
+ padding: 0 12px;
+ label {
+ width: 20px;
+ height: 20px;
+ transform: translateY(2px);
+ display: inline-block;
+ }
+ .i {
+ color: #535353;
+ width: 100%;
+ height: 100%;
+ }
+ input {
+ width: 200px;
+ height: 100%;
+ background-color: transparent;
+ border: 0;
+ margin-left: 5px;
+ outline: 0;
+ }
+}
+
+#search-form {
+ display: flex;
+ align-items: center;
+}
diff --git a/app/postcss/_sidebar.css b/app/postcss/_sidebar.css
new file mode 100644
index 0000000..57e811d
--- /dev/null
+++ b/app/postcss/_sidebar.css
@@ -0,0 +1,62 @@
+.sidebar {
+ min-height: 100%;
+ width: 300px;
+ background-color: #fff;
+ border-right: 1px solid $dividers;
+ overflow-y: auto;
+ overflow-x: hidden;
+ @mixin scrollbar;
+ padding-bottom: $PlayerHeight;
+}
+
+.sidebar__mylist {
+ position: relative;
+ color: $secondarytext;
+ h3 {
+ padding-left: 5px;
+ margin-top: 15px;
+ color: $primarytext;
+ height: 30px;
+ line-height: 30px;
+ border-bottom: 1px solid $dividers;
+ }
+}
+
+.sidebar__mylist__control {
+ position: absolute;
+ right: 10px;
+ top: 2px;
+ transition: transform ease 0.5s;
+ cursor: pointer;
+ @mixin scrollbar;
+}
+
+.sidebar__mylist__content {
+ overflow: hidden;
+ transition: height ease 0.5s;
+}
+
+.sidebar__mylist__content__list {
+ padding-left: 15px;
+ height: 30px;
+ line-height: 30px;
+ display: flex;
+ flex-wrap: nowrap;
+ align-items: center;
+ cursor: pointer;
+ * {
+ cursor: pointer;
+ }
+ .i {
+ fill: $secondarytext;
+ margin-right: 5px;
+ }
+ p {
+ white-space: nowrap;
+ width: 1px;
+ }
+ &:hover {
+ color: $primarytext;
+ background-color: $grey-300;
+ }
+}
diff --git a/app/postcss/_songcard.css b/app/postcss/_songcard.css
new file mode 100644
index 0000000..7ab7ee9
--- /dev/null
+++ b/app/postcss/_songcard.css
@@ -0,0 +1,18 @@
+.songcard {
+ height: 100px;
+ min-width: 400px;
+ max-width: 600px;
+ display: flex;
+ padding: 10px 10px;
+ align-items: center;
+ img {
+ height: 100%;
+ }
+}
+
+.songcard__info {
+ margin-left: 20px;
+ line-height: 30px;
+ font-size: 20px;
+ cursor: pointer;
+}
diff --git a/app/postcss/_songlist-content.css b/app/postcss/_songlist-content.css
new file mode 100644
index 0000000..e62f9c9
--- /dev/null
+++ b/app/postcss/_songlist-content.css
@@ -0,0 +1,3 @@
+#songlist-content .content__main__card {
+ padding: 20px 20px;
+}
diff --git a/app/postcss/_songlist.css b/app/postcss/_songlist.css
new file mode 100644
index 0000000..5755582
--- /dev/null
+++ b/app/postcss/_songlist.css
@@ -0,0 +1,99 @@
+.songlist {
+ width: 100%;
+ font-size: 16px;
+ background-color: $content2;
+ color: $primarytext;
+ tr {
+ height: 30px;
+ line-height: 30px;
+ border-top: 1px solid $dividers;
+ }
+
+ th,td {
+ padding-left: 5px;
+ }
+
+ th.th-center {
+ text-align: center;
+ }
+
+ thead tr {
+ padding: 5px 0;
+ color: rgba(0, 0, 0, .54);
+ }
+
+ tbody {
+ tr {
+ transition: all ease 0.5s;
+ }
+ tr:hover {
+ background-color: $grey-300;
+ }
+ }
+}
+.songlist-table__index {
+ width: 5%;
+ padding-right: 10px;
+ text-align: right;
+}
+
+.songlist-table__name {
+ width: 30%;
+ cursor: pointer;
+ &:hover {
+ color: black;
+ }
+}
+
+.songlist-table__button {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ .i {
+ fill: $icon;
+ height: 26px;
+ width: 26px;
+ /* FIXME: parent can't height 100% */
+ transform: translateY(6px);
+ }
+ * {
+ cursor: pointer;
+ }
+}
+
+.songlist-table__artists {
+ width: 20%;
+}
+
+.songlist-table__album {
+ width: 20%;
+}
+
+.songlist-table__duration {
+ width: 5%;
+}
+
+.songlist-table__hot {
+ width: 15%;
+}
+
+.songlist-table__hotbar-wrapper {
+ height: 10px;
+ width: 80%;
+ margin: auto;
+ border-radius: 5px;
+ background-color: $red-300;
+ overflow: hidden;
+}
+
+.songlist-table__hotbar {
+ background-color: $red-700;
+ border-radius: 5px;
+ height: 100%;
+}
+
+.songlist-table {
+ text-align: left;
+ width: 100%;
+}
diff --git a/app/postcss/_spinner.css b/app/postcss/_spinner.css
new file mode 100644
index 0000000..02c9732
--- /dev/null
+++ b/app/postcss/_spinner.css
@@ -0,0 +1,63 @@
+/* https://github.com/lukehaas/css-loaders */
+
+.loader,
+.loader:before,
+.loader:after {
+ background: #ffffff;
+ -webkit-animation: load1 1s infinite ease-in-out;
+ animation: load1 1s infinite ease-in-out;
+ width: 1em;
+ height: 4em;
+}
+.loader:before,
+.loader:after {
+ position: absolute;
+ top: 0;
+ content: '';
+}
+.loader:before {
+ left: -1.5em;
+ -webkit-animation-delay: -0.32s;
+ animation-delay: -0.32s;
+}
+.loader {
+ color: #ffffff;
+ text-indent: -9999em;
+ margin: 88px auto;
+ position: relative;
+ font-size: 11px;
+ -webkit-transform: translateZ(0);
+ -ms-transform: translateZ(0);
+ transform: translateZ(0);
+ -webkit-animation-delay: -0.16s;
+ animation-delay: -0.16s;
+}
+.loader:after {
+ left: 1.5em;
+}
+
+@-webkit-keyframes load1 {
+ 0%,
+ 80%,
+ 100% {
+ box-shadow: 0 0;
+ height: 4em;
+ }
+ 40% {
+ box-shadow: 0 -2em;
+ height: 5em;
+ }
+}
+
+@keyframes load1 {
+ 0%,
+ 80%,
+ 100% {
+ box-shadow: 0 0;
+ height: 4em;
+ }
+ 40% {
+ box-shadow: 0 -2em;
+ height: 5em;
+ }
+}
diff --git a/app/postcss/_user-state.css b/app/postcss/_user-state.css
new file mode 100644
index 0000000..ac53e68
--- /dev/null
+++ b/app/postcss/_user-state.css
@@ -0,0 +1,67 @@
+.header__user {
+ height: 50px;
+ display: flex;
+ align-items: center;
+ color: #D6D6D6;
+}
+
+.header__user__avatar {
+ width: 50px;
+ margin-right: 20px;
+ margin-left: 20px;
+ position: relative;
+ cursor: pointer;
+ * {
+ cursor: pointer;
+ }
+ img {
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ }
+
+ .loader {
+ font-size: 7px;
+ }
+}
+
+.header__user__menu {
+ position: absolute;
+ top: 70px;
+ width: 150px;
+ background-color: $content;
+ box-shadow: 0px 0px 14px rgba(0, 0, 0, .18);
+ color: $primarytext;
+ padding: 10px 0;
+ font-size: 17px;
+ &::before {
+ content: '';
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-left: 10px solid transparent;
+ border-right: 10px solid transparent;
+ border-bottom: 10px solid $content;
+ top: -10px;
+ left: 13px;
+ }
+}
+
+.header__user__menu__list {
+ li {
+ padding-left: 10px;
+ height: 25px;
+ line-height: 25px;
+ transition: background-color ease 0.5s;
+ cursor: pointer;
+ &:hover {
+ background-color: $grey-300;
+ }
+ }
+}
+
+.header__user__login, .header__user__name {
+ height: 30px;
+ line-height: 30px;
+ cursor: pointer;
+}
diff --git a/app/postcss/_volume.css b/app/postcss/_volume.css
new file mode 100644
index 0000000..cbdfda2
--- /dev/null
+++ b/app/postcss/_volume.css
@@ -0,0 +1,23 @@
+.player__volume {
+ display: flex;
+ align-items: center;
+ .i {
+ fill: $icon;
+ }
+}
+
+.player__volume__bar-wrapper {
+ width: 120px;
+ height: 10px;
+ margin-left: 10px;
+ background-color: $red-200;
+ border-radius: 10px;
+ overflow: hidden;
+}
+
+.player__volume__bar {
+ background-color: $red-500;
+ height: 100%;
+ border-top-right-radius: 10px;
+ border-bottom-right-radius: 10px;
+}
diff --git a/app/postcss/index.css b/app/postcss/index.css
new file mode 100644
index 0000000..1c143ee
--- /dev/null
+++ b/app/postcss/index.css
@@ -0,0 +1,31 @@
+@import "reset";
+@import "colors";
+@import "scrollbar";
+@import "header";
+@import "player";
+@import "content";
+@import "button";
+@import "spinner";
+@import "songcard";
+@import "albumcard";
+@import "songlist";
+@import "loginform";
+@import "card";
+@import "playcontentcard";
+
+* {
+ box-sizing: border-box;
+ cursor: default;
+ user-select: none;
+}
+
+.app {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ position: relative;
+}
+
+body {
+ overflow: hidden;
+}
diff --git a/app/reducers/alert.js b/app/reducers/alert.js
new file mode 100644
index 0000000..ed488e6
--- /dev/null
+++ b/app/reducers/alert.js
@@ -0,0 +1,24 @@
+'use strict'
+export default function alert(state, action) {
+ if (action.type !== 'USER') {
+ if (state) {
+ return state;
+ } else {
+ return {
+ showAlert: false,
+ };
+ }
+ }
+ newState = Object.assign({}, state);
+ switch (action.state) {
+ case 'NEWALERT':
+ newState.showAlert = true;
+ newState.body = action.payload;
+ return newState;
+ case 'CLOSEALERT':
+ newState.showAlert = false;
+ return newState;
+ default:
+ return newState;
+ }
+}
diff --git a/app/reducers/index.js b/app/reducers/index.js
new file mode 100644
index 0000000..a72f887
--- /dev/null
+++ b/app/reducers/index.js
@@ -0,0 +1,22 @@
+import { combineReducers } from 'redux';
+import player from './player';
+import search from './search';
+import song from './song';
+import user from './user';
+import usersong from './usersong';
+import router from './router';
+import songlist from './songlist';
+import playcontent from './playcontent';
+
+const cloudMusic = combineReducers({
+ player,
+ search,
+ song,
+ user,
+ usersong,
+ router,
+ songlist,
+ playcontent,
+});
+
+export default cloudMusic;
diff --git a/app/reducers/playcontent.js b/app/reducers/playcontent.js
new file mode 100644
index 0000000..eedf08e
--- /dev/null
+++ b/app/reducers/playcontent.js
@@ -0,0 +1,26 @@
+'use strict'
+export default function playcontent(state, action) {
+ if (action.type !== 'PLAYCONTENT') {
+ if (state) {
+ return state;
+ } else {
+ return {
+ mode: 'mini',
+ state: 'show',
+ };
+ }
+ }
+ newState = Object.assign({}, state);
+ switch (action.state) {
+ case 'SHOWMINI':
+ newState.mode = 'mini';
+ newState.state = 'show';
+ return newState;
+ case 'HIDDENMINI':
+ newState.mode = 'mini';
+ newState.state = 'hidden';
+ return newState;
+ default:
+ return newState;
+ }
+}
diff --git a/app/reducers/player.js b/app/reducers/player.js
new file mode 100644
index 0000000..880d8b1
--- /dev/null
+++ b/app/reducers/player.js
@@ -0,0 +1,18 @@
+'use strict'
+export default function player(state, action) {
+ switch (action.type) {
+ case 'PLAYER':
+ if (action.state == 'PLAYER_PLAY') {
+ return { isplay: true }
+ } else {
+ return { isplay: false }
+ }
+
+ default:
+ if (state) {
+ return state;
+ } else {
+ return { isplay: false }
+ }
+ }
+}
diff --git a/app/reducers/router.js b/app/reducers/router.js
new file mode 100644
index 0000000..d23e3db
--- /dev/null
+++ b/app/reducers/router.js
@@ -0,0 +1,35 @@
+'use strict'
+import HomeContent from '../components/HomeContent.jsx';
+
+export default function router(state, action) {
+ if (action.type !== 'ROUTER') {
+ if (state) {
+ return state;
+ } else {
+ return {
+ // default content
+ routerStack: [HomeContent],
+ canPop: false,
+ };
+ }
+ }
+ newState = Object.assign({}, state);
+ switch (action.state) {
+ case 'PUSH':
+ newState.routerStack.push(action.payload);
+ if (newState.routerStack.length > 1) {
+ newState.canPop = true;
+ }
+ return newState;
+ case 'POP':
+ if (newState.routerStack.length > 1) {
+ newState.routerStack.pop();
+ }
+ if (newState.routerStack.length > 1) {
+ newState.canPop = true;
+ }
+ return newState;
+ default:
+ return newState;
+ }
+}
diff --git a/app/reducers/search.js b/app/reducers/search.js
new file mode 100644
index 0000000..b2f3a1e
--- /dev/null
+++ b/app/reducers/search.js
@@ -0,0 +1,31 @@
+'use strict'
+export default function search(state, action) {
+ if (action.type !== 'SEARCH') {
+ if (state) {
+ return state;
+ } else {
+ return {
+ searchState: 'FINISH',
+ searchResponse: null,
+ errorInfo: null,
+ }
+ }
+ }
+ let newState = Object.assign({}, state);
+ newState.searchState = action.state;
+ switch (action.state) {
+ case 'START':
+ newState.searchInfo = action.payload;
+ return newState;
+ case 'CLOSE':
+ return newState;
+ case 'FINISH':
+ newState.searchResponse = action.payload;
+ return newState;
+ case 'ERROR':
+ newState.errorInfo = action.payload;
+ return newState;
+ default:
+ return newState;
+ }
+}
diff --git a/app/reducers/song.js b/app/reducers/song.js
new file mode 100644
index 0000000..d262a2b
--- /dev/null
+++ b/app/reducers/song.js
@@ -0,0 +1,193 @@
+'use strict'
+export default function song(state, action) {
+ rules = ['loop', ]
+ if (action.type !== 'SONG') {
+ if (state) {
+ return state;
+ } else {
+ return {
+ songlist: [],
+ playRule: 0, // loop one shuffle
+ rules: ['loop', 'one', 'shuffle'],
+ showplaylist: false,
+ };
+ }
+ }
+ newState = Object.assign({}, state);
+ switch (action.state) {
+ case 'CHANGE':
+ let index = isExist(action.payload, newState.songlist);
+ if (index) {
+ index--;
+ newState.currentSongIndex = index;
+ } else {
+ newState.songlist.push(action.payload);
+ newState.currentSongIndex = newState.songlist.length - 1;
+ }
+ return newState;
+ case 'SHOWPLAYLIST':
+ newState.showplaylist = true;
+ return newState;
+ case 'CLOSEPLAYLIST':
+ newState.showplaylist = false;
+ return newState;
+ case 'PLAYFROMLIST':
+ newState.currentSongIndex = action.payload;
+ // FIXME
+ if (newState.playRule == 2) {
+ let toShuffle = [];
+ for (let i = 0;i < newState.songlist.length;i++) {
+ if (i == 0) {
+ toShuffle[i] = newState.currentSongIndex;
+ } else if (i == newState.currentSongIndex) {
+ toShuffle[i] = 0;
+ } else {
+ toShuffle[i] = i;
+ }
+ }
+ newState.shuffleList = getShuffle(toShuffle, 1);
+ newState.shuffleIndex = 0;
+ }
+ return newState;
+ case 'ADD':
+ if (isExist(action.payload, state.songlist)) {
+ return state;
+ }
+ newState.songlist.push(action.payload);
+ // if shuffle
+ if (newState.playRule == 2) {
+ newState.shuffleList.push(newState.songlist.length - 1);
+ newState.shuffleList = getShuffle(
+ newState.shuffleList,
+ newState.shuffleIndex + 1
+ );
+ }
+ if (newState.songlist.length == 1) {
+ newState.currentSongIndex = 0;
+ }
+ return newState;
+ case 'ADDLIST':
+ if (action.payload.play) {
+ var playIndex = newState.songlist.length;
+ }
+ action.payload.songlist.map(song => {
+ if (isExist(song, newState.songlist)) {
+ return;
+ }
+ newState.songlist.push(song);
+ if (newState.playRule == 2) {
+ newState.shuffleList.push(newState.songlist.length - 1);
+ }
+ });
+ if (newState.playRule == 2) {
+ newState.shuffleList = getShuffle(
+ newState.shuffleList,
+ newState.shuffleIndex + 1
+ );
+ }
+ if (action.payload.play && newState.songlist.length > playIndex) {
+ newState.currentSongIndex = playIndex;
+ }
+ if (newState.songlist.length == 1) {
+ newState.currentSongIndex = 0;
+ }
+ return newState;
+ case 'CHANGERULE':
+ if (newState.playRule == 2) {
+ newState.playRule = 0;
+ } else {
+ newState.playRule++;
+ }
+
+ // if rule is shuffle
+ if (newState.playRule == 2) {
+ let toShuffle = [];
+ for (let i = 0;i < newState.songlist.length;i++) {
+ if (i == 0) {
+ toShuffle[i] = newState.currentSongIndex;
+ } else if (i == newState.currentSongIndex) {
+ toShuffle[i] = 0;
+ } else {
+ toShuffle[i] = i;
+ }
+ }
+ newState.shuffleList = getShuffle(toShuffle, 1);
+ newState.shuffleIndex = 0;
+ }
+
+ return newState;
+ case 'NEXT':
+ if (newState.songlist.length == 0) {
+ return newState;
+ }
+ if ((newState.playRule == 0) || (newState.playRule == 1)) {
+ if (newState.currentSongIndex === newState.songlist.length - 1){
+ newState.currentSongIndex = 0;
+ } else {
+ newState.currentSongIndex++;
+ }
+ return newState;
+ } else if (newState.playRule == 2) { // shuffle
+ if (newState.shuffleIndex === newState.shuffleList.length - 1){
+ newState.shuffleIndex = 0;
+ } else {
+ newState.shuffleIndex++;
+ }
+ newState.currentSongIndex = newState.shuffleList[newState.shuffleIndex];
+ return newState;
+ }
+ //TODO: shuffle
+ case 'PREVIOUS':
+ if (newState.songlist.length == 0) {
+ return newState;
+ }
+ if ((newState.playRule == 0) || (newState.playRule == 1)) {
+ if (newState.currentSongIndex === 0){
+ newState.currentSongIndex = newState.songlist.length - 1;
+ } else {
+ newState.currentSongIndex--;
+ }
+ return newState;
+ } else if (newState.playRule == 2) { // shuffle
+ if (newState.shuffleIndex === 0){
+ newState.shuffleIndex = newState.shuffleList.length - 1;
+ } else {
+ newState.shuffleIndex--;
+ }
+ newState.currentSongIndex = newState.shuffleList[newState.shuffleIndex];
+ return newState;
+ }
+ default:
+ return newState;
+ }
+}
+
+function getShuffle(lastshuffle, index) {
+ let toShuffle = lastshuffle.slice(index, lastshuffle.length);
+ lastshuffle = lastshuffle.slice(0, index);
+ doShuffle(toShuffle).map(value => {
+ lastshuffle.push(value);
+ });
+
+ return lastshuffle;
+}
+
+function doShuffle(list) {
+ for (let i = list.length;i > 0;i--) {
+ let j = Math.floor(Math.random() * i);
+ let x = list[i - 1];
+ list[i - 1] = list[j];
+ list[j] = x;
+ }
+
+ return list;
+}
+
+function isExist(newsong, list) {
+ for (let i = 0;i < list.length;i++) {
+ if (list[i].id == newsong.id) {
+ return i + 1;
+ }
+ }
+ return false;
+}
diff --git a/app/reducers/songlist.js b/app/reducers/songlist.js
new file mode 100644
index 0000000..713588f
--- /dev/null
+++ b/app/reducers/songlist.js
@@ -0,0 +1,28 @@
+'use strict'
+export default function songlist(state, action) {
+ if (action.type !== 'SONGLIST') {
+ if (state) {
+ return state;
+ } else {
+ return {
+ state: 'fetching',
+ };
+ }
+ }
+ newState = Object.assign({}, state);
+ switch (action.state) {
+ case 'FETCHING':
+ newState.state = 'fetching';
+ return newState;
+ case 'GET':
+ newState.content = action.payload;
+ newState.state = 'get';
+ return newState;
+ case 'ERROR':
+ newState.errorinfo = action.payload;
+ newState.state = 'error';
+ return newState;
+ default:
+ return newState;
+ }
+}
diff --git a/app/reducers/user.js b/app/reducers/user.js
new file mode 100644
index 0000000..db09fa2
--- /dev/null
+++ b/app/reducers/user.js
@@ -0,0 +1,44 @@
+'use strict'
+export default function user(state, action) {
+ if (action.type !== 'USER') {
+ if (state) {
+ return state;
+ } else {
+ return {
+ loginState: 'guest',
+ showForm: false,
+ };
+ }
+ }
+ newState = Object.assign({}, state);
+ switch (action.state) {
+ case 'LOGIN_STATE_LOGGING_IN':
+ newState.loginState = 'logging_in';
+ return newState;
+ case 'LOGIN_STATE_LOGGED_IN':
+ newState.loginState = 'logged_in';
+ newState.account = action.payload.account;
+ newState.profile = action.payload.profile;
+ return newState;
+ case 'LOGIN_STATE_LOGGED_FAILED':
+ newState.loginState = 'logged_failed';
+ newState.loginError = action.payload;
+ return newState;
+ case 'LOGINFORM':
+ newState.showForm = action.payload;
+ return newState;
+ case 'GUEST':
+ newState.loginState = 'guest';
+ removeCookie();
+ return newState;
+ default:
+ return newState;
+ }
+}
+
+function removeCookie() {
+ Electron.ipcRenderer.sendSync('removecookie', 'http://localhost:11015', 'MUSIC_U');
+ Electron.ipcRenderer.sendSync('removecookie', 'http://loaclhost:11015', 'NETEASE_WDA_UID');
+ Electron.ipcRenderer.sendSync('removecookie', 'http://localhost:11015', '__csrf');
+ Electron.ipcRenderer.sendSync('removecookie', 'http://localhost:11015', '__remember_me');
+}
diff --git a/app/reducers/usersong.js b/app/reducers/usersong.js
new file mode 100644
index 0000000..c8cd9ca
--- /dev/null
+++ b/app/reducers/usersong.js
@@ -0,0 +1,45 @@
+'use strict'
+export default function user(state, action) {
+ if (action.type !== 'USERSONG') {
+ if (state) {
+ return state;
+ } else {
+ return {
+ create: [],
+ collect: [],
+ state: 'nouser',
+ uid: null,
+ };
+ }
+ }
+ newState = Object.assign({}, state);
+ switch (action.state) {
+ case 'FETCHING':
+ newState.state = 'fetching';
+ newState.uid = action.payload;
+ return newState;
+ case 'GET':
+ [newState.create, newState.collect] = separatePlayList(newState.uid, action.payload);
+ newState.state = 'get';
+ return newState;
+ case 'ERROR':
+ newState.state = 'error';
+ newState.errorinfo = action.payload;
+ return newState;
+ default:
+ return newState;
+ }
+}
+
+function separatePlayList(id, list) {
+ let create = [], collect = [];
+ list.map(songlist => {
+ if (songlist.userId === id) {
+ create.push(songlist);
+ } else {
+ collect.push(songlist);
+ }
+ });
+
+ return [create, collect];
+}
diff --git a/app/server/crypto.js b/app/server/crypto.js
new file mode 100644
index 0000000..df9b6d5
--- /dev/null
+++ b/app/server/crypto.js
@@ -0,0 +1,65 @@
+// 参考 https://github.com/darknessomi/musicbox/wiki/
+'use strict'
+const crypto = require('crypto');
+const bigInt = require('big-integer');
+const modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
+const nonce = '0CoJUm6Qyw8W8jud'
+const pubKey = '010001'
+
+String.prototype.hexEncode = function(){
+ var hex, i;
+
+ var result = "";
+ for (i=0; i mp3url
+export function getSongUrl(song, callback) {
+ var id = song.id, br;
+ if (song.hMusic) {
+ br = song.hMusic.bitrate;
+ } else if (song.mMusic) {
+ br = song.mMusic.bitrate;
+ } else if (song.lMusic) {
+ br = song.lMusic.bitrate;
+ }
+ fetch('http://localhost:11015/music/url?id=' + id + '&br=' + br, {
+ credentials: 'include',
+ })
+ .then( res => {
+ return res.json();
+ }).then( json => {
+ callback(json.data[0]);
+ } )
+}
+
+// 搜索歌曲
+export function Search(keywords) {
+ return new Promise((resolve, reject) => {
+ fetch(
+ 'http://localhost:11015/search/?keywords=' + keywords + '&type=1&limit=40'
+ )
+ .then( res => {
+ return res.json();
+ }).then( json => {
+ resolve(json.result);
+ }).catch( e => {
+ reject(e);
+ });
+ })
+}
+
+// 登录
+export function Login(username, pw) {
+ return new Promise((resolve, reject) => {
+ 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;
+ const phoneReg = /^[0-9]{11}$/i
+ let fetchUrl= ''
+ if (phoneReg.test(username)) {
+ fetchUrl = 'http://localhost:11015/login/cellphone?phone=' + username + '&password=' + pw;
+ } else if (emailReg.test(username)) {
+ fetchUrl = 'http://localhost:11015/login?email=' + username + '&password=' + pw;
+ } else {
+ reject('用户名格式错误');
+ }
+
+ fetch(fetchUrl, {
+ credentials: 'include',
+ })
+ .then( res => {
+ return res.json();
+ }).then( json => {
+ if (json.code != 200) {
+ reject('Error:' + JSON.stringify(json));
+ } else {
+ console.log('resolve', json);
+ resolve(json);
+ }
+ }).catch( e => {
+ reject(e);
+ });
+ })
+}
+
+export function getPlayList(uid) {
+ return new Promise((resolve, reject) => {
+ fetch('http://localhost:11015/user/playlist?uid=' + uid, {
+ credentials: 'include',
+ })
+ .then( res => {
+ return res.json();
+ }).then( json => {
+ resolve(json);
+ }).catch( e => {
+ reject(e);
+ });
+ })
+}
+
+// 获取歌单详情
+export function SonglistDetail(id) {
+ return new Promise((resolve, reject) => {
+ fetch('http://localhost:11015/playlist/detail?id=' + id)
+ .then( res => {
+ return res.json();
+ }).then( json => {
+ resolve(json.playlist);
+ }).catch( e => {
+ reject(e);
+ });
+ })
+}
diff --git a/app/server/server.js b/app/server/server.js
new file mode 100644
index 0000000..372c375
--- /dev/null
+++ b/app/server/server.js
@@ -0,0 +1,291 @@
+var Encrypt = require('./crypto.js');
+var express = require('express');
+var http = require('http');
+var crypto = require('crypto');
+var tough = require('tough-cookie');
+var Cookie = tough.Cookie;
+
+var app = express();
+
+function createWebAPIRequest(host, path, method, data, cookie, callback) {
+ console.log('reqCookie', cookie);
+ var music_req = '';
+ var cryptoreq = Encrypt(data);
+ var http_client = http.request({
+ hostname: host,
+ method: method,
+ path: path,
+ headers: {
+ 'Accept': '*/*',
+ 'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4',
+ 'Connection': 'keep-alive',
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'Referer': 'http://music.163.com',
+ 'Host': 'music.163.com',
+ 'Cookie': cookie,
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36',
+
+ },
+ }, function(res) {
+ res.setEncoding('utf8');
+ if (res.statusCode == 500) {
+ console.log("500");
+ createWebAPIRequest(host, path, method, data, cookie, callback);
+ return;
+ } else {
+ console.log("200");
+ res.on('data', function (chunk) {
+ music_req += chunk;
+ });
+ res.on('end', function() {
+ if (music_req == '') {
+ console.log('empty');
+ createWebAPIRequest(host, path, method, data, cookie, callback);
+ return;
+ }
+ if (res.headers['set-cookie']) {
+ callback(music_req, res.headers['set-cookie']);
+ } else {
+ callback(music_req);
+ }
+ })
+ }
+ });
+ http_client.write('params=' + cryptoreq.params + '&encSecKey=' + cryptoreq.encSecKey);
+ http_client.end();
+}
+
+function createRequest(path, method, data, callback) {
+ var ne_req = '';
+ var http_client = http.request({
+ hostname: 'music.163.com',
+ method: method,
+ path: path,
+ headers: {
+ 'Referer': 'http://music.163.com',
+ 'Cookie' : 'appver=2.0.2',
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ }, function(res) {
+ res.setEncoding('utf8');
+ res.on('data', function (chunk) {
+ ne_req += chunk;
+ });
+ res.on('end', function() {
+ callback(ne_req);
+ })
+ });
+ console.log(data);
+ http_client.write(data);
+ http_client.end();
+}
+
+app.get('/music/url', function(request, response) {
+ var id = parseInt(request.query.id);
+ var br = parseInt(request.query.br);
+ var data = {
+ "ids": [id],
+ "br": br,
+ "csrf_token": ""
+ };
+ console.log(data);
+ var cookie = request.get('Cookie') ? request.get('Cookie') : '';
+
+ createWebAPIRequest(
+ 'music.163.com',
+ '/weapi/song/enhance/player/url',
+ 'POST',
+ data,
+ cookie,
+ function(music_req) {
+ response.setHeader("Content-Type", "application/json");
+ response.send(music_req);
+ }
+ )
+});
+
+app.get('/search', function(request, response) {
+ var keywords = request.query.keywords;
+ var type = request.query.type;
+ var limit = request.query.limit;
+ var data = 's=' + keywords + '&limit=' + limit + '&type=' + type + '&offset=0';
+ createRequest('/api/search/pc/', 'POST', data, function(res) {
+ response.setHeader("Content-Type", "application/json");
+ response.send(res);
+ });
+});
+
+app.get('/login/cellphone', function(request, response) {
+ var phone = request.query.phone;
+ var cookie = request.get('Cookie') ? request.get('Cookie') : '';
+ var md5sum = crypto.createHash('md5');
+ md5sum.update(request.query.password);
+ var data = {
+ 'phone': phone,
+ 'password': md5sum.digest('hex'),
+ 'rememberLogin': 'true'
+ };
+
+ console.log(data);
+
+ createWebAPIRequest(
+ 'music.163.com',
+ '/weapi/login/cellphone',
+ 'POST',
+ data,
+ cookie,
+ function(music_req, cookie) {
+ console.log(music_req);
+ response.set({
+ 'Set-Cookie': cookie,
+ });
+ response.send(music_req);
+ }
+ )
+});
+
+app.get('/login', function(request, response) {
+ var email = request.query.email;
+ var cookie = request.get('Cookie') ? request.get('Cookie') : '';
+ var md5sum = crypto.createHash('md5');
+ md5sum.update(request.query.password);
+ var data = {
+ 'username': email,
+ 'password': md5sum.digest('hex'),
+ 'rememberLogin': 'true'
+ };
+
+ console.log(data);
+
+ createWebAPIRequest(
+ 'music.163.com',
+ '/weapi/login',
+ 'POST',
+ data,
+ cookie,
+ function(music_req, cookie) {
+ console.log(music_req);
+ response.set({
+ 'Set-Cookie': cookie,
+ });
+ response.send(music_req);
+ }
+ )
+});
+
+app.get('/recommend/songs', function(request, response) {
+ var cookie = request.get('Cookie') ? request.get('Cookie') : '';
+ var data = {
+ "offset": 0,
+ "total": true,
+ "limit": 20,
+ "csrf_token": ""
+ };
+
+ console.log(data);
+
+ createWebAPIRequest(
+ 'music.163.com',
+ '/weapi/v1/discovery/recommend/songs',
+ 'POST',
+ data,
+ cookie,
+ function(music_req) {
+ console.log(music_req);
+ response.send(music_req);
+ }
+ )
+});
+
+app.get('/user/playlist', function(request, response) {
+ var cookie = request.get('Cookie') ? request.get('Cookie') : '';
+ var data = {
+ "offset": 0,
+ "uid": request.query.uid,
+ "limit": 1000,
+ "csrf_token": ""
+ };
+
+ console.log(data);
+
+ createWebAPIRequest(
+ 'music.163.com',
+ '/weapi/user/playlist',
+ 'POST',
+ data,
+ cookie,
+ function(music_req) {
+ console.log(music_req);
+ response.send(music_req);
+ }
+ )
+});
+
+app.get('/playlist/detail', function(request, response) {
+ var cookie = request.get('Cookie') ? request.get('Cookie') : '';
+ var detail, imgurl;
+ var data = {
+ "id": request.query.id,
+ "offset": 0,
+ "total": true,
+ "limit": 1000,
+ "n": 1000,
+ "csrf_token": ""
+ };
+
+ console.log(data);
+
+ createWebAPIRequest(
+ 'music.163.com',
+ '/weapi/v3/playlist/detail',
+ 'POST',
+ data,
+ cookie,
+ function(music_req) {
+ console.log(music_req);
+ detail = music_req;
+ mergeRes();
+ }
+ )
+
+ // FIXME:i dont know the api to get coverimgurl
+ // so i get it by parsing html
+ var http_client = http.get({
+ hostname: 'music.163.com',
+ path: '/playlist?id=' + request.query.id,
+ headers: {
+ 'Referer': 'http://music.163.com',
+ },
+ }, function(res) {
+ res.setEncoding('utf8');
+ var html = '';
+ res.on('data', function (chunk) {
+ html += chunk;
+ });
+ res.on('end', function() {
+ console.log('end', html);
+ var regImgCover = /\
+
+
+
+
+
+
+
+
+
+
diff --git a/main.js b/main.js
new file mode 100644
index 0000000..2bd02e7
--- /dev/null
+++ b/main.js
@@ -0,0 +1,97 @@
+'use strict';
+
+const electron = require('electron');
+const app = electron.app;
+const BrowserWindow = electron.BrowserWindow;
+const ipcMain = electron.ipcMain;
+const Tray = electron.Tray;
+const Menu = electron.Menu;
+
+const Childprocess = require('child_process');
+const path = require('path');
+const http = require('http');
+
+let server_process;
+
+let mainWindow;
+var appIcon = null;
+
+function createWindow () {
+ mainWindow = new BrowserWindow({
+ width: 1000,
+ height: 800,
+ webPreferences: {
+ nodeIntegration: 'iframe',
+ webSecurity: false,
+ },
+ title: 'CloudMusic',
+ frame: false,
+ icon: 'app/assets/icon.png',
+ });
+
+ mainWindow.loadURL('http://127.0.0.1:8080');
+ //mainWindow.loadURL('file://' + __dirname + '/index.html');
+
+ mainWindow.webContents.on('did-finish-load', function() {
+ var session = electron.session.fromPartition();
+ session.cookies.get({}, function(error, cookies) {
+ mainWindow.webContents.send('cookie', cookies);
+ });
+ });
+
+ //mainWindow.webContents.openDevTools();
+
+ mainWindow.on('closed', function() {
+ mainWindow = null;
+ });
+
+ server_process = Childprocess.spawn('node', ['server.js'], {
+ cwd: path.resolve(__dirname, './app/server/'),
+ stdio: ['ignore', process.stdout, process.stderr],
+ });
+
+ ipcMain.on('closeapp', function(e) {
+ mainWindow.close();
+ e.sender.send('closed');
+ });
+
+ ipcMain.on('minimize', function(e) {
+ mainWindow.minimize();
+ e.sender.send('minimize');
+ });
+
+ ipcMain.on('maximize', function(e) {
+ if (mainWindow.isMaximized()) {
+ mainWindow.unmaximize();
+ } else {
+ mainWindow.maximize();
+ }
+ e.sender.send('maximize');
+ });
+}
+
+app.on('ready', createWindow);
+
+app.on('window-all-closed', function () {
+ if (process.platform !== 'darwin') {
+ app.quit();
+ }
+});
+
+app.on('activate', function () {
+ if (mainWindow === null) {
+ createWindow();
+ }
+});
+
+process.on('exit', function() {
+ server_process.kill('SIGHUP');
+});
+
+ipcMain.on('removecookie', function(e, url, name) {
+ var session = electron.session.fromPartition();
+ session.cookies.remove(url, name, function() {
+ console.log('remove', url, name);
+ e.returnValue = 'OK';
+ });
+});
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..0d298dc
--- /dev/null
+++ b/package.json
@@ -0,0 +1,60 @@
+{
+ "name": "electron-cloud-music",
+ "version": "0.0.1",
+ "description": "",
+ "main": "main.js",
+ "scripts": {
+ "start": "babel-node server.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/disoul/electron-cloud-music.git"
+ },
+ "author": "disoul",
+ "license": "ISC",
+ "bugs": {
+ "url": "https://github.com/disoul/electron-cloud-music/issues"
+ },
+ "homepage": "https://github.com/disoul/electron-cloud-music#readme",
+ "devDependencies": {
+ "autoprefixer": "^6.3.6",
+ "babel-core": "^6.7.4",
+ "babel-loader": "^6.2.4",
+ "babel-preset-es2015-webpack": "^6.4.0",
+ "babel-preset-react": "^6.5.0",
+ "browserslist": "^1.3.1",
+ "css-loader": "^0.23.1",
+ "electron-packager": "^7.0.1",
+ "electron-prebuilt": "^0.37.7",
+ "eslint-loader": "^1.3.0",
+ "exports-loader": "^0.6.3",
+ "express": "^4.13.4",
+ "file-loader": "^0.8.5",
+ "imports-loader": "^0.6.5",
+ "less": "^2.6.1",
+ "less-loader": "^2.2.3",
+ "postcss-color-alpha": "^1.0.3",
+ "postcss-loader": "^0.8.2",
+ "precss": "^1.4.0",
+ "querystring": "^0.2.0",
+ "react": "^0.14.8",
+ "react-dom": "^0.14.8",
+ "react-hot-loader": "^1.3.0",
+ "react-redux": "^4.4.1",
+ "react-transform": "0.0.3",
+ "redux": "^3.3.1",
+ "redux-logger": "^2.6.1",
+ "redux-thunk": "^2.0.1",
+ "style-loader": "^0.13.1",
+ "svg-react-loader": "^0.3.3",
+ "tough-cookie": "^2.2.2",
+ "url-loader": "^0.5.7",
+ "webpack": "^2.1.0-beta.5",
+ "webpack-dev-middleware": "^1.6.1",
+ "webpack-hot-middleware": "^2.10.0"
+ },
+ "dependencies": {
+ "big-integer": "^1.6.15",
+ "whatwg-fetch": "^0.11.0"
+ }
+}
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000..8e135fa
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,42 @@
+var path = require('path');
+var webpack = require('webpack');
+var browserslist = require('browserslist');
+var precss = require('precss');
+var autoprefixer = require('autoprefixer');
+var postcsscoloralpha = require('postcss-color-alpha');
+
+module.exports = {
+ entry: path.join(__dirname, 'app/main.js'),
+ output: {
+ path: path.join(__dirname, 'dist'),
+ publicPath: '/dist/',
+ filename: 'main.js',
+ },
+ resolve: {
+ extensions: ['', '.js', '.jsx'],
+ },
+ module: {
+ preloaders: [
+ { test: /\.jsx?$/, loaders: ['eslint-loader']}
+ ],
+ loaders: [
+ { test: /\.json$/, loader: 'json' },
+ { test: /\.jsx?$/, exclude: /node_modules/, loader: "babel" },
+ { test: /\.css?$/, loader: "style-loader!css-loader!postcss-loader" },
+ { test: /\.svg?$/, loader: "babel!svg-react", exclude: [
+ path.resolve(__dirname, './app/assets/img'),
+ ]},
+ { test: /\.(png|jpg)?$/, loader: "url?name=[path]" },
+
+ ]
+ },
+ postcss: function() {
+ return [precss, autoprefixer({ browsers: browserslist('last 2 Chrome versions') }), postcsscoloralpha]
+ },
+ plugins: [
+ new webpack.HotModuleReplacementPlugin(),
+ new webpack.ProvidePlugin({
+ 'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch'
+ }),
+ ],
+};