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