From a802df64a8f6d16dd7843c25338300363024484e Mon Sep 17 00:00:00 2001 From: caijin Date: Sat, 30 Nov 2019 23:05:28 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=E6=89=93=E5=8C=85=E4=BA=86=20web?= =?UTF-8?q?=20=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- hereWebServer.js | 13 + srcWeb/App.js | 131 +++++ srcWeb/App.scss | 70 +++ srcWeb/api/index.js | 67 +++ srcWeb/api/search.js | 12 + srcWeb/base/Dialog/index.js | 25 + srcWeb/base/Dialog/style.scss | 41 ++ srcWeb/base/Loading/index.js | 16 + srcWeb/base/Loading/style.scss | 31 ++ srcWeb/base/Message/container.js | 38 ++ srcWeb/base/Message/index.js | 19 + srcWeb/base/Message/message.js | 24 + srcWeb/base/Message/style.scss | 16 + srcWeb/base/MusicDetail/index.js | 196 +++++++ srcWeb/base/MusicDetail/style.scss | 127 +++++ srcWeb/base/PlayList/index.js | 123 +++++ srcWeb/base/PlayList/style.scss | 112 ++++ srcWeb/base/PlayTime/index.js | 43 ++ srcWeb/base/PlayTime/style.scss | 10 + srcWeb/base/ProgressBar/index.js | 132 +++++ srcWeb/base/ProgressBar/style.scss | 35 ++ srcWeb/base/RenderSingers/index.js | 51 ++ srcWeb/base/ShowList/index.js | 140 +++++ srcWeb/base/ShowList/style.scss | 108 ++++ srcWeb/common/js/config.js | 9 + srcWeb/common/js/utl.js | 57 ++ srcWeb/common/scss/base.scss | 3 + srcWeb/common/scss/iconfont.scss | 64 +++ srcWeb/common/scss/normalize.scss | 354 +++++++++++++ srcWeb/common/scss/variable.scss | 21 + srcWeb/components/Header/index.js | 92 ++++ srcWeb/components/Header/style.scss | 48 ++ srcWeb/components/MusicList/index.js | 174 ++++++ srcWeb/components/MusicList/style.scss | 132 +++++ srcWeb/components/Player/index.js | 499 ++++++++++++++++++ srcWeb/components/Player/style.scss | 199 +++++++ srcWeb/components/SingerInfo/index.js | 204 +++++++ srcWeb/components/SingerInfo/style.scss | 153 ++++++ srcWeb/data/index.js | 8 + srcWeb/index.js | 13 + srcWeb/pages/About/index.js | 179 +++++++ srcWeb/pages/About/style.scss | 81 +++ srcWeb/pages/Collect/index.js | 244 +++++++++ srcWeb/pages/Collect/style.scss | 219 ++++++++ srcWeb/pages/Rank/index.js | 87 +++ srcWeb/pages/Rank/style.scss | 65 +++ srcWeb/pages/Recommend/index.js | 139 +++++ srcWeb/pages/Recommend/style.scss | 105 ++++ srcWeb/pages/Search/index.js | 445 ++++++++++++++++ srcWeb/pages/Search/style.scss | 266 ++++++++++ srcWeb/renderer/components/MyTitle/index.js | 25 + srcWeb/renderer/components/MyTitle/style.scss | 13 + srcWeb/renderer/components/TitleBtn/index.js | 31 ++ .../renderer/components/TitleBtn/style.scss | 8 + srcWeb/store/actionCreator.js | 451 ++++++++++++++++ srcWeb/store/actionTypes.js | 32 ++ srcWeb/store/index.js | 9 + srcWeb/store/reducer.js | 194 +++++++ 59 files changed, 6204 insertions(+), 1 deletion(-) create mode 100644 hereWebServer.js create mode 100644 srcWeb/App.js create mode 100644 srcWeb/App.scss create mode 100644 srcWeb/api/index.js create mode 100644 srcWeb/api/search.js create mode 100644 srcWeb/base/Dialog/index.js create mode 100644 srcWeb/base/Dialog/style.scss create mode 100644 srcWeb/base/Loading/index.js create mode 100644 srcWeb/base/Loading/style.scss create mode 100644 srcWeb/base/Message/container.js create mode 100644 srcWeb/base/Message/index.js create mode 100644 srcWeb/base/Message/message.js create mode 100644 srcWeb/base/Message/style.scss create mode 100644 srcWeb/base/MusicDetail/index.js create mode 100644 srcWeb/base/MusicDetail/style.scss create mode 100644 srcWeb/base/PlayList/index.js create mode 100644 srcWeb/base/PlayList/style.scss create mode 100644 srcWeb/base/PlayTime/index.js create mode 100644 srcWeb/base/PlayTime/style.scss create mode 100644 srcWeb/base/ProgressBar/index.js create mode 100644 srcWeb/base/ProgressBar/style.scss create mode 100644 srcWeb/base/RenderSingers/index.js create mode 100644 srcWeb/base/ShowList/index.js create mode 100644 srcWeb/base/ShowList/style.scss create mode 100644 srcWeb/common/js/config.js create mode 100644 srcWeb/common/js/utl.js create mode 100644 srcWeb/common/scss/base.scss create mode 100644 srcWeb/common/scss/iconfont.scss create mode 100644 srcWeb/common/scss/normalize.scss create mode 100644 srcWeb/common/scss/variable.scss create mode 100644 srcWeb/components/Header/index.js create mode 100644 srcWeb/components/Header/style.scss create mode 100644 srcWeb/components/MusicList/index.js create mode 100644 srcWeb/components/MusicList/style.scss create mode 100644 srcWeb/components/Player/index.js create mode 100644 srcWeb/components/Player/style.scss create mode 100644 srcWeb/components/SingerInfo/index.js create mode 100644 srcWeb/components/SingerInfo/style.scss create mode 100644 srcWeb/data/index.js create mode 100644 srcWeb/index.js create mode 100644 srcWeb/pages/About/index.js create mode 100644 srcWeb/pages/About/style.scss create mode 100644 srcWeb/pages/Collect/index.js create mode 100644 srcWeb/pages/Collect/style.scss create mode 100644 srcWeb/pages/Rank/index.js create mode 100644 srcWeb/pages/Rank/style.scss create mode 100644 srcWeb/pages/Recommend/index.js create mode 100644 srcWeb/pages/Recommend/style.scss create mode 100644 srcWeb/pages/Search/index.js create mode 100644 srcWeb/pages/Search/style.scss create mode 100644 srcWeb/renderer/components/MyTitle/index.js create mode 100644 srcWeb/renderer/components/MyTitle/style.scss create mode 100644 srcWeb/renderer/components/TitleBtn/index.js create mode 100644 srcWeb/renderer/components/TitleBtn/style.scss create mode 100644 srcWeb/store/actionCreator.js create mode 100644 srcWeb/store/actionTypes.js create mode 100644 srcWeb/store/index.js create mode 100644 srcWeb/store/reducer.js diff --git a/README.md b/README.md index 1fe90a0..32da728 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ [Download](https://github.com/caijinyc/Here/releases/download/v0.1.1/Here-0.1.1.dmg) the .dmg file. -因为之后一段时候需要复习春招,所以暂时只打包了 Mac 版本,其他平台之后会支持的,请给我点时间 😣。 +如果不方便安装的话项目也打包了 Web 版本,推荐使用 Chrome 打开:[Here Music Web](http://120.79.162.149:3004/) ## 预览 diff --git a/hereWebServer.js b/hereWebServer.js new file mode 100644 index 0000000..0e69a5d --- /dev/null +++ b/hereWebServer.js @@ -0,0 +1,13 @@ +const express = require('express'); +const app = express(); +app.use(express.static('./build')); + +const port = 3004; + +app.listen(port, function (err) { + if (err) { + console.log(err); + return; + } + console.log('Listening at http://localhost:' + port + '\n'); +}); diff --git a/srcWeb/App.js b/srcWeb/App.js new file mode 100644 index 0000000..befd48b --- /dev/null +++ b/srcWeb/App.js @@ -0,0 +1,131 @@ +import React, { Component } from 'react'; +import { BrowserRouter as Router, Route, Redirect } from 'react-router-dom'; +import { connect } from 'react-redux'; +import { + getChangeCollectorAction, + getLoadCacheAction +} from './store/actionCreator'; +import $db from './data'; + +import Recommend from './pages/Recommend'; +import Search from './pages/Search'; +import Collect from './pages/Collect'; +import Rank from './pages/Rank'; +// import About from './pages/About'; + +import Header from './components/Header'; +import Player from './components/Player'; +import MusicList from './components/MusicList'; +import SingerInfo from './components/SingerInfo'; + +import Loading from './base/Loading'; + +import MyTitle from './renderer/components/MyTitle'; + +import './App.scss'; + +class App extends Component { + constructor (props) { + super(props); + this.state = { + redirect: true + }; + } + + componentWillMount () { + // 初始化收藏夹 + $db.find({ name: 'collector' }, (err, res) => { + if (res.length === 0) { + $db.insert( + { + name: 'collector', + foundList: [ + { + name: '我喜欢的音乐', + tracks: [] + } + ], + collectList: [] + }, + (err, res) => { + this.props.handleChangeCollector(res[0]); + } + ); + } else { + this.props.handleChangeCollector(res[0]); + } + }); + // 初始化使用信息 + $db.find({ name: 'cache'}, (err, res) => { + if (res.length === 0) { + $db.insert( + { + name: 'cache', + cacheValue: { + playList: [], + currentIndex: -1, + volume: 0.35 + } + } + ); + } else { + this.props.handleLoadCache(res[0].cacheValue); + } + }); + } + + componentDidMount () { + this.setState(() => ({ + redirect: false + })); + } + + render () { + return ( + +
+
+ + + +
+ {/* exact 路径完全相等的时候才显示路由内的内容 */} + + + + + {/**/} + { this.state.redirect ? : null} + + {this.props.showLoading ? ( +
+ +
+ ) : null} +
+ + ); + } +} + +const mapStateToProps = (state) => { + return { + showLoading: state.showLoading + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + handleChangeCollector (value) { + dispatch(getChangeCollectorAction(value)); + }, + handleLoadCache (value) { + dispatch(getLoadCacheAction(value)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(App); diff --git a/srcWeb/App.scss b/srcWeb/App.scss new file mode 100644 index 0000000..0a12122 --- /dev/null +++ b/srcWeb/App.scss @@ -0,0 +1,70 @@ +@import './common/scss//iconfont.scss'; +@import './common/scss/normalize.scss'; +@import './common/scss/variable.scss'; + +html { + width: 100vw; + height: 100vh; + overflow: hidden; + min-width: 900px; + font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", STHeiti, "Microsoft YaHei", "Microsoft JhengHei", "Source Han Sans SC", "Noto Sans CJK SC", "Source Han Sans CN", "Noto Sans SC", "Source Han Sans TC", "Noto Sans CJK TC", "WenQuanYi Micro Hei", SimSun, sans-serif; +} + +body { + min-width: 900px; + color: $color-text; + + .app-background { + position: fixed; + top: -20%; + left: -20%; + width: 130%; + height: 130%; + background-image: linear-gradient(to right, #080e13 0%, #021524 100%); + // filter: blur(20px); + z-index: -1; + } + + // 定义滚动条背景色 + // ::-webkit-scrollbar-track-piece { + // // background-color: $color-background; + // } + + /* 定义滚动条高宽及背景 + 高宽分别对应横竖滚动条的尺寸 */ + ::-webkit-scrollbar { + width: 8px; + } + + /* 定义滑块 + 内阴影+圆角 */ + ::-webkit-scrollbar-thumb { + border-radius: 4px; + background-color: rgba(37, 37, 37, 0.753); + } +} + +.play-list-container { + position: fixed; + top: 0; + right: 0; + height: calc(100vh - 80px); + transition: all 0.3s; + transform: translate3d(0, 0, 0); + z-index: 99; +} + +.hide-play-list { + right: -300px; +} + +.app-loading-container { + position: fixed; + top: 45%; + left: 50%; + transform: translateX(-50%); + width: 200px; + height: 100px; + background: rgba(0, 0, 0, 0.822); + border-radius: 10px; +} \ No newline at end of file diff --git a/srcWeb/api/index.js b/srcWeb/api/index.js new file mode 100644 index 0000000..c68ed1c --- /dev/null +++ b/srcWeb/api/index.js @@ -0,0 +1,67 @@ +import axios from 'axios'; +import { HOST } from '../common/js/config'; + +// 获取推荐歌单 +export function getRecommendList (updateTime = null) { + let url = ''; + if (updateTime) { + url = HOST + `/top/playlist/highquality?before=${updateTime}&limit=30`; + } else { + url = HOST + '/top/playlist/highquality?limit=30'; + } + return axios.get(url); +} + +// 获取歌单详情 +export function getMusicListDetail (id) { + const url = HOST + `/playlist/detail?id=${id}`; + return axios.get(url); +} + +// 获取音乐播放地址 +export function getMusicUrl (id) { + const url = HOST + `/song/url?id=${id}`; + return axios.get(url); +} + +// 获取音乐详情(歌曲没有图片的时候要用) +export function getMusicDetail (id) { + const url = HOST + `/song/detail?ids=${id}`; + return axios.get(url); +} + +// 获取歌曲歌词 +export function getMusicLyric (id) { + const url = HOST + `/lyric?id=${id}`; + return axios.get(url); +} + +// 获取歌手单曲 +// https://binaryify.github.io/NeteaseCloudMusicApi/#/?id=%E8%8E%B7%E5%8F%96%E6%AD%8C%E6%89%8B%E5%8D%95%E6%9B%B2 +export function getSingerInfo (id) { + const url = HOST + `/artists?id=${id}`; + return axios.get(url); +} + +// 获取歌手专辑 +// https://binaryify.github.io/NeteaseCloudMusicApi/#/?id=%E8%8E%B7%E5%8F%96%E6%AD%8C%E6%89%8B%E4%B8%93%E8%BE%91 +export function getSingerAlbums (id) { + const url = HOST + `/artist/album?id=${id}`; + return axios.get(url); +} + +// 获取专辑详情 +// https://binaryify.github.io/NeteaseCloudMusicApi/#/?id=%E8%8E%B7%E5%8F%96%E4%B8%93%E8%BE%91%E5%86%85%E5%AE%B9 +export function getAlbumInfo (id) { + const url = HOST + `/album?id=${id}`; + return axios.get(url); +} + +/** + * 获取排行榜所有榜单 + * https://binaryify.github.io/NeteaseCloudMusicApi/#/?id=%E6%89%80%E6%9C%89%E6%A6%9C%E5%8D%95 + */ +export function getAllRank () { + const url = HOST + '/toplist'; + return axios.get(url); +} diff --git a/srcWeb/api/search.js b/srcWeb/api/search.js new file mode 100644 index 0000000..dc893ff --- /dev/null +++ b/srcWeb/api/search.js @@ -0,0 +1,12 @@ +import axios from 'axios'; +import {HOST} from '../common/js/config'; + +export const getHotSearch = () => { + const url = HOST + '/search/hot'; + return axios.get(url); +}; + +export const getSearchResult = (searchName, type) => { + const url = HOST + `/search?keywords=${searchName}&type=${type}&limit=80`; + return axios.get(url); +}; diff --git a/srcWeb/base/Dialog/index.js b/srcWeb/base/Dialog/index.js new file mode 100644 index 0000000..78aee4b --- /dev/null +++ b/srcWeb/base/Dialog/index.js @@ -0,0 +1,25 @@ +import React, { Component } from 'react'; +import './style.scss'; + +export default class Dialog extends Component { + render () { + return ( +
+
+

{this.props.text}

+ + +
+
+ ); + } +} + +Dialog.defaultProps = { + trueBtnText: '确定', + falseBtnText: '取消' +}; diff --git a/srcWeb/base/Dialog/style.scss b/srcWeb/base/Dialog/style.scss new file mode 100644 index 0000000..a3c0cb8 --- /dev/null +++ b/srcWeb/base/Dialog/style.scss @@ -0,0 +1,41 @@ +@import '../../common/scss/variable.scss'; + +.base-dialog { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 9999; + background: rgba(255, 255, 255, 0); + + .container { + position: absolute; + top: 40%; + left: 50%; + padding: 30px 50px; + text-align: center; + font-size: $font-size-xl; + background: $color-dark-opacity; + border-radius: 10px; + + p { + padding-bottom: 15px; + } + + button { + padding: 5px 10px 5px 10px; + background: none; + border: none; + color: $color-text; + font-size: $font-size-x; + cursor: pointer; + transition: all 0.3s; + outline: none; + + &:hover { + color: $color-theme; + } + } + } +} \ No newline at end of file diff --git a/srcWeb/base/Loading/index.js b/srcWeb/base/Loading/index.js new file mode 100644 index 0000000..21f6b25 --- /dev/null +++ b/srcWeb/base/Loading/index.js @@ -0,0 +1,16 @@ +import React, { Component } from 'react'; +import './style.scss'; + +export default class Loading extends Component { + render () { + return ( +
+
+
+
+
+
+
+ ); + } +} \ No newline at end of file diff --git a/srcWeb/base/Loading/style.scss b/srcWeb/base/Loading/style.scss new file mode 100644 index 0000000..a323022 --- /dev/null +++ b/srcWeb/base/Loading/style.scss @@ -0,0 +1,31 @@ +@import '../../common/scss/variable.scss'; + +.base-loding { + @keyframes bouncing-loader { + to { + opacity: 0.1; + transform: translate3d(0, -1rem, 0); + } + } + .bouncing-loader { + display: flex; + justify-content: center; + } + + .bouncing-loader > div { + width: 1rem; + height: 1rem; + margin: 3rem 0.2rem; + background: $color-theme; + border-radius: 50%; + animation: bouncing-loader 0.6s infinite alternate; + } + + .bouncing-loader > div:nth-child(2) { + animation-delay: 0.2s; + } + + .bouncing-loader > div:nth-child(3) { + animation-delay: 0.4s; + } +} \ No newline at end of file diff --git a/srcWeb/base/Message/container.js b/srcWeb/base/Message/container.js new file mode 100644 index 0000000..d56db38 --- /dev/null +++ b/srcWeb/base/Message/container.js @@ -0,0 +1,38 @@ +import React, { Component, Fragment } from 'react'; +import Message from './message'; + +export default class Container extends Component { + constructor (props) { + super(props); + + this.state = { + message: null + }; + } + + addMessage = (content) => { + this.setState(() => ({ + message: content + })); + }; + + removeMessage = () => { + + this.setState(() => ({ + message: null + })); + }; + + render () { + return ( + + {this.state.message ? ( + + ) : null} + + ); + } +} diff --git a/srcWeb/base/Message/index.js b/srcWeb/base/Message/index.js new file mode 100644 index 0000000..0c951fa --- /dev/null +++ b/srcWeb/base/Message/index.js @@ -0,0 +1,19 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import Container from './container'; + +const div = document.createElement('div'); +document.body.appendChild(div); +div.setAttribute('class', 'message-container'); + +function create (type) { + return (content) => { + if (type === 'info') { + ReactDom.render(, div).addMessage(content); + } + }; +} + +export default { + info: create('info') +}; diff --git a/srcWeb/base/Message/message.js b/srcWeb/base/Message/message.js new file mode 100644 index 0000000..45769e0 --- /dev/null +++ b/srcWeb/base/Message/message.js @@ -0,0 +1,24 @@ +import React, { Component } from 'react'; +import './style.scss'; + +export default class Message extends Component { + componentDidMount () { + const { duration } = this.props; + setTimeout(() => { + this.props.removeMessage(); + }, duration); + } + + render () { + return ( +
+ {this.props.content} +
+ ); + } +} + +Message.defaultProps = { + duration: 1500 +}; + diff --git a/srcWeb/base/Message/style.scss b/srcWeb/base/Message/style.scss new file mode 100644 index 0000000..ecd6d6a --- /dev/null +++ b/srcWeb/base/Message/style.scss @@ -0,0 +1,16 @@ +.hint-message { + position: fixed; + top: 45%; + left: 50%; + transform: translateX(-50%); + padding: 15px 20px; + border-radius: 5px; + background: rgba(0, 0, 0, 0.822); + z-index: 999; + transition: all 1s; + pointer-events: none; +} + +.message-container { + transition: all 1s; +} \ No newline at end of file diff --git a/srcWeb/base/MusicDetail/index.js b/srcWeb/base/MusicDetail/index.js new file mode 100644 index 0000000..27ea968 --- /dev/null +++ b/srcWeb/base/MusicDetail/index.js @@ -0,0 +1,196 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import Lyric from 'lyric-parser'; +import { If, Then, Else } from 'react-if'; +import { toggleShowMusicDetail, getAlbumInfoAction } from '../../store/actionCreator'; +import RenderSingrs from '../RenderSingers'; +import { imageRatio } from '../../common/js/utl'; + +import './style.scss'; + +class MusicDetail extends Component { + constructor (props) { + super(props); + this.state = { + lyric: null, + noLyric: false, + currentLineNum: 0, + musicTime: 0 + }; + } + + componentWillReceiveProps (nextProps) { + // 如果下一个 props 没有 currentMusic 就直接返回 + if (!nextProps.currentMusicLyric) { + return; + } + + // 歌词为 暂无歌词时 设置为暂无歌词 --> 返回 + if ( + 'nolyric' in nextProps.currentMusicLyric || + !('lrc' in nextProps.currentMusicLyric) + ) { + this.setState(() => ({ + noLyric: true + })); + return; + } + + // 当上一个props 的歌词和 这个 props 的歌词一样时,直接返回 + const r = + JSON.stringify(nextProps.currentMusicLyric) === + JSON.stringify(this.props.currentMusicLyric); + if (r) { + return; + } + + // 这个时候歌词已经发生了变化 + if (this.state.lyric !== null) { + // 如果之前已经有被处理过的歌词的话,先将原来的歌词暂停 + this.state.lyric.stop(); + } + // 初始化新的歌词,并进行替换 + const lyric = new Lyric( + nextProps.currentMusicLyric.lrc.lyric, + this.handleLyric + ); + this.setState( + () => ({ + lyric, + noLyric: false + }), + () => { + // 初始化完成之后,播放当前歌词 + this.state.lyric.play(); + this.refs.lyricList.scrollTo(0, 0); + } + ); + } + + displayMusicDetailGetMusicTime = (time) => { + this.setState(() => ({ + musicTime: time + }), () => { + if (this.state.lyric) { + this.seek(this.state.musicTime); + }; + }); + } + + togglePlay = () => { + this.state.lyric.togglePlay(); + }; + + seek = (startTime) => { + this.state.lyric.seek(startTime * 1000); + }; + + renderLyric = () => { + if (!this.state.lyric) { + return; + } + return this.state.lyric.lines.map((item, index) => { + return ( +
  • + {item.txt} +
  • + ); + }); + }; + + handleLyric = ({ lineNum }) => { + if (this.state.noLyric) { + return; + } + this.setState(() => ({ + currentLineNum: lineNum + })); + if (lineNum > 5) { + const parentDom = document.querySelector('.lyric-container'); + // const distance = parentDom.scrollHeight - (parentDom.childNodes[lineNum].offsetTop - 72); + const distance = + parentDom.childNodes[lineNum].offsetTop - + 72 - + (parentDom.childNodes[5].offsetTop - 72); + this.refs.lyricList.scrollTo(0, distance); + } else { + this.refs.lyricList.scrollTo(0, 0); + } + }; + + render () { + const { currentMusic, showMusicDetail } = this.props; + return ( +
    + +
    +
    +
    + +
    +
    +
    +
    +

    {currentMusic.musicName}

    +

    + 歌手: + +

    +

    this.props.handleGetAlbumInfo(currentMusic.album.id)}>专辑:{currentMusic.album.name}

    +
    +
    + + +
      + {this.renderLyric()} +
    +
    + +

    暂无歌词

    +
    +
    +
    +
    +
    +
    + ); + } +} + +const mapStateToProps = (state) => { + return { + showMusicDetail: state.showMusicDetail, + currentMusic: state.currentMusic, + currentMusicLyric: state.currentMusicLyric + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + handletoggleShowMusicDetail () { + dispatch(toggleShowMusicDetail()); + }, + handleGetAlbumInfo (albumId) { + this.handletoggleShowMusicDetail(); + dispatch(getAlbumInfoAction(albumId)); + }, + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps, + null, + { forwardRef: true } +)(MusicDetail); diff --git a/srcWeb/base/MusicDetail/style.scss b/srcWeb/base/MusicDetail/style.scss new file mode 100644 index 0000000..872426e --- /dev/null +++ b/srcWeb/base/MusicDetail/style.scss @@ -0,0 +1,127 @@ +@import '../../common/scss/variable.scss'; + +.hide-music-detail { + display: none; +} + +.music-detail { + position: fixed; + top: 0; + bottom: $player-height; + left: 0; + width: 100%; + background: rgba(0, 0, 0, 0.856); + z-index: 1; + + .hide-music-detail-btn { + position: fixed; + right: 20px; + top: 20px; + color: $color-text-gray; + outline: none; + background: none; + border: none; + cursor: pointer; + + &:hover { + color: $color-text; + } + } + + .detail-container { + position: absolute; + top: 30%; + left: 50%; + display: flex; + width: 70%; + max-width: 750px; + transform: translateX(-47%); + } + + .left-contanier { + display: inline-block; + + .img { + width: 250px; + height: 250px; + overflow: hidden; + border: 10px solid rgba(255, 255, 255, 0.123); + + img { + width: 250px; + min-height: 250px; + background: $color-text; + } + } + } + + .music-right-container { + position: absolute; + right: 0; + top: -50px; + display: inline-block; + width: 320px; + color: $color-text; + + .music-info { + margin-bottom: 20px; + + .music-name { + margin: 0; + padding-bottom: 10px; + font-weight: none; + font-size: $font-size-xxxl; + } + + .singer-name, .album-name { + display: inline-block; + font-size: $font-size-x; + color: $color-text-gray; + cursor: pointer; + + text-overflow: ellipsis; //让超出的用...实现 + white-space: nowrap; //禁止换行 + overflow: hidden; //超出的隐藏 + + &:hover { + color: $color-theme; + } + } + + .singer-name { + display: inline-block; + margin-right: 10px; + max-width: 150px; + } + + .album-name { + max-width: 150px; + } + } + } +} + +.lyric { + width: 310px; + overflow: hidden; +} + +.lyric-container { + width: 320px; + height: 300px; + overflow-y: scroll; + list-style-type: none; + color: $color-text-gray; + font-size: $font-size-x; + + li { + transition: all 1s; + line-height: 18px; + margin-bottom: 10px; + } + + .highlight { + color: $color-text; + font-size: $font-size-xl; + } +} \ No newline at end of file diff --git a/srcWeb/base/PlayList/index.js b/srcWeb/base/PlayList/index.js new file mode 100644 index 0000000..66b1d05 --- /dev/null +++ b/srcWeb/base/PlayList/index.js @@ -0,0 +1,123 @@ +/** + * 播放列表: + * 显示当前的播放列表 + * @param {Array} 使用 redux 中的 playlist 作为显示的数据 + * @param {Number} 使用 redux 中的 currentIndex 作为正在播放歌曲的索引 + * @param {Boolean} 接收传入的 hidePlayList + */ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { If, Then, Else } from 'react-if'; +import { + getChangeCurrentMusic, + getDeleteMusicAction, + emptyPlayList +} from '../../store/actionCreator'; +import RenderSingrs from '../RenderSingers'; + +import './style.scss'; + +class PlayList extends Component { + getAlert () { + alert('getAlert from Child'); + } + + // scroll 滚动到当前播放的歌曲 + scrollToCurrentMusic = () => { + if (this.props.playList.length === 0 || !this.refs.playListUl) { + return; + } + const distance = this.props.currentIndex * 51; + this.refs.playListUl.scrollTo(0, distance); + }; + + /** + * 1. 点击歌曲需要播放歌曲 + * 改变 currentIndex,以及 currentMusic 即可 + * 2. 点击歌手名跳转到歌手列表 + * 待定 + */ + renderPlayList = () => { + return this.props.playList.map((item, index) => { + return ( +
  • this.props.handleChangeCurrentMusic(item)} + > +
    + this.props.handleChangeCurrentMusic(item)}> + {item.musicName} + +
    +
    + +
    + this.props.handleDeleteMusic(item)} + /> +
  • + ); + }); + }; + + render () { + const length = this.props.playList.length; + return ( +
    { + e.nativeEvent.stopImmediatePropagation(); + }} + > + {/* e.nativeEvent.stopImmediatePropagation(); 阻止事件冒泡 */} +
    + 共 {length} 首 + + + 全部清空 + +
    + + +
    你还没有添加歌曲
    +
    + +
      {this.renderPlayList()}
    +
    +
    +
    + ); + } +} + +const mapStateToProps = (state) => { + return { + playList: state.playList, + currentIndex: state.currentIndex + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + handleChangeCurrentMusic (item) { + const action = getChangeCurrentMusic(item); + dispatch(action); + }, + handleDeleteMusic (item) { + const action = getDeleteMusicAction(item); + dispatch(action); + }, + emptyPlayList () { + dispatch(emptyPlayList()); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps, + null, + { forwardRef: true } +)(PlayList); diff --git a/srcWeb/base/PlayList/style.scss b/srcWeb/base/PlayList/style.scss new file mode 100644 index 0000000..b5097e6 --- /dev/null +++ b/srcWeb/base/PlayList/style.scss @@ -0,0 +1,112 @@ +@import '../../common/scss/variable.scss'; + +.play-list { + position: relative; + height: 100%; + width: 300px; + padding-top: 10px; + background: $color-dark; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + box-shadow: 0 0 12px 0 rgba(0, 0, 0, 0.664); + z-index: 10; + + .without-music { + margin-top: 150px; + text-align: center; + font-size: $font-size-x; + color: $color-text-gray; + } + + .list-info { + position: absolute; + width: 100%; + height: 23px; + font-size: $font-size-m; + color: $color-text-gray; + background: $color-dark; + + .music-count { + position: absolute; + left: 20px; + } + + .delete { + position: absolute; + right: 20px; + cursor: pointer; + } + } + + ul { + margin-top: 23px; + height: calc(100vh - 90px - 33px); + overflow-y: scroll; + overflow-x: hidden; + } + + li { + display: flex; + flex-direction: column; + position: relative; + padding: 9px 10px 9px 20px; + list-style-type: none; + font-size: $font-size-l; + color: $color-text; + border-bottom: 1px solid rgb(8, 8, 8); + transition: all 0.3s; + + &:hover { + background: $color-dark-gray; + } + + .music-name { + margin-bottom: 5px; + box-sizing: border-box; + } + + .singer-name { + font-size: $font-size-m; + color: $color-text-gray; + } + + .singer-name, .music-name { + display: inline-block; + max-width: 220px; + text-overflow: ellipsis; //让超出的用...实现 + white-space: nowrap; //禁止换行 + overflow: hidden; //超出的隐藏 + + span { + cursor: pointer; + + &:hover { + color: $color-theme; + } + } + } + + i { + position: absolute; + top: 16px; + right: 22px; + color: $color-text-gray; + cursor: pointer; + + &:hover { + color: $color-theme; + } + } + } + + .action::before { + position: absolute; + top: 1px; + left: 0; + content: ''; + display: inline-block; + width: 5px; + height: 51px; + background: $color-theme; + } +} \ No newline at end of file diff --git a/srcWeb/base/PlayTime/index.js b/srcWeb/base/PlayTime/index.js new file mode 100644 index 0000000..0ab14b6 --- /dev/null +++ b/srcWeb/base/PlayTime/index.js @@ -0,0 +1,43 @@ +/** + * 用来处理并显示歌曲播放时间 + * @currentTime 播放时间 + * @duration 歌曲时长 + */ +import React, { Component } from 'react'; + +import './style.scss'; + +class PlayTime extends Component { + format (interval = 0) { + if (isNaN(interval)) { + interval = 0; + } + let minute = Math.floor(interval / 60); + if (minute < 10) { + minute = '0' + minute; + } + let second = Math.floor(interval % 60); + if (second < 10) { + second = '0' + second; + } + return minute + ':' + second; + } + + render () { + return ( +
    + + {this.format(this.props.currentTime)} + + /{this.format(this.props.duration)} +
    + ); + } +} + +PlayTime.defaultProps = { + currentTime: 0, + duration: 0 +}; + +export default PlayTime; diff --git a/srcWeb/base/PlayTime/style.scss b/srcWeb/base/PlayTime/style.scss new file mode 100644 index 0000000..522d1e4 --- /dev/null +++ b/srcWeb/base/PlayTime/style.scss @@ -0,0 +1,10 @@ +@import '../../common/scss/variable.scss'; + +.play-time { + color: $color-text-gray; + font-size: $font-size-l; + + span { + padding: 0 2px; + } +} \ No newline at end of file diff --git a/srcWeb/base/ProgressBar/index.js b/srcWeb/base/ProgressBar/index.js new file mode 100644 index 0000000..142f692 --- /dev/null +++ b/srcWeb/base/ProgressBar/index.js @@ -0,0 +1,132 @@ +/** + * 歌曲播放进度条 Player 组件使用 + * 支持: + * 1. 拖动进度条跳转进度 + * 2. 点击进度条跳转进度 + * 3. 显示播放进度 + * @percent 接收歌曲播放进度并显示 + */ +import React, { Component } from 'react'; +// import PropTypes from 'prop-types'; + +import './style.scss'; + +class ProgressBar extends Component { + constructor (props) { + super(props); + this.state = { + mouseDown: false, + + // 进度条距离游览器左边的距离 + // 作用:用来计算拖动的百分比 + controlBarOffestLeft: null + }; + } + + componentDidMount () { + this.setState(() => ({ + controlBarOffestLeft: offset(this.refs.controlBar, 'left') + })); + window.addEventListener('resize', this.handleWindowResize); + } + + handleWindowResize = () => { + // 监听窗口大小变化,实时改变进度条距离游览器左边的距离 + this.setState(() => ({ + controlBarOffestLeft: offset(this.refs.controlBar, 'left') + })); + }; + + progressMouseDown = () => { + document.addEventListener('mousemove', this.progressMouseMove, false); + document.addEventListener('mouseup', this.progressMouseUp, false); + }; + + progressMouseMove = (e) => { + // 获得拖动后的百分比 = 拖动的距离(鼠标距离游览器左边的距离 - 进度条距离左边的距离 = 实际的距离) / 进度条的长度 + let percent = + (e.clientX - this.state.controlBarOffestLeft) / + this.refs.controlBar.clientWidth; + // 因为拖动时会超过进度条的范围,所以需要给一个临界值 0 和 1 + if (percent < 0) { + percent = 0; + } else if (percent > 1) { + percent = 1; + } + + // 告诉父组件当前移动的百分比 + this.props.percentChange(percent); + }; + + progressMouseUp = (e) => { + document.removeEventListener('mousemove', this.progressMouseMove, false); + document.removeEventListener('mouseup', this.progressMouseUp, false); + + // 同样的方式,获得最后拖动后的百分比 = 拖动的距离 / 进度条的长度 + let percent = + (e.clientX - this.state.controlBarOffestLeft) / + this.refs.controlBar.clientWidth; + + if (percent < 0) { + percent = 0; + } else if (percent > 1) { + percent = 1; + } + this.props.percentChangeEnd(percent); + }; + + clickToChangePercent = (e) => { + let percent = + (e.clientX - this.state.controlBarOffestLeft) / + this.refs.controlBar.clientWidth; + + if (percent < 0) { + percent = 0; + } else if (percent > 1) { + percent = 1; + } + this.props.percentChangeEnd(percent); + }; + + render () { + return ( +
    +
    +
    +
    + +
    +
    +
    +
    + ); + } +} + +export default ProgressBar; + +/** + * https://blog.csdn.net/w390058785/article/details/80461845 + * 全面解析offsetLeft、offsetTop + * + * 作用:获取距离 body 的距离 + * @param {DOM} obj + * @param {*} direction + */ +function offset (obj, direction) { + // 将top,left首字母大写,并拼接成 offsetTop,offsetLeft + const offsetDir = + 'offset' + direction[0].toUpperCase() + direction.substring(1); + + let realNum = obj[offsetDir]; + let positionParent = obj.offsetParent; // 获取上一级定位元素对象 + + while (positionParent != null) { + realNum += positionParent[offsetDir]; + positionParent = positionParent.offsetParent; + } + return realNum; +} diff --git a/srcWeb/base/ProgressBar/style.scss b/srcWeb/base/ProgressBar/style.scss new file mode 100644 index 0000000..4d21316 --- /dev/null +++ b/srcWeb/base/ProgressBar/style.scss @@ -0,0 +1,35 @@ +@import '../../common/scss/variable.scss'; + +.progress-bar { + padding: 3px 0; + cursor: pointer; + + .add-click-scope { + height: 5px; + border-radius: 3px; + background: rgb(0, 0, 0); + } + + .control-bar { + height: 100%; + + .elapsed-bar { + position: relative; + // width: 30%; + height: 100%; + background: rgba(255, 255, 255, 0.534); + border-radius: 5px; + } + + .btn { + position: absolute; + top: -4px; + right: -10px; + width: 13px; + height: 13px; + background: $color-text; + border-radius: 50%; + cursor: pointer; + } + } +} \ No newline at end of file diff --git a/srcWeb/base/RenderSingers/index.js b/srcWeb/base/RenderSingers/index.js new file mode 100644 index 0000000..fa2aa3e --- /dev/null +++ b/srcWeb/base/RenderSingers/index.js @@ -0,0 +1,51 @@ +import React, { Component, Fragment } from 'react'; +import { connect } from 'react-redux'; +import { + getSingerInfoAction +} from '../../store/actionCreator'; + +class RenderSingers extends Component { + renderSingers = () => { + return this.props.singers.map((item, index) => { + if (index !== this.props.singers.length - 1) { + return ( + + this.props.handleGetSingerInfo(item.id)} + > + {item.name} + {' '} + /{' '} + + ); + } else { + return ( + this.props.handleGetSingerInfo(item.id)}> + {item.name} + + ); + } + }); + }; + + render () { + return ( + + {Array.isArray(this.props.singers) ? this.renderSingers() : ''} + + ); + } +} + +const mapDispatchToProps = (dispatch) => { + return { + handleGetSingerInfo (id) { + dispatch(getSingerInfoAction(id)); + } + }; +}; + +export default connect( + null, + mapDispatchToProps, +)(RenderSingers); diff --git a/srcWeb/base/ShowList/index.js b/srcWeb/base/ShowList/index.js new file mode 100644 index 0000000..501f433 --- /dev/null +++ b/srcWeb/base/ShowList/index.js @@ -0,0 +1,140 @@ +/** + * 歌曲列表展示组件 MusicList 组件使用 + * + * 功能: + * 1. 负责显示传入的所有歌曲 + * 2. 点击歌曲进行播放 + */ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { If, Then, Else } from 'react-if'; +import { + getChangeCurrentMusic, + getAlbumInfoAction, + getAddToLikeListAction +} from '../../store/actionCreator'; +import { findIndex } from '../../common/js/utl'; + +import RenderSingrs from '../RenderSingers'; + +import './style.scss'; + +class ShowList extends Component { + renderMusicList = () => { + return this.props.list.map((item, index) => { + let count = index + 1; + if (count < 10) { + count = '0' + count; + } + return ( +
  • +
    {count}
    +
    + this.props.handleChangeCurrentMusic(item)} + > + {item.musicName} + +
    +
    + +
    +
    + this.props.handleGetAlbumInfo(item.album.id)} + > + {item.album.name} + +
    +
    + + + this.props.handleAddToLikeList(item)} + > + + + + + this.props.handleAddToLikeList(item)} + > + + + + +
    +
  • + ); + }); + }; + + render () { + return ( +
    +
      + +
    • +
      +
      + 歌曲名 +
      +
      + 歌手 +
      +
      + 专辑 +
      +
    • +
      + {this.renderMusicList()} +
    +
    + ); + } +} + +ShowList.defaultProps = { + showTitle: true +}; + +const mapStateToProps = (state) => { + return { + playList: state.playList, + likesList: state.collector ? state.collector.foundList[0].tracks : null + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + handleChangeCurrentMusic (item) { + dispatch(getChangeCurrentMusic(item)); + }, + handleGetAlbumInfo (albumId) { + dispatch(getAlbumInfoAction(albumId)); + }, + handleAddToLikeList (value) { + dispatch(getAddToLikeListAction(value)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ShowList); + +/** + * 点击歌曲: + * 1. 获得当前的播放列表 + * 2. 通过 findIndex 查看列表中是否已经有点击的歌曲存在 + * 3. 如果已经有点击的歌曲存在,那就改变 currentIndex 以及 currentMusic 为已经存在的那首歌曲 + * 4. 如果没有,那就将被点击的歌曲插入到播放列表的最后一个,并播放 + */ diff --git a/srcWeb/base/ShowList/style.scss b/srcWeb/base/ShowList/style.scss new file mode 100644 index 0000000..52c7860 --- /dev/null +++ b/srcWeb/base/ShowList/style.scss @@ -0,0 +1,108 @@ +@import '../../common/scss/variable.scss'; + +.show-list-container { + color: $color-text; + // background: $color-background; + + ul { + list-style-type: none; + + .title { + background: $color-background-opacity; + border: none; + + span { + color: $color-text; + } + } + + li { + padding: 13px 0; + display: flex; + align-items: center; + font-size: $font-size-x; + border-bottom: 1px solid rgba(255, 255, 255, 0.062); + + &:last-child { + border: none; + } + } + + .list-li { + .highlight { + cursor: pointer; + transition: all 0.3s; + + &:hover { + color: $color-theme; + } + } + } + + .count { + display: inline-block; + box-sizing: border-box; + width: 8%; + padding-left: 5px; + } + + .music-name, + .singer-name, + .album-name { + display: inline-block; + padding-left: 20px; + line-height: 18px; + // box-sizing: border-box; + + text-overflow:ellipsis;//让超出的用...实现 + white-space:nowrap;//禁止换行 + overflow:hidden;//超出的隐藏 + } + + .music-name { + width: 32%; + } + + .singer-name { + width: 28%; + color: $color-text-gray; + } + + .album-name { + width: 32%; + padding-right: 10px; + color: $color-text-gray; + } + + .control-btn { + position: absolute; + left: 38px; + + i { + position: relative; + font-size: 20px; + cursor: pointer; + + &:hover { + color: $color-theme; + } + } + + .dislike-music { + color: $color-theme; + + &:hover { + i { + color: $color-text-gray; + } + } + } + } + + .list-li:hover { + i { + font-size: 20px; + } + } + } +} \ No newline at end of file diff --git a/srcWeb/common/js/config.js b/srcWeb/common/js/config.js new file mode 100644 index 0000000..b8aeeb1 --- /dev/null +++ b/srcWeb/common/js/config.js @@ -0,0 +1,9 @@ +// export const HOST = 'http://192.168.31.28:3005'; +// export const HOST = 'http://120.79.162.149:3102'; +export const HOST = 'http://120.79.162.149:3001'; + +export const PLAY_MODE_TYPES = { + SEQUENCE_PLAY: 0, + RANDOM_PLAY: 1, + LOOP_PLAY: 2 +}; diff --git a/srcWeb/common/js/utl.js b/srcWeb/common/js/utl.js new file mode 100644 index 0000000..e96ea65 --- /dev/null +++ b/srcWeb/common/js/utl.js @@ -0,0 +1,57 @@ +export const formatDate = ( + obj, + opt = { + y: true, + m: true, + d: true + } +) => { + const t = new Date(obj); + const y = t.getFullYear(); + let m = '0' + (t.getMonth() + 1); + m = m.substring(m.length - 2, m.length); + let d = '0' + t.getDate(); + d = d.substring(d.length - 2, d.length); + + const res = []; + if (opt.y) { res.push(y); } + if (opt.m) { res.push(m); } + if (opt.d) { res.push(d); } + + return res.join('-'); +}; + +// 截流函数 +export function debounce (func, delay) { + let timer; + + return function (...args) { + if (timer) { + clearTimeout(timer); + } + timer = setTimeout(() => { + func.apply(this, args); + }, delay); + }; +} + +export function findIndex (allList, list) { + return allList.findIndex((item) => { + return item.id === list.id; + }); +} + +export function formatPlayCount (count) { + if (!count) { + return 0; + } + if (count < 1e5) { + return Math.floor(count); + } else { + return Math.floor(count / 10000) + '万'; + } +} + +export function imageRatio (width = 0, height = width) { + return `?param=${window.devicePixelRatio * width}x${window.devicePixelRatio * height}`; +} diff --git a/srcWeb/common/scss/base.scss b/srcWeb/common/scss/base.scss new file mode 100644 index 0000000..7f12a53 --- /dev/null +++ b/srcWeb/common/scss/base.scss @@ -0,0 +1,3 @@ +@mixin reset_lable_a { + +} \ No newline at end of file diff --git a/srcWeb/common/scss/iconfont.scss b/srcWeb/common/scss/iconfont.scss new file mode 100644 index 0000000..d574937 --- /dev/null +++ b/srcWeb/common/scss/iconfont.scss @@ -0,0 +1,64 @@ + +@font-face {font-family: "iconfont"; + src: url('//at.alicdn.com/t/font_976773_p3q65jy0fe.eot?t=1546133130788'); /* IE9*/ + src: url('//at.alicdn.com/t/font_976773_p3q65jy0fe.eot?t=1546133130788#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAA/MAAsAAAAAGKgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADMAAABCsP6z7U9TLzIAAAE8AAAARAAAAFY8xUtWY21hcAAAAYAAAAD7AAADEuaevXdnbHlmAAACfAAACoMAAA+8zavk/2hlYWQAAA0AAAAAMQAAADYsP8LCaGhlYQAADTQAAAAgAAAAJCBiHCVobXR4AAANVAAAABsAAABkfOP//2xvY2EAAA1wAAAANAAAADQoJiyKbWF4cAAADaQAAAAfAAAAIAE1AMJuYW1lAAANxAAAAUUAAAJtPlT+fXBvc3QAAA8MAAAAwAAAAPuGNPZueJxjYGRgYOBikGPQYWB0cfMJYeBgYGGAAJAMY05meiJQDMoDyrGAaQ4gZoOIAgCKIwNPAHicY2Bk+c84gYGVgYOpk+kMAwNDP4RmfM1gxMjBwMDEwMrMgBUEpLmmMDg8Y3iRytzwv4EhhrmB4TxQmBEkBwA6Tw2YeJzlkjtywkAQRJ+Q+MvGX5nAASGBA5dJyEg5CNyAjCtwGB/IkYOGW+AeDSlcwLP1tmpnS5rZ6Qa6QGk+TAXFLwURP84Wbb5k1OYrvn1+58GZWmM1mmmuhZZaaaOd9joc16ft+Qzi1u3VKPz3rysrboeMeeSOe55p3Mebe5rS4ZXa7ygZ0OOFCU/uuO8Pejdq/ZeoYys+L6dpaJOEurrgyaJO4hmjMvG0UZV47qibWAHUS6wF6idWBQ0S64OGiZVCoyRcpnHS9tMkRP1ZYkXRPIkXaJGEW7VMwrVaJUTNTUL0tUvsBrRP7At0SOwQjusk3H3aJvT/AJL1XpQAeJytVwtsHMUZnn9mH/fe29u9W7/tu/Xt+ZWLfXuPxCZ2Epw4kARCEoeXkGMSU54h0DwhBAM2tAqUR0TUqqQRKi1gSxBAVBGlLVVMH0SN1KQkJHJJoFVJAxKordqo9c313z1bsVqgldq7m535Z/6Znfn/b77/P8IIKXWwf7Am4icGqSIEJCLHiJEnBYuwNKS6oVAHRgggCJS8xz8QRah/7yTUiyL/4OTBc6J47uDLzpMfg118hDU53bPUpu6YUXj54Dm67f0zhBAJ3/kL9jJrJPXEJCmSJ0vIakI0W6sDO57JFzS7G7JWK6SylhlPSFHZTEh6LGkbcZHFmZbB4RzuLQRRnBHP2M6UbBokmekxOZPPqVkLZ6l6bNNaCxQ/TPkVsNb6FcV/PKj4+VG/EjyOEjRMBfl5eDrJC9YogD9UA2EDHwpEUFKgJmSE8bHdHw7HwmH6aLHfpyg+Og6KoSwNKEpgKTYG2/LFu+k3is9B2A9/8is07OchRw+FMMW3hfzh6aGygCZGGxD2Ft3kWAJSXpDBZj/p5SN8dAn8QKfNS/gI7FrCL0c16uoOo5sUUo3a8RDIKjokjn5R08DMqJkzc3bOjtqstTjW2A7Q3kjXufU9Y+vG8McIds0e4m73mLN26VvsHbYNV24jREzlC/lsCn1uJWR8jaQb+KJYppAvGMlEmma7aaaO6iHKfvm9S+LxuDnSv/q1Nf0jCfyM9K95bfX89hf54iv2bOrs3LTnmXKV/X7PqjWvXlRymmY8XpF8ce9FJadyj+k+bqY/JQFSibjA03Y4jrdFBw6sLBgIkKwF7R0hwBFXMFlv8ZGwrofpTkXXlVnt4o6L3fQtvV4H0Oui/HfROh2w8N87InbP+GSRa2fiBQsR59jZzhQgn2Utxd/4AmoMIKYGfLQtCi/BCslQi6tUQ4blaufM/LP0ddKMgolGRAuaWKJoQwegiMryVVIds3YBCjn03422OP7A8LiYyfUB9OWuUWIM2P0D6+9noqAHr3F74b7Rg4wdHMXxebmlVFMkCcjQKKWjQyXi9wZCdCkO9Dl7KJXYAwzKSHFvgayajqEMGw/j3Iy4a8qFvJTMN0KpOpmsLkFjPgmATXgLq8bGau50NQIvOQKAI0yfb5yeIOpFFLqXFVFoxmG8RJIIsIxJx8yMU/Nx+i7ijRMzkzFh3My4/i2dZ9eyAAnhKlFCDNe6KbBwmzEjpnmpaqsTskT7/f7ieKASICDV02Dxz3R1iybAFk+1Bx8hpWgoMqPv8kP8ED059SGLu/YvDQuE3eucXfTisghi9CNCxvAC4iZT8EIM0cI+44NwIBxvU/kgH1Tb4mE4wAcdGQ7AAZQT03X4C/TK53hFiLBe0kCWkSHnDjt4idWDy5Ry3JL0evdlcwGpqpAGSJWZNI6ntDPdYM3FnYWgHhyE1EE9xd5C3koibhZAiMqStQAQMgWc4lS2GgI2LEmSoooy6+wG4eAIPxeMeHZ6K1TP0uP7jwns8mIFu7zfGYEuURKYX5ECvu1eQwVP79/3/1qADdcyKkoRjyhBVvB4RdUIeEPgk72h2oBPFiUv+OhjAotGJdqyvffe5wRQDe9mT9jwLhQO773qMW5C0+gq7BcoBHRNuFNWDG+PMLF34MBcBqD4oreBV74gBAIi83kFv0gFSfFKEgN+TPaRMpfNstv/zWr/R8v8jwbAM0YcHLJn2TCiRMaYqhAN42r1dKRrJXOJTQqki/SQS0mfc5Oin/tltmpqn1Pgi/ozsJ9v+LxyXNNSmmbN+sHBf+8qajMfPULXThGB/PeFsFKpNCkQihRAFrj3IZPvcQgOo0gUSdqRUg4X6ejsEIZXp7sO5ITkNN3YbpRn5JzgIyYcIGALCw4iOjIxA5xYNHu9rEU7PIqwK752TofHEw17VPmpp7xmJXg8HQtub94lSKr/u56Q+EDzxpyq6+qS3bmHEfoeMQQ/VHWlI7mkhvfn9qyM1kCl2r25GcZ8zQO5XMAXVXHWk08G4TuScFdNQm4VgkJQw/bWsIJDgiW0Nd4lBsRwWLpOErY3NGiVGrS0bUM1T0DwDGrVsiVU6PBQe84ZaU3y+6WahKdVCHmCmsezdRej5ViHOCkhTpy8K/UveZdFUthAUSKaYxJJlpxrUkh2A5RO8TOYXcVPnYYGSeJnTx/mUygLhydAwKRsKrnwlUVVSUoBaMqAjNjIhkV+5jTOwrDhzIpPzUP1iWn1iQk+BZ+0fqUpHKy5auvPawKBN2Ca4/7C+llshqshISONGhBDxrdSFkOuNlV2iSwVx/1+2j9N1kjVweJLLUwT+KPI1vgIKfSPyNbFVlgGy4rNrG7qDJk5P/J1+fzVX3x+jGNxSmbOfAriuOkzpyZ4EWWGh2AoF/k3YRk/9J8PSm/nhzA8uBvYQM/OupUEnJRH1V1IYsx28WurWReWCzBmirPvpx01Wd8b/gBE1flVJuSStVaags+zz+tTi/2wg39tphw5Qt+3GGZ+GyNVYlBqqrJaZNk7z4It3gCto0HPLf7Q+viNiRuxzNilxE5gDNdwX4txX5jnUiRAHfeHEddJ0QpWCGJGmTVDmKFhGHX5MokhTzTLmW8XiM5FcVJhFpCjRsTD5ERCZn49qAezUihhsGCuk9LOXJD5g7oSuHQ5wHJ+1hfz8U9ORCoqIidADQQjlQFa7fUIoVjMm96U9lUYQVD6XviVn9UkAs1bFi/e0hwwgwr4cvvWrdv3uCT9uDLCJyOVb8qVEToW0NG7pPRgzSi7z/VzkqRJJ/LeerKHPE72ktfRGaZmMuQwZmu2GJenv1octDhzBuJIFDGJmTLGhBSGyNkqpiabKWeyBjaYBeQSyUplnf8PTEL72DGnKaadeGG4F0h2LeZkZwU3HcpiSElD1gm0dj5maLhUwSzYZeIpK0clx76Oqctrd7tMRE+eX3Vh7fkj51dOrfiYNfD1Fmxs4Qda4PmW4p1H6MP0uhHOb7lmxcdHYOjboxRuObs7fe/qLqDtwhDf6Co/k5pRFr5+fvDVfdcPnX9n8kHo/Ij2HgslB+1LdoiiODB5vYBxMNa8MLR2koaNZfV5zLLSA0P9mMybHU9cdvOT9n5j5VcX1FQM3NDf3DTn4SvX7u746Ld7J6OT5iQcVlJVsVS4wvTgbWlIz2+/Z86DKze8gAvnPrx1zabE3fA0v419zHfCI0d/dHTiKPu0OIZh4z7+EPtUeH771c/DFZPC1ebcuUJ5dEqFzy5c4GH2Ke+FN6fUxY1JOrDVmKPBezwfC6dM6FgIPztTVQWJigZfRLttxeIbEm1N0DXv0bpGqyKqi2pkcNtQU1sr4N+mjfT0TeO8KlatRiqutKNzoq3zIftsE6aPw4/XXlL7t6c90NU8nTdMsE9YLfJEDm8E+qKcX6NrkCM6uqAD2aLbSRe08lgaCm6yXT9dGzGJ/mH5zkRi5/JNeyltyPdBey1ALbT35Rso3dvUfHrHztPNdg1lw+sX3VpXW1t366L1wwxqWC1c1tNzGbAn7shs7l3F/1pRAb5VvZszdzzBMx1v33TT2x2LhloHdlPoKeCnB+jugdahcj46s+cv3fGX7utL3k7IPwG/tz0aAHicY2BkYGAA4tUBcVfj+W2+MnCzMIDADT/7Lhj9//9/Q5lW5gYgl4OBCSQKAEfODBkAAAB4nGNgZGBgbvjfwBAj0/3/PwODTCsDUAQFSAIAkbIFwHicY2FgYGAhFetAMZj//z82NTLdCDYAbgIDXgAAAAAAADoAugDQAP4BRgGEAaQB5gIUAjYCZAKaAzYDqAQiBLwFDAU8BXoFyAYwB1IHrAfeeJxjYGRgYJBk2MYgxAACTEDMBYQMDP/BfAYAHlEB+AB4nGWPTU7DMBCFX/oHpBKqqGCH5AViASj9EatuWFRq911036ZOmyqJI8et1ANwHo7ACTgC3IA78EgnmzaWx9+8eWNPANzgBx6O3y33kT1cMjtyDRe4F65TfxBukF+Em2jjVbhF/U3YxzOmwm10YXmD17hi9oR3YQ8dfAjXcI1P4Tr1L+EG+Vu4iTv8CrfQ8erCPuZeV7iNRy/2x1YvnF6p5UHFockikzm/gple75KFrdLqnGtbxCZTg6BfSVOdaVvdU+zXQ+ciFVmTqgmrOkmMyq3Z6tAFG+fyUa8XiR6EJuVYY/62xgKOcQWFJQ6MMUIYZIjK6Og7VWb0r7FDwl57Vj3N53RbFNT/c4UBAvTPXFO6stJ5Ok+BPV8bUnV0K27LnpQ0kV7NSRKyQl7WtlRC6gE2ZVeOEXpc0Yk/KGdI/wAJWm7IAAAAeJxtjEtSwzAQBfViRWATSPgT7uAFt+AasjXGSo01Kkk2ye3BxZZevE2/arVRfzTqf47YoILGFgZXuEaNBjfY4RZ32OOAezzgEU94xgte8YYj3pWObC+VI977XsIgoZy8DUVmY53r5Gwy2dSPmn0u2/X7oSmdfPVrzSDsKOlCuVT9aDWLRB3oXPBpkg1OpqaTwYavNa1jokXnInF38YGJPXXeSr0IzxO1c2xGStROc/Z9/e2ZW5aF9DpK/QA/wDvy') format('woff'), + url('//at.alicdn.com/t/font_976773_p3q65jy0fe.ttf?t=1546133130788') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ + url('//at.alicdn.com/t/font_976773_p3q65jy0fe.svg?t=1546133130788#iconfont') format('svg'); /* iOS 4.1- */ +} + +.iconfont { + font-family:"iconfont" !important; + font-size:16px; + font-style:normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-play:before { content: "\e600"; } + +.icon-del:before { content: "\e616"; } + +.icon-iconfontjiantou:before { content: "\e638"; } + +.icon-addbox:before { content: "\e60a"; } + +.icon-search:before { content: "\e633"; } + +.icon-list:before { content: "\e664"; } + +.icon-play1:before { content: "\e865"; } + +.icon-erji:before { content: "\e63f"; } + +.icon-add:before { content: "\e601"; } + +.icon-folder:before { content: "\e748"; } + +.icon-test:before { content: "\e602"; } + +.icon-cha:before { content: "\e628"; } + +.icon-loop:before { content: "\e604"; } + +.icon-next:before { content: "\e605"; } + +.icon-H:before { content: "\e67b"; } + +.icon-random:before { content: "\e608"; } + +.icon-bofangicon:before { content: "\e603"; } + +.icon-prev:before { content: "\e68c"; } + +.icon-stop:before { content: "\e606"; } + +.icon-yinleliebiao:before { content: "\e674"; } + +.icon-volume-up:before { content: "\e620"; } + +.icon-here-music:before { content: "\e607"; } + +.icon-will-love:before { content: "\e609"; } + +.icon-love:before { content: "\e60b"; } diff --git a/srcWeb/common/scss/normalize.scss b/srcWeb/common/scss/normalize.scss new file mode 100644 index 0000000..d338bfd --- /dev/null +++ b/srcWeb/common/scss/normalize.scss @@ -0,0 +1,354 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + + html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers. + */ + +body { + margin: 0; +} + +/** + * Render the `main` element consistently in IE. + */ + +main { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Remove the gray background on active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove the border on images inside links in IE 10. + */ + +img { + border-style: none; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Remove the default vertical scrollbar in IE 10+. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + +details { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Misc + ========================================================================== */ + +/** + * Add the correct display in IE 10+. + */ + +template { + display: none; +} + +/** + * Add the correct display in IE 10. + */ + +[hidden] { + display: none; +} + +ul, p, h1 { + padding: 0; + margin: 0; +} \ No newline at end of file diff --git a/srcWeb/common/scss/variable.scss b/srcWeb/common/scss/variable.scss new file mode 100644 index 0000000..8b28ec2 --- /dev/null +++ b/srcWeb/common/scss/variable.scss @@ -0,0 +1,21 @@ +$color-background: #292929; +$color-background-dark: #202020; +$color-background-opacity: #ffffff11; +$color-dark: #000000; +$color-dark-gray:rgb(22, 22, 22)0; +$color-dark-opacity: rgba(0, 0, 0, 0.822); +$color-text-gray: #9e9e9e; +$color-text: #ebebeb; +$color-text-highlight: #5de6f8; +$color-theme: #f92e58; + +$font-size-title: 24px; +$font-size-xxxl: 20px; +$font-size-xxl: 18px; +$font-size-xl: 16px; +$font-size-x: 14px; +$font-size-l: 13px; +$font-size-m: 12px; + +$header-height: 95px; +$player-height: 80px; \ No newline at end of file diff --git a/srcWeb/components/Header/index.js b/srcWeb/components/Header/index.js new file mode 100644 index 0000000..dad6f3d --- /dev/null +++ b/srcWeb/components/Header/index.js @@ -0,0 +1,92 @@ +import React, { Component } from 'react'; +import { NavLink, withRouter } from 'react-router-dom'; +import { connect } from 'react-redux'; +import { + getHideMusicListAction, + getHideSingerInfoAction +} from '../../store/actionCreator'; + +import './style.scss'; + +class Header extends Component { + isActive = (pathname) => { + if (this.props.location.pathname === pathname) { + return 'active'; + } else { + return ''; + } + } + + render () { + return ( +
    + +
    + +
    +
    + +
    + ); + } +} + +const mapDispatchToProps = (dispatch) => { + return { + handleHideMusicListAndSingerInfo () { + dispatch(getHideMusicListAction()); + dispatch(getHideSingerInfoAction()); + } + }; +}; + +export default withRouter( + connect( + null, + mapDispatchToProps + )(Header) +); diff --git a/srcWeb/components/Header/style.scss b/srcWeb/components/Header/style.scss new file mode 100644 index 0000000..24a4978 --- /dev/null +++ b/srcWeb/components/Header/style.scss @@ -0,0 +1,48 @@ +@import '../../common/scss/variable.scss'; + +header { + position: relative; + width: 100%; + height: $header-height; + + .icon { + position: absolute; + left: 30px; + top: 50%; + transform: translateY(-50%); + font-size: $font-size-xxxl; + font-weight: bold; + color: $color-text; + cursor: pointer; + transition: all 0.5s; + + &:hover { + color: $color-theme; + } + } + + nav { + position: absolute; + margin: 0; + right: 30px; + top: 50%; + transform: translateY(-50%); + + a { + display: inline-block; + margin: 0 5px; + font-size: $font-size-x; + color: $color-text-gray; + text-decoration: none; + transition: all 0.3s; + + &:hover { + color: $color-theme; + } + } + + .active { + color: $color-theme; + } + } +} \ No newline at end of file diff --git a/srcWeb/components/MusicList/index.js b/srcWeb/components/MusicList/index.js new file mode 100644 index 0000000..caa5ebb --- /dev/null +++ b/srcWeb/components/MusicList/index.js @@ -0,0 +1,174 @@ +/** + * 歌曲展示组件, + * 用于显示歌曲列表中的所有歌曲 + */ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { + getChangePlayListAction, + getChangeCurrentIndex, + playNextMusicAction, + getToggleCollectPlaylist +} from '../../store/actionCreator'; +import { If } from 'react-if'; +import { formatDate, findIndex, imageRatio } from '../../common/js/utl'; + +import ShowList from '../../base/ShowList'; + +import './style.scss'; + +class MusicList extends Component { + constructor (props) { + super(props); + this.state = { + scrollToTop: false + }; + } + + componentWillReceiveProps (nextProps) { + if (!this.props.musicList) { + return; + } + // 当 musicList 发生改变的时候,滚动条置于最上层 + if (nextProps.musicList.id !== this.props.musicList.id) { + this.setState(() => ({ + scrollToTop: true + })); + } + } + + componentDidUpdate () { + if (this.state.scrollToTop) { + this.refs.musicList.scrollTo(0, 0); + this.setState(() => ({ + scrollToTop: false + })); + } + } + + handleCollectList = () => { + const musicList = this.props.musicList; + + // 处理一下需要存储的数据 + const list = { + tracks: musicList.tracks, + name: musicList.name, + id: musicList.id, + coverImgUrl: musicList.coverImgUrl, + tags: musicList.tags, + updateTime: musicList.updateTime, + playCount: musicList.playCount, + description: musicList.description, + artist: musicList.artist ? musicList.artist : null, + publishTime: musicList.publishTime ? musicList.publishTime : null + }; + + this.props.handleToggleCollectPlaylist(list); + }; + + renderListInfo () { + const musicList = this.props.musicList; + if (!musicList) { + return null; + } + + // 当简介的 length 超过 200 的时候扔掉多余的,在结尾加上 ... + let description = musicList.description; + if (description.length > 200) { + description = description.substring(0, 180) + ' ...'; + } + + return ( +
    +
    + +
    +

    {musicList.name}

    + +
    +

    + {musicList.artist + ? musicList.artist.name + ? musicList.artist.name + : '' + : ''} +

    + +

    + {formatDate(musicList.publishTime)} +

    +
    + 0 + } + > +

    发行:{musicList.company}

    +
    +
    +
    +

    {description}

    +
    + + this.props.changeMusicList(this.props.musicList.tracks) + } + /> + +
    +
    + ); + } + + render () { + const musicList = this.props.musicList; + return ( +
    + {this.renderListInfo()} + +
    + ); + } +} + +const mapStateToProps = (state) => { + return { + musicList: state.musicList, + showMusicList: state.showMusicList, + showSingerInfo: state.showSingerInfo, + collectedPlaylist: state.collector ? state.collector.collectList : null + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + changeMusicList (value) { + dispatch(getChangePlayListAction(value)); + dispatch(getChangeCurrentIndex(-1)); + dispatch(playNextMusicAction()); + }, + handleToggleCollectPlaylist (list) { + dispatch(getToggleCollectPlaylist(list)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(MusicList); diff --git a/srcWeb/components/MusicList/style.scss b/srcWeb/components/MusicList/style.scss new file mode 100644 index 0000000..82a1c36 --- /dev/null +++ b/srcWeb/components/MusicList/style.scss @@ -0,0 +1,132 @@ +@import '../../common/scss/variable.scss'; + +.hide-music-list-container { + display: none; +} + +.music-list-container { + position: fixed; + top: $header-height; + bottom: $player-height; + left: 0; + display: flex; + width: 100%; + box-sizing: border-box; + padding-left: 25px; + color: $color-text; + overflow-y: scroll; + z-index: 1; + + .list-info { + position: fixed; + top: $header-height; + width: 200px; + height: 200px; + background: #fff; + + .list-img { + width: 200px; + height: 200px; + overflow: hidden; + + img { + width: 200px; + min-height: 200px; + } + } + + .control { + padding-top: 5px; + + i { + position: relative; + display: inline-block; + font-size: 20px; + color: $color-text-gray; + cursor: pointer; + + &:hover { + color: $color-text; + } + + &:hover:after { + position: absolute; + top: 25px; + font-size: $font-size-m; + } + } + + .icon-play1 { + padding-right: 8px; + + &:hover:after { + content: "播放"; + left: 0; + } + } + + .icon-addbox { + padding-right: 10px; + + &:hover:after { + content: "添加到播放列表"; + left: -30px; + width: 100px; + } + } + + .icon-folder { + &:hover:after { + content: "添加到我的歌单"; + left: -30px; + width: 100px; + } + } + + .collected { + color: $color-theme; + } + } + + .name { + margin-top: 10px; + margin-bottom: 12px; + line-height: 24px; + font-size: $font-size-xxl; + font-weight: bold; + } + + .album-info { + margin-bottom: 12px; + color: $color-text-gray; + + .artist { + margin-bottom: 5px; + font-size: $font-size-xl; + } + + .company { + font-size: $font-size-l; + } + + .publish-time { + margin-bottom: 5px; + font-size: $font-size-l; + } + } + + .description { + position: relative; + font-size: $font-size-m; + line-height: 22px; + width: 100%; + } + + } + + .show-list-container { + position: absolute; + left: 240px; + right: 20px; + } +} \ No newline at end of file diff --git a/srcWeb/components/Player/index.js b/srcWeb/components/Player/index.js new file mode 100644 index 0000000..c56664a --- /dev/null +++ b/srcWeb/components/Player/index.js @@ -0,0 +1,499 @@ +/** + * Player 组件 + * 只负责歌曲的播放,以及控制歌曲的播放模式 + * 不用关心歌曲列表,以及歌曲的播放顺序的逻辑处理 + */ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +// 使用 withRouter 之后就可以使用 this.props.history.push(value) +import { withRouter } from 'react-router-dom'; +import { If, Then, Else } from 'react-if'; +import { + getChangePlayingStatusAction, + playPrevMusicAction, + playNextMusicAction, + getChangePlayModeAction, + toggleShowMusicDetail, + getAddToLikeListAction, + getHideAllAction, + getChangeVolumeAction +} from '../../store/actionCreator'; +import { PLAY_MODE_TYPES } from '../../common/js/config'; +import { findIndex, imageRatio } from '../../common/js/utl'; + +import ProgressBar from '../../base/ProgressBar'; +import PlayTime from '../../base/PlayTime'; +import PlayList from '../../base/PlayList'; +import MusicDetail from '../../base/MusicDetail'; +import RenderSingers from '../../base/RenderSingers'; + +import './style.scss'; + +// const { ipcRenderer } = window.require('electron'); + +const DEFAULT_TIME = 0; +const PLAYING_STATUS = { + playing: true, + paused: false +}; +const VOLUME_UP = 'VOLUME_UP'; +const VOLUME_DOWN = 'VOLUME_DOWN'; + +class Player extends Component { + constructor (props) { + super(props); + this.state = { + duration: DEFAULT_TIME, + currentTime: DEFAULT_TIME, + move: false, + percent: 0, + showPlayList: false + }; + } + + componentDidMount () { + this.refs.audio.volume = this.props.volume; + + // 全局快捷键按键 + // ipcRenderer.on('store-data', (event, store) => { + // this.handleGlobalShortcut(store); + // }); + + // 快捷键 + document.addEventListener('keydown', this.handleShortcut); + } + + handleGlobalShortcut = (e) => { + if (e === 'volumeUp') { + this.handleChangeVolume(VOLUME_UP); + return; + } + if (e === 'volumeDown') { + this.handleChangeVolume(VOLUME_DOWN); + return; + } + if (e === 'nextMusic') { + this.props.playNextMusic(); + return; + } + if (e === 'prevMusic') { + this.props.playPrevMusic(); + return; + } + if (e === 'changePlayingStatus') { + if (this.props.playing) { + this.handleChangePlayingStatus(PLAYING_STATUS.paused); + } else { + this.handleChangePlayingStatus(PLAYING_STATUS.playing); + } + } + } + + handleShortcut = (e) => { + if (e.target.tagName === 'INPUT') { + return; + } + if (e.key === ' ') { + e.preventDefault(); + if (this.props.playing) { + this.handleChangePlayingStatus(PLAYING_STATUS.paused); + } else { + this.handleChangePlayingStatus(PLAYING_STATUS.playing); + } + return; + } + if (e.key === 'ArrowUp') { + e.preventDefault(); + this.handleChangeVolume(VOLUME_UP); + return; + } + if (e.key === 'ArrowDown') { + e.preventDefault(); + this.handleChangeVolume(VOLUME_DOWN); + return; + } + if (e.key === 'ArrowRight' && e.metaKey) { + e.preventDefault(); + this.props.playNextMusic(); + return; + } + if (e.key === 'ArrowLeft' && e.metaKey) { + e.preventDefault(); + this.props.playPrevMusic(); + return; + } + if ((e.key === 'L' || e.key === 'l') && e.metaKey) { + this.props.handleAddToLikeList(this.props.currentMusic); + return; + } + if ((e.key === 'F' || e.key === 'f') && e.metaKey) { + this.props.handleHideAll(); + this.props.history.push('/search'); + return; + } + } + + handleChangeVolume = (type) => { + if (type === VOLUME_UP) { + if (this.props.volume === 1) { + return; + } else { + const volume = this.props.volume + 0.05 > 1 ? 1 : this.props.volume + 0.05; + this.volumeChange(volume); + } + } else { + if (this.props.volume === 0) { + return; + } else { + const volume = this.props.volume - 0.05 < 0 ? 0 : this.props.volume - 0.05; + this.volumeChange(volume); + } + } + } + + componentWillReceiveProps ({ playing }) { + if (!playing) { + this.refs.audio.pause(); + } + } + + // 音乐播放触发 audio 标签的 updatetime 事件 + // 这个时候获取 currentTime 得到音乐的时间 + handleUpdateTime = (e) => { + if (this.state.move) { + return; + } + const { currentTime, duration } = e.target; + let percent = Math.floor((currentTime / duration) * 1000) / 1000; + if (isNaN(percent)) { + percent = 0; + } + this.setState(() => { + return { + currentTime, + percent, + duration + }; + }); + }; + + // 歌曲进度控制 + percentChange = (percent) => { + if (this.props.showMusicDetail) { + const currentTime = this.state.duration * percent; + this.refs.musicDetail.seek(currentTime); + } + this.setState(() => { + return { + percent, + move: true + }; + }); + }; + + // 歌曲进度控制 + percentChangeEnd = (percent) => { + const currentTime = this.state.duration * percent; + this.refs.audio.currentTime = currentTime; + if (this.props.showMusicDetail) { + this.refs.musicDetail.seek(currentTime); + } + this.setState(() => { + return { + currentTime, + percent, + move: false + }; + }); + }; + + // 音量控制 + volumeChange = (percent) => { + this.refs.audio.volume = percent; + this.props.handleChangeVolume(percent); + }; + + handleChangePlayingStatus (status) { + if (this.props.playList.length === 0) { + return; + } + this.props.changePlayingStatus(status); + const audio = this.refs.audio; + if (status === PLAYING_STATUS.playing) { + audio.play(); + } else { + audio.pause(); + } + // 如果歌曲详情已经显示了,就对歌词进行暂停 + if (this.props.showMusicDetail) { + this.refs.musicDetail.togglePlay(); + } + } + + handleShowPlayList = () => { + if (!this.state.showPlayList) { + document.addEventListener('click', this.handleShowPlayList); + } else { + document.removeEventListener('click', this.handleShowPlayList); + } + this.setState((pervState) => ({ + showPlayList: !pervState.showPlayList + }), () => { + this.refs.playList.scrollToCurrentMusic(); + }); + }; + + handleShowMusicDetial = () => { + this.props.toggleShowMusicDetail(); + this.refs.musicDetail.displayMusicDetailGetMusicTime(this.state.currentTime); + } + + handlePlayNextMusic = () => { + if (this.props.playMode === PLAY_MODE_TYPES.LOOP_PLAY) { + const currentTime = 0; + this.refs.audio.currentTime = currentTime; + this.refs.audio.play(); + this.setState(() => { + return { + currentTime + }; + }); + } else { + this.props.playNextMusic(); + } + }; + + renderPlayerControl = () => { + return ( +
    +
    +
    + +
    +
    + + {/* 如果正在播放,显示暂停按钮 */} + + + this.handleChangePlayingStatus(PLAYING_STATUS.paused) + } + /> + + {/* 如果音乐暂停,显示播放按钮 */} + + + this.handleChangePlayingStatus(PLAYING_STATUS.playing) + } + /> + + +
    +
    + +
    +
    +
    + ); + }; + + render () { + const { currentMusic } = this.props; + + return ( +
    +
    + {this.renderPlayerControl()} +
    + +
    +
    +
    +
    +

    + {currentMusic ? currentMusic.musicName : ''} +

    +

    + {currentMusic ? : ''} +

    +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    + + this.props.changePlayMode(PLAY_MODE_TYPES.RANDOM_PLAY) + } + /> + + this.props.changePlayMode(PLAY_MODE_TYPES.SEQUENCE_PLAY) + } + /> + + this.props.changePlayMode(PLAY_MODE_TYPES.LOOP_PLAY) + } + /> +
    + + +
    this.props.handleAddToLikeList(currentMusic)}> + +
    +
    + +
    this.props.handleAddToLikeList(currentMusic)}> + +
    +
    +
    +
    +
    + + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    + ); + } +} + +const mapStateToProps = (state) => { + return { + playList: state.playList, + currentMusic: state.currentMusic, + playing: state.playing, + volume: state.volume, + playMode: state.playMode, + showMusicDetail: state.showMusicDetail, + likesList: state.collector ? state.collector.foundList[0].tracks : null + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + changePlayingStatus (status) { + dispatch(getChangePlayingStatusAction(status)); + }, + changePlayMode (value) { + dispatch(getChangePlayModeAction(value)); + }, + playPrevMusic () { + dispatch(playPrevMusicAction()); + }, + playNextMusic () { + dispatch(playNextMusicAction()); + }, + toggleShowMusicDetail () { + dispatch(toggleShowMusicDetail()); + }, + handleAddToLikeList (value) { + dispatch(getAddToLikeListAction(value)); + }, + /** + * 隐藏 *歌手详情* *歌曲列表* *歌曲详情* + */ + handleHideAll () { + dispatch(getHideAllAction()); + }, + handleChangeVolume (value) { + dispatch(getChangeVolumeAction(value)); + } + }; +}; + +export default withRouter( + connect( + mapStateToProps, + mapDispatchToProps + )(Player) +); + + +/** + * 点击歌曲播放逻辑: + * 1. 点击歌曲的时候使用 getChangeCurrentMusic + * 2. 使用 redux-thunk 中间件,在 actoin 中发出获取歌曲 url 的请求 + * 3. 获取 url 之后在 action 中直接调用 actionCreator 中的 changeCurrentMusicAction 来对 redux 中的 currentMusic 进行修改 + * + * 点击下一首: + * 1. 修改 currentIndex 也就是前播放列表 playList 中的歌曲索引 + * 1. + * 2. 更改 redux 中的 currentMusic 修改为 playList[currentIndex] + * 3. 重复 播放逻辑 + * + * 播放完当前歌曲下一首: + */ diff --git a/srcWeb/components/Player/style.scss b/srcWeb/components/Player/style.scss new file mode 100644 index 0000000..e1b8d22 --- /dev/null +++ b/srcWeb/components/Player/style.scss @@ -0,0 +1,199 @@ +@import '../../common/scss/variable.scss'; + +.player-container { + display: flex; + position: fixed; + left: 0; + bottom: 0; + width: 100%; + height: $player-height; + // background-image: linear-gradient(-20deg, #2b5876 0%, #4e4376 100%); + z-index: 2; + + // flex 布局 三等分 + .player-left-container { + flex-basis: 225px; + } + + .player-middle-container { + flex-grow: 1; + } + + .player-right-container { + flex-basis: 250px; + } +} + +.player-left-container { + .music-img { + position: absolute; + top: 0; + left: 130px; + margin: 8px 10px; + display: inline-block; + width: 64px; + height: 64px; + border-radius: 5%; + overflow: hidden; + cursor: pointer; + + img { + width: 64px; + min-height: 64px; + border-radius: 5%; + } + } + + .player-control-container { + .play-control-btn { + padding-left: 20px; + display: flex; + align-items: center; + // width: 110px; + height: $player-height; + color: $color-text; + } + + .prev-music, + .play, + .next-music { + display: inline-block; + cursor: pointer; + + i { + color: $color-text; + font-size: 28px; + } + } + + .play { + padding: 0 10px; + } + } +} + +.player-middle-container { + + .progress-bar-container { + margin-top: 47px; + width: 100%; + } + + .music-info { + position: absolute; + top: 15px; + + p { + text-overflow:ellipsis;//让超出的用...实现 + white-space:nowrap;//禁止换行 + overflow:hidden;//超出的隐藏 + } + + .music-name { + position: relative; + top: 1px; + display: inline-block; + margin-right: 15px; + font-size: $font-size-x; + color: $color-text; + cursor: pointer; + max-width: 350px; + } + + .singer-name { + display: inline-block; + font-size: $font-size-l; + color: $color-text-gray; + cursor: pointer; + max-width: 350px; + } + } +} + +.player-right-container { + position: relative; + + .right-control-btn { + position: absolute; + top: 7px; + right: 35px; + padding: 3px; + color: $color-text-gray; + + i { + cursor: pointer; + + &:hover { + color: $color-text; + } + } + + .like-music, .dislike-music { + position: absolute; + right: 50px; + top: 3px; + } + + .dislike-music { + i { + color: $color-theme; + } + } + + + .change-play-mode { + position: absolute; + top: 3px; + right: 25px; + + .hide { + display: none; + } + } + } + + .play-time-container { + position: absolute; + left: 20px; + top: 45px; + } + + .audio-volume { + position: absolute; + top: 47px; + left: 150px; + width: 60px; + + i { + position: absolute; + top: -3px; + left: -23px; + color: $color-text-gray; + } + } +} + +.player-background { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: $player-height; + background: rgba(0, 0, 0, 0.856); + z-index: -1; +} + +.music-detail-background { + position: fixed; + left: -20vw; + top: -20vw; + width: 150vw; + height: 150vh; + background: $color-background; + z-index: -10; + + img { + filter: blur(50px); + width: 150vw; + } +} \ No newline at end of file diff --git a/srcWeb/components/SingerInfo/index.js b/srcWeb/components/SingerInfo/index.js new file mode 100644 index 0000000..9733645 --- /dev/null +++ b/srcWeb/components/SingerInfo/index.js @@ -0,0 +1,204 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { If } from 'react-if'; +import { + getChangeCurrentMusic, + getChangePlayListAction, + getChangeCurrentIndex, + playNextMusicAction, + getHideSingerInfoAction, + getAlbumInfoAction +} from '../../store/actionCreator'; +import { getSingerAlbums } from '../../api'; +import { formatDate, imageRatio } from '../../common/js/utl'; +import ShowList from '../../base/ShowList'; + +import './style.scss'; + +class SingerInfo extends Component { + constructor (props) { + super(props); + this.state = { + gotSingerAlbums: false, + albums: null + }; + } + + componentWillReceiveProps (nextProps) { + if (!nextProps.singerInfo) { + this.setState(() => ({ + gotSingerAlbums: false, + albums: null + })); + } + } + + componentDidUpdate () { + const singerInfo = this.refs.singerInfo; + if (singerInfo) { + // 如果发现内容高度不足以让用户滚动,那么就直接获取专辑内容 + if (singerInfo.scrollHeight === singerInfo.clientHeight && !this.state.albums) { + getSingerAlbums(this.props.singerInfo.artist.id).then((res) => { + this.setState(() => ({ + albums: res.data + })); + }); + } + } + } + + handleUserScroll = () => { + const singerInfo = this.refs.singerInfo; + const scrollAtBottom = singerInfo.scrollHeight - (singerInfo.scrollTop + singerInfo.clientHeight) < 100; + if (scrollAtBottom && !this.state.gotSingerAlbums && !this.state.albums) { + this.setState(() => ({ + gotSingerAlbums: true + }), () => { + getSingerAlbums(this.props.singerInfo.artist.id).then((res) => { + this.setState(() => ({ + albums: res.data + })); + }); + }); + } + } + + renderAlbums = () => { + const albums = this.state.albums; + if (!albums) { + return null; + } + + return albums.hotAlbums.map((item) => { + return ( +
  • +
    this.props.handleGetAlbumInfo(item.id)}> + 专辑图像 +
    +

    {formatDate(item.publishTime)}

    +

    {item.name}

    +
  • + ); + }); + } + + render () { + if (this.props.singerInfo === null) { + return null; + } + const { singerInfo, showSingerInfo } = this.props; + const tracks = formatMusic(this.props.singerInfo.hotSongs); + const { artist } = singerInfo; + return ( +
    + + + +
    +
    +
    + +
    +
    +

    {artist.name}

    +

    + {(artist.trans ? artist.trans : '') + + (artist.alias.length > 0 && artist.trans ? ' / ' : '') + + artist.alias.join(' / ')} +

    +

    + 简介:{artist.briefDesc ? artist.briefDesc : '暂无简介'} +

    +
    +
    +
    +
    +

    + 热门歌曲 + + this.props.changeMusicList(tracks) + } + > + 播放歌曲 + + +

    + +
    + +
    +

    专辑 + {this.state.albums ? this.state.albums.hotAlbums.length + ' ALBUMS' : ''} +

    +
      + {this.renderAlbums()} +
    +
    +
    +
    +
    +
    + ); + } +} + +const mapStateToProps = (state) => { + return { + singerInfo: state.singerInfo, + showSingerInfo: state.showSingerInfo + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + handleChangeCurrentMusic (item) { + dispatch(getChangeCurrentMusic(item)); + }, + changeMusicList (value) { + dispatch(getChangePlayListAction(value)); + dispatch(getChangeCurrentIndex(-1)); + dispatch(playNextMusicAction()); + }, + hideSingerInfo () { + dispatch(getHideSingerInfoAction(false)); + }, + handleGetAlbumInfo (albumId) { + dispatch(getAlbumInfoAction(albumId)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(SingerInfo); + +function formatMusic (list) { + return list.map((item) => { + const singers = item.ar.map((item) => { + return { + id: item.id, + name: item.name + }; + }); + return { + id: item.id, + musicName: item.name, + imgUrl: item.al.picUrl, + singers, + album: { + id: item.al.id, + name: item.al.name + } + }; + }); +}; diff --git a/srcWeb/components/SingerInfo/style.scss b/srcWeb/components/SingerInfo/style.scss new file mode 100644 index 0000000..1151ca7 --- /dev/null +++ b/srcWeb/components/SingerInfo/style.scss @@ -0,0 +1,153 @@ +@import '../../common/scss/variable.scss'; + +.hide-singer-info { + display: none; +} + +.hide-singer-info-btn { + position: fixed; + top: 100px; + right: 80px; + color: $color-text; + cursor: pointer; + z-index: 2; + + &:hover { + color: $color-theme; + } +} + +.singer-info { + position: fixed; + top: $header-height; + bottom: $player-height; + width: 100%; + overflow-y: scroll; + + .singer-info-container { + position: absolute; + left: 50%; + width: 800px; + transform: translateX(-50%); + } +} + +.singer-introduction { + display: flex; + margin-bottom: 30px; + height: 150px; + color: $color-text; + + .singer-img { + flex-basis: 150px; + + img { + width: 150px; + min-height: 150px; + border-radius: 50%; + } + } + + .singer-describe { + flex: 1; + padding-left: 25px; + + .name { + margin-bottom: 8px; + } + + .other-name { + margin-bottom: 15px; + font-size: $font-size-l; + color: $color-text-gray; + } + + .brief-desc { + font-size: $font-size-l; + line-height: 24px; + max-height: 72px; + overflow-y: scroll; + } + } +} + +.singer-music { + color: $color-text; + + .songs-list-title { + position: relative; + font-weight: normal; + font-size: $font-size-xxl; + margin-bottom: 15px; + + .btn { + position: absolute; + top: 3px; + right: 0; + font-size: $font-size-l; + cursor: pointer; + + &:hover { + color: $color-theme; + } + + i { + font-size: $font-size-l; + } + } + } + + .albums-list { + .albums-list-title { + margin-top: 30px; + margin-bottom: 35px; + font-weight: normal; + font-size: $font-size-xxl; + + span { + display: inline-block; + padding-left: 20px; + color: $color-text-gray; + } + } + + ul { + width: 860px; + transform: translateX(-30px); + } + + li { + position: relative; + display: inline-block; + width: 100px; + margin: 0 36px 15px 36px; + font-size: $font-size-m; + + .album-img-container { + width: 100px; + height: 100px; + cursor: pointer; + + img { + width: 100%; + height: 100%; + } + } + + .name { + float: left; + width: 100px; + line-height: 16px; + } + + .time { + float: left; + margin-top: 5px; + margin-bottom: 5px; + width: 100px; + color: $color-text-gray; + } + + } + } +} \ No newline at end of file diff --git a/srcWeb/data/index.js b/srcWeb/data/index.js new file mode 100644 index 0000000..3342dad --- /dev/null +++ b/srcWeb/data/index.js @@ -0,0 +1,8 @@ +import Datastore from 'nedb'; +import path from 'path'; +// const remote = window.require('electron').remote; + +export default new Datastore({ + filename: path.join(__dirname, '/data.db'), + autoload: true +}); diff --git a/srcWeb/index.js b/srcWeb/index.js new file mode 100644 index 0000000..8d7f7cd --- /dev/null +++ b/srcWeb/index.js @@ -0,0 +1,13 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; +import { Provider } from 'react-redux'; + +import store from './store'; + +ReactDOM.render( + + + , + document.getElementById('root') +); diff --git a/srcWeb/pages/About/index.js b/srcWeb/pages/About/index.js new file mode 100644 index 0000000..bc81cab --- /dev/null +++ b/srcWeb/pages/About/index.js @@ -0,0 +1,179 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import message from '../../base/Message'; +import { getChangeCollectorAction } from '../../store/actionCreator'; +import $db from '../../data'; + +import './style.scss'; + +const shell = window.require('electron').shell; +const fs = window.require('fs'); +const { dialog } = window.require('electron').remote; + +class Rank extends Component { + constructor (props) { + super(props); + this.state = { + rankList: null + }; + } + + handleOpenExternalUrl = (url) => { + shell.openExternal(url); + }; + + handleExportCollector = () => { + const filters = [ + { + name: 'json', + extensions: ['json'] // 文件后缀名类型, 如 md json + } + ]; + // https://electronjs.org/docs/api/dialog#dialogshowsavedialogbrowserwindow-options-callback + dialog.showSaveDialog( + { + filters, + defaultPath: 'here-music-collector' + }, + (filename) => { + if (!filename) { + return; + } + // http://nodejs.cn/api/fs.html#fs_fs_writefile_file_data_options_callback + fs.writeFile(filename, JSON.stringify(this.props.collector), (err) => { + message.info('!Congratulation! 备份成功 !Congratulation!'); + if (err) { throw err; } + }); + } + ); + }; + + handleImportCollector = () => { + // https://electronjs.org/docs/api/dialog#dialogshowopendialogbrowserwindow-options-callback + dialog.showOpenDialog( + { + properties: ['openFile'] + }, + (filename) => { + if (!filename || filename.length === 0) { return; } + fs.readFile(filename[0], (err, fd) => { + if (err) { + if (err.code === 'ENOENT') { + return; + } + throw err; + } + try { + const collector = JSON.parse(fd); + if (collector.name !== 'collector') { + message.info('!!请导入正确的备份文件!!'); + return; + } else { + $db.update({ name: 'collector' }, collector, () => { + this.props.handleChangeCollector(collector); + message.info('!Congratulation! 导入成功 !Congratulation!'); + }); + } + } catch { + message.info('!!请导入正确的备份文件!!'); + } + }); + } + ); + }; + + renderExportCollector () { + return ( +
  • +

    导出我的收藏

    +

    + 因为重装软件会导致收藏夹数据丢失,所以强烈建议在重装软件前导出收藏夹进行备份。 +

    + +
  • + ); + } + + renderImportCollector () { + return ( +
  • +

    导入我的收藏夹

    +

    + 导入备份文件,恢复到我的收藏(会覆盖当前的收藏夹)。 +

    + +
  • + ); + } + + renderAbout () { + return ( +
  • +

    关于 Here Music

    +
    +

    + Here Music :{' '} + + this.handleOpenExternalUrl('https://github.com/caijinyc/here') + } + > + https://github.com/caijinyc/here + +

    +

    + 因为软件暂时是个人开发维护,所以难免会有一些没有注意到的问题,请见谅。 +

    +

    + 如果对 Here Music 有任何建议,或者有 Bug 需要反馈的话欢迎在{' '} + + this.handleOpenExternalUrl( + 'https://github.com/caijinyc/here/issues' + ) + } + > + Issues + {' '} + 中提出。 +

    +

    如果您喜欢 Here Music 的话,欢迎 Star 和 Fork 本项目。

    +

    Version 0.1.1 本软件基于 MIT 协议开源

    +
    +
  • + ); + } + + render () { + return ( +
    +
      + {this.renderExportCollector()} + {this.renderImportCollector()} + {this.renderAbout()} +
    +
    + ); + } +} + +const mapStateToProps = (state) => { + return { + showMusicList: state.showMusicList, + showSingerInfo: state.showSingerInfo, + collector: state.collector + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + handleChangeCollector (value) { + dispatch(getChangeCollectorAction(value)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Rank); diff --git a/srcWeb/pages/About/style.scss b/srcWeb/pages/About/style.scss new file mode 100644 index 0000000..a07bae7 --- /dev/null +++ b/srcWeb/pages/About/style.scss @@ -0,0 +1,81 @@ +@import '../../common/scss/variable.scss'; + +// .hide-page-about { +// display: none; +// } + +.page-about { + position: fixed; + top: 125px; + left: 0; + bottom: 80px; + width: 100%; + z-index: 0; + overflow-y: scroll; +} + +.page-about-container { + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 800px; + list-style-type: none; + + li { + margin-bottom: 35px; + + .title { + font-size: $font-size-title; + padding-bottom: 5px; + margin-bottom: 10px; + // border-bottom: 1px solid ; + } + + .description { + margin-bottom: 20px; + color: $color-text-gray; + font-size: $font-size-x; + } + + button { + padding: 7px 10px; + background: $color-background-opacity; + color: $color-text; + font-size: $font-size-x; + // border: 1px solid $color-text-gray; + border: none; + border-radius: 5px; + outline: none; + cursor: pointer; + transition: all 0.3s; + + &:hover { + background: $color-theme; + } + } + } + + .about { + p { + margin-bottom: 5px; + } + + span { + color: $color-text-gray; + border-bottom: 1px solid; + cursor: pointer; + + &:hover { + color: $color-theme; + } + } + + .here { + margin-bottom: 15px; + } + + .license { + margin-top: 20px; + } + } +} \ No newline at end of file diff --git a/srcWeb/pages/Collect/index.js b/srcWeb/pages/Collect/index.js new file mode 100644 index 0000000..743888b --- /dev/null +++ b/srcWeb/pages/Collect/index.js @@ -0,0 +1,244 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { If, Then, Else } from 'react-if'; +import { formatPlayCount, imageRatio } from '../../common/js/utl'; +import { + getChangePlayListAction, + getChangeCurrentIndex, + playNextMusicAction, + getToggleCollectPlaylist +} from '../../store/actionCreator'; +import './style.scss'; +import ShowList from '../../base/ShowList'; +import Dialog from '../../base/Dialog'; + +const COLLECT = 0, + FOUND = 1; + +class Collect extends Component { + constructor (props) { + super(props); + this.state = { + currentList: this.props.collector + ? this.props.collector.foundList[0] + : null, + listType: FOUND, + activeList: 0, + showDialog: false, + willDelList: null + }; + } + + handleChangeCurrentList = (list, type) => { + this.refs.pageCollect.scrollTo(0, 0); + this.setState(() => ({ + currentList: list, + listType: type + })); + }; + + handleDelCollectPlaylist = (list) => { + this.setState(() => ({ + showDialog: true, + willDelList: list + })); + }; + + handleClickDialog = (bol) => { + if (bol) { + this.props.handleToggleCollectPlaylist(this.state.willDelList); + } + this.setState(() => ({ + showDialog: false + })); + }; + + renderCollectList = () => { + const collector = this.props.collector; + if (!collector) { + return null; + } + return collector.collectList.map((item, index) => { + return ( +
  • + + + this.handleChangeCurrentList( + collector.collectList[index], + COLLECT + ) + } + > + {item.name} + + this.handleDelCollectPlaylist(item)} + /> +
  • + ); + }); + }; + + renderFoundList = () => { + const collector = this.props.collector; + if (!collector) { + return null; + } + return collector.foundList.map((item, index) => { + if (index === 0) { + return ( +
  • + this.handleChangeCurrentList(collector.foundList[index], FOUND) + } + > + + {item.name} +
  • + ); + } + return ( +
  • + this.handleChangeCurrentList(collector.foundList[index], FOUND) + } + > + + {item.name} +
  • + ); + }); + }; + + renderFoundListImg = (tracks) => { + for (let i = 0; i < tracks.length; i++) { + if (tracks[i].imgUrl) { + return 歌单图片; + } + } + }; + + renderCurrentList = () => { + const list = this.state.currentList; + if (!list) { + return null; + } + return ( +
    +
    +
    + + + 歌单图片 + + +
    +
    + {this.renderFoundListImg(list.tracks)} + +
    + + +
    +
    +

    {list.name}

    + 0 + } + > +

    简介:{list.description}

    +
    +
    +

    + 歌曲数 {list.tracks.length} +

    + +

    + 收听数 {formatPlayCount(list.playCount)} +

    +
    +
    + +
    +
    +
    + +
    +
    + ); + }; + + render () { + return ( +
    +
    +
    +

    创建的歌单

    +
      {this.renderFoundList()}
    +
    +
    +

    收藏的歌单

    +
      {this.renderCollectList()}
    +
    +
    +
    {this.renderCurrentList()}
    + + + +
    + ); + } +} + +const mapStateToProps = (state) => { + return { + collector: state.collector, + showMusicList: state.showMusicList, + showSingerInfo: state.showSingerInfo + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + changeMusicList (value) { + dispatch(getChangePlayListAction(value)); + dispatch(getChangeCurrentIndex(-1)); + dispatch(playNextMusicAction()); + }, + handleToggleCollectPlaylist (list) { + dispatch(getToggleCollectPlaylist(list)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Collect); diff --git a/srcWeb/pages/Collect/style.scss b/srcWeb/pages/Collect/style.scss new file mode 100644 index 0000000..7fcaceb --- /dev/null +++ b/srcWeb/pages/Collect/style.scss @@ -0,0 +1,219 @@ +@import '../../common/scss/variable.scss'; + +.hide-page-collect { + display: none; +} + +.page-collect { + position: fixed; + top: 95px; + left: 0; + bottom: 80px; + width: 100%; + z-index: 0; + overflow-y: scroll; + color: $color-text; + + // 定义滚动条背景色 + ::-webkit-scrollbar-track-piece { + // background-color: $color-background; + } + + /* 定义滚动条高宽及背景 + 高宽分别对应横竖滚动条的尺寸 */ + ::-webkit-scrollbar { + width: 3px; + } + + /* 定义滑块 + 内阴影+圆角 */ + ::-webkit-scrollbar-thumb { + // border-radius: 4px; + background-color: rgba(214, 214, 214, 0.397); + } + + .left-nav { + position: fixed; + top: 95px; + left: 30px; + bottom: 80px; + box-sizing: border-box; + padding-top: 5px; + padding-left: 5px; + padding-right: 5px; + width: 240px; + background: rgba(255, 255, 255, 0.027); + border-radius: 5px; + overflow-y: scroll; + + .title { + margin-top: 10px; + margin-bottom: 10px; + padding-left: 5px; + color: $color-text-gray; + font-size: $font-size-l; + } + } +} + +.left-nav { + ul { + list-style-type: none; + font-size: $font-size-x; + line-height: 20px; + + li { + position: relative; + box-sizing: border-box; + padding: 0 20px 8px 30px; + width: 223px; + + text-overflow:ellipsis;//让超出的用...实现 + white-space:nowrap;//禁止换行 + overflow:hidden;//超出的隐藏 + cursor: pointer; + + span { + transition: all 0.2s; + } + + &:hover { + span { + color: $color-theme; + } + + .icon-del { + font-size: 15px; + } + } + + + .icon-yinleliebiao, .icon-will-love { + position: absolute; + left: 7px; + font-size: 20px; + } + + .icon-will-love { + font-size: 15px; + } + + .icon-del { + position: absolute; + right: 0; + font-size: 0; + } + } + } +} + +.collect-container { + position: absolute; + left: 285px; + // transform: translateX(-50%); + width: calc(100vw - 30px - 290px); + + .list-info { + position: relative; + display: flex; + margin-bottom: 20px; + + .list-img { + position: relative; + flex-basis: 190px; + height: 150px; + overflow: hidden; + + .found-list { + .filter { + position: absolute; + width: 150px; + height: 150px; + background: rgba(0, 0, 0, 0.493); + } + } + + i { + position: absolute; + font-size: 80px; + left: 36px; + top: 30px; + color: rgba(255, 255, 255, 0.705); + } + + img { + width: 150px; + min-height: 150px; + } + } + + .list-info-right { + flex: 1; + } + + .name { + font-size: $font-size-title; + box-sizing: border-box; + padding-right: 100px; + margin-bottom: 10px; + } + + .desc { + font-size: $font-size-l; + line-height: 24px; + max-height: 72px; + overflow-y: scroll; + margin-bottom: 40px; + } + + .count { + position: absolute; + left: 190px; + bottom: 0; + font-size: $font-size-l; + color: $color-text-gray; + + p { + display: inline-block; + margin-right: 20px; + + span { + font-size: $font-size-xl; + } + } + } + + .play-btn { + position: absolute; + right: 0; + bottom: 0; + padding: 8px 10px; + box-sizing: border-box; + // width: 100px; + // height: 25px; + border: none; + background: $color-background-opacity; + color: $color-text; + font-size: $font-size-l; + border-radius: 4px; + outline: none; + cursor: pointer; + transition: all 0.3s; + + &:hover { + color: $color-theme; + } + + i { + position: absolute; + left: 8px; + top: 8px; + font-size: 14px; + } + + p { + padding-left: 15px; + } + } + } +} \ No newline at end of file diff --git a/srcWeb/pages/Rank/index.js b/srcWeb/pages/Rank/index.js new file mode 100644 index 0000000..c9e1c1d --- /dev/null +++ b/srcWeb/pages/Rank/index.js @@ -0,0 +1,87 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import { getAllRank } from '../../api'; +import { formatDate, imageRatio } from '../../common/js/utl'; + +import { getMusicListDetailAction } from '../../store/actionCreator'; + +import './style.scss'; + +class Rank extends Component { + constructor (props) { + super(props); + this.state = { + rankList: null + }; + } + + componentDidMount () { + getAllRank().then((res) => { + this.setState(() => ({ + rankList: res.data.list + })); + }); + } + + renderList = () => { + const list = this.state.rankList; + if (!list) { + return null; + } else { + return list.map((item) => { + return ( +
  • this.props.handleGetMusicListDetail(item.id)}> +
    + +
    +

    {item.name}

    +

    {item.updateFrequency}

    +

    + 最后更新:{' '} + {formatDate(item.updateTime, { + y: false, + d: true, + m: true + })} +

    +
  • + ); + }); + } + }; + + render () { + return ( +
    +
      {this.renderList()}
    +
    + ); + } +} + +const mapStateToProps = (state) => { + return { + showMusicList: state.showMusicList, + showSingerInfo: state.showSingerInfo + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + handleGetMusicListDetail (id) { + dispatch(getMusicListDetailAction(id)); + }}; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Rank); diff --git a/srcWeb/pages/Rank/style.scss b/srcWeb/pages/Rank/style.scss new file mode 100644 index 0000000..3ce0f33 --- /dev/null +++ b/srcWeb/pages/Rank/style.scss @@ -0,0 +1,65 @@ +@import '../../common/scss/variable.scss'; + +.hide-page-rank { + display: none; +} + +.page-rank { + position: fixed; + top: 95px; + left: 0; + bottom: 80px; + width: 100%; + z-index: 0; + overflow-y: scroll; +} + +.rank-container { + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 900px; + + li { + display: inline-block; + margin: 0 calc((100% / 5 - 130px) / 2); + width: 130px; + margin-bottom: 30px; + font-size: $font-size-l; + cursor: pointer; + + &:hover { + .name { + color: $color-theme; + } + } + + p { + float: left; + margin-bottom: 5px; + width: 130px; + color: $color-text-gray; + } + + .name { + font-size: $font-size-xl; + color: $color-text; + transition: all 0.3s; + line-height: 20px; + } + + .img-container { + width: 130px; + height: 130px; + overflow: hidden; + margin-bottom: 10px; + border-radius: 5px; + + img { + width: 130px; + min-height: 130px; + border-radius: 5px; + } + } + } +} diff --git a/srcWeb/pages/Recommend/index.js b/srcWeb/pages/Recommend/index.js new file mode 100644 index 0000000..4116385 --- /dev/null +++ b/srcWeb/pages/Recommend/index.js @@ -0,0 +1,139 @@ +import React, { Component } from 'react'; +import { withRouter } from 'react-router-dom'; +import { connect } from 'react-redux'; +import { getRecommendList } from '../../api'; +import { formatPlayCount, imageRatio } from '../../common/js/utl'; +import { + getChangeCurrentMusicListAction, + getChangeShowLoadingAction, + getMusicListDetailAction +} from '../../store/actionCreator'; +import Loding from '../../base/Loading'; +import message from '../../base/Message'; + +import './style.scss'; + +class Recommend extends Component { + constructor (props) { + super(props); + this.state = { + recommendList: [], + gotRecommend: false, + showLoding: true + }; + } + + componentWillMount () { + // 获取推荐歌单 + this.handleGetRecommendList(); + } + + handleGetRecommendList = (updateTime = null) => { + getRecommendList(updateTime).then(({ data }) => { + if (data.playlists && data.playlists.length === 0) { + message.info('已经到底啦~'); + this.setState(() => ({ + gotRecommend: false, + showLoding: false + })); + return; + } + this.setState((prevState) => ({ + recommendList: prevState.recommendList.concat(data.playlists), + gotRecommend: false, + showLoding: false + })); + }); + }; + + handleUserScroll = () => { + const recommendList = this.refs.recommendList; + const scrollAtBottom = + recommendList.scrollHeight - + (recommendList.scrollTop + recommendList.clientHeight) === + 0; + if (scrollAtBottom && !this.state.gotRecommend) { + this.setState(() => ({ + gotRecommend: true, + showLoding: true + }), () => { + const index = this.state.recommendList.length - 1; + this.handleGetRecommendList(this.state.recommendList[index].updateTime); + }); + } + }; + + // 歌单列表展示 + renderRecommendList = () => { + return this.state.recommendList.map((item) => { + return ( +
  • +
    this.props.handleGetMusicListDetail(item.id)} + > + + +
    + + {formatPlayCount(item.playCount)} +
    +
    +
    +

    {item.name}

    +
  • + ); + }); + }; + + render () { + return ( +
    +
      + {this.renderRecommendList()} + {this.state.showLoding && } +
    +
    + ); + } +} + +const mapStateToProps = (state) => { + return { + showMusicList: state.showMusicList, + showSingerInfo: state.showSingerInfo + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + handleChangeCurrentMusicList (list) { + const action = getChangeCurrentMusicListAction(list); + dispatch(action); + }, + handleChangeShowLoadingAction (value) { + dispatch(getChangeShowLoadingAction(value)); + }, + handleGetMusicListDetail (id) { + dispatch(getMusicListDetailAction(id)); + } + }; +}; + +export default withRouter( + connect( + mapStateToProps, + mapDispatchToProps + )(Recommend) +); diff --git a/srcWeb/pages/Recommend/style.scss b/srcWeb/pages/Recommend/style.scss new file mode 100644 index 0000000..56338c4 --- /dev/null +++ b/srcWeb/pages/Recommend/style.scss @@ -0,0 +1,105 @@ +@import '../../common/scss/variable.scss'; + +.hide-recommend-container { + display: none; +} + +.recommend-container { + position: relative; + width: 100%; + list-style-type: none; + + .recommend-list { + box-sizing: border-box; + padding: 0 calc((100% - 900px) / 2); + margin: 0; + height: calc(100vh - 175px); + overflow-y: scroll; + + + li { + position: relative; + display: inline-block; + width: 20%; + height: 200px; + color: $color-text; + margin-bottom: 25px; + + .played-counts { + position: absolute; + top: 5px; + right: 10px; + font-size: $font-size-m; + z-index: 1; + + span { + display: inline-block; + margin-left: 3px; + color: rgb(219, 219, 219); + } + + i { + font-size: $font-size-l; + } + } + + .list-img-container { + position: absolute; + top: 0; + width: 86%; + height: 153px; + margin: 0 7%; + cursor: pointer; + backface-visibility: hidden; + + .shadow { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 35px; + background: linear-gradient(rgba(5, 5, 5, 0.37), hsla(0, 0%, 100%, 0)); + } + + .icon-play { + display: none; + } + + &:hover { + // 播放 icon + .icon-play { + display: inline-block; + position: absolute; + left: 50%; + top: 50%; + transform: translate3d(-50%, -50%, 0); + font-size: 40px; + color: rgb(233, 233, 233); + z-index: 2; + } + } + } + + .list-img { + position: absolute; + left: 50%; + top: 50%; + transform: translate3d(-50%, -50%, 0); + width: 153px; + height: 153px; + border-radius: 8px; + box-shadow: 0 0 13px rgba(0, 0, 0, 0.534); + backface-visibility: hidden; + } + + .list-name { + position: absolute; + top: 165px; + line-height: 18px; + font-size: $font-size-l; + width: 86%; + margin: 0 7%; + } + } + } +} \ No newline at end of file diff --git a/srcWeb/pages/Search/index.js b/srcWeb/pages/Search/index.js new file mode 100644 index 0000000..27ebd06 --- /dev/null +++ b/srcWeb/pages/Search/index.js @@ -0,0 +1,445 @@ +import React, { Component } from 'react'; +import { formatDate, imageRatio } from '../../common/js/utl'; +import { connect } from 'react-redux'; +import { If } from 'react-if'; +import { + getAlbumInfoAction, + getSingerInfoAction, + getMusicListDetailAction +} from '../../store/actionCreator'; +import { getHotSearch, getSearchResult } from '../../api/search'; + +import ShowList from '../../base/ShowList'; +import Loading from '../../base/Loading'; + +import './style.scss'; + +const SEARCH_TYPES = { + SONGS: 1, + ALBUMS: 10, + SINGERS: 100, + PLAYLIST: 1000 +}; +const KEYBOARY_ENTER_CODE = 13; + +class Search extends Component { + constructor (props) { + super(props); + this.state = { + hotSearch: null, + searchVal: '', + result: { + songs: null, + albums: null, + singers: null, + playlist: null + }, + searchType: 'songs', + showLoading: false + }; + } + + componentDidMount () { + getHotSearch().then(({ data: { result: { hots } } }) => { + this.setState(() => ({ + hotSearch: hots + })); + }); + } + + changeCurrentSearchType = (searchType) => { + if (searchType === this.state.searchType) { + return; + } else if (this.state.searchVal === '') { + this.setState(() => ({ + searchType + })); + return; + } + this.setState( + () => ({ + searchType + }), + () => { + this.handleGetType(); + } + ); + }; + + toggleShowLoading = () => { + this.setState((prevProps) => ({ + showLoading: !prevProps.showLoading + })); + } + + handleGetSongs = () => { + this.toggleShowLoading(); + getSearchResult(this.state.searchVal, SEARCH_TYPES.SONGS).then( + ({ + data: { + result: { songs } + } + }) => { + const r = JSON.parse(JSON.stringify(this.state.result)); + r.songs = formatTracks(songs); + this.setState(() => ({ + result: r + })); + this.toggleShowLoading(); + } + ).catch(() => { + this.toggleShowLoading(); + }); + }; + + handleGetAlbums = () => { + this.toggleShowLoading(); + getSearchResult(this.state.searchVal, SEARCH_TYPES.ALBUMS).then( + ({ data }) => { + const r = JSON.parse(JSON.stringify(this.state.result)); + r.albums = data.result.albums; + this.setState(() => ({ + result: r + })); + this.toggleShowLoading(); + } + ).catch(() => { + this.toggleShowLoading(); + }); + }; + + handleGetSingers = () => { + this.toggleShowLoading(); + getSearchResult(this.state.searchVal, SEARCH_TYPES.SINGERS).then(({ data }) => { + const r = JSON.parse(JSON.stringify(this.state.result)); + r.singers = data.result.artists; + this.setState(() => ({ + result: r + })); + this.toggleShowLoading(); + }).catch(() => { + this.toggleShowLoading(); + }); + } + + handleGetPlaylist = () => { + this.toggleShowLoading(); + getSearchResult(this.state.searchVal, SEARCH_TYPES.PLAYLIST).then(({ data }) => { + const r = JSON.parse(JSON.stringify(this.state.result)); + r.playlist = data.result.playlists; + this.setState(() => ({ + result: r + })); + this.toggleShowLoading(); + }).catch(() => { + this.toggleShowLoading(); + }); + } + + handleKeydown = (e) => { + if (e.keyCode === KEYBOARY_ENTER_CODE) { + this.handleGetType(); + } + }; + + handleGetType () { + this.setState( + () => ({ + result: { + songs: null, + albums: null, + singers: null, + playlist: null + } + }), + () => { + switch (this.state.searchType) { + case 'songs': + this.handleGetSongs(); + break; + case 'albums': + this.handleGetAlbums(); + break; + case 'singers': + this.handleGetSingers(); + break; + case 'playlist': + this.handleGetPlaylist(); + break; + default: + break; + } + } + ); + } + + handleClickHotSearch = (val) => { + this.setState( + () => ({ searchVal: val }), + () => { + this.handleGetType(); + } + ); + }; + + renderHotSearch = () => { + const { hotSearch } = this.state; + if (!hotSearch) { + return null; + } else { + return hotSearch.map((item, index) => { + if (index === 0) { + return ( + this.handleClickHotSearch(item.first)} + key={item.first} + className="first-hot-search" + > + {item.first} + HOT + + ); + } else { + return ( + this.handleClickHotSearch(item.first)} + key={item.first} + > + {item.first} + + ); + } + }); + } + }; + + renderResult = () => { + switch (this.state.searchType) { + case 'songs': + return this.renderResultSongs(); + case 'albums': + return this.renderResultAlbums(); + case 'singers': + return this.renderResultSingers(); + case 'playlist': + return this.renderResultPlayList(); + default: + break; + } + }; + + renderResultSongs = () => { + if (!this.state.result.songs) { + return null; + } else { + return ; + } + }; + + renderResultAlbums = () => { + if (!this.state.result.albums) { + return null; + } else { + return ( +
      + {this.state.result.albums.map((item) => { + return ( +
    • +
      this.props.handleGetAlbumInfo(item.id)} + > + 专辑图片 +
      +

      this.props.handleGetAlbumInfo(item.id)} + > + {item.name} +

      +

      this.props.handleGetSingerInfo(item.artist.id)} + > + {item.artist.name} +

      +
      + {formatDate(item.publishTime)} +
      +
    • + ); + })} +
    + ); + } + }; + + renderResultSingers = () => { + if (!this.state.result.singers) { + return null; + } else { + return ( +
      + {this.state.result.singers.map((item) => { + return ( +
    • this.props.handleGetSingerInfo(item.id)}> +
      + +
      +

      {item.name}

      +
    • + ); + })} +
    + ); + } + } + + renderResultPlayList = () => { + if (!this.state.result.playlist) { + return null; + } else { + return ( +
      + {this.state.result.playlist.map((item) => { + return ( +
    • this.props.handleGetMusicListDetail(item.id)}> +
      + +
      +

      TRACKS: {item.trackCount}

      +

      {item.name}

      +
    • + ); + })} +
    + ); + } + } + + render () { + const { searchType } = this.state; + return ( +
    +
    +
    + + { + const val = e.target.value; + this.setState(() => ({ searchVal: val })); + }} + onKeyDown={(e) => { + this.handleKeydown(e); + }} + /> +
    +
    {this.renderHotSearch()}
    +
    + +
    {this.renderResult()}
    +
    + +
    + +
    +
    +
    +
    + ); + } +} + +const mapStateToProps = (state) => { + return { + showMusicList: state.showMusicList, + showSingerInfo: state.showSingerInfo + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + handleGetSingerInfo (id) { + dispatch(getSingerInfoAction(id)); + }, + handleGetAlbumInfo (albumId) { + dispatch(getAlbumInfoAction(albumId)); + }, + handleGetMusicListDetail (id) { + dispatch(getMusicListDetailAction(id)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Search); + +function formatTracks (list) { + return list.map((item) => { + const singers = item.artists.map((item) => { + return { + id: item.id, + name: item.name + }; + }); + return { + id: item.id, + musicName: item.name, + imgUrl: null, + singers, + album: { + id: item.album.id, + name: item.album.name + } + }; + }); +} diff --git a/srcWeb/pages/Search/style.scss b/srcWeb/pages/Search/style.scss new file mode 100644 index 0000000..41ae9c6 --- /dev/null +++ b/srcWeb/pages/Search/style.scss @@ -0,0 +1,266 @@ +@import '../../common/scss/variable.scss'; + +.hide-page-search { + display: none; +} + +.page-search { + position: fixed; + top: 95px; + left: 0; + bottom: 80px; + width: 100%; + z-index: 0; + overflow-y: scroll; +} + +.search-container{ + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 800px; + + .search-input-container { + position: relative; + width: 100%; + margin-bottom: 15px; + + i { + position: absolute; + top: 11px; + left: 5px; + color: $color-text; + } + + input { + padding-left: 30px; + padding-right: 40px; + box-sizing: border-box; + width: 800px; + height: 40px; + color: $color-text; + font-size: $font-size-xxl; + background: none; + border: none; + border-bottom: 1px solid $color-text-gray; + outline: none; + } + } + + .hot-search-container { + color: $color-text; + margin-bottom: 10px; + + span { + display: inline-block; + padding: 8px 10px; + margin-right: 10px; + margin-bottom: 5px; + background: $color-background-opacity; + font-size: $font-size-m; + border-radius: 5px; + cursor: pointer; + transition: all 0.3s; + + &:hover { + color: $color-theme; + + i { + background: $color-theme; + color: $color-text; + } + } + } + + .first-hot-search { + position: relative; + padding-right: 45px; + + i { + position: absolute; + top: 8px; + right: 9px; + padding: 0 2px 1px 2px; + font-size: $font-size-m; + background: red; + } + } + } + + .search-result { + width: 100%; + + nav { + display: flex; + align-items: center; + margin-bottom: 10px; + width: 100%; + height: 50px; + color: $color-text; + background: $color-background-opacity; + + span { + display: inline-block; + margin: 0 10px; + padding: 0 10px; + line-height: 50px; + font-size: $font-size-xl; + cursor: pointer; + transition: all 0.3s; + + &:hover { + color: $color-theme; + } + } + + .active { + color: $color-theme; + } + } + + .result-albums { + width: 100%; + + li { + position: relative; + display: flex; + align-items: center; + margin: 10px 0; + color: $color-text; + font-size: $font-size-x; + } + + .album-img { + flex: 0 0 90px; + width: 60px; + height: 60px; + overflow: hidden; + cursor: pointer; + + img { + width: 60px; + height: 60px; + min-height: 60px; + } + } + + .album-name, .singer-name { + transition: all 0.3s; + + &:hover { + color: $color-theme; + } + } + + .album-name { + flex: 0 0 300px; + cursor: pointer; + } + + .singer-name { + flex: 0 0 200px; + cursor: pointer; + } + + .publish-time { + position: absolute; + right: 0; + } + } + + .result-singers { + margin-top: 20px; + width: 830px; + transform: translateX(-15px); + + li { + display: inline-block; + box-sizing: border-box; + padding: 0 calc((100% / 6 - 100px) / 2); + margin-bottom: 20px; + width: calc(100% / 6); + color: $color-text; + font-size: $font-size-l; + cursor: pointer; + transition: all 0.3s; + + &:hover { + color: $color-theme; + } + + p { + float: left; + } + + .img-container { + width: 130px; + height: 130px; + overflow: hidden; + margin-bottom: 5px; + border-radius: 5px; + + img { + border-radius: 5px; + width: 100px; + min-height: 100px; + // float: left; + } + } + } + } + + .result-playlist { + margin-top: 20px; + width: 830px; + transform: translateX(-15px); + + li { + display: inline-block; + margin: 0 calc((100% / 5 - 130px) / 2); + margin-bottom: 20px; + width: 130px; + color: $color-text; + font-size: $font-size-l; + cursor: pointer; + + &:hover { + .name { + color: $color-theme; + } + } + + p { + float: left; + width: 130px; + line-height: 18px; + transition: all 0.3s; + } + + .count { + color: $color-text-gray; + } + + .img-container { + width: 130px; + height: 130px; + overflow: hidden; + margin-bottom: 5px; + border-radius: 5px; + + img { + border-radius: 5px; + width: 130px; + min-height: 130px; + } + } + } + } + } + + .loading-container { + position: fixed; + top: 40vh; + left: 50%; + transform: translateX(-50%); + } +} \ No newline at end of file diff --git a/srcWeb/renderer/components/MyTitle/index.js b/srcWeb/renderer/components/MyTitle/index.js new file mode 100644 index 0000000..dd97afb --- /dev/null +++ b/srcWeb/renderer/components/MyTitle/index.js @@ -0,0 +1,25 @@ +import React, { Component } from 'react'; + +// import TitleBtn from '../TitleBtn'; + +import './style.scss'; + +// const { remote } = window.require('electron'); +// const currentWindow = remote.getCurrentWindow(); + +class MyTitle extends Component { + render () { + return ( + //
    currentWindow.minimize()}> +
    +
    + {/* + + */} +
    +
    + ); + } +} + +export default MyTitle; diff --git a/srcWeb/renderer/components/MyTitle/style.scss b/srcWeb/renderer/components/MyTitle/style.scss new file mode 100644 index 0000000..40d2f1e --- /dev/null +++ b/srcWeb/renderer/components/MyTitle/style.scss @@ -0,0 +1,13 @@ +.my-title { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 32px; + // background-color: rgb(198, 47, 47); + -webkit-app-region: drag; +} + +.title-btn-container { + margin-top: 8px; +} \ No newline at end of file diff --git a/srcWeb/renderer/components/TitleBtn/index.js b/srcWeb/renderer/components/TitleBtn/index.js new file mode 100644 index 0000000..532e22b --- /dev/null +++ b/srcWeb/renderer/components/TitleBtn/index.js @@ -0,0 +1,31 @@ +import React, { Component } from 'react'; +import './style.scss'; + +const { ipcRenderer: ipc } = window.require('electron'); + +const STYLE = { + min: { + backgroundColor: 'green', + right: '100px' + }, + max: { + backgroundColor: 'yellow', + right: '60px' + }, + close: { + backgroundColor: 'black', + right: '20px' + } +}; + +class TitleBtn extends Component { + handleBtnClick = () => { + ipc.send(this.props.type); + } + + render () { + return
    ; + } +} + +export default TitleBtn; diff --git a/srcWeb/renderer/components/TitleBtn/style.scss b/srcWeb/renderer/components/TitleBtn/style.scss new file mode 100644 index 0000000..e5912b2 --- /dev/null +++ b/srcWeb/renderer/components/TitleBtn/style.scss @@ -0,0 +1,8 @@ +.title-btn { + display: inline-block; + margin: 0 3px; + width: 12px; + height: 12px; + border-radius: 50%; + -webkit-app-region: no-drag; +} \ No newline at end of file diff --git a/srcWeb/store/actionCreator.js b/srcWeb/store/actionCreator.js new file mode 100644 index 0000000..4a411b9 --- /dev/null +++ b/srcWeb/store/actionCreator.js @@ -0,0 +1,451 @@ +import * as types from './actionTypes'; +import { getMusicUrl, getMusicLyric, getSingerInfo, getAlbumInfo, getMusicDetail, getMusicListDetail } from '../api'; +import $db from '../data'; +import { findIndex } from '../common/js/utl'; +import message from '../base/Message'; + +import { PLAY_MODE_TYPES } from '../common/js/config'; + +export const getChangeCollectorAction = (value) => ({ + type: types.CHANGE_COLLECTOR, + value +}); + +export const getRefreshCollectorAction = (value) => ({ + type: types.REFRESH_COLLECTOR, +}); + +export const getChangeCurrentMusicListAction = (value) => ({ + type: types.CHANGE_CURRENT_MUSIC_LIST, + value +}); + +/** + * 获取歌单详情,并显示歌单 + * @param {number} id + */ +export const getMusicListDetailAction = (id) => { + return (dispatch) => { + dispatch(getChangeShowLoadingAction(true)); + getMusicListDetail(id).then(({ data }) => { + // 将歌单传入 redux 中的 musicList + data.playlist.tracks = formatMusicListTracks(data.playlist.tracks); + dispatch(getChangeCurrentMusicListAction(data.playlist)); + dispatch(getChangeShowLoadingAction(false)); + }).catch(() => { + dispatch(getChangeShowLoadingAction(false)); + }); + }; +}; + +/** + * 控制 Loading 的显示 + * @param {Boolean} value + */ +export const getChangeShowLoadingAction = (value) => ({ + type: types.CHANGE_SHOW_LOADING, + value +}); + +/** + * 隐藏 *歌曲列表* + */ +export const getHideMusicListAction = () => ({ + type: types.HIDE_MUSIC_LIST +}); + +/** + * 隐藏 *歌手详情* + */ +export const getHideSingerInfoAction = () => ({ + type: types.HIDE_SINGER_INFO +}); + +/** + * 隐藏 *歌手详情* *歌曲列表* *歌曲详情* + */ +export const getHideAllAction = () => ({ + type: types.HIDE_ALL +}); + +/** + * 开关:显示 / 隐藏 *歌曲详情* + */ +export const toggleShowMusicDetail = () => ({ + type: types.TOGGLE_SHOW_MUSIC_DETAIL +}); + +/** + * 改变当前播放歌曲信息 + * @param {Object} value + */ +export const changeCurrentMusicAction = (value) => ({ + type: types.CHANGE_CURRENT_MUSIC, + value +}); + +/** + * 改变歌手信息 + * @param {Object} value + */ +export const changeSingerInfoAction = (value) => ({ + type: types.CHANGE_SINGER_INFO, + value +}); + +/** + * 获取歌手信息 + */ +export const getSingerInfoAction = (singerId) => { + return (dispatch) => { + dispatch(getChangeShowLoadingAction(true)); + dispatch(changeSingerInfoAction(null)); + getSingerInfo(singerId).then((res) => { + dispatch(changeSingerInfoAction(res.data)); + dispatch(getChangeShowLoadingAction(false)); + }).catch(() => { + dispatch(getChangeShowLoadingAction(false)); + message.info('暂时不能查询到此歌手'); + dispatch(getHideSingerInfoAction()); + }); + }; +}; + +/** + * 获取专辑内容 + */ +export const getAlbumInfoAction = (albumId) => { + return (dispatch) => { + dispatch(getChangeShowLoadingAction(true)); + getAlbumInfo(albumId).then(({ data: {album, songs} }) => { + const list = { + name: album.name, + id: album.id, + description: album.description ? album.description : '', + coverImgUrl: album.picUrl, + tracks: formatAlbumTracks(songs), + company: album.company, + publishTime: album.publishTime, + artist: album.artist, + type: album.type + }; + dispatch(getChangeShowLoadingAction(false)); + dispatch(getChangeCurrentMusicListAction(list)); + + // 隐藏歌手详情,歌手详情遮挡住专辑内容 + dispatch(getHideSingerInfoAction()); + }).catch(() => { + dispatch(getChangeShowLoadingAction(false)); + }); + }; +}; + +/** + * 改变当前播放列表 + */ +export const getChangePlayListAction = (value) => ({ + type: types.CHANGE_PLAY_LIST, + value +}); + +/** + * 清空播放列表 + */ +export const emptyPlayList = () => { + return (dispatch) => { + const EMPTY_PLAY_LIST = []; + const STOP = false; + dispatch(getChangePlayListAction(EMPTY_PLAY_LIST)); + dispatch(getChangePlayingStatusAction(STOP)); + }; +}; + +/** + * 改变当前播放索引 currentIndex + */ +export const getChangeCurrentIndex = (index) => ({ + type: types.CHANGE_CURRENT_INDEX, + index +}); + +/** + * 改变音量 + */ +export const getChangeVolumeAction = (value) => ({ + type: types.CHANGE_VOLUME, + value +}); + +/** + * 改变音乐播放状态 + * @param {Boolean} status + */ +export const getChangePlayingStatusAction = (status) => ({ + type: types.CHANGE_PLAYING_STATUS, + status +}); + +/** + * 改变音乐播放模式 + */ +export const getChangePlayModeAction = (value) => ({ + type: types.CHANGE_PLAY_MODE, + value +}); + +export const changeCurrentMusicLyric = (value) => ({ + type: types.CHANGE_CURRENT_MUSIC_LYRIC, + value +}); + +function getCurrentMusicLyric () { + return (dispatch, getState) => { + const state = JSON.parse(JSON.stringify(getState())); + const currentMusic = state.currentMusic; + const id = currentMusic.id; + // 清空之前的歌词 + dispatch(changeCurrentMusicLyric(null)); + + // 获取新的歌词 + getMusicLyric(id).then(({ data }) => { + dispatch(changeCurrentMusicLyric(data)); + }); + }; +} + +/** + * **点击歌曲播放逻辑:** + * 1. 点击歌曲的时候使用 getChangeCurrentMusic + * 2. 使用 redux-thunk 中间件,在 actoin 中发出获取歌曲 url 的请求 + * 3. 获取 url 之后在 action 中直接调用 actionCreator 中的 changeCurrentMusicAction + * 来对 redux 中的 currentMusic 进行修改 + */ +export const getChangeCurrentMusic = (value, loadCacheMusic = false) => { + return (dispatch, getState) => { + const state = getState(); + const list = state.playList; + // 从歌曲列表中寻找当前歌曲的 index + const index = findIndex(list, value); + // 当点击的歌曲是正在播放的歌曲,直接返回 + if (index === state.currentIndex && !loadCacheMusic) { + return; + } + if (index >= 0) { + // 如果 index >= 0 就直接修改 currentIndex + dispatch(getChangeCurrentIndex(index)); + } else { + // 如果没有这首歌 + // 1. push 这首歌到 playList 中 + // 2. 改变 index + list.push(value); + dispatch(getChangePlayListAction(list)); + dispatch(getChangeCurrentIndex(list.length - 1)); + } + dispatch(changeCurrentMusicAction(value)); + dispatch(getCurrentMusicLyric()); + getMusicUrl(value.id).then(({ data: { data } }) => { + if (!data[0].url) { + message.info('歌曲暂无版权,我帮你换首歌吧'); + if (index !== list.length - 1) { + dispatch(playNextMusicAction()); + } + return; + } + value.musicUrl = data[0].url; + dispatch(changeCurrentMusicAction(value)); + + // 因为是打开程序的时候加载上次关闭的时候播放的歌,但是不能播放,所以需要暂停 + if (loadCacheMusic) { + const STOP = false; + dispatch(getChangePlayingStatusAction(STOP)); + } + + // 搜索的歌曲会没有图片,所以去歌曲详情弄一张图片回来 + if (!value.imgUrl) { + getMusicDetail(value.id).then(({data}) => { + value.imgUrl = data.songs[0].al.picUrl; + dispatch(changeCurrentMusicAction(value)); + }); + } + }); + }; +}; + +export const playPrevMusicAction = () => { + return (dispatch, getState) => { + const state = getState(); + let { currentIndex } = state; + const { playList } = state; + const length = playList.length; + if (length === 0 || length === 1) { + return; + } + if (state.playMode === PLAY_MODE_TYPES.RANDOM_PLAY) { + // 返回值不能等于原来的 index + currentIndex = random(currentIndex, length); + } else if (currentIndex > 0) { + currentIndex--; + } else { + currentIndex = length - 1; + } + dispatch(getChangeCurrentMusic(playList[currentIndex])); + dispatch(getChangeCurrentIndex(currentIndex)); + }; +}; + +export const playNextMusicAction = () => { + return (dispatch, getState) => { + const state = getState(); + let { currentIndex } = state; + const { playList } = state; + const length = playList.length; + if (length === 0 || length === 1) { + return; + } + if (state.playMode === PLAY_MODE_TYPES.RANDOM_PLAY) { + currentIndex = random(currentIndex, length); + } else if (currentIndex < length - 1) { + currentIndex++; + } else { + currentIndex = 0; + } + dispatch(getChangeCurrentMusic(playList[currentIndex])); + dispatch(getChangeCurrentIndex(currentIndex)); + }; +}; + +export const getDeleteMusicAction = (item) => { + return (dispatch, getState) => { + const state = getState(); + let { currentIndex } = JSON.parse(JSON.stringify(state)); + const { playList } = JSON.parse(JSON.stringify(state)); + const index = findIndex(playList, item); + playList.splice(index, 1); + if (index < currentIndex) { + currentIndex--; + dispatch(getChangeCurrentIndex(currentIndex)); + } else if (index === currentIndex) { + // 先播放下一首 + dispatch(playNextMusicAction()); + // 然后将 currentIndex 修改回来 + dispatch(getChangeCurrentIndex(currentIndex)); + } + // 当 playList 已经没有的时候,删除掉当前音乐的 url + // 音乐就会暂停播放 + if (playList.length === 0) { + const { currentMusic } = JSON.parse(JSON.stringify(state)); + currentMusic.musicUrl = ''; + dispatch(changeCurrentMusicAction(currentMusic)); + } + dispatch(getChangePlayListAction(playList)); + }; +}; + +/** + * 实现喜欢歌曲的功能 + */ +export const getAddToLikeListAction = (value) => { + return (dispatch, getState) => { + let collector = null; + $db.find({name: 'collector'}, (err, res) => { + collector = res[0]; + const index = findIndex(collector.foundList[0].tracks, value); + if (index < 0) { + collector.foundList[0].tracks.unshift(value); + message.info('已经加入到喜欢的歌曲中'); + } else { + collector.foundList[0].tracks.splice(index, 1); + } + $db.update({ name: 'collector' }, collector, () => { + dispatch(getChangeCollectorAction(collector)); + }); + }); + }; +}; + +/** + * 收藏 / 取消收藏 歌单 + */ +export const getToggleCollectPlaylist = (list) => { + return (dispatch) => { + $db.find({ name: 'collector' }, (err, res) => { + const collector = res[0]; + const index = findIndex(collector.collectList, list); + if (index < 0) { + collector.collectList.push(list); + dispatch(getChangeCollectorAction(collector)); + $db.update({ name: 'collector' }, collector, () => { + message.info('收藏歌单成功'); + }); + } else { + collector.collectList.splice(index, 1); + dispatch(getChangeCollectorAction(collector)); + $db.update({ name: 'collector' }, collector); + } + }); + }; +}; + +/** + * 加载缓存信息 + */ +export const getLoadCacheAction = (cache) => { + return (dispatch) => { + dispatch(getChangePlayListAction(cache.playList)); + dispatch(getChangeVolumeAction(cache.volume)); + dispatch(getChangeCurrentIndex(cache.currentIndex)); + if (cache.currentIndex !== -1 && cache.playList.length !== 0) { + dispatch(getChangeCurrentMusic(cache.playList[cache.currentIndex], true)); + } + }; +}; + +function random (index, length) { + const res = Math.floor(Math.random() * length); + if (res === index) { + return random(index, length); + } + return res; +} + +function formatAlbumTracks (list) { + return list.map((item) => { + const singers = item.ar.map((item) => { + return { + id: item.id, + name: item.name + }; + }); + return { + id: item.id, + musicName: item.name, + imgUrl: item.al.picUrl, + singers, + album: { + id: item.al.id, + name: item.al.name + } + }; + }); +} + +function formatMusicListTracks (list) { + return list.map((item) => { + const singers = item.ar.map((item) => { + return { + id: item.id, + name: item.name + }; + }); + return { + id: item.id, + musicName: item.name, + imgUrl: item.al.picUrl, + singers, + album: { + id: item.al.id, + name: item.al.name + } + }; + }); +} diff --git a/srcWeb/store/actionTypes.js b/srcWeb/store/actionTypes.js new file mode 100644 index 0000000..ef0a987 --- /dev/null +++ b/srcWeb/store/actionTypes.js @@ -0,0 +1,32 @@ +export const CHANGE_CURRENT_MUSIC_LIST = 'CHANGE_CURRENT_MUSIC_LIST'; + +export const HIDE_MUSIC_LIST = 'HIDE_MUSIC_LIST'; + +export const CHANGE_CURRENT_MUSIC = 'CHANGE_CURRENT_MUSIC'; + +export const CHANGE_PLAYING_STATUS = 'CHANGE_PLAYING_STATUS'; + +export const CHANGE_PLAY_LIST = 'CHANGE_PLAY_LIST'; + +export const CHANGE_CURRENT_INDEX = 'CHANGE_CURRENT_INDEX'; + +export const CHANGE_PLAY_MODE = 'CHANGE_PLAY_MODE'; + +export const TOGGLE_SHOW_MUSIC_DETAIL = 'TOGGLE_SHOW_MUSIC_DETAIL'; + +export const CHANGE_CURRENT_MUSIC_LYRIC = 'CHANGE_CURRENT_MUSIC_LYRIC'; + +export const CHANGE_SINGER_INFO = 'CHANGE_SINGER_INFO'; + +export const HIDE_SINGER_INFO = 'HIDE_SINGER_INFO'; + +export const CHANGE_COLLECTOR = 'CHANGE_COLLECTOR'; + +export const REFRESH_COLLECTOR = 'REFRESH_COLLECTOR'; + +export const CHANGE_SHOW_LOADING = 'CHANGE_SHOW_LOADING'; + +export const HIDE_ALL = 'HIDE_ALL'; + +export const CHANGE_VOLUME = 'CHANGE_VOLUME'; + diff --git a/srcWeb/store/index.js b/srcWeb/store/index.js new file mode 100644 index 0000000..eaad023 --- /dev/null +++ b/srcWeb/store/index.js @@ -0,0 +1,9 @@ +import { createStore, applyMiddleware, compose } from 'redux'; +import reducer from './reducer'; +import thunk from 'redux-thunk'; + +const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + +const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk))); + +export default store; diff --git a/srcWeb/store/reducer.js b/srcWeb/store/reducer.js new file mode 100644 index 0000000..e89756c --- /dev/null +++ b/srcWeb/store/reducer.js @@ -0,0 +1,194 @@ +import * as types from './actionTypes'; +import $db from '../data'; + +import { PLAY_MODE_TYPES } from '../common/js/config'; + +const DEFAULT_VOLUME = 0.35; + +// 给一个初始的 state +const defaultState = { + // 当前展示的歌单列表 + musicList: null, + + // 控制歌单列表的显示 + showMusicList: false, + + // 控制歌曲详情的显示 + showMusicDetail: false, + + // 控制歌手详情的显示 + showSingerInfo: false, + + // 歌手详情 + singerInfo: null, + + // 当前播放的歌曲 + currentMusic: { + id: 442009238, + musicName: '上野公园', + musicUrl: '', + imgUrl: + 'http://p2.music.126.net/64JozXeLm7ErtXpwGrwwEw==/109951162811190850.jpg', + singers: [{ + id: 12195169, + name: 'Atta Girl' + }], + album: { + id: null, + name: 'Everyone Loves You When You Were Still A Kid' + } + }, + + currentMusicLyric: null, + + // 播放状态 + playing: false, + + // 播放列表 + playList: [], + + // 当前播放索引 + currentIndex: 0, + + // 播放模式 + playMode: PLAY_MODE_TYPES.SEQUENCE_PLAY, + + // 收藏 + collector: null, + + // 显示全局的 Loding + showLoading: false, + + // 音量 + volume: DEFAULT_VOLUME +}; + +// state 里面存放了所有的数据 +// reducer 可以接收 state,但是绝对不可以修改 state +export default (state = defaultState, action) => { + if (action.type === types.CHANGE_CURRENT_MUSIC_LIST) { + const newState = deepCopy(state); + newState.musicList = action.value; + if (action.value) { + newState.showMusicList = true; + } + return newState; + } + if (action.type === types.HIDE_MUSIC_LIST) { + const newState = deepCopy(state); + newState.showMusicList = false; + return newState; + } + if (action.type === types.CHANGE_CURRENT_MUSIC) { + const newState = deepCopy(state); + newState.currentMusic = action.value; + newState.playing = true; + return newState; + } + if (action.type === types.CHANGE_PLAYING_STATUS) { + const newState = deepCopy(state); + newState.playing = action.status; + return newState; + } + if (action.type === types.CHANGE_PLAY_LIST) { + const newState = deepCopy(state); + newState.playList = action.value; + cacheLastUseInfo({ playList: action.value }); + return newState; + } + if (action.type === types.CHANGE_CURRENT_INDEX) { + const newState = deepCopy(state); + newState.currentIndex = action.index; + cacheLastUseInfo({ currentIndex: action.index, playList: newState.playList }); + return newState; + } + if (action.type === types.CHANGE_PLAY_MODE) { + const newState = deepCopy(state); + newState.playMode = action.value; + return newState; + } + if (action.type === types.TOGGLE_SHOW_MUSIC_DETAIL) { + const newState = deepCopy(state); + newState.showMusicDetail = !newState.showMusicDetail; + return newState; + } + if (action.type === types.CHANGE_CURRENT_MUSIC_LYRIC) { + const newState = deepCopy(state); + newState.currentMusicLyric = action.value; + return newState; + } + if (action.type === types.CHANGE_SINGER_INFO) { + const newState = deepCopy(state); + newState.singerInfo = action.value; + newState.showSingerInfo = true; + return newState; + } + if (action.type === types.HIDE_SINGER_INFO) { + const newState = deepCopy(state); + newState.showSingerInfo = false; + return newState; + } + if (action.type === types.CHANGE_COLLECTOR) { + const newState = deepCopy(state); + newState.collector = action.value; + return newState; + } + if (action.type === types.REFRESH_COLLECTOR) { + const newState = deepCopy(state); + newState.collector = getNewCollector(); + return newState; + } + if (action.type === types.CHANGE_SHOW_LOADING) { + const newState = deepCopy(state); + newState.showLoading = action.value; + return newState; + } + if (action.type === types.HIDE_ALL) { + const newState = deepCopy(state); + newState.showMusicList = false; + newState.showSingerInfo = false; + newState.showMusicDetail = false; + return newState; + } + if (action.type === types.CHANGE_VOLUME) { + const newState = deepCopy(state); + newState.volume = action.value; + cacheLastUseInfo({ volume: action.value }); + return newState; + } + return state; +}; + +function deepCopy (val) { + return JSON.parse(JSON.stringify(val)); +} + +function getNewCollector () { + let newCollector = null; + $db.find({ name: 'collector' }, function (err, res) { + newCollector = res[0]; + }); + return newCollector; +} + +function cacheLastUseInfo (obj = {}) { + let cache = null, needUpdate = false; + $db.find({ name: 'cache' }, (err, res) => { + cache = res[0]; + if (obj.volume !== undefined) { + cache.cacheValue.volume = obj.volume; + needUpdate = true; + } + if (obj.playList && JSON.stringify(obj.playList) !== JSON.stringify(cache.cacheValue.playList)) { + cache.cacheValue.playList = obj.playList; + needUpdate = true; + } + if (obj.currentIndex !== undefined && obj.currentIndex !== cache.cacheValue.currentIndex) { + cache.cacheValue.currentIndex = obj.currentIndex; + needUpdate = true; + } + if (needUpdate) { + $db.update({ name: 'cache' }, cache); + } + }); +} From 2d9bc5ce3091f13f2898baad90b25f8568b48ea5 Mon Sep 17 00:00:00 2001 From: caijin Date: Fri, 13 Dec 2019 22:36:53 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=E6=89=93=E5=8C=85=E4=BA=86=20web?= =?UTF-8?q?=20=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- srcWeb/App.js | 6 +- srcWeb/pages/About/index.js | 177 ++++++++++++++++++------------------ 2 files changed, 92 insertions(+), 91 deletions(-) diff --git a/srcWeb/App.js b/srcWeb/App.js index befd48b..7cd8095 100644 --- a/srcWeb/App.js +++ b/srcWeb/App.js @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import { BrowserRouter as Router, Route, Redirect } from 'react-router-dom'; +import { HashRouter as Router, Route, Redirect } from 'react-router-dom'; import { connect } from 'react-redux'; import { getChangeCollectorAction, @@ -11,7 +11,7 @@ import Recommend from './pages/Recommend'; import Search from './pages/Search'; import Collect from './pages/Collect'; import Rank from './pages/Rank'; -// import About from './pages/About'; +import About from './pages/About'; import Header from './components/Header'; import Player from './components/Player'; @@ -94,7 +94,7 @@ class App extends Component { - {/**/} + { this.state.redirect ? : null} {this.props.showLoading ? ( diff --git a/srcWeb/pages/About/index.js b/srcWeb/pages/About/index.js index bc81cab..642fde4 100644 --- a/srcWeb/pages/About/index.js +++ b/srcWeb/pages/About/index.js @@ -6,9 +6,9 @@ import $db from '../../data'; import './style.scss'; -const shell = window.require('electron').shell; -const fs = window.require('fs'); -const { dialog } = window.require('electron').remote; +// const shell = window.require('electron').shell; +// const fs = window.require('fs'); +// const { dialog } = window.require('electron').remote; class Rank extends Component { constructor (props) { @@ -19,92 +19,93 @@ class Rank extends Component { } handleOpenExternalUrl = (url) => { - shell.openExternal(url); + // shell.openExternal(url); + window.open(url, '_blank'); }; - handleExportCollector = () => { - const filters = [ - { - name: 'json', - extensions: ['json'] // 文件后缀名类型, 如 md json - } - ]; - // https://electronjs.org/docs/api/dialog#dialogshowsavedialogbrowserwindow-options-callback - dialog.showSaveDialog( - { - filters, - defaultPath: 'here-music-collector' - }, - (filename) => { - if (!filename) { - return; - } - // http://nodejs.cn/api/fs.html#fs_fs_writefile_file_data_options_callback - fs.writeFile(filename, JSON.stringify(this.props.collector), (err) => { - message.info('!Congratulation! 备份成功 !Congratulation!'); - if (err) { throw err; } - }); - } - ); - }; + // handleExportCollector = () => { + // const filters = [ + // { + // name: 'json', + // extensions: ['json'] // 文件后缀名类型, 如 md json + // } + // ]; + // // https://electronjs.org/docs/api/dialog#dialogshowsavedialogbrowserwindow-options-callback + // dialog.showSaveDialog( + // { + // filters, + // defaultPath: 'here-music-collector' + // }, + // (filename) => { + // if (!filename) { + // return; + // } + // // http://nodejs.cn/api/fs.html#fs_fs_writefile_file_data_options_callback + // fs.writeFile(filename, JSON.stringify(this.props.collector), (err) => { + // message.info('!Congratulation! 备份成功 !Congratulation!'); + // if (err) { throw err; } + // }); + // } + // ); + // }; - handleImportCollector = () => { - // https://electronjs.org/docs/api/dialog#dialogshowopendialogbrowserwindow-options-callback - dialog.showOpenDialog( - { - properties: ['openFile'] - }, - (filename) => { - if (!filename || filename.length === 0) { return; } - fs.readFile(filename[0], (err, fd) => { - if (err) { - if (err.code === 'ENOENT') { - return; - } - throw err; - } - try { - const collector = JSON.parse(fd); - if (collector.name !== 'collector') { - message.info('!!请导入正确的备份文件!!'); - return; - } else { - $db.update({ name: 'collector' }, collector, () => { - this.props.handleChangeCollector(collector); - message.info('!Congratulation! 导入成功 !Congratulation!'); - }); - } - } catch { - message.info('!!请导入正确的备份文件!!'); - } - }); - } - ); - }; - - renderExportCollector () { - return ( -
  • -

    导出我的收藏

    -

    - 因为重装软件会导致收藏夹数据丢失,所以强烈建议在重装软件前导出收藏夹进行备份。 -

    - -
  • - ); - } + // handleImportCollector = () => { + // // https://electronjs.org/docs/api/dialog#dialogshowopendialogbrowserwindow-options-callback + // dialog.showOpenDialog( + // { + // properties: ['openFile'] + // }, + // (filename) => { + // if (!filename || filename.length === 0) { return; } + // fs.readFile(filename[0], (err, fd) => { + // if (err) { + // if (err.code === 'ENOENT') { + // return; + // } + // throw err; + // } + // try { + // const collector = JSON.parse(fd); + // if (collector.name !== 'collector') { + // message.info('!!请导入正确的备份文件!!'); + // return; + // } else { + // $db.update({ name: 'collector' }, collector, () => { + // this.props.handleChangeCollector(collector); + // message.info('!Congratulation! 导入成功 !Congratulation!'); + // }); + // } + // } catch { + // message.info('!!请导入正确的备份文件!!'); + // } + // }); + // } + // ); + // }; - renderImportCollector () { - return ( -
  • -

    导入我的收藏夹

    -

    - 导入备份文件,恢复到我的收藏(会覆盖当前的收藏夹)。 -

    - -
  • - ); - } + // renderExportCollector () { + // return ( + //
  • + //

    导出我的收藏

    + //

    + // 因为重装软件会导致收藏夹数据丢失,所以强烈建议在重装软件前导出收藏夹进行备份。 + //

    + // + //
  • + // ); + // } + // + // renderImportCollector () { + // return ( + //
  • + //

    导入我的收藏夹

    + //

    + // 导入备份文件,恢复到我的收藏(会覆盖当前的收藏夹)。 + //

    + // + //
  • + // ); + // } renderAbout () { return ( @@ -134,7 +135,7 @@ class Rank extends Component { } > Issues - {' '} + 中提出。

    如果您喜欢 Here Music 的话,欢迎 Star 和 Fork 本项目。

    @@ -148,8 +149,8 @@ class Rank extends Component { return (
      - {this.renderExportCollector()} - {this.renderImportCollector()} + {/*{this.renderExportCollector()}*/} + {/*{this.renderImportCollector()}*/} {this.renderAbout()}
    From fcdbccf747514866cd1290984a0846bf101cdc08 Mon Sep 17 00:00:00 2001 From: caijin Date: Fri, 13 Dec 2019 22:40:48 +0800 Subject: [PATCH 3/7] feat: config rename --- {config => projectConfig}/index.js | 2 +- public/electron.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename {config => projectConfig}/index.js (69%) diff --git a/config/index.js b/projectConfig/index.js similarity index 69% rename from config/index.js rename to projectConfig/index.js index 8229566..dc872c0 100644 --- a/config/index.js +++ b/projectConfig/index.js @@ -6,4 +6,4 @@ const config = { module.exports = config; -// export default config; \ No newline at end of file +// export default projectConfig; diff --git a/public/electron.js b/public/electron.js index b49c5b5..44780f4 100644 --- a/public/electron.js +++ b/public/electron.js @@ -2,7 +2,7 @@ const { app, BrowserWindow, ipcMain, Menu, Tray, globalShortcut } = require('electron'); const path = require('path'); const api = require('../NeteaseCloudMusicApi/app'); -const config = require('../config'); +const config = require('../projectConfig'); const GLOBAL_SHORTCUT = { 'CommandOrControl+Alt+Right': 'nextMusic', From 5bf93faca201d1529f24dfe14ed8611ec0a7ee3d Mon Sep 17 00:00:00 2001 From: caijin Date: Sat, 14 Dec 2019 02:18:52 +0800 Subject: [PATCH 4/7] feat: eject --- package.json | 109 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 102 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 686da29..868970a 100644 --- a/package.json +++ b/package.json @@ -14,24 +14,70 @@ }, "homepage": ".", "dependencies": { + "@babel/core": "7.1.0", + "@svgr/webpack": "2.4.1", "antd": "^3.11.2", "apicache": "^1.2.1", "axios": "^0.18.0", + "babel-core": "7.0.0-bridge.0", + "babel-eslint": "9.0.0", + "babel-jest": "23.6.0", + "babel-loader": "8.0.4", + "babel-plugin-named-asset-import": "^0.2.3", + "babel-preset-react-app": "^6.1.0", + "bfj": "6.1.1", + "case-sensitive-paths-webpack-plugin": "2.1.2", + "chalk": "2.4.1", + "css-loader": "1.0.0", + "dotenv": "6.0.0", + "dotenv-expand": "4.2.0", + "eslint": "5.6.0", + "eslint-config-react-app": "^3.0.5", + "eslint-loader": "2.1.1", + "eslint-plugin-flowtype": "2.50.1", + "eslint-plugin-import": "2.14.0", + "eslint-plugin-jsx-a11y": "6.1.2", + "eslint-plugin-react": "7.11.1", "express": "^4.16.4", + "file-loader": "2.0.0", + "fork-ts-checker-webpack-plugin-alt": "0.4.14", + "fs-extra": "7.0.0", + "html-webpack-plugin": "4.0.0-alpha.2", + "identity-obj-proxy": "3.0.0", + "jest": "23.6.0", + "jest-pnp-resolver": "1.0.1", + "jest-resolve": "23.6.0", "lyric-parser": "^1.0.1", + "mini-css-extract-plugin": "0.4.3", "nedb": "^1.8.0", "node-sass": "^4.11.0", + "optimize-css-assets-webpack-plugin": "5.0.1", + "pnp-webpack-plugin": "1.1.0", + "postcss-flexbugs-fixes": "4.1.0", + "postcss-loader": "3.0.0", + "postcss-preset-env": "6.0.6", + "postcss-safe-parser": "4.0.1", "react": "^16.9.0", + "react-app-polyfill": "^0.1.3", + "react-dev-utils": "^6.1.1", "react-dom": "^16.9.0", "react-if": "^3.1.2", "react-redux": "^6.0.0", "react-router-dom": "^4.3.1", - "react-scripts": "2.1.1", "react-transition-group": "^2.5.1", "redux": "^4.0.1", "redux-devtools-extension": "^2.13.7", "redux-thunk": "^2.3.0", - "request": "^2.85.0" + "request": "^2.85.0", + "resolve": "1.8.1", + "sass-loader": "7.1.0", + "style-loader": "0.23.0", + "terser-webpack-plugin": "1.1.0", + "url-loader": "1.1.1", + "webpack": "4.19.1", + "webpack-dev-server": "3.1.9", + "webpack-manifest-plugin": "2.0.4", + "workbox-webpack-plugin": "3.6.3" }, "devDependencies": { "concurrently": "^4.1.0", @@ -42,10 +88,10 @@ }, "main": "./public/electron.js", "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject", + "start": "cross-env APP_TYPE=electron node scripts/start.js", + "start:web": "cross-env APP_TYPE=web node scripts/start.js", + "build": "node scripts/build.js", + "test": "node scripts/test.js", "electron-start": "cross-env NODE_ENV=development electron .", "electron-dev": "concurrently \"BROWSER=none yarn start\" \"wait-on http://localhost:3000 && cross-env NODE_ENV=development electron . \"", "pack": "electron-builder --dir", @@ -74,5 +120,54 @@ "not dead", "not ie <= 11", "not op_mini all" - ] + ], + "jest": { + "collectCoverageFrom": [ + "src/**/*.{js,jsx,ts,tsx}", + "!src/**/*.d.ts" + ], + "resolver": "jest-pnp-resolver", + "setupFiles": [ + "react-app-polyfill/jsdom" + ], + "testMatch": [ + "/src/**/__tests__/**/*.{js,jsx,ts,tsx}", + "/src/**/?(*.)(spec|test).{js,jsx,ts,tsx}" + ], + "testEnvironment": "jsdom", + "testURL": "http://localhost", + "transform": { + "^.+\\.(js|jsx|ts|tsx)$": "/node_modules/babel-jest", + "^.+\\.css$": "/config/jest/cssTransform.js", + "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "/config/jest/fileTransform.js" + }, + "transformIgnorePatterns": [ + "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$", + "^.+\\.module\\.(css|sass|scss)$" + ], + "moduleNameMapper": { + "^react-native$": "react-native-web", + "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy" + }, + "moduleFileExtensions": [ + "web.js", + "js", + "web.ts", + "ts", + "web.tsx", + "tsx", + "json", + "web.jsx", + "jsx", + "node" + ] + }, + "babel": { + "presets": [ + "react-app" + ] + }, + "eslintConfig": { + "extends": "react-app" + } } From b9db8c5285d846fc260458f3cc85358980969647 Mon Sep 17 00:00:00 2001 From: caijin Date: Sat, 14 Dec 2019 02:19:59 +0800 Subject: [PATCH 5/7] feat: eject and & webApp dev --- config/env.js | 93 + config/jest/cssTransform.js | 14 + config/jest/fileTransform.js | 30 + config/paths.js | 91 + config/webpack.config.dev.js | 423 + config/webpack.config.prod.js | 539 + config/webpackDevServer.config.js | 105 + package-lock.json | 20388 ++++++++++++++++++++++++++++ scripts/build.js | 189 + scripts/start.js | 116 + scripts/startWeb.js | 116 + scripts/test.js | 53 + src/{index.js => index.jsx} | 0 srcWeb/{index.js => index.jsx} | 0 srcWeb/pages/About/index.js | 6 +- 15 files changed, 22160 insertions(+), 3 deletions(-) create mode 100644 config/env.js create mode 100644 config/jest/cssTransform.js create mode 100644 config/jest/fileTransform.js create mode 100644 config/paths.js create mode 100644 config/webpack.config.dev.js create mode 100644 config/webpack.config.prod.js create mode 100644 config/webpackDevServer.config.js create mode 100644 package-lock.json create mode 100644 scripts/build.js create mode 100644 scripts/start.js create mode 100644 scripts/startWeb.js create mode 100644 scripts/test.js rename src/{index.js => index.jsx} (100%) rename srcWeb/{index.js => index.jsx} (100%) diff --git a/config/env.js b/config/env.js new file mode 100644 index 0000000..b0344c5 --- /dev/null +++ b/config/env.js @@ -0,0 +1,93 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const paths = require('./paths'); + +// Make sure that including paths.js after env.js will read .env variables. +delete require.cache[require.resolve('./paths')]; + +const NODE_ENV = process.env.NODE_ENV; +if (!NODE_ENV) { + throw new Error( + 'The NODE_ENV environment variable is required but was not specified.' + ); +} + +// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use +var dotenvFiles = [ + `${paths.dotenv}.${NODE_ENV}.local`, + `${paths.dotenv}.${NODE_ENV}`, + // Don't include `.env.local` for `test` environment + // since normally you expect tests to produce the same + // results for everyone + NODE_ENV !== 'test' && `${paths.dotenv}.local`, + paths.dotenv, +].filter(Boolean); + +// Load environment variables from .env* files. Suppress warnings using silent +// if this file is missing. dotenv will never modify any environment variables +// that have already been set. Variable expansion is supported in .env files. +// https://github.com/motdotla/dotenv +// https://github.com/motdotla/dotenv-expand +dotenvFiles.forEach(dotenvFile => { + if (fs.existsSync(dotenvFile)) { + require('dotenv-expand')( + require('dotenv').config({ + path: dotenvFile, + }) + ); + } +}); + +// We support resolving modules according to `NODE_PATH`. +// This lets you use absolute paths in imports inside large monorepos: +// https://github.com/facebook/create-react-app/issues/253. +// It works similar to `NODE_PATH` in Node itself: +// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders +// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. +// Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. +// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 +// We also resolve them to make sure all tools using them work consistently. +const appDirectory = fs.realpathSync(process.cwd()); +process.env.NODE_PATH = (process.env.NODE_PATH || '') + .split(path.delimiter) + .filter(folder => folder && !path.isAbsolute(folder)) + .map(folder => path.resolve(appDirectory, folder)) + .join(path.delimiter); + +// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be +// injected into the application via DefinePlugin in Webpack configuration. +const REACT_APP = /^REACT_APP_/i; + +function getClientEnvironment(publicUrl) { + const raw = Object.keys(process.env) + .filter(key => REACT_APP.test(key)) + .reduce( + (env, key) => { + env[key] = process.env[key]; + return env; + }, + { + // Useful for determining whether we’re running in production mode. + // Most importantly, it switches React into the correct mode. + NODE_ENV: process.env.NODE_ENV || 'development', + // Useful for resolving the correct path to static assets in `public`. + // For example, . + // This should only be used as an escape hatch. Normally you would put + // images into the `src` and `import` them in code to get their paths. + PUBLIC_URL: publicUrl, + } + ); + // Stringify all values so we can feed into Webpack DefinePlugin + const stringified = { + 'process.env': Object.keys(raw).reduce((env, key) => { + env[key] = JSON.stringify(raw[key]); + return env; + }, {}), + }; + + return { raw, stringified }; +} + +module.exports = getClientEnvironment; diff --git a/config/jest/cssTransform.js b/config/jest/cssTransform.js new file mode 100644 index 0000000..8f65114 --- /dev/null +++ b/config/jest/cssTransform.js @@ -0,0 +1,14 @@ +'use strict'; + +// This is a custom Jest transformer turning style imports into empty objects. +// http://facebook.github.io/jest/docs/en/webpack.html + +module.exports = { + process() { + return 'module.exports = {};'; + }, + getCacheKey() { + // The output is always the same. + return 'cssTransform'; + }, +}; diff --git a/config/jest/fileTransform.js b/config/jest/fileTransform.js new file mode 100644 index 0000000..07010e3 --- /dev/null +++ b/config/jest/fileTransform.js @@ -0,0 +1,30 @@ +'use strict'; + +const path = require('path'); + +// This is a custom Jest transformer turning file imports into filenames. +// http://facebook.github.io/jest/docs/en/webpack.html + +module.exports = { + process(src, filename) { + const assetFilename = JSON.stringify(path.basename(filename)); + + if (filename.match(/\.svg$/)) { + return `module.exports = { + __esModule: true, + default: ${assetFilename}, + ReactComponent: (props) => ({ + $$typeof: Symbol.for('react.element'), + type: 'svg', + ref: null, + key: null, + props: Object.assign({}, props, { + children: ${assetFilename} + }) + }), + };`; + } + + return `module.exports = ${assetFilename};`; + }, +}; diff --git a/config/paths.js b/config/paths.js new file mode 100644 index 0000000..de331cc --- /dev/null +++ b/config/paths.js @@ -0,0 +1,91 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const url = require('url'); + +// Make sure any symlinks in the project folder are resolved: +// https://github.com/facebook/create-react-app/issues/637 +const appDirectory = fs.realpathSync(process.cwd()); +const resolveApp = relativePath => path.resolve(appDirectory, relativePath); + +const envPublicUrl = process.env.PUBLIC_URL; + +function ensureSlash(inputPath, needsSlash) { + const hasSlash = inputPath.endsWith('/'); + if (hasSlash && !needsSlash) { + return inputPath.substr(0, inputPath.length - 1); + } else if (!hasSlash && needsSlash) { + return `${inputPath}/`; + } else { + return inputPath; + } +} + +const getPublicUrl = appPackageJson => + envPublicUrl || require(appPackageJson).homepage; + +// We use `PUBLIC_URL` environment variable or "homepage" field to infer +// "public path" at which the app is served. +// Webpack needs to know it to put the right