├── .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 | [](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 | 
17 | 
18 | 
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 |
96 |
--------------------------------------------------------------------------------
/app/assets/icon/add.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/assets/icon/addall.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/icon/back.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/app/assets/icon/card.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/icon/close.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/assets/icon/loop.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/assets/icon/max.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/app/assets/icon/min.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/app/assets/icon/music.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/icon/next.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/assets/icon/one.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/assets/icon/pause.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/assets/icon/play.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/assets/icon/playlist.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/assets/icon/previous.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/assets/icon/remove.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/icon/removeall.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/icon/search.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/assets/icon/shuffle.svg:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/app/assets/icon/star.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/app/assets/icon/unmax.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/app/assets/icon/volume.svg:
--------------------------------------------------------------------------------
1 |
2 |
23 |
--------------------------------------------------------------------------------
/app/assets/icon/volume_max.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/assets/icon/volume_min.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/app/assets/icon/volume_mute.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/app/assets/img/up.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/app/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
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 |
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 |
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 |
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 | {index + 1} |
61 |
62 | this._addsong(e, song)}
65 | />
66 | |
67 | this._playsong(e, song)}
70 | >
71 | {this.getShortName(song.name, 30)}
72 | |
73 |
74 | {this.getShortName(song.artists[0].name, 15)}
75 | |
76 |
77 | {this.getShortName(song.album.name, 18)}
78 | |
79 |
80 | {this.secToTime(song.duration)}
81 | |
82 |
83 |
89 | |
90 |
91 | );
92 | })}
93 |
94 |
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 |
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 |
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 |
97 |
98 |
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 |
--------------------------------------------------------------------------------