Initial commit
This commit is contained in:
commit
5bb231e2e6
30
.eslintrc
Normal file
30
.eslintrc
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"extends": "airbnb",
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"commonjs": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-const-assign": "warn",
|
||||
"no-this-before-super": "warn",
|
||||
"no-undef": "warn",
|
||||
"no-unreachable": "warn",
|
||||
"no-unused-vars": "warn",
|
||||
"constructor-super": "warn",
|
||||
"valid-typeof": "warn",
|
||||
"semi": [2, "always"],
|
||||
"indent": [2,4],
|
||||
"no-path-concat": 0
|
||||
}
|
||||
}
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
dist
|
||||
node_modules
|
||||
.DS_Store
|
||||
.vscode
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Zijin Xiao
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
27
README.md
Normal file
27
README.md
Normal file
|
@ -0,0 +1,27 @@
|
|||
# private-music-react
|
||||
A front-end impl of [Private Cloud Music](https://github.com/BLumia/Private-Cloud-Music) by React and Antd.
|
||||
|
||||
## Install
|
||||
``` bash
|
||||
# Clone the repository once
|
||||
$ git clone https://github.com/BearKidsTeam/private-music-react
|
||||
|
||||
# Go into the repository (rename it as you wish)
|
||||
$ cd private-music-react
|
||||
|
||||
# Install the dependencies once
|
||||
$ npm install
|
||||
```
|
||||
|
||||
## Usage
|
||||
``` bash
|
||||
$ npm start
|
||||
```
|
||||
|
||||
## Build
|
||||
``` bash
|
||||
$ npm run build
|
||||
```
|
||||
The files will be in ```./dist```
|
||||
## License
|
||||
[MIT](http://opensource.org/licenses/MIT)
|
16
index.html
Normal file
16
index.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Demo</title>
|
||||
<link rel="stylesheet" href="index.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="common.js"></script>
|
||||
<script src="index.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
14
index.jsx
Normal file
14
index.jsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import Player from './player';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div id="app" style={{ height: '100%' }}>
|
||||
<Player />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
44
package.json
Normal file
44
package.json
Normal file
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"name": "private-music-react",
|
||||
"version": "1.0.0",
|
||||
"description": "A front-end impl of Private Cloud Music by React and Antd",
|
||||
"author": "Zijin Xiao <ZijinX@outlook.com>",
|
||||
"repository": "BearKidsTeam/private-music-react",
|
||||
"license": "MIT",
|
||||
"entry": {
|
||||
"index": "./index.jsx"
|
||||
},
|
||||
"dependencies": {
|
||||
"antd": "^2.7.1",
|
||||
"axios": "^0.15.3",
|
||||
"react": "^15.1.0",
|
||||
"react-dom": "^15.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"atool-build": "^0.9.0",
|
||||
"atool-test-mocha": "^0.1.4",
|
||||
"babel-eslint": "^7.0.0",
|
||||
"babel-plugin-import": "^1.0.1",
|
||||
"babel-plugin-transform-runtime": "^6.8.0",
|
||||
"babel-runtime": "^6.9.2",
|
||||
"dora": "0.4.x",
|
||||
"dora-plugin-webpack": "^0.8.1",
|
||||
"eslint": "^3.15.0",
|
||||
"eslint-config-airbnb": "^12.0.0",
|
||||
"eslint-plugin-import": "^2.2.0",
|
||||
"eslint-plugin-jsx-a11y": "^2.2.3",
|
||||
"eslint-plugin-react": "^6.9.0",
|
||||
"expect": "^1.20.1",
|
||||
"pre-commit": "1.x",
|
||||
"redbox-react": "^1.2.6"
|
||||
},
|
||||
"pre-commit": [
|
||||
"lint"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "atool-build",
|
||||
"lint": "eslint --ext .js,.jsx src/",
|
||||
"start": "dora --plugins webpack",
|
||||
"test": "atool-test-mocha ./**/__tests__/*-test.js"
|
||||
}
|
||||
}
|
247
player.jsx
Normal file
247
player.jsx
Normal file
|
@ -0,0 +1,247 @@
|
|||
import React from "react";
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Row,Col,Button,Checkbox,Layout, Menu, Icon, Table, Slider, Modal, Input } from 'antd';
|
||||
import axios from 'axios';
|
||||
|
||||
const { Header, Content, Footer, Sider } = Layout;
|
||||
const SubMenu = Menu.SubMenu;
|
||||
const ButtonGroup = Button.Group;
|
||||
|
||||
const columns = [{
|
||||
title: 'File Name',
|
||||
dataIndex: 'fileName',
|
||||
key: 'fileName',
|
||||
render: text => <a href="#">{text}</a>,
|
||||
}];
|
||||
|
||||
function formatTime(t) {
|
||||
const m = Math.floor(t / 60);
|
||||
const s = Math.round(t - Math.floor(t / 60) * 60);
|
||||
if (s < 10) {
|
||||
return m + ":0" + s;
|
||||
}
|
||||
else if (s === 60) {
|
||||
return (m + 1) + ":00";
|
||||
}
|
||||
else {
|
||||
return m + ":" + s;
|
||||
}
|
||||
}
|
||||
|
||||
class Player extends React.Component {
|
||||
audio = null;
|
||||
currentFolder = '';
|
||||
pagination = {
|
||||
total: 0,
|
||||
showSizeChanger: true
|
||||
};
|
||||
state = {
|
||||
playing:'',
|
||||
loop: false,
|
||||
order: false,
|
||||
playIcon: 'caret-right',
|
||||
percent: 0,
|
||||
curTime: 0,
|
||||
totTime: 0,
|
||||
data: [],
|
||||
fdlist: [],
|
||||
collapsed: false,
|
||||
source: 'http://direct.blumia.cn/hidden',
|
||||
newSource: 'http://direct.blumia.cn/hidden',
|
||||
settingsVisible: false
|
||||
};
|
||||
onCollapse = (collapsed) => {
|
||||
this.setState({ collapsed });
|
||||
};
|
||||
init = () => {
|
||||
this.audio = document.getElementsByTagName('audio')[0];
|
||||
this.fetchPlaylist();
|
||||
this.audio.ontimeupdate = () => {
|
||||
this.setState({curTime: this.audio.currentTime, totTime: this.audio.duration, percent: this.audio.currentTime * 1.0 / this.audio.duration * 100.0});
|
||||
};
|
||||
this.audio.onpause = () => {
|
||||
this.setState({playIcon: 'caret-right'});
|
||||
}
|
||||
this.audio.onplay = () => {
|
||||
this.setState({playIcon: 'pause'});
|
||||
}
|
||||
};
|
||||
fetchPlaylist = () => {
|
||||
axios({method: 'POST', url: this.state.source + '/api.php',data:'do=getplaylist&folder=/', headers:{'Content-Type':'application/x-www-form-urlencoded'}})
|
||||
.then((response) => {
|
||||
this.setState({fdlist: response.data.result.data.subFolderList.map(x => decodeURIComponent(x))});
|
||||
}).catch(() => {});
|
||||
};
|
||||
fetchMusic = (folder) => {
|
||||
axios({method: 'POST', url: this.state.source + '/api.php',data:'do=getplaylist&folder=' + folder, headers:{'Content-Type':'application/x-www-form-urlencoded'}})
|
||||
.then((response) => {
|
||||
this.pagination = {
|
||||
total: response.data.result.data.musicList.length,
|
||||
showSizeChanger: true
|
||||
}
|
||||
this.setState({data: response.data.result.data.musicList.map(x=> { x.fileName = decodeURIComponent(x.fileName); return x})});
|
||||
}).catch(() => {});
|
||||
|
||||
};
|
||||
onMenuClick = ({item, key, keyPath}) => {
|
||||
if (key === 'settings') {
|
||||
this.setState({settingsVisible : true, newRandomKey: Math.random()});
|
||||
return;
|
||||
}
|
||||
if (key !== 'settings' && this.state.collapsed) {
|
||||
this.setState({collapsed:false});
|
||||
}
|
||||
this.currentFolder = key;
|
||||
this.fetchMusic(key);
|
||||
};
|
||||
componentDidMount() {
|
||||
this.init();
|
||||
};
|
||||
playAtIndex = (i) => {
|
||||
const filename = this.state.data[i].fileName;
|
||||
this.audio.pause();
|
||||
this.audio.src = this.state.source + this.currentFolder + '/' + filename;
|
||||
this.audio.load();
|
||||
this.audio.play();
|
||||
this.setState({playing: filename});
|
||||
};
|
||||
onRowClick = (record,index) => {
|
||||
this.audio.pause();
|
||||
this.audio.src = this.state.source + this.currentFolder + '/' + record.fileName;
|
||||
this.audio.load();
|
||||
this.audio.play();
|
||||
this.setState({playing: record.fileName});
|
||||
};
|
||||
onPlayClick = () => {
|
||||
if(this.state.playIcon === 'pause') {
|
||||
this.audio.pause();
|
||||
} else {
|
||||
this.audio.play();
|
||||
}
|
||||
};
|
||||
onPrevClick = () => {
|
||||
const currentIndex = this.state.data.findIndex(x => x.fileName === this.state.playing);
|
||||
if (currentIndex === -1) {
|
||||
this.playAtIndex(0);
|
||||
} else if (currentIndex === 0) {
|
||||
this.playAtIndex(this.state.data.length - 1);
|
||||
} else {
|
||||
this.playAtIndex(Number(currentIndex) - 1);
|
||||
}
|
||||
};
|
||||
onNextClick = () => {
|
||||
const currentIndex = this.state.data.findIndex(x => x.fileName === this.state.playing);
|
||||
if (currentIndex === -1) {
|
||||
this.playAtIndex(0);
|
||||
} else if (currentIndex === (this.state.data.length - 1)) {
|
||||
this.playAtIndex(0);
|
||||
} else {
|
||||
this.playAtIndex(Number(currentIndex) + 1);
|
||||
}
|
||||
};
|
||||
onLoopChange = (value) => {
|
||||
if (value.target.checked) {
|
||||
this.audio.loop = true;
|
||||
} else {
|
||||
this.audio.loop = false;
|
||||
}
|
||||
};
|
||||
onOrderChange = (value) => {
|
||||
if (value.target.checked) {
|
||||
this.audio.onended = () => {
|
||||
if (this.audio.loop === 0) {
|
||||
this.onNextClick();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
this.audio.onended = undefined;
|
||||
}
|
||||
};
|
||||
onSettingsOk = () => {
|
||||
this.setState({source: this.state.newSource,settingsVisible:false});
|
||||
this.init();
|
||||
};
|
||||
onSettingsCancel = () => {
|
||||
this.setState({settingsVisible: false,newSource: this.state.source});
|
||||
};
|
||||
onChange = (event) => {
|
||||
this.setState({newSource: event.target.value});
|
||||
};
|
||||
onProgressChange = (value) => {
|
||||
this.audio.currentTime = value;
|
||||
this.setState({percent: value * 1.0 / this.state.totTime,curTime: value});
|
||||
};
|
||||
onAfterChange = (value) => {
|
||||
this.audio.currentTime = value;
|
||||
this.setState({percent: value * 1.0 / this.state.totTime,curTime: value});
|
||||
};
|
||||
render() {
|
||||
return (
|
||||
<div style={{ height: '100%' }}>
|
||||
<audio></audio>
|
||||
<Layout style={{ height: '100%' }}>
|
||||
<Layout style={{ height: '100%' }}>
|
||||
<Sider collapsible collapsed={this.state.collapsed} onCollapse={this.onCollapse} style={{ height: '100%' }}>
|
||||
<Menu theme="dark" mode="inline" openKeys={['sub1']} selectedKeys={this.state.collapsed ? [] : [this.currentFolder]} style={{ height: '100%' }} onClick={this.onMenuClick}>
|
||||
{ !this.state.collapsed &&
|
||||
<SubMenu key="sub1" title={<span><Icon type="folder" /><span>Folder List</span></span>}>
|
||||
{this.state.fdlist.map(x => <Menu.Item key={x}>{x}</Menu.Item>)}
|
||||
</SubMenu>
|
||||
}
|
||||
{ this.state.collapsed &&
|
||||
<Menu.Item key="1">
|
||||
<Icon type="folder"/>
|
||||
</Menu.Item>
|
||||
}
|
||||
<Menu.Item key="settings">
|
||||
<Icon type="setting" /> { !this.state.collapsed && 'Settings' }
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</Sider>
|
||||
<Layout style={{ background: '#fff' }}>
|
||||
<Content style={{ margin: '0px' }}>
|
||||
<Table dataSource={this.state.data} columns={columns} style={{ background: '#fff' }} showHeader={false} pagination={this.pagination} onRowClick = {this.onRowClick}/>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
<Footer style={{ textAlign: 'center', background: '#494949', paddingLeft:0, paddingRight:0 }}>
|
||||
<Layout style={{background: '#494949'}}>
|
||||
<Sider style={{background: '#494949', color:'#fff'}}>
|
||||
<Row type="flex" justify="space-around" align="middle" style={{height: '100%'}}>
|
||||
<Col span={24}>
|
||||
{this.state.playing}
|
||||
</Col>
|
||||
</Row>
|
||||
</Sider>
|
||||
<Content style={{paddingLeft:0,paddingRight:0}}>
|
||||
<Row type="flex" justify="space-around" align="middle">
|
||||
<Col style={{ width:30, color:'#fff' }}> {formatTime(this.state.curTime)} </Col>
|
||||
<Col style={{ width:'calc(100% - 340px)', padding:5 }}>
|
||||
<Slider value={this.state.curTime} min={0} max={this.state.totTime} step={1} onChange={this.onProgressChange} tipFormatter={formatTime} style={{ borderTop:'4px solid #494949', borderBottom:'4px solid #494949' }} />
|
||||
</Col>
|
||||
<Col style={{width:30, color:'#fff'}}> {formatTime(this.state.totTime)} </Col>
|
||||
<Col style={{overflowY:'hidden', width:280, display:'block'}}>
|
||||
<ButtonGroup>
|
||||
<Button type="primary" size='large' icon="step-backward" onClick={this.onPrevClick}/>
|
||||
<Button type="primary" size='large' icon={this.state.playIcon} onClick={this.onPlayClick} />
|
||||
<Button type="primary" size='large' icon="step-forward" onClick={this.onNextClick}/>
|
||||
</ButtonGroup>
|
||||
|
||||
<Checkbox style={{ color:'#fff' }} onChange={this.onLoopChange}>Loop</Checkbox>
|
||||
<Checkbox style={{ color:'#fff' }} onChange={this.onOrderChange}>Order</Checkbox>
|
||||
</Col>
|
||||
</Row>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Footer>
|
||||
</Layout>
|
||||
<Modal title="Settings" key={this.state.newRandomKey} visible={this.state.settingsVisible}
|
||||
onOk={this.onSettingsOk} onCancel={this.onSettingsCancel}>
|
||||
<Input addonBefore={'PCM Source'} defaultValue={this.state.source} value={this.state.newSource} onChange={this.onChange} />
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Player;
|
12
webpack.config.js
Normal file
12
webpack.config.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
// Learn more on how to config.
|
||||
// - https://github.com/ant-tool/atool-build#配置扩展
|
||||
|
||||
module.exports = function (webpackConfig) {
|
||||
webpackConfig.babel.plugins.push('transform-runtime');
|
||||
webpackConfig.babel.plugins.push(['import', {
|
||||
libraryName: 'antd',
|
||||
style: 'css',
|
||||
}]);
|
||||
|
||||
return webpackConfig;
|
||||
};
|
Loading…
Reference in New Issue
Block a user