├── .babelrc ├── .codeclimate.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .gitmodules ├── .travis.yml ├── AUTHORS.md ├── LICENSE ├── README.md ├── azure-pipelines.yml ├── bin ├── clean-commits.sh ├── publish.sh └── update-authors.sh ├── common ├── cache.js └── storage.js ├── config ├── index.js ├── theme.js ├── webpack.config.base.js ├── webpack.config.dev.js ├── webpack.config.electron.js └── webpack.config.production.js ├── main.js ├── package.json ├── resource ├── 128x128.png ├── 16x16.png ├── 24x24.png ├── 256x256.png ├── 32x32.png ├── 48x48.png ├── 64x64.png ├── 96x96.png ├── dock.icns ├── donate.png └── ieaseMusic.ico ├── screenshots ├── alfred.png ├── artist.png ├── cmd+p.png ├── comments.png ├── cover.png ├── downloader.png ├── fm.png ├── home.png ├── lyrics.png ├── menu.png ├── player-2.png ├── player.png ├── playlist.png ├── preview.gif ├── search.png ├── settings.png ├── top.png ├── upnext.png └── user.png ├── server ├── api.js ├── dev.js ├── provider │ ├── Baidu.js │ ├── Kugou.js │ ├── Kuwo.js │ ├── MiGu.js │ ├── Netease.js │ ├── QQ.js │ ├── Xiami.js │ ├── index.js │ └── test.js ├── router │ ├── artist.js │ ├── comments.js │ ├── fm.js │ ├── home.js │ ├── lyrics.js │ ├── player.js │ ├── playlist.js │ ├── qrcode │ │ ├── index.js │ │ ├── wechat.js │ │ └── weibo.js │ ├── search.js │ ├── top.js │ └── user.js └── usocket.js ├── src ├── app.js ├── assets │ ├── bgcolorful.jpg │ ├── close-white.png │ ├── close.png │ ├── dock.png │ ├── loading.gif │ ├── notplaying-dark-panel.png │ ├── notplaying.png │ ├── playing-dark-panel.png │ ├── playing.png │ ├── qrcode-placeholder.png │ ├── social-facebook.png │ ├── social-google.png │ └── social-twitter.png ├── global.css ├── index.html ├── index.js └── js │ ├── components │ ├── AudioPlayer │ │ └── index.js │ ├── Controller │ │ ├── classes.js │ │ └── index.js │ ├── Header │ │ ├── classes.js │ │ └── index.js │ ├── Hero │ │ ├── classes.js │ │ └── index.js │ ├── Menu │ │ ├── classes.js │ │ └── index.js │ ├── Playing │ │ ├── classes.js │ │ └── index.js │ ├── Preferences │ │ ├── classes.js │ │ └── index.js │ ├── Ripple │ │ ├── PlayerMode.js │ │ ├── PlayerNavigation.js │ │ ├── PlayerStatus.js │ │ ├── VolumeUpDown.js │ │ └── classes.js │ ├── Share │ │ ├── classes.js │ │ └── index.js │ └── UpNext │ │ ├── classes.js │ │ └── index.js │ ├── pages │ ├── Artist │ │ ├── classes.js │ │ └── index.js │ ├── Comments │ │ ├── classes.js │ │ └── index.js │ ├── FM │ │ ├── classes.js │ │ └── index.js │ ├── Layout.js │ ├── Login │ │ ├── Legacy │ │ │ ├── classes.js │ │ │ └── index.js │ │ ├── QRCode │ │ │ ├── classes.js │ │ │ └── index.js │ │ └── index.js │ ├── Lyrics │ │ ├── classes.js │ │ └── index.js │ ├── Player │ │ ├── Search │ │ │ ├── classes.js │ │ │ └── index.js │ │ ├── classes.js │ │ └── index.js │ ├── Playlist │ │ ├── classes.js │ │ └── index.js │ ├── Search │ │ ├── classes.js │ │ └── index.js │ ├── Singleton │ │ ├── classes.js │ │ └── index.js │ ├── Top │ │ ├── classes.js │ │ └── index.js │ ├── User │ │ ├── classes.js │ │ └── index.js │ └── Welcome │ │ ├── classes.js │ │ └── index.js │ ├── routes.js │ ├── stores │ ├── artist.js │ ├── comments.js │ ├── controller.js │ ├── fm.js │ ├── home.js │ ├── index.js │ ├── lyrics.js │ ├── me.js │ ├── menu.js │ ├── player.js │ ├── playing.js │ ├── playlist.js │ ├── preferences.js │ ├── search.js │ ├── share.js │ ├── top.js │ ├── upnext.js │ └── user.js │ ├── ui │ ├── Confirm │ │ ├── classes.js │ │ └── index.js │ ├── FadeImage │ │ ├── classes.js │ │ └── index.js │ ├── Indicator │ │ ├── classes.js │ │ └── index.js │ ├── Loader │ │ ├── classes.js │ │ └── index.js │ ├── Modal │ │ ├── classes.css │ │ └── index.js │ ├── Offline │ │ ├── classes.js │ │ └── index.js │ ├── ProgressImage │ │ ├── classes.js │ │ └── index.js │ └── Switch │ │ ├── classes.js │ │ └── index.js │ └── utils │ ├── albumColors.js │ ├── colors.js │ ├── helper.js │ ├── lastfm.js │ ├── sine.js │ └── wave.js └── submodules ├── downloader ├── index.js └── viewport │ ├── index.html │ ├── index.js │ ├── stores.js │ └── views │ ├── Downloader │ ├── classes.js │ └── index.js │ └── List │ ├── classes.js │ └── index.js ├── index.js └── updater.js /.babelrc: -------------------------------------------------------------------------------- 1 | // NOTE: These options are overriden by the babel-loader configuration 2 | // for webpack, which can be found in ~/build/webpack.config. 3 | // 4 | // Why? The react-transform-hmr plugin depends on HMR (and throws if 5 | // module.hot is disabled), so keeping it and related plugins contained 6 | // within webpack helps prevent unexpected errors. 7 | { 8 | "presets": ['es2015', "react", "stage-0"], 9 | "plugins": ["babel-polyfill", "transform-decorators-legacy", "react-hot-loader/babel"], 10 | "env": { 11 | "production": { 12 | "presets": ["react-optimize"] 13 | } 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" # required to adjust maintainability checks 2 | checks: 3 | argument-count: 4 | config: 5 | threshold: 4 6 | complex-logic: 7 | config: 8 | threshold: 4 9 | file-lines: 10 | config: 11 | threshold: 1000 12 | method-complexity: 13 | config: 14 | threshold: 6 15 | method-count: 16 | config: 17 | threshold: 20 18 | method-lines: 19 | config: 20 | threshold: 500 21 | nested-control-flow: 22 | config: 23 | threshold: 4 24 | return-statements: 25 | config: 26 | threshold: 4 27 | 28 | engines: 29 | duplication: 30 | enabled: false 31 | config: 32 | languages: 33 | javascript: 34 | mass_threshold: 65 35 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 4 12 | charset = utf-8 13 | max_line_length = 80 14 | 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | 2 | __tests__/* 3 | dist/* 4 | node_modules/* 5 | src/assets/* 6 | server/api/* 7 | NeteaseCloudMusicApi/* 8 | 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 8, 4 | "sourceType": "module", 5 | "ecmaFeatures": { 6 | "impliedStrict": true, 7 | "jsx": true, 8 | "js": true, 9 | "legacyDecorators": true 10 | } 11 | }, 12 | "env": { 13 | "es6": true, 14 | "node": true 15 | }, 16 | "parser": "babel-eslint", 17 | "plugins": ["react"], 18 | "rules": { 19 | "semi": [2, "always"], 20 | "new-cap": [0], 21 | "prefer-promise-reject-errors": [1], 22 | "indent": [2, 4, { 23 | "SwitchCase": 1 24 | }], 25 | "comma-dangle": [2, "only-multiline"], 26 | "space-before-function-paren": [2, "never"], 27 | "operator-linebreak": [2, "before"], 28 | "no-floating-decimal": [0], 29 | "react/jsx-indent": [2, 4], 30 | "react/jsx-indent-props": [2, 4], 31 | "react/jsx-boolean-value": [2, "always"], 32 | "react/prop-types": [0], 33 | "jsx-quotes": [2, "prefer-double"] 34 | }, 35 | "extends": ["standard", "standard-react"] 36 | } 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Other 6 | # 7 | dist/ 8 | release/ 9 | *-lock.json 10 | 11 | # Xcode 12 | # 13 | build/ 14 | *.pbxuser 15 | !default.pbxuser 16 | *.mode1v3 17 | !default.mode1v3 18 | *.mode2v3 19 | !default.mode2v3 20 | *.perspectivev3 21 | !default.perspectivev3 22 | xcuserdata 23 | *.xccheckout 24 | *.moved-aside 25 | DerivedData 26 | *.hmap 27 | *.ipa 28 | *.xcuserstate 29 | project.xcworkspace 30 | 31 | # Android/IntelliJ 32 | # 33 | build/ 34 | .idea 35 | .gradle 36 | local.properties 37 | *.iml 38 | 39 | # node.js 40 | # 41 | .vscode 42 | node_modules/ 43 | npm-debug.log 44 | 45 | # BUCK 46 | buck-out/ 47 | \.buckd/ 48 | android/app/libs 49 | *.keystore 50 | /Requests.paw 51 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "NeteaseCloudMusicApi"] 2 | path = NeteaseCloudMusicApi 3 | url = https://github.com/trazyn/NeteaseCloudMusicApi 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - 9 7 | - 8 8 | 9 | script: npm run lint && npm run build 10 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Authors 2 | 3 | #### Ordered by first contribution. 4 | 5 | - trazyn (var.darling@gmail.com) 6 | - kenshinji (gundam0083ster@gmail.com) 7 | - nashaofu (diaocheng@outlook.com) 8 | - Gemer Cheung (gemercheung@gmail.com) 9 | - songjiayang (songjiayang@users.noreply.github.com) 10 | - SeptemberHX (tianjianshan@outlook.com) 11 | - unreal0 (unreal0@sina.cn) 12 | - Shi Liang (shilianggoo@gmail.com) 13 | 14 | #### Generated by bin/update-authors.sh. 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 var.darling@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Node.js with webpack 2 | # Build a Node.js project using the webpack CLI. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | trigger: 7 | - master 8 | 9 | pool: 10 | vmImage: 'Ubuntu-16.04' 11 | 12 | steps: 13 | - task: AppCenterTest@1 14 | inputs: 15 | versionSpec: '8.x' 16 | displayName: 'Install Node.js' 17 | 18 | - script: | 19 | npm install -g webpack webpack-cli --save-dev 20 | npm i 21 | npm run lint && npm run build 22 | -------------------------------------------------------------------------------- /bin/clean-commits.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | git filter-branch -f --env-filter ' 4 | 5 | an="$GIT_AUTHOR_NAME" 6 | am="$GIT_AUTHOR_EMAIL" 7 | cn="$GIT_COMMITTER_NAME" 8 | cm="$GIT_COMMITTER_EMAIL" 9 | 10 | if [ "$GIT_AUTHOR_EMAIL" = "var.daring@gmail.com" ] 11 | then 12 | an="trazyn" 13 | am="var.darling@gmail.com" 14 | cn="trazyn" 15 | cm="var.darling@gmail.com" 16 | fi 17 | if [ "$GIT_AUTHOR_EMAIL" = "tn.razy@gmail.com" ] 18 | then 19 | an="trazyn" 20 | am="var.darling@gmail.com" 21 | cn="trazyn" 22 | cm="var.darling@gmail.com" 23 | fi 24 | 25 | export GIT_AUTHOR_NAME="$an" 26 | export GIT_AUTHOR_EMAIL="$am" 27 | export GIT_COMMITTER_NAME="$cn" 28 | export GIT_COMMITTER_EMAIL="$cm" 29 | ' 30 | -------------------------------------------------------------------------------- /bin/publish.sh: -------------------------------------------------------------------------------- 1 | 2 | #!/bin/sh 3 | 4 | if [ -z "$GH_TOKEN" ]; then 5 | echo "You must set the GH_TOKEN environment variable." 6 | echo "See README.md for more details." 7 | exit 1 8 | fi 9 | 10 | # This will build, package and upload the app to GitHub. 11 | npm run build && node_modules/.bin/build --projectDir ./dist --mac --linux -p always 12 | -------------------------------------------------------------------------------- /bin/update-authors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Update AUTHORS.md based on git history. 4 | 5 | git log --reverse --format='%aN (%aE)' | perl -we ' 6 | BEGIN { 7 | %seen = (), @authors = (); 8 | } 9 | while (<>) { 10 | next if $seen{$_}; 11 | next if /(ing.98k\@gmail.com)/; 12 | next if /衣带渐宽人渐悔/; 13 | $seen{$_} = push @authors, "- ", $_; 14 | } 15 | END { 16 | print "# Authors\n\n"; 17 | print "#### Ordered by first contribution.\n\n"; 18 | print @authors, "\n"; 19 | print "#### Generated by bin/update-authors.sh.\n"; 20 | } 21 | ' > AUTHORS.md 22 | -------------------------------------------------------------------------------- /common/cache.js: -------------------------------------------------------------------------------- 1 | 2 | const cache = {}; 3 | 4 | export default { 5 | get: (key) => { 6 | var cached = cache[key] || {}; 7 | 8 | if (false 9 | || !cached.ttl 10 | || +cached.ttl <= +new Date()) { 11 | // Expired 12 | delete cache[key]; 13 | } 14 | 15 | return cached.value; 16 | }, 17 | 18 | set: (key, value, ttl = 10 * 60 * 1000) => { 19 | cache[key] = { 20 | value, 21 | ttl: Date.now() + ttl 22 | }; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /common/storage.js: -------------------------------------------------------------------------------- 1 | 2 | import storage from 'electron-json-storage'; 3 | 4 | export default { 5 | get: (key) => { 6 | return new Promise((resolve, reject) => { 7 | storage.get(key, (err, data) => { 8 | if (err) { 9 | reject(err); 10 | } else { 11 | resolve(data); 12 | } 13 | }); 14 | }); 15 | }, 16 | 17 | set: (key, data) => { 18 | return new Promise((resolve, reject) => { 19 | storage.set(key, data, err => { 20 | if (err) { 21 | reject(err); 22 | } else { 23 | resolve(data); 24 | } 25 | }); 26 | }); 27 | }, 28 | 29 | remove: (key) => { 30 | return new Promise((resolve, reject) => { 31 | storage.remove(key, err => { 32 | if (err) { 33 | reject(err); 34 | } else { 35 | resolve(); 36 | } 37 | }); 38 | }); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 2 | import path from 'path'; 3 | 4 | const config = { 5 | server: { 6 | port: process.env.PORT || 3001, 7 | host: 'localhost' 8 | }, 9 | 10 | api: { 11 | port: process.env.API_PORT || 10086, 12 | }, 13 | 14 | client: path.resolve(__dirname, '../src'), 15 | submodules: path.resolve(__dirname, '../submodules'), 16 | assets: path.resolve(__dirname, '../src/assets'), 17 | dist: path.resolve(__dirname, '../dist'), 18 | }; 19 | 20 | export default config; 21 | -------------------------------------------------------------------------------- /config/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | 2 | import path from 'path'; 3 | import config from './index'; 4 | 5 | export default { 6 | 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.jsx?$/, 11 | use: [ 12 | 'babel-loader', 13 | 'eslint-loader' 14 | ], 15 | exclude: /node_modules/, 16 | }, 17 | { 18 | test: /\.css$/, 19 | use: [ 20 | 'style-loader', 21 | 'css-loader', 22 | ], 23 | }, 24 | { 25 | test: /\.html/, 26 | use: 'html-loader', 27 | }, 28 | { 29 | test: /\.(png|jpg|jpeg|gif)$/, 30 | use: 'url-loader' 31 | }, 32 | { 33 | test: /\.(eot|woff|woff2|ttf)([?]?.*)$/, 34 | loader: [{ 35 | loader: 'url-loader', 36 | options: { 37 | name: 'fonts/[name].[ext]', 38 | }, 39 | }], 40 | }, 41 | { 42 | test: /\.svg$/, 43 | use: ['svg-inline-loader'], 44 | include: path.resolve(__dirname, 'src'), 45 | }, 46 | { 47 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 48 | loader: [{ 49 | loader: 'url-loader', 50 | options: { 51 | mimetype: 'image/svg+xml', 52 | }, 53 | }], 54 | include: /node_modules/, 55 | }, 56 | ] 57 | }, 58 | 59 | output: { 60 | path: config.dist, 61 | filename: '[name].js', 62 | 63 | // https://github.com/webpack/webpack/issues/1114 64 | libraryTarget: 'commonjs2' 65 | }, 66 | 67 | resolve: { 68 | extensions: ['.js', '.jsx', '.json'], 69 | alias: { 70 | root: path.join(config.client, '../'), 71 | config: path.join(config.client, '../config'), 72 | common: path.join(config.client, '../common'), 73 | app: path.join(config.client, './'), 74 | ui: path.join(config.client, 'js/ui/'), 75 | utils: path.join(config.client, 'js/utils/'), 76 | components: path.join(config.client, 'js/components/'), 77 | stores: path.join(config.client, 'js/stores/'), 78 | }, 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | 2 | import webpack from 'webpack'; 3 | import config from './index'; 4 | import baseConfig from './webpack.config.base'; 5 | 6 | const { host, port } = config.server; 7 | 8 | export default { 9 | 10 | ...baseConfig, 11 | 12 | mode: 'development', 13 | devtool: 'cheap-module-eval-source-map', 14 | 15 | entry: { 16 | main: [ 17 | `webpack-hot-middleware/client?path=http://${host}:${port}/__webpack_hmr`, 18 | 'babel-polyfill', 19 | `${config.client}/index.js`, 20 | ], 21 | 22 | downloader: [ 23 | 'babel-polyfill', 24 | `${config.submodules}/downloader/viewport/index.js`, 25 | ] 26 | }, 27 | 28 | output: { 29 | ...baseConfig.output, 30 | publicPath: `http://${host}:${port}/dist/`, 31 | }, 32 | 33 | plugins: [ 34 | // “If you are using the CLI, the webpack process will not exit with an error code by enabling this plugin.” 35 | // https://github.com/webpack/docs/wiki/list-of-plugins#noerrorsplugin 36 | new webpack.NoEmitOnErrorsPlugin(), 37 | 38 | // https://webpack.github.io/docs/hot-module-replacement-with-webpack.html 39 | new webpack.HotModuleReplacementPlugin(), 40 | ], 41 | 42 | // https://github.com/chentsulin/webpack-target-electron-renderer#how-this-module-works 43 | target: 'electron-renderer' 44 | }; 45 | -------------------------------------------------------------------------------- /config/webpack.config.electron.js: -------------------------------------------------------------------------------- 1 | 2 | import MinifyPlugin from 'terser-webpack-plugin'; 3 | import baseConfig from './webpack.config.base'; 4 | 5 | export default { 6 | 7 | ...baseConfig, 8 | 9 | mode: 'production', 10 | devtool: false, 11 | 12 | entry: { 13 | main: [ 14 | 'babel-polyfill', 15 | './main.js', 16 | ], 17 | downloader: [ 18 | 'babel-polyfill', 19 | './submodules/downloader/index.js', 20 | ] 21 | }, 22 | 23 | plugins: [ 24 | // Minify the output 25 | new MinifyPlugin(), 26 | ], 27 | 28 | // https://github.com/chentsulin/webpack-target-electron-renderer#how-this-module-works 29 | target: 'electron-main', 30 | 31 | /** 32 | * Disables webpack processing of __dirname and __filename. 33 | * If you run the bundle in node.js it falls back to these values of node.js. 34 | * https://github.com/webpack/webpack/issues/2010 35 | */ 36 | node: { 37 | __dirname: false, 38 | __filename: false 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /config/webpack.config.production.js: -------------------------------------------------------------------------------- 1 | 2 | import path from 'path'; 3 | import webpack from 'webpack'; 4 | import MinifyPlugin from 'terser-webpack-plugin'; 5 | import config from './index'; 6 | import CopyWebpackPlugin from 'copy-webpack-plugin'; 7 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 8 | import baseConfig from './webpack.config.base'; 9 | 10 | export default { 11 | 12 | ...baseConfig, 13 | 14 | mode: 'production', 15 | devtool: false, 16 | 17 | entry: { 18 | main: [ 19 | 'babel-polyfill', 20 | `${config.client}/index.js`, 21 | ], 22 | 23 | downloader: [ 24 | 'babel-polyfill', 25 | `${config.submodules}/downloader/viewport/index.js`, 26 | ] 27 | }, 28 | 29 | output: { 30 | path: config.dist, 31 | filename: '[name].[hash].js' 32 | }, 33 | 34 | plugins: [ 35 | // https://github.com/webpack/webpack/issues/2545 36 | // Use babel-minify-webpack-plugin minify code 37 | new MinifyPlugin(), 38 | 39 | // https://webpack.github.io/docs/list-of-plugins.html#occurrenceorderplugin 40 | // https://github.com/webpack/webpack/issues/864 41 | new webpack.optimize.OccurrenceOrderPlugin(), 42 | 43 | new CopyWebpackPlugin([ 44 | { 45 | from: `${config.assets}/**/*`, 46 | to: `${config.dist}`, 47 | }, 48 | { 49 | from: `${path.resolve(__dirname, '../NeteaseCloudMusicApi')}/module/*`, 50 | to: config.dist, 51 | }, 52 | { 53 | from: `${path.resolve(__dirname, '../NeteaseCloudMusicApi')}/util/*`, 54 | to: config.dist, 55 | }, 56 | { 57 | from: path.resolve(__dirname, '../package.json'), 58 | to: config.dist, 59 | }, 60 | ]), 61 | 62 | new HtmlWebpackPlugin({ 63 | filename: `${config.dist}/src/index.html`, 64 | template: './src/index.html', 65 | inject: 'body', 66 | hash: true, 67 | minify: true, 68 | chunks: ['main'] 69 | }), 70 | 71 | new HtmlWebpackPlugin({ 72 | filename: `${config.dist}/src/downloader.html`, 73 | template: './submodules/downloader/viewport/index.html', 74 | inject: 'body', 75 | hash: true, 76 | minify: true, 77 | chunks: ['downloader'] 78 | }) 79 | ], 80 | 81 | // https://github.com/chentsulin/webpack-target-electron-renderer#how-this-module-works 82 | target: 'electron-renderer' 83 | }; 84 | -------------------------------------------------------------------------------- /resource/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/resource/128x128.png -------------------------------------------------------------------------------- /resource/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/resource/16x16.png -------------------------------------------------------------------------------- /resource/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/resource/24x24.png -------------------------------------------------------------------------------- /resource/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/resource/256x256.png -------------------------------------------------------------------------------- /resource/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/resource/32x32.png -------------------------------------------------------------------------------- /resource/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/resource/48x48.png -------------------------------------------------------------------------------- /resource/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/resource/64x64.png -------------------------------------------------------------------------------- /resource/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/resource/96x96.png -------------------------------------------------------------------------------- /resource/dock.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/resource/dock.icns -------------------------------------------------------------------------------- /resource/donate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/resource/donate.png -------------------------------------------------------------------------------- /resource/ieaseMusic.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/resource/ieaseMusic.ico -------------------------------------------------------------------------------- /screenshots/alfred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/screenshots/alfred.png -------------------------------------------------------------------------------- /screenshots/artist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/screenshots/artist.png -------------------------------------------------------------------------------- /screenshots/cmd+p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/screenshots/cmd+p.png -------------------------------------------------------------------------------- /screenshots/comments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/screenshots/comments.png -------------------------------------------------------------------------------- /screenshots/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/screenshots/cover.png -------------------------------------------------------------------------------- /screenshots/downloader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/screenshots/downloader.png -------------------------------------------------------------------------------- /screenshots/fm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/screenshots/fm.png -------------------------------------------------------------------------------- /screenshots/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/screenshots/home.png -------------------------------------------------------------------------------- /screenshots/lyrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/screenshots/lyrics.png -------------------------------------------------------------------------------- /screenshots/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/screenshots/menu.png -------------------------------------------------------------------------------- /screenshots/player-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/screenshots/player-2.png -------------------------------------------------------------------------------- /screenshots/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/screenshots/player.png -------------------------------------------------------------------------------- /screenshots/playlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/screenshots/playlist.png -------------------------------------------------------------------------------- /screenshots/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/screenshots/preview.gif -------------------------------------------------------------------------------- /screenshots/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/screenshots/search.png -------------------------------------------------------------------------------- /screenshots/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/screenshots/settings.png -------------------------------------------------------------------------------- /screenshots/top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/screenshots/top.png -------------------------------------------------------------------------------- /screenshots/upnext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/screenshots/upnext.png -------------------------------------------------------------------------------- /screenshots/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/screenshots/user.png -------------------------------------------------------------------------------- /server/api.js: -------------------------------------------------------------------------------- 1 | 2 | /* eslint-disable */ 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import express from 'express'; 6 | import cookieParser from 'cookie-parser'; 7 | import apicache from 'apicache' 8 | import bodyParser from 'body-parser'; 9 | import axios from 'axios'; 10 | /* eslint-enable */ 11 | 12 | const app = express(); 13 | const cache = apicache.middleware; 14 | const onlyStatus200 = (req, res) => res.statusCode === 200; 15 | const port = process.env.API_PORT || 8000; 16 | 17 | app.use(bodyParser.json()); 18 | app.use(bodyParser.urlencoded({ extended: false })); 19 | app.use(cookieParser()); 20 | 21 | axios.defaults.baseURL = `http://localhost:${port}`; 22 | 23 | // Set cookie for axios 24 | app.use((req, res, next) => { 25 | axios.defaults.headers = req.headers; 26 | next(); 27 | }); 28 | 29 | function isDev() { 30 | return process.mainModule.filename.indexOf('app.asar') === -1; 31 | } 32 | 33 | function mount(proxy) { 34 | let special = { 35 | 'daily_signin.js': '/daily_signin', 36 | 'fm_trash.js': '/fm_trash', 37 | 'personal_fm.js': '/personal_fm' 38 | }; 39 | 40 | fs.readdirSync(path.join(__dirname, `${isDev() ? '..' : '.'}/NeteaseCloudMusicApi/module`)).reverse().forEach(file => { 41 | if (!/\.js$/i.test(file)) { 42 | return; 43 | } 44 | 45 | try { 46 | // https://stackoverflow.com/questions/42797313/webpack-dynamic-module-loader-by-require 47 | let route = (file in special) ? special[file] : '/' + file.replace(/\.js$/i, '').replace(/_/g, '/'); 48 | let question = require('../NeteaseCloudMusicApi/module/' + file); 49 | let request = require('../NeteaseCloudMusicApi/util/request'); 50 | 51 | app.use(route, (req, res) => { 52 | let query = Object.assign({}, req.query, req.body, { cookie: req.cookies }, { proxy }); 53 | question(query, request) 54 | .then(answer => { 55 | res.append('Set-Cookie', answer.cookie); 56 | res.status(answer.status).send(answer.body); 57 | }) 58 | .catch(answer => { 59 | if (1 60 | && answer.body 61 | && answer.body.code 62 | && answer.status !== 200 63 | ) { 64 | answer.status = 200; 65 | } 66 | res.append('Set-Cookie', answer.cookie); 67 | res.status(answer.status).send(answer.body); 68 | }); 69 | }); 70 | } catch (ex) { 71 | console.error(ex); 72 | } 73 | }); 74 | 75 | app.use('/api/home', require('./router/home')); 76 | app.use('/api/player', require('./router/player')); 77 | app.use('/api/user', require('./router/user')); 78 | app.use('/api/artist', require('./router/artist')); 79 | app.use('/api/top', cache('1 hour', onlyStatus200), require('./router/top')); 80 | app.use('/api/playlist', cache('10 minutes', onlyStatus200), require('./router/playlist')); 81 | app.use('/api/fm', require('./router/fm')); 82 | app.use('/api/search', require('./router/search')); 83 | app.use('/api/comments', require('./router/comments')); 84 | app.use('/api/lyrics', cache('360 minutes'), require('./router/lyrics')); 85 | app.use('/api/qrcode', require('./router/qrcode')); 86 | } 87 | 88 | if (process.env.APIONLY) { 89 | mount(); 90 | console.log(`API Server run with port: ${port}`); 91 | } 92 | 93 | export default (port, proxy, callback) => { 94 | mount(proxy); 95 | app.listen(port, callback); 96 | }; 97 | -------------------------------------------------------------------------------- /server/dev.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Setup and run the development server for Hot-Module-Replacement 4 | * https://webpack.github.io/docs/hot-module-replacement-with-webpack.html 5 | * @flow 6 | */ 7 | import express from 'express'; 8 | import webpack from 'webpack'; 9 | import webpackDevMiddleware from 'webpack-dev-middleware'; 10 | import webpackHotMiddleware from 'webpack-hot-middleware'; 11 | import _debug from 'debug'; 12 | 13 | import config from '../config'; 14 | import webpackConfig from '../config/webpack.config.dev'; 15 | 16 | const debug = _debug('dev:server'); 17 | const app = new express(); 18 | const compiler = webpack(webpackConfig); 19 | 20 | app.use( 21 | webpackDevMiddleware(compiler, { 22 | publicPath: webpackConfig.output.publicPath, 23 | stats: { 24 | colors: true 25 | }, 26 | }) 27 | ); 28 | app.use(webpackHotMiddleware(compiler)); 29 | 30 | app.listen(config.server.port, config.server.host, err => { 31 | if (err) { 32 | throw err; 33 | } 34 | 35 | debug(`Hot reload server is running with port ${config.server.port} 👏`); 36 | }); 37 | -------------------------------------------------------------------------------- /server/provider/Baidu.js: -------------------------------------------------------------------------------- 1 | 2 | import _debug from 'debug'; 3 | import chalk from 'chalk'; 4 | 5 | const debug = _debug('dev:plugin:Baidu'); 6 | 7 | export default async(request, keyword, artists) => { 8 | debug(chalk.black.bgGreen('💊 Loaded Baidu music.')); 9 | 10 | try { 11 | var response = await request({ 12 | uri: 'http://sug.music.baidu.com/info/suggestion', 13 | qs: { 14 | word: [keyword].concat(artists.split(',')).join('+'), 15 | version: 2, 16 | from: 0, 17 | _: +new Date(), 18 | }, 19 | }); 20 | var songs = (response.data || {}).song; 21 | var song = (songs || []).find(e => artists.indexOf(e.artistname) > -1); 22 | 23 | if (!song) { 24 | return Promise.reject(Error(404)); 25 | } 26 | 27 | console.log('done'); 28 | 29 | response = await request({ 30 | uri: 'http://music.taihe.com/data/music/fmlink', 31 | qs: { 32 | songIds: song.songid, 33 | }, 34 | }); 35 | 36 | if ( 37 | false 38 | || +response.errorCode !== 22000 39 | || response.data.songList.length === 0 40 | ) { 41 | return Promise.reject(Error(404)); 42 | } 43 | 44 | song = response.data.songList[0]; 45 | 46 | song.src = song.songLink; 47 | song.bitRate = song.rate * 1000; 48 | 49 | if (!song.src) { 50 | return Promise.reject(Error(404)); 51 | } 52 | } catch (ex) { 53 | // Anti-warnning 54 | return Promise.reject(ex); 55 | } 56 | 57 | return song; 58 | }; 59 | -------------------------------------------------------------------------------- /server/provider/Kugou.js: -------------------------------------------------------------------------------- 1 | 2 | import _debug from 'debug'; 3 | import chalk from 'chalk'; 4 | import md5 from 'md5'; 5 | 6 | const debug = _debug('dev:plugin:Kugou'); 7 | 8 | let rp; 9 | 10 | async function getURL(hash) { 11 | var key = md5(`${hash}kgcloud`); 12 | 13 | var response = await rp({ 14 | url: `http://trackercdn.kugou.com/i/?acceptMp3=1&cmd=4&pid=6&hash=${hash}&key=${key}`, 15 | }); 16 | 17 | if (response.error 18 | || +response.status !== 1) { 19 | return false; 20 | } else { 21 | return response; 22 | } 23 | } 24 | 25 | export default async(request, keyword, artists) => { 26 | debug(chalk.black.bgGreen('💊 Loaded Kugou music.')); 27 | 28 | rp = request; 29 | 30 | try { 31 | var response = await rp({ 32 | uri: 'http://mobilecdn.kugou.com/api/v3/search/song', 33 | qs: { 34 | format: 'json', 35 | keyword: [keyword].concat(artists.split(',')).join('+'), 36 | page: 1, 37 | pagesize: 1, 38 | showtype: 1, 39 | }, 40 | }); 41 | 42 | var data = response.data; 43 | 44 | if (response.status !== 1 45 | || data.info.length === 0) { 46 | return Promise.reject(Error(404)); 47 | } 48 | 49 | for (let e of data.info) { 50 | if ( 51 | artists.split(',').findIndex( 52 | artist => e.singername.indexOf(artist) > -1 53 | ) === -1 54 | ) { 55 | continue; 56 | } 57 | 58 | let song = await getURL(e['320hash'] || e['hash']); 59 | 60 | if (song) { 61 | song.src = song.url; 62 | return song; 63 | } 64 | } 65 | } catch (ex) { 66 | return Promise.reject(ex); 67 | } 68 | 69 | return Promise.reject(Error(405)); 70 | }; 71 | -------------------------------------------------------------------------------- /server/provider/Kuwo.js: -------------------------------------------------------------------------------- 1 | 2 | import _debug from 'debug'; 3 | import chalk from 'chalk'; 4 | 5 | const debug = _debug('dev:plugin:Kuwo'); 6 | 7 | export default async(request, keyword, artists) => { 8 | debug(chalk.black.bgGreen('💊 Loaded Kuwo music.')); 9 | 10 | try { 11 | // Apply cookie 12 | await request({ 13 | uri: 'http://www.kuwo.cn/', 14 | method: 'HEAD', 15 | }); 16 | 17 | var response = await request({ 18 | uri: 'http://search.kuwo.cn/r.s', 19 | qs: { 20 | ft: 'music', 21 | itemset: 'web_2013', 22 | client: 'kt', 23 | rformat: 'json', 24 | encoding: 'utf8', 25 | all: [keyword].concat(artists.split(',')).join('+'), 26 | pn: 0, 27 | rn: 20, 28 | }, 29 | }); 30 | // eslint-disable-next-line 31 | response = eval('(' + response + ')'); 32 | artists = artists.split(','); 33 | 34 | var songs = response.abslist || []; 35 | var payload = songs.find( 36 | e => artists.findIndex(artist => e.ARTIST.indexOf(artist) !== -1) > -1 37 | ); 38 | 39 | if (!payload) { 40 | return Promise.reject(Error(404)); 41 | } 42 | 43 | response = await request({ 44 | uri: 'http://antiserver.kuwo.cn/anti.s', 45 | qs: { 46 | type: 'convert_url', 47 | format: 'aac|mp3|wma', 48 | response: 'url', 49 | rid: payload.MP3RID, 50 | }, 51 | }); 52 | 53 | if (!response || response === 'IPDeny') { 54 | return Promise.reject(Error(404)); 55 | } 56 | 57 | var song = { 58 | src: response, 59 | isFlac: response.endsWith('.aac') 60 | }; 61 | } catch (ex) { 62 | return Promise.reject(ex); 63 | } 64 | 65 | return song; 66 | }; 67 | -------------------------------------------------------------------------------- /server/provider/MiGu.js: -------------------------------------------------------------------------------- 1 | 2 | import _debug from 'debug'; 3 | import chalk from 'chalk'; 4 | 5 | const debug = _debug('dev:plugin:MiGu'); 6 | 7 | export default async(request, keyword, artists) => { 8 | debug(chalk.black.bgGreen('💊 Loaded MiGu music.')); 9 | 10 | try { 11 | var response = await request({ 12 | uri: 'http://m.music.migu.cn/migu/remoting/scr_search_tag', 13 | qs: { 14 | keyword: [keyword].concat(artists.split(',')).join('+'), 15 | type: 2, 16 | rows: 20, 17 | pgc: 1, 18 | } 19 | }); 20 | 21 | if ( 22 | false 23 | || response.success !== true 24 | || !response.musics 25 | || response.musics.length === 0 26 | ) { 27 | return Promise.reject(Error(404)); 28 | } 29 | 30 | for (let e of response.musics) { 31 | if ( 32 | artists.split(',').find( 33 | artist => e.singerName.indexOf(artist) !== -1 34 | ) 35 | ) { 36 | return Object.assign({}, e, { src: e.mp3 }); 37 | } 38 | } 39 | } catch (ex) { 40 | return Promise.reject(ex); 41 | } 42 | 43 | return Promise.reject(Error(405)); 44 | }; 45 | -------------------------------------------------------------------------------- /server/provider/Netease.js: -------------------------------------------------------------------------------- 1 | 2 | import _debug from 'debug'; 3 | import chalk from 'chalk'; 4 | import config from '../../config'; 5 | 6 | const debug = _debug('dev:plugin:Netease'); 7 | 8 | export default async(request, keyword, artists, id) => { 9 | debug(chalk.black.bgGreen('💊 Loaded Netease music.')); 10 | 11 | try { 12 | var response = await request({ 13 | uri: `http://127.0.0.1:${config.api.port}/song/url`, 14 | qs: { 15 | id, 16 | } 17 | }); 18 | var song = {}; 19 | 20 | if ( 21 | false 22 | || response.code !== 200 23 | || (!response.data || response.data.length === 0) 24 | ) { 25 | return Promise.reject(Error(404)); 26 | } 27 | 28 | song = response.data[0]; 29 | 30 | if (!song.url) { 31 | return Promise.reject(Error(404)); 32 | } 33 | 34 | song = { 35 | id: song.id.toString(), 36 | src: `https://music.163.com/song/media/outer/url?id=${song.id.toString()}.mp3`, 37 | md5: song.md5, 38 | bitRate: song.br, 39 | }; 40 | } catch (ex) { 41 | return Promise.reject(ex); 42 | } 43 | 44 | return song; 45 | }; 46 | -------------------------------------------------------------------------------- /server/provider/QQ.js: -------------------------------------------------------------------------------- 1 | 2 | import _debug from 'debug'; 3 | import chalk from 'chalk'; 4 | 5 | const debug = _debug('dev:plugin:QQ'); 6 | 7 | let rp; 8 | 9 | async function getSong(mid, isFlac) { 10 | var currentMs = (new Date()).getUTCMilliseconds(); 11 | var guid = Math.round(2147483647 * Math.random()) * currentMs % 1e10; 12 | var file = await genKey(mid); 13 | var response = file 14 | ? ( 15 | await rp({ 16 | uri: 'https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg', 17 | qs: { 18 | cid: '205361747', 19 | uin: 0, 20 | songmid: mid, 21 | filename: genFilename(file, mid), 22 | format: 'json', 23 | guid: guid.toString(), 24 | }, 25 | }) 26 | ) 27 | : {} 28 | ; 29 | 30 | if ( 31 | false 32 | || response.code !== 0 33 | || response.data.items.length === 0 34 | ) { 35 | return {}; 36 | } 37 | 38 | var key = response.data.items[0].vkey || await rp('https://public.nondanee.tk/qq/ticket', { agent: null }); 39 | if (!key) { 40 | throw Error('Invalid key'); 41 | } 42 | 43 | if (isFlac) { 44 | if (file.size_flac) { 45 | return { 46 | isFlac: true, 47 | src: getURL(`F000${mid}.flac`, key, guid), 48 | }; 49 | } 50 | 51 | // Not found flac track 52 | return {}; 53 | } 54 | 55 | if (file.size_320mp3) { 56 | return { 57 | bitRate: 320000, 58 | src: getURL(`M800${mid}.mp3`, key, guid), 59 | }; 60 | } 61 | 62 | if (file.size_128mp3) { 63 | return { 64 | bitRate: 128000, 65 | src: getURL(`M500${mid}.mp3`, key, guid), 66 | }; 67 | } 68 | } 69 | 70 | function genFilename(file, mid) { 71 | var prefix = 'C400'; 72 | 73 | switch (true) { 74 | case file.size_flac: 75 | prefix = 'F000'; 76 | break; 77 | 78 | case file.size_320mp3: 79 | prefix = 'M800'; 80 | break; 81 | 82 | case file.size_128mp3: 83 | prefix = 'M500'; 84 | break; 85 | } 86 | 87 | return `${prefix}${mid}.m4a`; 88 | } 89 | 90 | async function genKey(mid) { 91 | var response = await rp({ 92 | uri: 'http://c.y.qq.com/v8/fcg-bin/fcg_play_single_song.fcg', 93 | qs: { 94 | songmid: mid, 95 | format: 'json', 96 | }, 97 | }); 98 | var data = response.data; 99 | 100 | if (response.code !== 0 101 | || data.length === 0) { 102 | return false; 103 | } 104 | 105 | return data[0]['file']; 106 | } 107 | 108 | function getURL(filename, key, guid) { 109 | return `http://streamoc.music.tc.qq.com/${filename}?vkey=${key}&guid=${guid}&fromtag=53`; 110 | } 111 | 112 | export default async(request, keyword, artists, isFlac) => { 113 | debug(chalk.black.bgGreen('💊 Loaded QQ music.')); 114 | 115 | rp = request; 116 | 117 | try { 118 | var response = await rp({ 119 | uri: 'http://c.y.qq.com/soso/fcgi-bin/search_cp', 120 | qs: { 121 | w: [keyword].concat(artists.split(',')).join('+'), 122 | p: 1, 123 | n: 100, 124 | aggr: 1, 125 | lossless: 1, 126 | cr: 1, 127 | format: 'json', 128 | inCharset: 'utf8', 129 | outCharset: 'utf-8' 130 | }, 131 | }); 132 | 133 | var data = response.data; 134 | 135 | if (response.code !== 0 136 | || data.song.list.length === 0) { 137 | return Promise.reject(Error(404)); 138 | } 139 | 140 | for (let e of data.song.list) { 141 | let song = {}; 142 | 143 | // Match the artists 144 | if (e.singer.find(e => artists.indexOf(e.name) === -1)) { 145 | continue; 146 | } 147 | 148 | song = await getSong(e.media_mid, isFlac); 149 | 150 | if (!song.src) { 151 | return Promise.reject(Error(404)); 152 | } 153 | 154 | return song; 155 | } 156 | } catch (ex) { 157 | return Promise.reject(ex); 158 | } 159 | 160 | return Promise.reject(Error(405)); 161 | }; 162 | -------------------------------------------------------------------------------- /server/provider/Xiami.js: -------------------------------------------------------------------------------- 1 | 2 | import _debug from 'debug'; 3 | import chalk from 'chalk'; 4 | 5 | const debug = _debug('dev:plugin:Xiami'); 6 | const headers = { 7 | cookie: 'user_from=2;XMPLAYER_addSongsToggler=0;XMPLAYER_isOpen=0;_xiamitoken=cb8bfadfe130abdbf5e2282c30f0b39a;', 8 | referer: 'http://h.xiami.com/', 9 | user_agent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36', 10 | }; 11 | 12 | export default async(request, keyword, artists) => { 13 | debug(chalk.black.bgGreen('💊 Loaded Xiami music.')); 14 | 15 | try { 16 | var response = await request({ 17 | uri: 'http://api.xiami.com/web', 18 | qs: { 19 | v: '2.0', 20 | key: [keyword].concat(artists.split(',')).join('+'), 21 | limit: 100, 22 | page: 1, 23 | r: 'search/songs', 24 | app_key: 1, 25 | }, 26 | json: true, 27 | headers, 28 | }); 29 | 30 | var data = response.data; 31 | 32 | if (response.state !== 0 33 | || data.songs.length === 0) { 34 | return Promise.reject(Error(404)); 35 | } 36 | 37 | for (let e of data.songs) { 38 | if ( 39 | artists.split(',').findIndex( 40 | artist => e.artist_name.indexOf(artist) !== -1 41 | ) === -1 42 | ) { 43 | continue; 44 | } 45 | 46 | let song = e; 47 | 48 | song.src = e.listen_file; 49 | 50 | if (!song.src) { 51 | return Promise.reject(Error(404)); 52 | } else { 53 | return song; 54 | } 55 | } 56 | } catch (ex) { 57 | return Promise.reject(ex); 58 | } 59 | 60 | return Promise.reject(Error(405)); 61 | }; 62 | -------------------------------------------------------------------------------- /server/provider/test.js: -------------------------------------------------------------------------------- 1 | 2 | import search from './Kuwo'; 3 | 4 | async function test() { 5 | var res = {}; 6 | var rp = require('request-promise-native').defaults({ 7 | strictSSL: true, 8 | json: true, 9 | }); 10 | 11 | try { 12 | res = await search(rp, '演员', '薛之谦'); 13 | console.log(res); 14 | console.log(res.purview_roles); 15 | } catch (ex) { 16 | console.error(ex); 17 | } 18 | return res; 19 | } 20 | 21 | test(); 22 | -------------------------------------------------------------------------------- /server/router/comments.js: -------------------------------------------------------------------------------- 1 | 2 | import express from 'express'; 3 | import axios from 'axios'; 4 | import _debug from 'debug'; 5 | 6 | const debug = _debug('dev:api'); 7 | const error = _debug('dev:error'); 8 | const router = express(); 9 | 10 | router.get('/like/:id/:songid/:like', async(req, res) => { 11 | debug('Handle request for /comments/like'); 12 | 13 | var result = { success: false }; 14 | var id = req.params.id; 15 | var songid = req.params.songid; 16 | var like = req.params.like; 17 | 18 | debug('Params \'id\': %s', id); 19 | debug('Params \'songid\': %s', songid); 20 | debug('Params \'liked\': %s', like); 21 | 22 | try { 23 | var response = await axios.get('/comment/like', { 24 | params: { 25 | id: songid, 26 | cid: id, 27 | type: 0, 28 | t: +like, 29 | } 30 | }); 31 | var data = response.data; 32 | 33 | if (data.code !== 200) { 34 | throw data; 35 | } else { 36 | result = { 37 | success: true, 38 | liked: !!+like, 39 | }; 40 | } 41 | } catch (ex) { 42 | error('Failed to like comment: %O', ex); 43 | } 44 | 45 | res.send(result); 46 | }); 47 | 48 | router.get('/:id/:offset?', async(req, res) => { 49 | debug('Handle request for /comments'); 50 | 51 | var result = {}; 52 | var id = req.params.id; 53 | var offset = req.params.offset || 0; 54 | 55 | debug('Params \'id\': %s', id); 56 | debug('Params \'offset\': %s', offset); 57 | 58 | try { 59 | var response = await axios.get(`/comment/music`, { 60 | params: { 61 | id, 62 | limit: 30, 63 | offset, 64 | } 65 | }); 66 | var data = response.data; 67 | 68 | if (data.code !== 200) { 69 | throw data; 70 | } else { 71 | result = { 72 | newestList: data.comments, 73 | hotList: data.hotComments, 74 | total: data.total, 75 | nextHref: data.more ? `/api/comments/${id}/${+offset + 30}` : '', 76 | }; 77 | } 78 | } catch (ex) { 79 | error('Failed to get comments: %O', ex); 80 | } 81 | 82 | res.send(result); 83 | }); 84 | 85 | module.exports = router; 86 | -------------------------------------------------------------------------------- /server/router/fm.js: -------------------------------------------------------------------------------- 1 | 2 | import express from 'express'; 3 | import axios from 'axios'; 4 | import _debug from 'debug'; 5 | 6 | const debug = _debug('dev:api'); 7 | const error = _debug('dev:error'); 8 | const router = express(); 9 | 10 | async function getPlaylist() { 11 | let response = await axios.get(`/personal_fm`); 12 | let data = response.data; 13 | var songs = []; 14 | 15 | if (data.code !== 200) { 16 | throw data; 17 | } 18 | 19 | songs = data.data || []; 20 | 21 | if (songs.length === 0) { 22 | return getPlaylist(); 23 | } 24 | 25 | return songs; 26 | } 27 | 28 | router.get('/', async(req, res) => { 29 | debug('Handle request for /fm'); 30 | 31 | let songs = []; 32 | 33 | try { 34 | songs = await getPlaylist(); 35 | } catch (ex) { 36 | error('Failed to get FM: %O', ex); 37 | } 38 | 39 | res.send({ 40 | id: 'PERSONAL_FM', 41 | name: 'Made For You', 42 | link: '/fm', 43 | size: songs.length, 44 | songs: songs.map(e => { 45 | var { album, artists } = e; 46 | 47 | return { 48 | id: e.id.toString(), 49 | name: e.name, 50 | duration: e.duration, 51 | album: { 52 | id: album.id.toString(), 53 | name: album.name, 54 | cover: album.picUrl, 55 | link: `/player/1/${album.id}`, 56 | }, 57 | artists: artists.map(e => ({ 58 | id: e.id.toString(), 59 | name: e.name, 60 | // Broken link 61 | link: e.id ? `/artist/${e.id}` : '', 62 | })) 63 | }; 64 | }), 65 | }); 66 | }); 67 | 68 | module.exports = router; 69 | -------------------------------------------------------------------------------- /server/router/lyrics.js: -------------------------------------------------------------------------------- 1 | 2 | import express from 'express'; 3 | import axios from 'axios'; 4 | import _debug from 'debug'; 5 | 6 | const debug = _debug('dev:api'); 7 | const error = _debug('dev:error'); 8 | const router = express(); 9 | 10 | router.get('/:id', async(req, res) => { 11 | debug('Handle request for /lyrics'); 12 | 13 | var result = {}; 14 | var id = req.params.id; 15 | 16 | debug('Params \'id\': %s', id); 17 | 18 | try { 19 | var response = await axios.get(`/lyric`, { 20 | params: { 21 | id, 22 | } 23 | }); 24 | var data = response.data; 25 | 26 | if (data.code !== 200) { 27 | throw data; 28 | } else { 29 | if (data.lrc === undefined) { 30 | return; 31 | } 32 | 33 | let lyrics = data.lrc.lyric.split('\n'); 34 | 35 | lyrics.map(e => { 36 | let match = e.match(/\[.+\]/); 37 | 38 | if (!match) { 39 | return; 40 | } 41 | 42 | let timestamp = match[0].replace(/\D/g, ':').replace(/^:|:$/g, '').split(':'); 43 | let content = e.replace(/\[.+\]/, ''); 44 | let times = parseInt(+timestamp[0] * 60 * 1000) + parseInt(+timestamp[1] * 1000) + parseInt(timestamp[2]); 45 | 46 | result[times] = content; 47 | }); 48 | } 49 | } catch (ex) { 50 | error('Failed to get lyrics: %O', ex); 51 | } 52 | 53 | res.send(result); 54 | }); 55 | 56 | module.exports = router; 57 | -------------------------------------------------------------------------------- /server/router/playlist.js: -------------------------------------------------------------------------------- 1 | 2 | import express from 'express'; 3 | import axios from 'axios'; 4 | import _debug from 'debug'; 5 | 6 | const debug = _debug('dev:api'); 7 | const error = _debug('dev:error'); 8 | const router = express(); 9 | const limit = 50; 10 | 11 | router.get('/:type?/:offset?', async(req, res) => { 12 | debug('Handle request for /playlist'); 13 | 14 | var playlists = []; 15 | var type = req.params.type || '全部'; 16 | var offset = +req.params.offset || 0; 17 | var nextHref = ''; 18 | 19 | debug('Params \'type\': %s', type); 20 | 21 | try { 22 | let response = await axios.get('/top/playlist', { 23 | params: { 24 | cat: type, 25 | limit, 26 | offset, 27 | order: 'hot', 28 | }, 29 | }); 30 | let data = response.data; 31 | 32 | if (data.code !== 200) { 33 | throw data; 34 | } else { 35 | data.playlists.map(e => { 36 | var creator = e.creator; 37 | 38 | playlists.push({ 39 | id: e.id.toString(), 40 | name: e.name, 41 | played: e.playCount, 42 | size: e.trackCount, 43 | link: `/player/0/${e.id}`, 44 | cover: `${e.coverImgUrl}?param=100y100`, 45 | user: { 46 | id: creator.userId.toString(), 47 | name: creator.nickname, 48 | link: `/user/${creator.userId}`, 49 | }, 50 | }); 51 | }); 52 | } 53 | 54 | if (data.more) { 55 | offset += limit; 56 | nextHref = `/api/playlist/${type}/${offset}`; 57 | } 58 | } catch (ex) { 59 | error('Failed to get playlist: %O', ex); 60 | } 61 | 62 | res.send({ 63 | playlists, 64 | nextHref, 65 | }); 66 | }); 67 | 68 | module.exports = router; 69 | -------------------------------------------------------------------------------- /server/router/qrcode/index.js: -------------------------------------------------------------------------------- 1 | 2 | import express from 'express'; 3 | import http from 'http'; 4 | import rp from 'request-promise-native'; 5 | import setCookie from 'set-cookie-parser'; 6 | import _debug from 'debug'; 7 | import wechat from './wechat'; 8 | import weibo from './weibo'; 9 | 10 | const debug = _debug('dev:api'); 11 | const error = _debug('dev:api:error'); 12 | const router = express(); 13 | const adaptors = { 14 | '10': wechat, 15 | '2': weibo, 16 | }; 17 | 18 | router.get('/generate/:type', async(req, res) => { 19 | debug('Handle request for /qrcode/generate'); 20 | 21 | var type = req.params.type; 22 | var adaptor = adaptors[type]; 23 | 24 | /** 25 | * 10: wechat 26 | * 2: weibo 27 | * */ 28 | debug('Params \'type\': %d', type); 29 | 30 | try { 31 | var response = await rp.get({ 32 | uri: `http://music.163.com/api/sns/authorize?snsType=${type}&clientType=web2&callbackType=Login&forcelogin=true`, 33 | resolveWithFullResponse: true, 34 | }); 35 | var payload = await adaptor.qrcode(response); 36 | res.send( 37 | Object.assign(payload, { type }) 38 | ); 39 | } catch (ex) { 40 | error(ex); 41 | res.send({ 42 | success: false, 43 | }); 44 | } 45 | }); 46 | 47 | router.post('/polling', async(req, res) => { 48 | debug('Handle request for /qrcode/polling'); 49 | 50 | var { ticket, state, type } = req.body; 51 | var adaptor = adaptors[type]; 52 | 53 | debug('Params \'ticket\': %s', ticket); 54 | debug('Params \'state\': %s', state); 55 | debug('Params \'type\': %d', type); 56 | 57 | try { 58 | var code = await adaptor.polling(ticket); 59 | 60 | switch (+type) { 61 | case 10: 62 | type = 'weichat'; 63 | break; 64 | case 2: 65 | type = 'weibo'; 66 | break; 67 | default: 68 | throw Error('Unknow type: %d', type); 69 | } 70 | 71 | http.get(`http://music.163.com/back/${type}?code=${code}&state=${state}`, response => { 72 | var cookies = setCookie.parse(response, { 73 | decodeValues: true 74 | }); 75 | 76 | if (cookies.length === 0) { 77 | throw Error('No Cookie'); 78 | } 79 | 80 | cookies.forEach( 81 | e => { 82 | res.cookie(e.name, e.value); 83 | } 84 | ); 85 | res.send({ success: true }); 86 | }); 87 | } catch (ex) { 88 | error(ex); 89 | res.send({ 90 | success: false, 91 | }); 92 | } 93 | }); 94 | 95 | module.exports = router; 96 | -------------------------------------------------------------------------------- /server/router/qrcode/wechat.js: -------------------------------------------------------------------------------- 1 | 2 | import url from 'url'; 3 | import rp from 'request-promise-native'; 4 | 5 | const wechat = { 6 | qrcode(response) { 7 | var matched = response.body.match(/(\/connect\/qrcode\/[\w-_]+)/); 8 | // eslint-disable-next-line 9 | var q = url.parse(response.request.href, true); 10 | 11 | if (!matched) { 12 | throw Error('Failed to generate wechat QRCode.'); 13 | } 14 | 15 | var ticket = matched[1].split('/')[3]; 16 | 17 | return { 18 | ticket, 19 | url: `https://open.weixin.qq.com/connect/qrcode/${ticket}`, 20 | state: q.query.state, 21 | }; 22 | }, 23 | 24 | async polling(ticket) { 25 | var response = await rp.get({ 26 | uri: `https://long.open.weixin.qq.com/connect/l/qrconnect?uuid=${ticket}&_=${Date.now()}`, 27 | jar: true, 28 | json: false, 29 | resolveWithFullResponse: true, 30 | }); 31 | var matched = response.body.match(/wx_errcode=(\d+).*'(.*)';$/); 32 | 33 | if (!matched) { 34 | throw Error('Invalid wechat ticket'); 35 | } 36 | 37 | /** 38 | * 408: continue 39 | * 404: scanned 40 | * 403: canceled 41 | * 405: done 42 | * */ 43 | var res = { 44 | wx_errcode: +matched[1], 45 | wx_code: matched[2], 46 | }; 47 | 48 | switch (res.wx_errcode) { 49 | case 405: 50 | return res.wx_code; 51 | case 408: 52 | case 404: 53 | return wechat.polling(ticket); 54 | case 403: 55 | throw Error('Login by wechat, canceled'); 56 | default: 57 | throw Error('An error occurred while login by wechat'); 58 | } 59 | } 60 | }; 61 | 62 | module.exports = wechat; 63 | -------------------------------------------------------------------------------- /server/router/qrcode/weibo.js: -------------------------------------------------------------------------------- 1 | 2 | import url from 'url'; 3 | import rp from 'request-promise-native'; 4 | 5 | const weibo = { 6 | async qrcode(response) { 7 | // eslint-disable-next-line 8 | var q = url.parse(response.request.href, true); 9 | var state = q.query.state; 10 | var res = await rp.get({ 11 | uri: `https://api.weibo.com/oauth2/qrcode_authorize/generate`, 12 | qs: { 13 | client_id: '301575942', 14 | redirect_uri: 'http://music.163.com/back/weibo', 15 | scope: 'friendships_groups_read,statuses_to_me_read,follow_app_official_microblog', 16 | response_type: 'code', 17 | state, 18 | _rnd: +new Date(), 19 | }, 20 | jar: true, 21 | json: true, 22 | resolveWithFullResponse: true, 23 | }); 24 | 25 | return { 26 | state, 27 | ticket: res.body.vcode, 28 | url: res.body.url, 29 | }; 30 | }, 31 | 32 | async polling(ticket) { 33 | var response = await rp.get({ 34 | uri: `https://api.weibo.com/oauth2/qrcode_authorize/query`, 35 | qs: { 36 | _rnd: Date.now(), 37 | vcode: ticket, 38 | }, 39 | jar: true, 40 | json: true, 41 | resolveWithFullResponse: true, 42 | }); 43 | 44 | /** 45 | * 1: continue 46 | * 2: scanned 47 | * 3: done 48 | * */ 49 | switch (+response.body.status) { 50 | case 1: 51 | case 2: 52 | return weibo.polling(ticket); 53 | case 3: 54 | // eslint-disable-next-line 55 | let q = url.parse(response.body.url, true); 56 | return q.query.code; 57 | default: 58 | throw Error('An error occurred while login by weibo'); 59 | } 60 | } 61 | }; 62 | 63 | module.exports = weibo; 64 | -------------------------------------------------------------------------------- /server/router/top.js: -------------------------------------------------------------------------------- 1 | 2 | import express from 'express'; 3 | import axios from 'axios'; 4 | import _debug from 'debug'; 5 | 6 | const debug = _debug('dev:api'); 7 | const error = _debug('dev:error'); 8 | const router = express(); 9 | 10 | router.get('/', async(req, res) => { 11 | debug('Handle request for /top'); 12 | 13 | var list = []; 14 | 15 | try { 16 | let response = await axios.get(`/toplist`); 17 | let data = response.data.result; 18 | 19 | if (response.data.code !== 200) { 20 | throw data; 21 | } 22 | 23 | list = response.data.list.map(data => { 24 | return { 25 | name: data.name, 26 | played: data.playCount, 27 | updateTime: data.updateTime, 28 | size: data.trackCount, 29 | link: `/player/0/${data.id}`, 30 | cover: data.coverImgUrl, 31 | }; 32 | }); 33 | } catch (ex) { 34 | error('Failed to get top list: %O', ex); 35 | } 36 | 37 | res.send({ 38 | list, 39 | }); 40 | }); 41 | 42 | module.exports = router; 43 | -------------------------------------------------------------------------------- /server/router/user.js: -------------------------------------------------------------------------------- 1 | 2 | import express from 'express'; 3 | import axios from 'axios'; 4 | import _debug from 'debug'; 5 | 6 | const debug = _debug('dev:api'); 7 | const error = _debug('dev:error'); 8 | const router = express(); 9 | 10 | async function getUser(id) { 11 | var user = {}; 12 | 13 | try { 14 | let response = await axios.get(`/user/detail?uid=${id}`); 15 | let data = response.data; 16 | 17 | if (data.code !== 200) { 18 | throw data; 19 | } else { 20 | user = data.profile; 21 | 22 | user = { 23 | id: user.userId.toString(), 24 | name: user.nickname, 25 | signature: user.signature, 26 | avatar: user.avatarUrl, 27 | followed: user.followed, 28 | followers: user.followeds, 29 | following: user.follows, 30 | }; 31 | } 32 | } catch (ex) { 33 | error('Failed to get user: %O', ex); 34 | } 35 | 36 | return user; 37 | } 38 | 39 | async function getPlaylist(id) { 40 | var list = []; 41 | 42 | try { 43 | let response = await axios.get(`/user/playlist?uid=${id}`); 44 | let data = response.data; 45 | 46 | if (data.code !== 200) { 47 | throw data; 48 | } else { 49 | list = data.playlist.map(e => ({ 50 | id: e.id.toString(), 51 | name: e.name, 52 | cover: e.coverImgUrl, 53 | played: e.playCount, 54 | size: e.trackCount, 55 | link: `/player/0/${e.id}` 56 | })); 57 | } 58 | } catch (ex) { 59 | error('Failed to get playlist: %O', ex); 60 | } 61 | 62 | return list; 63 | } 64 | 65 | router.get('/:id', async(req, res) => { 66 | debug('Handle request for /user'); 67 | 68 | var id = req.params.id; 69 | 70 | debug('Params \'id\': %s', id); 71 | 72 | res.send({ 73 | profile: await getUser(id), 74 | playlists: await getPlaylist(id), 75 | }); 76 | }); 77 | 78 | router.get('/unfollow/:id', async(req, res) => { 79 | debug('Handle request for /user/unfollow'); 80 | 81 | var id = req.params.id; 82 | var success = false; 83 | 84 | debug('Params \'id\': %s', id); 85 | 86 | try { 87 | let response = await axios.get(`/follow/?id=${id}&&t=0`); 88 | let data = response.data; 89 | 90 | success = data.code === 200; 91 | 92 | if (data.code !== 200) { 93 | throw data; 94 | } 95 | } catch (ex) { 96 | error('Failed to unfollow user: %O', ex); 97 | } 98 | 99 | res.send({ 100 | success, 101 | }); 102 | }); 103 | 104 | router.get('/follow/:id', async(req, res) => { 105 | debug('Handle request for /user/follow'); 106 | 107 | var id = req.params.id; 108 | var success = false; 109 | 110 | debug('Params \'id\': %s', id); 111 | 112 | try { 113 | let response = await axios.get(`/follow/?id=${id}&&t=1`); 114 | let data = response.data; 115 | 116 | success = data.code === 200; 117 | 118 | if (data.code !== 200) { 119 | throw data; 120 | } 121 | } catch (ex) { 122 | error('Failed to follow user: %O', ex); 123 | } 124 | 125 | res.send({ 126 | success, 127 | }); 128 | }); 129 | 130 | module.exports = router; 131 | -------------------------------------------------------------------------------- /server/usocket.js: -------------------------------------------------------------------------------- 1 | 2 | import net from 'net'; 3 | import fs from 'fs'; 4 | import _debug from 'debug'; 5 | 6 | const SOCKET_FILENAME = '/var/tmp/ieasemusic.sock'; 7 | let debug = _debug('dev:usocket'); 8 | let error = _debug('dev:usocket:error'); 9 | 10 | function handler(data, player) { 11 | var payload = JSON.parse(data.toString()); 12 | 13 | switch (payload.command) { 14 | case 'toggle': 15 | player.webContents.send('player-toggle'); 16 | break; 17 | 18 | case 'next': 19 | player.webContents.send('player-next'); 20 | break; 21 | 22 | case 'prev': 23 | player.webContents.send('player-previous'); 24 | break; 25 | 26 | case 'play': 27 | player.webContents.send('player-play', { 28 | id: payload.id, 29 | }); 30 | break; 31 | 32 | case 'changeMode': 33 | player.webContents.send('player-mode', { 34 | mode: payload.mode, 35 | }); 36 | break; 37 | 38 | case 'goodbye': 39 | player.goodbye(); 40 | break; 41 | 42 | case 'show': 43 | let isVisible = player.isVisible(); 44 | isVisible ? player.hide() : player.show(); 45 | break; 46 | 47 | case 'bug': 48 | require('electron').shell.openExternal('https://github.com/trazyn/ieaseMusic/issues'); 49 | break; 50 | } 51 | } 52 | 53 | function createServer(shared, player) { 54 | fs.unlink(SOCKET_FILENAME, () => { 55 | var server = net.createServer( 56 | s => { 57 | debug('🐶 Client connected >>>'); 58 | 59 | s.on('end', () => debug('🐱 Client disconnected <<<')); 60 | s.on('data', data => handler(data, player)); 61 | s.on('error', err => console.error(err)); 62 | s.write(JSON.stringify(shared)); 63 | } 64 | ); 65 | 66 | server.on( 67 | 'error', 68 | err => { 69 | error(err); 70 | } 71 | ); 72 | 73 | server.listen( 74 | SOCKET_FILENAME, 75 | () => { 76 | debug('🐤 Server bound on %s', SOCKET_FILENAME); 77 | } 78 | ); 79 | }); 80 | } 81 | 82 | export default createServer; 83 | -------------------------------------------------------------------------------- /src/assets/bgcolorful.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/src/assets/bgcolorful.jpg -------------------------------------------------------------------------------- /src/assets/close-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/src/assets/close-white.png -------------------------------------------------------------------------------- /src/assets/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/src/assets/close.png -------------------------------------------------------------------------------- /src/assets/dock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/src/assets/dock.png -------------------------------------------------------------------------------- /src/assets/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/src/assets/loading.gif -------------------------------------------------------------------------------- /src/assets/notplaying-dark-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/src/assets/notplaying-dark-panel.png -------------------------------------------------------------------------------- /src/assets/notplaying.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/src/assets/notplaying.png -------------------------------------------------------------------------------- /src/assets/playing-dark-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/src/assets/playing-dark-panel.png -------------------------------------------------------------------------------- /src/assets/playing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/src/assets/playing.png -------------------------------------------------------------------------------- /src/assets/qrcode-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/src/assets/qrcode-placeholder.png -------------------------------------------------------------------------------- /src/assets/social-facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/src/assets/social-facebook.png -------------------------------------------------------------------------------- /src/assets/social-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/src/assets/social-google.png -------------------------------------------------------------------------------- /src/assets/social-twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trazyn/ieaseMusic/a09864b41a28ed04ebec3ce822b391194c2ea2a2/src/assets/social-twitter.png -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ieaseMusic 7 | 8 | 9 | 10 |
11 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | import { webFrame } from 'electron'; 3 | import React from 'react'; 4 | import { render } from 'react-dom'; 5 | import { ThemeProvider } from 'react-jss'; 6 | 7 | import App from './app'; 8 | import theme from 'config/theme'; 9 | 10 | webFrame.setVisualZoomLevelLimits(1, 1); 11 | webFrame.setLayoutZoomLevelLimits(0, 0); 12 | 13 | render( 14 | 15 | 16 | , 17 | document.getElementById('root') 18 | ); 19 | -------------------------------------------------------------------------------- /src/js/components/Header/classes.js: -------------------------------------------------------------------------------- 1 | 2 | import colors from 'utils/colors'; 3 | 4 | export default theme => ({ 5 | container: { 6 | position: 'absolute', 7 | left: 0, 8 | top: 0, 9 | zIndex: 99, 10 | 11 | '& section': { 12 | position: 'relative', 13 | background: 'rgba(0, 0, 0, .6)', 14 | display: 'flex', 15 | justifyContent: 'space-between', 16 | alignItems: 'center', 17 | height: 38, 18 | padding: '0 16px', 19 | width: 'calc(100vw - 32px)', 20 | zIndex: 1, 21 | }, 22 | 23 | '& i': { 24 | display: 'inline-block', 25 | width: 32, 26 | marginRight: 4, 27 | fontSize: 20, 28 | color: '#eee', 29 | textAlign: 'center', 30 | cursor: 'pointer', 31 | 32 | '&:hover': { 33 | color: `${theme.header.iconHoverColor} !important`, 34 | textShadow: `0 0 24px ${colors.pallet.primary}`, 35 | }, 36 | }, 37 | 38 | '& i:last-child': { 39 | marginRight: 0, 40 | } 41 | }, 42 | 43 | transparent: { 44 | background: 'transparent !important', 45 | }, 46 | 47 | backward: { 48 | height: 12, 49 | width: 12, 50 | borderRadius: 12, 51 | border: 'thin solid #ddd', 52 | marginLeft: 58, 53 | background: '#eee', 54 | boxSizing: 'border-box', 55 | }, 56 | 57 | subscribed: { 58 | color: `${colors.pallet.sunflower} !important` 59 | }, 60 | }); 61 | -------------------------------------------------------------------------------- /src/js/components/Hero/classes.js: -------------------------------------------------------------------------------- 1 | 2 | import colors from 'utils/colors'; 3 | 4 | export default theme => ({ 5 | container: { 6 | position: 'relative', 7 | display: 'flex', 8 | flexDirection: 'column', 9 | justifyContent: 'space-between', 10 | width: '40vw', 11 | height: '100vh', 12 | overflow: 'hidden', 13 | 14 | '& aside': { 15 | padding: '24px', 16 | paddingTop: 60, 17 | fontSize: 24, 18 | }, 19 | 20 | '& summary': { 21 | flexDirection: 'column', 22 | position: 'relative', 23 | padding: '24px', 24 | paddingTop: 64, 25 | fontSize: 24, 26 | zIndex: 9, 27 | }, 28 | 29 | '& nav': { 30 | position: 'absolute', 31 | top: '45vh', 32 | left: 0, 33 | }, 34 | 35 | '& nav i': { 36 | color: '#fff', 37 | fontSize: 20, 38 | width: 32, 39 | textAlign: 'center', 40 | cursor: 'pointer', 41 | }, 42 | 43 | '& footer': { 44 | position: 'relative', 45 | padding: '12px 24px', 46 | }, 47 | 48 | '& article': { 49 | padding: 0, 50 | margin: 0, 51 | }, 52 | 53 | '& figure': { 54 | position: 'absolute', 55 | left: 0, 56 | top: 0, 57 | zIndex: -1, 58 | 59 | '&::after': { 60 | content: '""', 61 | position: 'absolute', 62 | left: 0, 63 | top: 0, 64 | display: 'block', 65 | width: '100%', 66 | height: '100%', 67 | backgroundColor: 'rgba(0, 0, 0, .3)' 68 | } 69 | }, 70 | 71 | '&:before': { 72 | content: '""', 73 | position: 'absolute', 74 | left: 0, 75 | bottom: 0, 76 | display: 'block', 77 | width: '100%', 78 | height: '20vh', 79 | background: 'linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(216, 216, 216, .9))', 80 | }, 81 | 82 | '& nav article': { 83 | color: '#fff', 84 | }, 85 | 86 | '& nav a': { 87 | display: 'inline-block', 88 | padding: '12px 24px', 89 | fontSize: 12, 90 | color: '#fff', 91 | background: '#000', 92 | textTransform: 'uppercase', 93 | transition: '.2s', 94 | cursor: 'pointer', 95 | }, 96 | 97 | '& nav a:hover': { 98 | color: colors.pallet.dribbble, 99 | }, 100 | }, 101 | 102 | share: { 103 | position: 'absolute', 104 | top: 0, 105 | right: 0, 106 | height: 38, 107 | width: 38, 108 | display: 'flex', 109 | justifyContent: 'center', 110 | alignItems: 'center', 111 | color: 'white', 112 | fontSize: 16, 113 | backgroundColor: 'transparent', 114 | transition: '.2s', 115 | zIndex: 99, 116 | 117 | '&:hover': { 118 | backgroundColor: 'black', 119 | color: colors.pallet.dribbble, 120 | } 121 | }, 122 | 123 | author: { 124 | marginTop: 2, 125 | fontSize: 11, 126 | maxWidth: 400, 127 | overflow: 'hidden', 128 | textOverflow: 'ellipsis', 129 | whiteSpace: 'nowrap', 130 | 131 | '& a': { 132 | display: 'inline-block', 133 | paddingBottom: 2, 134 | borderBottom: 'thin solid rgba(255, 255, 255, 0)', 135 | color: '#4a4a4a', 136 | transition: '.2s', 137 | 138 | '&:after': { 139 | content: '"/"', 140 | display: 'inline-block', 141 | margin: '0 5px', 142 | }, 143 | 144 | '&:hover': { 145 | borderBottomColor: '#000', 146 | }, 147 | }, 148 | 149 | '& a:last-child:after': { 150 | content: 'none', 151 | } 152 | }, 153 | 154 | liked: { 155 | color: colors.pallet.grape, 156 | textShadow: `0 0 24px ${colors.pallet.grape}`, 157 | }, 158 | 159 | badge: { 160 | display: 'table', 161 | padding: '6px 12px', 162 | marginTop: 24, 163 | letterSpacing: 1, 164 | textTransform: 'uppercase', 165 | fontFamily: 'Roboto', 166 | fontSize: 12, 167 | color: '#fff', 168 | background: colors.pallet.dribbble, 169 | boxShadow: `0 0 24px ${colors.pallet.dribbble}`, 170 | zoom: .8, 171 | }, 172 | }); 173 | -------------------------------------------------------------------------------- /src/js/components/Menu/classes.js: -------------------------------------------------------------------------------- 1 | 2 | export default theme => ({ 3 | container: { 4 | outline: 0, 5 | }, 6 | 7 | body: { 8 | position: 'fixed', 9 | left: 0, 10 | top: 0, 11 | display: 'flex', 12 | flexDirection: 'column', 13 | justifyContent: 'space-around', 14 | height: '100vh', 15 | width: '40vw', 16 | paddingLeft: 78, 17 | fontFamily: 'HelveticaNeue-UltraLight', 18 | background: '#fff', 19 | boxShadow: '0 30px 80px 0 rgba(97, 45, 45, .25)', 20 | zIndex: 999, 21 | 22 | '& a': { 23 | color: '#000', 24 | } 25 | }, 26 | 27 | overlay: { 28 | position: 'fixed', 29 | left: 0, 30 | top: 0, 31 | width: '100vw', 32 | height: '100vh', 33 | background: 'rgba(255, 255, 255, .3)', 34 | zIndex: 999, 35 | }, 36 | 37 | navs: { 38 | '& a': { 39 | position: 'relative', 40 | fontSize: 20, 41 | textIndent: 4, 42 | letterSpacing: 4, 43 | cursor: 'pointer', 44 | }, 45 | 46 | '& a:after': { 47 | content: '""', 48 | position: 'absolute', 49 | left: 0, 50 | bottom: 0, 51 | height: 1, 52 | width: 100, 53 | background: '#000', 54 | transform: 'translateX(-160px) translateY(-11px)', 55 | opacity: 0, 56 | transition: '.2s ease-out', 57 | }, 58 | 59 | '& a:hover:after': { 60 | width: 160, 61 | opacity: 1, 62 | transform: 'translateX(-100px) translateY(-11px)', 63 | } 64 | }, 65 | 66 | profile: { 67 | display: 'flex', 68 | justifyContent: 'flex-start', 69 | alignItems: 'center', 70 | marginTop: 24, 71 | marginBottom: 32, 72 | 73 | '& img': { 74 | height: 64, 75 | width: 64, 76 | borderRadius: 64, 77 | marginRight: 20, 78 | }, 79 | 80 | '& $username a': { 81 | display: 'inline-block', 82 | marginBottom: 4, 83 | fontSize: 24, 84 | letterSpacing: 1, 85 | textIndent: 0, 86 | maxWidth: 200, 87 | whiteSpace: 'nowrap', 88 | textOverflow: 'ellipsis', 89 | overflow: 'hidden', 90 | }, 91 | 92 | '& $logout': { 93 | fontSize: 14, 94 | letterSpacing: 2, 95 | } 96 | }, 97 | 98 | info: {}, 99 | 100 | username: { 101 | padding: 0, 102 | margin: 0, 103 | }, 104 | 105 | logout: { 106 | display: 'inline-table', 107 | textIndent: 4, 108 | paddingBottom: 2, 109 | borderBottom: 'thin solid transparent', 110 | transition: '.4s', 111 | 112 | '&:hover': { 113 | borderBottomColor: '#000', 114 | } 115 | }, 116 | 117 | social: { 118 | '& a': { 119 | display: 'inline-block', 120 | width: 48, 121 | height: 48, 122 | fontSize: 14, 123 | borderRadius: 48, 124 | marginRight: 20, 125 | lineHeight: '48px', 126 | textAlign: 'center', 127 | cursor: 'pointer', 128 | color: '#fff', 129 | letterSpacing: 0, 130 | transition: '.2s', 131 | }, 132 | 133 | '& a:hover': { 134 | boxShadow: '0 1px 24px rgba(0, 0, 0, .24)', 135 | }, 136 | 137 | '& $twitter': { 138 | background: '#55acee', 139 | }, 140 | 141 | '& $github': { 142 | background: 'rgba(0, 0, 0, .7)', 143 | }, 144 | }, 145 | 146 | github: {}, 147 | twitter: {}, 148 | }); 149 | -------------------------------------------------------------------------------- /src/js/components/Playing/classes.js: -------------------------------------------------------------------------------- 1 | 2 | export default theme => ({ 3 | container: { 4 | position: 'fixed', 5 | left: 0, 6 | top: 0, 7 | width: 'calc(40vw + 78px)', 8 | color: '#4a4a4a', 9 | background: '#fff', 10 | boxShadow: '0 30px 80px 0 rgba(97, 45, 45, .25)', 11 | zIndex: 999, 12 | outline: 0, 13 | 14 | '& section': { 15 | position: 'relative', 16 | zIndex: 1, 17 | outline: 0, 18 | }, 19 | 20 | '& header': { 21 | padding: '40px 78px 20px', 22 | }, 23 | 24 | '& input': { 25 | height: 40, 26 | lineHeight: '40px', 27 | width: '100%', 28 | fontFamily: 'HelveticaNeue-UltraLight', 29 | fontSize: 24, 30 | border: 0, 31 | outline: 0, 32 | }, 33 | }, 34 | 35 | overlay: { 36 | position: 'fixed', 37 | left: 0, 38 | top: 0, 39 | width: '100vw', 40 | height: '100vh', 41 | background: 'rgba(255, 255, 255, .3)', 42 | }, 43 | 44 | list: { 45 | listStyle: 'none', 46 | height: 'calc(100vh - 102px)', 47 | padding: 0, 48 | margin: 0, 49 | overflow: 'hidden', 50 | overflowY: 'auto', 51 | 52 | '& li': { 53 | display: 'flex', 54 | justifyContent: 'flex-start', 55 | alignItems: 'center', 56 | flexDirection: 'row', 57 | } 58 | }, 59 | 60 | song: { 61 | position: 'relative', 62 | display: 'flex', 63 | width: 'calc(100% - 78px)', 64 | justifyContent: 'flex-start', 65 | alignItems: 'center', 66 | padding: '24px 0', 67 | transition: '.4s', 68 | cursor: 'pointer', 69 | 70 | '& img': { 71 | height: 48, 72 | width: 48, 73 | margin: '0 24px', 74 | boxShadow: '0 0 24px 0 rgba(0, 0, 0, .3)', 75 | }, 76 | 77 | '& p': { 78 | margin: 0, 79 | padding: 0, 80 | }, 81 | 82 | '&$active $mask, &:hover $mask': { 83 | opacity: 1, 84 | } 85 | }, 86 | 87 | actions: { 88 | display: 'flex', 89 | width: 78, 90 | justifyContent: 'center', 91 | alignItems: 'center', 92 | flexDirection: 'row', 93 | }, 94 | 95 | active: {}, 96 | 97 | title: { 98 | maxWidth: 200, 99 | color: '#081600', 100 | fontSize: 14, 101 | whiteSpace: 'nowrap', 102 | overflow: 'hidden', 103 | textOverflow: 'ellipsis', 104 | }, 105 | 106 | author: { 107 | maxWidth: 200, 108 | whiteSpace: 'nowrap', 109 | overflow: 'hidden', 110 | 111 | '& a': { 112 | display: 'inline-block', 113 | color: '#4a4a4a', 114 | paddingBottom: 2, 115 | borderBottom: 'thin solid rgba(255, 255, 255, 0)', 116 | transition: '.2s', 117 | 118 | '&:hover': { 119 | borderBottomColor: '#000', 120 | }, 121 | 122 | '&:after': { 123 | content: '"/"', 124 | display: 'inline-block', 125 | margin: '0 5px', 126 | }, 127 | }, 128 | 129 | '& a:last-child:after': { 130 | content: 'none', 131 | } 132 | }, 133 | 134 | playing: { 135 | padding: 0, 136 | 137 | '& img': { 138 | height: 96, 139 | width: 96, 140 | margin: 0, 141 | marginRight: 24, 142 | }, 143 | }, 144 | 145 | mask: { 146 | position: 'absolute', 147 | left: 0, 148 | top: 0, 149 | width: '100%', 150 | height: 96, 151 | opacity: 0, 152 | transition: '.4s', 153 | zIndex: -1, 154 | 155 | '&:before': { 156 | position: 'absolute', 157 | top: 0, 158 | left: 0, 159 | content: '""', 160 | display: 'block', 161 | height: '100%', 162 | width: '100%', 163 | background: 'linear-gradient(to left, transparent, rgba(255, 255, 255, 1))', 164 | }, 165 | }, 166 | 167 | nothing: { 168 | display: 'flex', 169 | justifyContent: 'center', 170 | alignItems: 'center', 171 | height: 'calc(100vh - 180px)', 172 | fontFamily: 'HelveticaNeue-UltraLight', 173 | fontSize: 36, 174 | color: '#000', 175 | letterSpacing: 1, 176 | }, 177 | }); 178 | -------------------------------------------------------------------------------- /src/js/components/Ripple/PlayerMode.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import { inject, observer } from 'mobx-react'; 4 | import injectSheet from 'react-jss'; 5 | 6 | import classes from './classes'; 7 | import { PLAYER_LOOP, PLAYER_SHUFFLE, PLAYER_REPEAT } from 'stores/controller'; 8 | 9 | @inject(stores => ({ 10 | mode: stores.controller.mode, 11 | })) 12 | @observer 13 | class PlayerMode extends Component { 14 | componentWillUpdate() { 15 | this.animationDone(); 16 | } 17 | 18 | componentDidUpdate() { 19 | this.container.classList.add(this.props.classes.animated); 20 | } 21 | 22 | animationDone() { 23 | this.container.classList.remove(this.props.classes.animated); 24 | } 25 | 26 | renderIndicator(mode) { 27 | switch (mode) { 28 | case PLAYER_SHUFFLE: 29 | return ; 30 | 31 | case PLAYER_REPEAT: 32 | return ; 33 | 34 | case PLAYER_LOOP: 35 | return ; 36 | } 37 | } 38 | 39 | render() { 40 | var { classes, mode } = this.props; 41 | 42 | return ( 43 |
this.animationDone()} 46 | ref={ 47 | ele => (this.container = ele) 48 | } 49 | > 50 | { 51 | this.renderIndicator(mode) 52 | } 53 |
54 | ); 55 | } 56 | } 57 | 58 | export default injectSheet(classes)(PlayerMode); 59 | -------------------------------------------------------------------------------- /src/js/components/Ripple/PlayerNavigation.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import { ipcRenderer } from 'electron'; 4 | import injectSheet from 'react-jss'; 5 | 6 | import classes from './classes'; 7 | 8 | class PlayerNavigation extends Component { 9 | state = { 10 | // true: prev, false: next 11 | direction: true, 12 | }; 13 | 14 | componentWillUpdate() { 15 | this.animationDone(); 16 | } 17 | 18 | componentDidUpdate() { 19 | this.container.classList.add(this.props.classes.animated); 20 | } 21 | 22 | animationDone() { 23 | this.shouldUpdate = false; 24 | this.container.classList.remove(this.props.classes.animated); 25 | } 26 | 27 | shouldComponentUpdate() { 28 | return !!this.shouldUpdate; 29 | } 30 | 31 | componentDidMount() { 32 | ipcRenderer.on('player-previous', () => { 33 | this.shouldUpdate = true; 34 | this.setState({ 35 | direction: true, 36 | }); 37 | }); 38 | 39 | ipcRenderer.on('player-next', () => { 40 | this.shouldUpdate = true; 41 | this.setState({ 42 | direction: false, 43 | }); 44 | }); 45 | } 46 | 47 | render() { 48 | var classes = this.props.classes; 49 | 50 | return ( 51 |
this.animationDone()} 54 | ref={ 55 | ele => (this.container = ele) 56 | } 57 | > 58 | { 59 | this.state.direction 60 | ? 61 | : 62 | } 63 |
64 | ); 65 | } 66 | } 67 | 68 | export default injectSheet(classes)(PlayerNavigation); 69 | -------------------------------------------------------------------------------- /src/js/components/Ripple/PlayerStatus.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import { inject, observer } from 'mobx-react'; 4 | import injectSheet from 'react-jss'; 5 | 6 | import classes from './classes'; 7 | 8 | @inject(stores => ({ 9 | playing: stores.controller.playing, 10 | })) 11 | @observer 12 | class PlayerStatus extends Component { 13 | componentWillUpdate() { 14 | this.animationDone(); 15 | } 16 | 17 | componentWillReceiveProps(nextProps) { 18 | if (nextProps.playing !== this.props.playing) { 19 | // Force show the animation 20 | this.animationDone(); 21 | } 22 | } 23 | 24 | componentDidUpdate() { 25 | this.container.classList.add(this.props.classes.animated); 26 | } 27 | 28 | animationDone() { 29 | this.container.classList.remove(this.props.classes.animated); 30 | } 31 | 32 | render() { 33 | var { classes, playing } = this.props; 34 | 35 | return ( 36 |
this.animationDone()} 39 | ref={ 40 | ele => (this.container = ele) 41 | } 42 | > 43 | { 44 | playing 45 | ? 46 | : 47 | } 48 |
49 | ); 50 | } 51 | } 52 | 53 | export default injectSheet(classes)(PlayerStatus); 54 | -------------------------------------------------------------------------------- /src/js/components/Ripple/VolumeUpDown.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import { inject, observer } from 'mobx-react'; 4 | import { ipcRenderer } from 'electron'; 5 | import injectSheet from 'react-jss'; 6 | 7 | import classes from './classes'; 8 | 9 | @inject(stores => ({ 10 | isMuted: () => stores.preferences.volume === 0, 11 | })) 12 | @observer 13 | class VolumeUpDown extends Component { 14 | state = { 15 | // true: up, false: down 16 | direction: true, 17 | }; 18 | 19 | componentWillUpdate() { 20 | this.animationDone(); 21 | } 22 | 23 | componentDidUpdate() { 24 | this.container.classList.add(this.props.classes.animated); 25 | } 26 | 27 | animationDone() { 28 | this.container.classList.remove(this.props.classes.animated); 29 | } 30 | 31 | componentDidMount() { 32 | ipcRenderer.on('player-volume-up', () => { 33 | this.setState({ 34 | direction: true, 35 | }); 36 | }); 37 | 38 | ipcRenderer.on('player-volume-down', () => { 39 | this.setState({ 40 | direction: false, 41 | }); 42 | }); 43 | } 44 | 45 | render() { 46 | var { classes, isMuted } = this.props; 47 | 48 | return ( 49 |
this.animationDone()} 52 | ref={ 53 | ele => (this.container = ele) 54 | } 55 | > 56 | { 57 | isMuted() 58 | ? 63 | : ( 64 | this.state.direction 65 | ? 66 | : 67 | ) 68 | } 69 |
70 | ); 71 | } 72 | } 73 | 74 | export default injectSheet(classes)(VolumeUpDown); 75 | -------------------------------------------------------------------------------- /src/js/components/Ripple/classes.js: -------------------------------------------------------------------------------- 1 | 2 | import helper from 'utils/helper'; 3 | 4 | export default theme => { 5 | var animationName = helper.randomName(); 6 | 7 | return { 8 | container: { 9 | position: 'fixed', 10 | right: '50%', 11 | top: '50%', 12 | width: 64, 13 | height: 64, 14 | lineHeight: '64px', 15 | borderRadius: 64, 16 | marginRight: -32, 17 | marginTop: -32, 18 | color: '#fff', 19 | fontSize: 24, 20 | textAlign: 'center', 21 | background: '#000', 22 | boxShadow: '0 30px 80px 0 rgba(97, 45, 45, .25)', 23 | zIndex: 999, 24 | opacity: 0, 25 | visibility: 'hidden', 26 | }, 27 | 28 | animated: { 29 | animationName, 30 | animationDuration: '800ms', 31 | animationIterationCount: 1, 32 | }, 33 | 34 | [`@keyframes ${animationName}`]: { 35 | '0%': { 36 | opacity: 0, 37 | transform: 'scale(.8)', 38 | visibility: 'hidden', 39 | }, 40 | 41 | '40%': { 42 | opacity: 1, 43 | transform: 'scale(1)', 44 | visibility: 'visible', 45 | }, 46 | 47 | '100%': { 48 | opacity: 0, 49 | transform: 'scale(1.2)', 50 | visibility: 'hidden', 51 | }, 52 | }, 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /src/js/components/Share/classes.js: -------------------------------------------------------------------------------- 1 | 2 | export default theme => { 3 | return { 4 | modal: {}, 5 | 6 | container: { 7 | width: '100vw', 8 | height: '100vh', 9 | background: 'rgba(255, 255, 255, .9)', 10 | 11 | '& main': { 12 | position: 'absolute', 13 | top: 0, 14 | left: '50%', 15 | marginLeft: -150, 16 | display: 'flex', 17 | height: '100vh', 18 | width: 300, 19 | justifyContent: 'center', 20 | alignItems: 'center', 21 | flexDirection: 'column', 22 | paddingLeft: 20, 23 | paddingRight: 20, 24 | }, 25 | 26 | '& summary': { 27 | paddingRight: 20, 28 | paddingLeft: 20, 29 | textAlign: 'center', 30 | }, 31 | 32 | '& h2': { 33 | fontFamily: 'HelveticaNeue-UltraLight', 34 | fontSize: 24, 35 | letterSpacing: 2, 36 | color: 'black', 37 | }, 38 | 39 | '& p': { 40 | paddingBottom: 1, 41 | maxWidth: 400, 42 | marginTop: 0, 43 | marginBottom: 32, 44 | fontSize: 16, 45 | color: '#333', 46 | overflow: 'hidden', 47 | textOverflow: 'ellipsis', 48 | whiteSpace: 'nowrap', 49 | }, 50 | 51 | '& section img': { 52 | width: 64, 53 | height: 64, 54 | } 55 | }, 56 | 57 | qrcode: { 58 | marginBottom: 64, 59 | background: '#ddd', 60 | transform: 'translateY(12px)', 61 | 62 | '& img': { 63 | width: 128, 64 | height: 128, 65 | }, 66 | 67 | '& figcaption': { 68 | height: 24, 69 | marginTop: -4, 70 | lineHeight: '24px', 71 | textAlign: 'center', 72 | background: 'white', 73 | color: '#333', 74 | textTransform: 'uppercase', 75 | } 76 | }, 77 | 78 | close: { 79 | '& img': { 80 | width: 48, 81 | height: 48, 82 | } 83 | }, 84 | }; 85 | }; 86 | -------------------------------------------------------------------------------- /src/js/components/UpNext/classes.js: -------------------------------------------------------------------------------- 1 | 2 | import colors from 'utils/colors'; 3 | import helper from 'utils/helper'; 4 | 5 | export default theme => { 6 | var animationName = helper.randomName(); 7 | 8 | return { 9 | modal: {}, 10 | 11 | container: { 12 | display: 'flex', 13 | flexDirection: 'column', 14 | justifyContent: 'center', 15 | alignItems: 'center', 16 | marginTop: -40, 17 | 18 | '& h2': { 19 | fontFamily: 'HelveticaNeue-UltraLight', 20 | fontSize: 24, 21 | letterSpacing: 2, 22 | }, 23 | 24 | '& p': { 25 | paddingBottom: 1, 26 | maxWidth: 400, 27 | marginTop: 0, 28 | marginBottom: 32, 29 | fontSize: 16, 30 | color: '#ddd', 31 | overflow: 'hidden', 32 | textOverflow: 'ellipsis', 33 | whiteSpace: 'nowrap', 34 | }, 35 | 36 | '& button': { 37 | padding: '6px 14px', 38 | background: 'rgba(255, 255, 255, .7)', 39 | textTransform: 'uppercase', 40 | fontSize: 14, 41 | border: 0, 42 | color: '#333', 43 | cursor: 'pointer', 44 | }, 45 | }, 46 | 47 | circle: { 48 | position: 'relative', 49 | overflow: 'hidden', 50 | 51 | '&:hover $mask': { 52 | opacity: .2, 53 | }, 54 | 55 | '&:hover $play': { 56 | opacity: 1, 57 | transform: 'scale(1)', 58 | } 59 | }, 60 | 61 | mask: { 62 | position: 'absolute', 63 | left: 0, 64 | top: 0, 65 | width: '100%', 66 | height: '100%', 67 | borderRadius: '100%', 68 | background: '#000', 69 | opacity: 0, 70 | transition: 'opacity .5s', 71 | zIndex: 9, 72 | }, 73 | 74 | cover: { 75 | height: 140, 76 | width: 140, 77 | borderRadius: 140, 78 | overflow: 'hidden' 79 | }, 80 | 81 | play: { 82 | position: 'absolute', 83 | left: '50%', 84 | top: '50%', 85 | width: 40, 86 | height: 40, 87 | lineHeight: '40px', 88 | borderRadius: 40, 89 | marginLeft: -20, 90 | marginTop: -20, 91 | background: '#fff', 92 | color: '#000', 93 | opacity: 0, 94 | transform: 'scale(.75)', 95 | transition: '.5s', 96 | textAlign: 'center', 97 | zIndex: 9, 98 | cursor: 'pointer', 99 | }, 100 | 101 | svg: { 102 | position: 'absolute', 103 | left: 0, 104 | top: 0, 105 | transform: 'rotate(-90deg)', 106 | }, 107 | 108 | outter: { 109 | fill: 'transparent', 110 | stroke: colors.pallet.twitter, 111 | strokeWidth: 4, 112 | // 2 * Math.PI * R 113 | strokeDasharray: 427, 114 | strokeDashoffset: 0, 115 | 116 | animationName, 117 | animationDuration: '8s', 118 | animationIterationCount: 1, 119 | animationTimingFunction: 'linear', 120 | }, 121 | 122 | [`@keyframes ${animationName}`]: { 123 | '0%': { 124 | strokeDashoffset: 427, 125 | }, 126 | 127 | '100%': { 128 | strokeDashoffset: 0, 129 | }, 130 | }, 131 | }; 132 | }; 133 | -------------------------------------------------------------------------------- /src/js/components/UpNext/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import { inject, observer } from 'mobx-react'; 4 | import { Modal, ModalBody } from 'ui/Modal'; 5 | import injectSheet, { ThemeProvider } from 'react-jss'; 6 | import clazz from 'classname'; 7 | 8 | import classes from './classes'; 9 | import ProgressImage from 'ui/ProgressImage'; 10 | import theme from 'config/theme'; 11 | 12 | @inject('upnext', 'controller') 13 | @observer 14 | class UpNext extends Component { 15 | close() { 16 | this.props.upnext.show = false; 17 | } 18 | 19 | renderContent() { 20 | var { classes, upnext, controller } = this.props; 21 | var song = upnext.song; 22 | 23 | return ( 24 |
25 |

Up Next

26 | 27 |

28 | {song.name} - { 29 | song.artists.map((e, index) => e.name).join() 30 | } 31 |

32 | 33 |
37 |
45 | 46 | { 50 | this.close(); 51 | controller.play(song.id); 52 | } 53 | } 54 | /> 55 | 56 | 64 |
65 | 66 | 71 | { 75 | if (!upnext.show) { 76 | return; 77 | } 78 | this.close(); 79 | controller.play(song.id); 80 | } 81 | } 82 | cx="70" 83 | cy="70" 84 | 85 | // (140 / 2) - (12 / 2) = 64 86 | r="68" 87 | /> 88 | 89 |
90 | 91 | 103 |
104 | ); 105 | } 106 | 107 | render() { 108 | var { classes, upnext } = this.props; 109 | 110 | return ( 111 | 114 | 117 | 118 | 119 | { 120 | this.renderContent() 121 | } 122 | 123 | 124 | 125 | ); 126 | } 127 | } 128 | 129 | export default injectSheet(classes)(UpNext); 130 | -------------------------------------------------------------------------------- /src/js/pages/Comments/classes.js: -------------------------------------------------------------------------------- 1 | 2 | import colors from 'utils/colors'; 3 | 4 | export default theme => ({ 5 | container: { 6 | position: 'fixed', 7 | left: 0, 8 | top: 0, 9 | display: 'flex', 10 | height: '100vh', 11 | width: '100vw', 12 | backgroundColor: 'white', 13 | zIndex: 99, 14 | 15 | '& h3': { 16 | paddingBottom: 4, 17 | fontFamily: 'HelveticaNeue-UltraLight', 18 | fontSize: 24, 19 | fontWeight: 'lighter', 20 | borderBottom: 'thin solid white', 21 | letterSpacing: 1, 22 | wordSpacing: 2, 23 | }, 24 | }, 25 | 26 | list: { 27 | width: '60vw', 28 | height: '100vh', 29 | padding: '16px 24px', 30 | color: '#777', 31 | overflow: 'hidden', 32 | overflowY: 'auto', 33 | 34 | '& h3': { 35 | display: 'inline-block', 36 | color: 'dimgray', 37 | borderBottomColor: 'dimgray', 38 | } 39 | }, 40 | 41 | comment: { 42 | display: 'flex', 43 | justifyContent: 'flex-start', 44 | marginBottom: 24, 45 | 46 | '& aside': { 47 | flex: 1, 48 | }, 49 | 50 | '& figure': { 51 | marginRight: 24, 52 | borderRadius: 48, 53 | }, 54 | 55 | '& p': { 56 | marginTop: 0, 57 | }, 58 | }, 59 | 60 | meta: { 61 | display: 'flex', 62 | justifyContent: 'space-between', 63 | alignItems: 'center', 64 | fontSize: 12, 65 | color: 'rgba(0, 0, 0, .3)', 66 | 67 | '& span': { 68 | marginRight: 10, 69 | }, 70 | 71 | '$thumbsup': { 72 | cursor: 'pointer', 73 | transition: '.3s', 74 | 75 | '&:hover, &:hover i': { 76 | color: colors.pallet.primary, 77 | } 78 | }, 79 | }, 80 | 81 | thumbsup: {}, 82 | 83 | nestest: { 84 | padding: 0, 85 | margin: 0, 86 | marginTop: 14, 87 | listStyle: 'none', 88 | 89 | '& a': { 90 | display: 'inline-block', 91 | padding: '1px 2px', 92 | color: '#2d82ca', 93 | whiteSpace: 'nowrap', 94 | borderBottom: 'thin solid transparent', 95 | transition: '.2s', 96 | 97 | '&:hover': { 98 | borderBottomColor: '#2d82ca', 99 | } 100 | }, 101 | 102 | '& li': { 103 | background: 'rgba(0, 0, 0, 0.03)', 104 | padding: '4px 12px', 105 | fontSize: 12, 106 | lineHeight: '20px', 107 | } 108 | }, 109 | 110 | loadmore: {}, 111 | 112 | liked: { 113 | color: colors.pallet.grape, 114 | textShadow: `0 0 24px ${colors.pallet.grape}`, 115 | }, 116 | }); 117 | -------------------------------------------------------------------------------- /src/js/pages/Login/Legacy/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import { inject, observer } from 'mobx-react'; 4 | import { Link } from 'react-router-dom'; 5 | import injectSheet from 'react-jss'; 6 | import clazz from 'classname'; 7 | 8 | import classes from './classes'; 9 | 10 | @inject(stores => ({ 11 | login: stores.me.login, 12 | logining: stores.me.logining, 13 | })) 14 | @observer 15 | class Legacy extends Component { 16 | state = { 17 | showError: false, 18 | } 19 | 20 | async doLogin() { 21 | var phone = this.phone.value; 22 | var password = this.password.value; 23 | 24 | // Test phone and password not empty 25 | if (!phone.trim() || !password.trim()) { 26 | this.setState({ 27 | showError: true, 28 | }); 29 | 30 | return; 31 | } 32 | 33 | this.setState({ 34 | showError: false 35 | }); 36 | 37 | if (await this.props.login(phone, password)) { 38 | // Login success 39 | this.props.history.replace(+this.props.match.params.fm ? '/fm' : '/'); 40 | return; 41 | } 42 | 43 | // Login failed 44 | this.setState({ 45 | showError: true, 46 | }); 47 | } 48 | 49 | async handleEnter(e) { 50 | if (e.charCode !== 13) { 51 | return; 52 | } 53 | 54 | this.doLogin(); 55 | } 56 | 57 | render() { 58 | var { classes, logining } = this.props; 59 | 60 | return ( 61 |
62 | 66 | 67 | Discover music 68 | 69 | 70 |
71 |

Sign in

72 |

Hello there! Sign in and start playing with ieaseMusic <3

73 |
74 | 75 |
76 | (this.phone = ele) 79 | } 80 | type="text" 81 | placeholder="Your phone number" 82 | /> 83 | this.handleEnter(e)} 85 | placeholder="Password" 86 | ref={ 87 | ele => (this.password = ele) 88 | } 89 | type="password" /> 90 | 91 |

98 | Invalid username or password, Please try again. 99 |

100 |
101 | 102 |
103 | 114 | 115 |
116 | 120 | Login with WeChat 121 | 122 | 123 | 127 | Login with Weibo 128 | 129 |
130 |
131 |
132 | ); 133 | } 134 | } 135 | 136 | export default injectSheet(classes)(Legacy); 137 | -------------------------------------------------------------------------------- /src/js/pages/Login/QRCode/classes.js: -------------------------------------------------------------------------------- 1 | 2 | import colors from 'utils/colors'; 3 | 4 | export default theme => ({ 5 | container: { 6 | display: 'flex', 7 | flexDirection: 'column', 8 | justifyContent: 'space-between', 9 | alignItems: 'center', 10 | paddingTop: '8vh', 11 | textAlign: 'center', 12 | 13 | '& header, & figure': { 14 | zIndex: 1, 15 | }, 16 | 17 | '& figure': { 18 | display: 'flex', 19 | width: 200, 20 | flexDirection: 'column', 21 | marginTop: '5vh', 22 | justifyContent: 'center', 23 | alignItems: 'center', 24 | }, 25 | 26 | '& h1': { 27 | fontFamily: 'HelveticaNeue-UltraLight', 28 | fontWeight: 'lighter', 29 | fontSize: 44, 30 | letterSpacing: 2, 31 | wordSpacing: 6, 32 | color: colors.pallet.dribbble, 33 | }, 34 | 35 | '& p': { 36 | fontSize: 12, 37 | maxWidth: 300, 38 | lineHeight: '24px', 39 | color: '#000', 40 | wordSpacing: 2, 41 | }, 42 | 43 | '& figure p': { 44 | color: '#666', 45 | }, 46 | 47 | '& figure a': { 48 | paddingBottom: 3, 49 | color: colors.pallet.google, 50 | textTransform: 'uppercase', 51 | borderBottom: '2px solid #999', 52 | }, 53 | 54 | '& a': { 55 | color: '#000', 56 | }, 57 | 58 | '&:before': { 59 | content: '""', 60 | position: 'absolute', 61 | left: 0, 62 | top: 0, 63 | display: 'block', 64 | height: '100vh', 65 | width: '100vw', 66 | background: 'rgba(255, 255, 255, .9)', 67 | }, 68 | }, 69 | 70 | back: { 71 | position: 'fixed', 72 | top: 0, 73 | right: 12, 74 | height: 40, 75 | lineHeight: '40px', 76 | fontSize: 12, 77 | textTransform: 'uppercase', 78 | zIndex: 9, 79 | transition: '.2s', 80 | 81 | '& i': { 82 | display: 'inline-block', 83 | marginRight: 8, 84 | fontSize: 20, 85 | transform: 'translateY(3px)', 86 | }, 87 | 88 | '&:hover': { 89 | color: colors.pallet.primary, 90 | }, 91 | }, 92 | 93 | wraped: { 94 | position: 'relative', 95 | width: 128, 96 | height: 128, 97 | }, 98 | 99 | qrcode: { 100 | position: 'absolute', 101 | top: 0, 102 | left: 0, 103 | width: '100%', 104 | height: '100%', 105 | }, 106 | }); 107 | -------------------------------------------------------------------------------- /src/js/pages/Login/QRCode/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import { inject, observer } from 'mobx-react'; 4 | import { Link } from 'react-router-dom'; 5 | import injectSheet from 'react-jss'; 6 | 7 | import classes from './classes'; 8 | import FadeImage from 'ui/FadeImage'; 9 | 10 | @inject('me') 11 | @observer 12 | class QRCode extends Component { 13 | async pleaseLogin() { 14 | var { fm, type } = this.props.match.params; 15 | 16 | await this.props.me.generate(+type); 17 | this.props.me.waiting( 18 | () => { 19 | this.props.history.replace(+fm ? '/fm' : '/'); 20 | } 21 | ); 22 | } 23 | 24 | tick() { 25 | clearInterval(this.timer); 26 | 27 | // 5 mins refresh QR Code 28 | this.timer = setInterval( 29 | () => { 30 | this.pleaseLogin(); 31 | }, 32 | 5 * 60 * 1000 33 | ); 34 | } 35 | 36 | refresh() { 37 | this.pleaseLogin(); 38 | this.tick(); 39 | } 40 | 41 | componentDidMount = () => this.refresh(); 42 | componentWillUnmount = () => clearInterval(this.timer); 43 | 44 | render() { 45 | var { classes, me: { qrcode } } = this.props; 46 | 47 | return ( 48 |
49 | 53 | 54 | Login by Phone 55 | 56 | 57 |
58 |

Sign in

59 |

Hello there! Sign in and start playing with ieaseMusic <3

60 |
61 | 62 |
63 |
64 | { 65 | qrcode.url 66 | ? ( 67 | 71 | ) 72 | : ( 73 | 77 | ) 78 | } 79 |
80 | 81 |
82 |

Please use WeChat or Weibo to scan QR code to log in.

83 |
84 | 85 | { 88 | e.preventDefault(); 89 | this.refresh(); 90 | }} 91 | > 92 | Refresh 93 | 94 |
95 |
96 | ); 97 | } 98 | } 99 | 100 | export default injectSheet(classes)(QRCode); 101 | -------------------------------------------------------------------------------- /src/js/pages/Login/index.js: -------------------------------------------------------------------------------- 1 | 2 | import Legacy from './Legacy'; 3 | import QRCode from './QRCode'; 4 | 5 | export { Legacy, QRCode }; 6 | -------------------------------------------------------------------------------- /src/js/pages/Lyrics/classes.js: -------------------------------------------------------------------------------- 1 | 2 | import colors from 'utils/colors'; 3 | 4 | export default theme => ({ 5 | container: { 6 | position: 'fixed', 7 | left: 0, 8 | top: 0, 9 | display: 'flex', 10 | height: '100vh', 11 | width: '100vw', 12 | backgroundColor: 'white', 13 | zIndex: 99, 14 | 15 | '& h3': { 16 | paddingBottom: 4, 17 | fontFamily: 'HelveticaNeue-UltraLight', 18 | fontSize: 24, 19 | fontWeight: 'lighter', 20 | borderBottom: 'thin solid white', 21 | letterSpacing: 1, 22 | wordSpacing: 2, 23 | }, 24 | }, 25 | 26 | lyrics: { 27 | position: 'relative', 28 | width: '60vw', 29 | textAlign: 'center', 30 | fontWeight: 'lighter', 31 | fontSize: 16, 32 | lineHeight: '36px', 33 | wordSpacing: '1px', 34 | 35 | '& figure': { 36 | filter: 'blur(40px)', 37 | }, 38 | 39 | '& section': { 40 | position: 'absolute', 41 | width: '60vw', 42 | height: '100vh', 43 | top: 0, 44 | overflow: 'hidden', 45 | overflowY: 'auto', 46 | 47 | '&:before': { 48 | content: '""', 49 | position: 'fixed', 50 | top: 0, 51 | left: '40vw', 52 | display: 'block', 53 | height: '100vh', 54 | width: '60vw', 55 | background: 'rgba(0, 0, 0, .3)', 56 | } 57 | }, 58 | 59 | '& [playing] span': { 60 | display: 'inline-block', 61 | paddingBottom: 4, 62 | fontSize: 24, 63 | color: colors.pallet.mint, 64 | borderBottom: `thin solid ${colors.pallet.mint}`, 65 | }, 66 | 67 | '& figure > img': { 68 | height: '100vh', 69 | } 70 | }, 71 | 72 | placeholder: { 73 | paddingTop: '20vh', 74 | fontFamily: 'HelveticaNeue-UltraLight', 75 | fontSize: 32, 76 | letterSpacing: 1, 77 | wordSpacing: 3, 78 | textAlign: 'center', 79 | color: '#fff', 80 | }, 81 | }); 82 | -------------------------------------------------------------------------------- /src/js/pages/Lyrics/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import { inject, observer } from 'mobx-react'; 4 | import injectSheet from 'react-jss'; 5 | 6 | import classes from './classes'; 7 | import ProgressImage from 'ui/ProgressImage'; 8 | import Loader from 'ui/Loader'; 9 | import Hero from 'components/Hero'; 10 | import Header from 'components/Header'; 11 | 12 | @inject(stores => ({ 13 | loading: stores.lyrics.loading, 14 | getLyrics: () => stores.lyrics.getLyrics(), 15 | lyrics: stores.lyrics.list, 16 | song: stores.controller.song, 17 | })) 18 | @observer 19 | class Lyrics extends Component { 20 | componentWillMount() { 21 | this.props.getLyrics(); 22 | } 23 | 24 | componentWillReceiveProps(nextProps) { 25 | if (this.props.song.id !== nextProps.song.id) { 26 | this.props.getLyrics(); 27 | } 28 | } 29 | 30 | renderLyrics() { 31 | var { lyrics, classes } = this.props; 32 | var times = Object.keys(lyrics); 33 | 34 | if (times.length === 0) { 35 | return ( 36 |
37 | 38 | Nothing ... 39 | 40 |
41 | ); 42 | } 43 | 44 | return times.map( 45 | (e, index) => { 46 | return ( 47 |

51 | 52 | { 53 | lyrics[e] 54 | } 55 | 56 |

57 | ); 58 | } 59 | ); 60 | } 61 | 62 | render() { 63 | var { classes, loading, song } = this.props; 64 | 65 | if (loading || !song.id) { 66 | return ; 67 | } 68 | 69 | return ( 70 |
71 |
75 | 76 | 77 | 78 | 115 |
116 | ); 117 | } 118 | } 119 | 120 | export default injectSheet(classes)(Lyrics); 121 | -------------------------------------------------------------------------------- /src/js/pages/Player/Search/classes.js: -------------------------------------------------------------------------------- 1 | 2 | export default theme => ({ 3 | container: { 4 | position: 'fixed', 5 | top: 0, 6 | right: 0, 7 | width: 'calc(100vw - 260px)', 8 | height: 'calc(100vh - 52px)', 9 | background: '#fff', 10 | overflow: 'hidden', 11 | zIndex: 99, 12 | 13 | '& header': { 14 | display: 'flex', 15 | justifyContent: 'space-between', 16 | alignItems: 'center', 17 | height: 66, 18 | paddingLeft: 24, 19 | paddingRight: 16, 20 | boxShadow: 'inset 0 0 0.7px 0 rgba(0, 0, 0, .5)', 21 | }, 22 | 23 | '& header input': { 24 | height: 40, 25 | lineHeight: '40px', 26 | width: '100%', 27 | fontFamily: 'HelveticaNeue-UltraLight', 28 | fontSize: 24, 29 | border: 0, 30 | outline: 0, 31 | } 32 | }, 33 | 34 | close: { 35 | height: 32, 36 | cursor: 'pointer', 37 | }, 38 | 39 | list: { 40 | '& ul': { 41 | height: 'calc(100vh - 52px - 66px) !important', 42 | } 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /src/js/pages/Player/Search/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import injectSheet from 'react-jss'; 5 | 6 | import classes from './classes'; 7 | 8 | class Search extends Component { 9 | static propsTypes = { 10 | show: PropTypes.bool.isRequired, 11 | close: PropTypes.func.isRequired, 12 | }; 13 | 14 | pressEscExit(e) { 15 | if (e.keyCode === 27) { 16 | this.props.close(); 17 | } 18 | } 19 | 20 | render() { 21 | var { classes, show, close, filter, children } = this.props; 22 | 23 | if (!show) { 24 | return false; 25 | } 26 | 27 | return ( 28 |
this.pressEscExit(e)}> 31 |
32 | filter(e.target.value)} 36 | placeholder="Search..." /> 37 | Close 42 |
43 | 44 |
45 | {children} 46 |
47 |
48 | ); 49 | } 50 | } 51 | 52 | export default injectSheet(classes)(Search); 53 | -------------------------------------------------------------------------------- /src/js/pages/Singleton/classes.js: -------------------------------------------------------------------------------- 1 | 2 | import colors from 'utils/colors'; 3 | import helper from 'utils/helper'; 4 | 5 | export default theme => { 6 | var animationName = helper.randomName(); 7 | 8 | return { 9 | container: { 10 | position: 'relative', 11 | height: '100vh', 12 | width: '100vw', 13 | 14 | '& summary': { 15 | position: 'relative', 16 | padding: '24px', 17 | paddingTop: 64, 18 | fontSize: 24, 19 | zIndex: 9, 20 | }, 21 | 22 | '& main': { 23 | position: 'absolute', 24 | width: '100%', 25 | height: '100%', 26 | top: 0, 27 | left: 0, 28 | overflow: 'hidden' 29 | }, 30 | }, 31 | 32 | liked: { 33 | color: colors.pallet.grape, 34 | textShadow: `0 0 24px ${colors.pallet.grape}`, 35 | }, 36 | 37 | highquality: { 38 | display: 'table', 39 | padding: '6px 12px', 40 | marginTop: 24, 41 | letterSpacing: 1, 42 | textTransform: 'uppercase', 43 | fontFamily: 'Roboto', 44 | fontSize: 12, 45 | color: '#fff', 46 | background: colors.pallet.dribbble, 47 | boxShadow: `0 0 24px ${colors.pallet.dribbble}`, 48 | zoom: .8, 49 | }, 50 | 51 | circle: { 52 | position: 'absolute', 53 | left: '50%', 54 | top: '50%', 55 | width: 260, 56 | height: 260, 57 | marginLeft: -130, 58 | marginTop: -130, 59 | borderRadius: 260, 60 | overflow: 'hidden', 61 | boxShadow: '0 0 24px 0 rgba(0, 0, 0, .4)', 62 | zIndex: 9, 63 | 64 | '& figure': { 65 | animationName, 66 | animationDuration: `${360 * .2}s`, 67 | animationIterationCount: 'infinite', 68 | animationTimingFunction: 'linear', 69 | } 70 | }, 71 | 72 | pause: { 73 | animationPlayState: 'paused', 74 | }, 75 | 76 | [`@keyframes ${animationName}`]: { 77 | '0%': { 78 | transform: 'rotate(0deg)', 79 | }, 80 | 81 | '100%': { 82 | transform: 'rotate(360deg)', 83 | }, 84 | }, 85 | }; 86 | }; 87 | -------------------------------------------------------------------------------- /src/js/pages/Singleton/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import { inject, observer } from 'mobx-react'; 4 | import injectSheet from 'react-jss'; 5 | import clazz from 'classname'; 6 | 7 | import classes from './classes'; 8 | import colors from 'utils/colors'; 9 | import helper from 'utils/helper'; 10 | import ProgressImage from 'ui/ProgressImage'; 11 | import Header from 'components/Header'; 12 | 13 | @inject(stores => ({ 14 | song: stores.controller.song, 15 | like: stores.me.like, 16 | unlike: stores.me.unlike, 17 | isLiked: stores.me.isLiked, 18 | playing: stores.controller.playing, 19 | })) 20 | @observer 21 | class Singleton extends Component { 22 | componentWillReceiveProps(nextProps) { 23 | var classes = this.props.classes; 24 | var ele = this.circle; 25 | 26 | if (!ele) return; 27 | 28 | ele = ele.firstElementChild; 29 | 30 | if (nextProps.playing) { 31 | ele.classList.remove(classes.pause); 32 | } else { 33 | ele.classList.add(classes.pause); 34 | } 35 | } 36 | 37 | render() { 38 | var { classes, song, isLiked, like, unlike } = this.props; 39 | var liked = isLiked(song.id); 40 | 41 | return ( 42 |
43 |
49 | 50 | 51 | liked ? unlike(song) : like(song)} 56 | style={{ 57 | cursor: 'pointer', 58 | display: 'table', 59 | }} 60 | /> 61 | 62 | 65 | { 66 | helper.getRate(song) 67 | } 68 | 69 | 70 | 71 |
72 |
(this.circle = ele) 81 | } 82 | > 83 | 90 |
91 |
92 |
93 | ); 94 | } 95 | } 96 | 97 | export default injectSheet(classes)(Singleton); 98 | -------------------------------------------------------------------------------- /src/js/pages/Top/classes.js: -------------------------------------------------------------------------------- 1 | 2 | export default theme => ({ 3 | container: { 4 | '& ul': { 5 | padding: 0, 6 | margin: 0, 7 | whiteSpace: 'nowrap', 8 | overflowY: 'hidden', 9 | overflowX: 'auto', 10 | scrollBehavior: 'smooth', 11 | }, 12 | }, 13 | 14 | item: { 15 | position: 'relative', 16 | display: 'flex', 17 | height: 'calc((100vh - 50px) / 2)', 18 | width: 'calc((100vh - 50px) / 2)', 19 | alignItems: 'center', 20 | justifyContent: 'center', 21 | flexDirection: 'column', 22 | color: '#fff', 23 | textAlign: 'center', 24 | overflow: 'hidden', 25 | 26 | '& p': { 27 | maxWidth: 210, 28 | lineHeight: '32px', 29 | fontSize: 24, 30 | whiteSpace: 'normal', 31 | }, 32 | 33 | '& img:first-of-type': { 34 | width: '100%', 35 | height: '100%', 36 | transition: '.2s ease-in', 37 | }, 38 | 39 | '&:after': { 40 | content: '""', 41 | position: 'absolute', 42 | left: 0, 43 | top: 0, 44 | display: 'block', 45 | height: '100%', 46 | width: '100%', 47 | background: 'rgba(0, 0, 0, .7)', 48 | visibility: 'visible', 49 | transition: '.2s', 50 | }, 51 | 52 | '&:hover img:first-of-type': { 53 | transform: 'scale(1.1)', 54 | }, 55 | 56 | '&:hover:after': { 57 | background: 'rgba(0, 0, 0, .3)', 58 | } 59 | }, 60 | 61 | info: { 62 | position: 'absolute', 63 | color: '#fff', 64 | zIndex: 1, 65 | }, 66 | 67 | line: { 68 | height: 4, 69 | width: 170, 70 | margin: '0 auto', 71 | marginTop: -10, 72 | marginBottom: 24, 73 | background: '#fff', 74 | } 75 | }); 76 | -------------------------------------------------------------------------------- /src/js/pages/Top/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | import { inject, observer } from 'mobx-react'; 5 | import injectSheet from 'react-jss'; 6 | import clazz from 'classname'; 7 | import moment from 'moment'; 8 | 9 | import classes from './classes'; 10 | import ProgressImage from 'ui/ProgressImage'; 11 | import Loader from 'ui/Loader'; 12 | import Header from 'components/Header'; 13 | import Controller from 'components/Controller'; 14 | 15 | @inject(stores => ({ 16 | loading: stores.top.loading, 17 | list: stores.top.list, 18 | getList: stores.top.getList, 19 | })) 20 | @observer 21 | class Top extends Component { 22 | componentWillMount() { 23 | this.props.getList(); 24 | } 25 | 26 | renderItem(item) { 27 | if (!item) { 28 | return false; 29 | } 30 | 31 | var classes = this.props.classes; 32 | var height = (window.innerHeight - 50) / 2; 33 | 34 | return ( 35 | 38 | 43 | 44 |
45 |

46 | {item.name} 47 |

48 | 49 |
50 | 51 | 52 | { 53 | moment(item.updateTime).startOf('hour').fromNow() 54 | } 55 | 56 |
57 | 58 | ); 59 | } 60 | 61 | renderList() { 62 | var list = this.props.list; 63 | var columns = []; 64 | 65 | for (var i = 0, length = Math.ceil(list.length / 2); i < length; ++i) { 66 | var item = list[i * 2]; 67 | var next = list[i * 2 + 1]; 68 | 69 | columns.push( 70 |
  • 71 | { 72 | this.renderItem(item) 73 | } 74 | { 75 | this.renderItem(next) 76 | } 77 |
  • 78 | ); 79 | } 80 | 81 | return columns; 82 | } 83 | 84 | render() { 85 | var { classes, loading } = this.props; 86 | 87 | if (loading) { 88 | return ; 89 | } 90 | 91 | return ( 92 |
    93 |
    97 | 98 |
      { 102 | e.currentTarget.scrollLeft -= (e.deltaY * 5); 103 | e.preventDefault(); 104 | } 105 | } 106 | > 107 | { 108 | this.renderList() 109 | } 110 |
    111 | 112 | 113 |
    114 | ); 115 | } 116 | } 117 | 118 | export default injectSheet(classes)(Top); 119 | -------------------------------------------------------------------------------- /src/js/routes.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { Route, Switch, Redirect, withRouter } from 'react-router-dom'; 4 | 5 | import Layout from './pages/Layout'; 6 | import Welcome from './pages/Welcome'; 7 | import Player from './pages/Player'; 8 | import User from './pages/User'; 9 | import Artist from './pages/Artist'; 10 | import Top from './pages/Top'; 11 | import Playlist from './pages/Playlist'; 12 | import FM from './pages/FM'; 13 | import Singleton from './pages/Singleton'; 14 | import Comments from './pages/Comments'; 15 | import Lyrics from './pages/Lyrics'; 16 | import Search from './pages/Search'; 17 | import { Legacy as LoginByLegacy, QRCode as LoginByQrCode } from './pages/Login'; 18 | import stores from 'stores'; 19 | 20 | function requireAuth(component, props) { 21 | if (stores.me.hasLogin()) { 22 | return ( 23 | React.createElement( 24 | component, 25 | { 26 | ...props, 27 | } 28 | ) 29 | ); 30 | } 31 | 32 | return ; 33 | } 34 | 35 | const Main = withRouter(props => ); 36 | 37 | export default () => { 38 | return ( 39 | /* eslint-disable */ 40 |
    41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | requireAuth(FM, props)} 59 | /> 60 | 61 |
    62 | /* eslint-enable */ 63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /src/js/stores/artist.js: -------------------------------------------------------------------------------- 1 | 2 | import { observable, action } from 'mobx'; 3 | import axios from 'axios'; 4 | 5 | class Artist { 6 | @observable loading = true; 7 | 8 | // Profile of the artist 9 | @observable profile = {}; 10 | 11 | // All albums of artist 12 | @observable albums = []; 13 | 14 | // Similar artists 15 | @observable similar = []; 16 | 17 | // Contains 'id' and 'songs' 18 | @observable playlist = { 19 | songs: [], 20 | }; 21 | 22 | @action async getArtist(id) { 23 | self.loading = true; 24 | 25 | var response = await axios.get(`/api/artist/${id}`); 26 | var data = response.data; 27 | 28 | if (data) { 29 | self.profile = data.profile; 30 | self.playlist = data.playlist; 31 | self.albums = data.albums; 32 | self.similar = data.similar; 33 | } 34 | 35 | self.loading = false; 36 | } 37 | 38 | @action async follow(followed, id = self.profile.id) { 39 | var response = await axios.get( 40 | followed 41 | ? `/api/artist/unfollow/${id}` 42 | : `/api/artist/follow/${id}` 43 | ); 44 | var data = response.data; 45 | 46 | if (data.success) { 47 | self.profile = Object.assign({}, self.profile, { 48 | followed: !followed, 49 | }); 50 | } 51 | 52 | return data.success; 53 | } 54 | } 55 | 56 | const self = new Artist(); 57 | export default self; 58 | -------------------------------------------------------------------------------- /src/js/stores/comments.js: -------------------------------------------------------------------------------- 1 | 2 | import { observable, action } from 'mobx'; 3 | import axios from 'axios'; 4 | 5 | import controller from './controller'; 6 | 7 | class Comments { 8 | @observable loading = true; 9 | @observable hotList = []; 10 | @observable newestList = []; 11 | @observable total = 0; 12 | @observable song = { 13 | album: {}, 14 | artist: [], 15 | }; 16 | 17 | nextHref = ''; 18 | 19 | @action async getList(song) { 20 | self.loading = true; 21 | 22 | var response = await axios.get(`/api/comments/${song.id}`); 23 | var data = response.data; 24 | 25 | self.song = song; 26 | self.hotList = data.hotList; 27 | self.newestList = data.newestList; 28 | self.total = data.total; 29 | self.nextHref = data.nextHref; 30 | self.loading = false; 31 | } 32 | 33 | @action async like(id, liked) { 34 | var response = await axios.get(`/api/comments/like/${id}/${controller.song.id}/${+liked}`); 35 | var data = response.data; 36 | 37 | if (data.success === true) { 38 | let comment = [...self.hotList.slice(), ...self.newestList.slice()].find(e => e.commentId === id); 39 | 40 | comment.likedCount += liked ? 1 : -1; 41 | comment.liked = liked; 42 | } 43 | } 44 | 45 | @action async loadmore() { 46 | if (!self.nextHref) { 47 | return; 48 | } 49 | 50 | var response = await axios.get(self.nextHref); 51 | var data = response.data; 52 | 53 | self.newestList.push(...data.newestList); 54 | self.nextHref = data.nextHref; 55 | } 56 | } 57 | 58 | const self = new Comments(); 59 | export default self; 60 | -------------------------------------------------------------------------------- /src/js/stores/fm.js: -------------------------------------------------------------------------------- 1 | 2 | import { observable, action } from 'mobx'; 3 | import axios from 'axios'; 4 | 5 | import controller from './controller'; 6 | 7 | class FM { 8 | @observable loading = true; 9 | @observable song = {}; 10 | @observable playlist = { 11 | songs: [], 12 | }; 13 | 14 | preload() { 15 | controller.changeMode(); 16 | self.shuffle(); 17 | self.preload = Function; 18 | } 19 | 20 | @action async shuffle() { 21 | self.loading = true; 22 | 23 | var response = await axios.get(`/api/fm`); 24 | self.playlist = response.data; 25 | self.song = self.playlist.songs[0]; 26 | self.loading = false; 27 | } 28 | 29 | @action play() { 30 | if (controller.playlist.id === self.playlist.id) { 31 | controller.toggle(); 32 | return; 33 | } 34 | 35 | controller.setup(self.playlist); 36 | controller.play(); 37 | } 38 | 39 | // Ban a song 40 | @action async ban(id) { 41 | var response = await axios.get(`/fm_trash?id=${id}`); 42 | 43 | if (response.data.code === 200) { 44 | self.next(); 45 | } 46 | } 47 | 48 | @action async next() { 49 | var index = self.playlist.songs.findIndex(e => e.id === controller.song.id); 50 | 51 | if (controller.playlist.id !== self.playlist.id) { 52 | self.play(); 53 | return; 54 | } 55 | 56 | if (++index < self.playlist.songs.length) { 57 | let next = self.playlist.songs[index]; 58 | 59 | controller.play(next.id); 60 | return; 61 | } 62 | 63 | // Refresh the playlist 64 | await self.shuffle(); 65 | controller.setup(self.playlist); 66 | controller.play(); 67 | } 68 | } 69 | 70 | const self = new FM(); 71 | export default self; 72 | -------------------------------------------------------------------------------- /src/js/stores/home.js: -------------------------------------------------------------------------------- 1 | 2 | import { observable, action } from 'mobx'; 3 | import axios from 'axios'; 4 | import { CronJob } from 'cron'; 5 | 6 | import helper from 'utils/helper'; 7 | import me from './me'; 8 | import preferences from './preferences'; 9 | import controller from './controller'; 10 | 11 | class Home { 12 | @observable loading = true; 13 | @observable list = []; 14 | 15 | constructor() { 16 | // eslint-disable-next-line 17 | new CronJob('00 05 06 * * *', () => { 18 | // Every 6:05 refresh daily playlist, #331 19 | self.load(); 20 | }).start(); 21 | } 22 | 23 | @action async load() { 24 | var res; 25 | 26 | if (me.hasLogin()) { 27 | res = await axios.get(`/api/home/${me.profile.userId}`); 28 | 29 | let favorite = res.data.list[0]; 30 | let recommend = res.data.list[1]; 31 | 32 | // Save the songs of red heart 33 | me.rocking(favorite); 34 | 35 | if (favorite.size) { 36 | controller.setup(favorite); 37 | } else if (recommend.size) { 38 | // Play the recommend songs 39 | controller.setup(recommend); 40 | } else { 41 | // Some user no favorite and recommend, set a fallback playlist 42 | controller.setup(res.data.list[2]); 43 | } 44 | } else { 45 | res = await axios.get(`/api/home`); 46 | controller.setup(res.data.list[0]); 47 | } 48 | 49 | if (preferences.autoPlay) { 50 | controller.play(); 51 | } else { 52 | controller.song = controller.playlist.songs[0]; 53 | } 54 | 55 | res.data.list.map(e => (e.pallet = false)); 56 | 57 | self.list = res.data.list; 58 | 59 | // Get the color pallets 60 | self.list.map(async(e, index) => { 61 | if (!e.cover) return; 62 | 63 | var pallet = await helper.getPallet(e.cover.replace(/\?param=.*/, '') + '?param=20y20'); 64 | e.pallet = pallet; 65 | 66 | // Force update list 67 | self.updateShadow(e, index); 68 | }); 69 | 70 | return self.list; 71 | } 72 | 73 | @action async getList() { 74 | self.loading = true; 75 | 76 | await self.load(); 77 | 78 | // Just call once for init player 79 | self.getList = Function; 80 | self.loading = false; 81 | } 82 | 83 | @action updateShadow(e, index) { 84 | self.list = [ 85 | ...self.list.slice(0, index), 86 | e, 87 | ...self.list.slice(index + 1, self.list.length), 88 | ]; 89 | } 90 | } 91 | 92 | const self = new Home(); 93 | export default self; 94 | -------------------------------------------------------------------------------- /src/js/stores/index.js: -------------------------------------------------------------------------------- 1 | 2 | import me from './me'; 3 | import menu from './menu'; 4 | import home from './home'; 5 | import user from './user'; 6 | import controller from './controller'; 7 | import player from './player'; 8 | import artist from './artist'; 9 | import top from './top'; 10 | import playlist from './playlist'; 11 | import fm from './fm'; 12 | import playing from './playing'; 13 | import search from './search'; 14 | import comments from './comments'; 15 | import lyrics from './lyrics'; 16 | import preferences from './preferences'; 17 | import upnext from './upnext'; 18 | import share from './share'; 19 | 20 | const stores = { 21 | me, 22 | menu, 23 | home, 24 | user, 25 | controller, 26 | player, 27 | artist, 28 | top, 29 | playlist, 30 | fm, 31 | playing, 32 | search, 33 | comments, 34 | lyrics, 35 | preferences, 36 | upnext, 37 | share, 38 | }; 39 | 40 | export default stores; 41 | -------------------------------------------------------------------------------- /src/js/stores/lyrics.js: -------------------------------------------------------------------------------- 1 | 2 | import { observable, action } from 'mobx'; 3 | import axios from 'axios'; 4 | 5 | import controller from './controller'; 6 | 7 | class Lyrics { 8 | @observable loading = true; 9 | @observable list = {}; 10 | 11 | @action async getLyrics() { 12 | self.loading = true; 13 | 14 | var response = await axios.get(`/api/lyrics/${controller.song.id}`); 15 | var data = response.data; 16 | 17 | self.list = data; 18 | self.loading = false; 19 | } 20 | } 21 | 22 | const self = new Lyrics(); 23 | export default self; 24 | -------------------------------------------------------------------------------- /src/js/stores/menu.js: -------------------------------------------------------------------------------- 1 | 2 | import { observable, action } from 'mobx'; 3 | 4 | class Menu { 5 | @observable show = false; 6 | 7 | @action toggle(show = !self.show) { 8 | self.show = show; 9 | } 10 | } 11 | 12 | const self = new Menu(); 13 | export default self; 14 | -------------------------------------------------------------------------------- /src/js/stores/player.js: -------------------------------------------------------------------------------- 1 | 2 | import { observable, action } from 'mobx'; 3 | import axios from 'axios'; 4 | import han from 'han'; 5 | 6 | import helper from 'utils/helper'; 7 | 8 | class Player { 9 | @observable loading = true; 10 | @observable songs = []; 11 | @observable filtered = []; 12 | @observable meta = { 13 | pallet: [ 14 | [0, 0, 0], 15 | ], 16 | author: [], 17 | }; 18 | 19 | // Show filter 20 | @observable searching = false; 21 | @observable keywords; 22 | 23 | // Recommend albums and playlist 24 | @observable recommend = []; 25 | // Recent user 26 | @observable users = []; 27 | // Similar artist 28 | @observable artists = []; 29 | 30 | @action async getDetail(type, id) { 31 | var response = await axios.get(`/api/player/${type}/${id}`); 32 | 33 | var detail = response.data; 34 | var pallet = await helper.getPallet(detail.meta.cover); 35 | 36 | detail.meta.pallet = pallet; 37 | 38 | self.songs.replace(detail.songs); 39 | self.meta = detail.meta; 40 | } 41 | 42 | @action async getRelated(song) { 43 | var response = await axios.get(`/api/player/related/${song.id}/${song.artists[0]['id']}`); 44 | var data = response.data; 45 | 46 | if (data) { 47 | self.recommend = data.playlists; 48 | self.users = data.users; 49 | self.artists = data.artists; 50 | } 51 | } 52 | 53 | @action async subscribe(subscribed) { 54 | var response = await axios.get( 55 | subscribed 56 | ? `/api/player/subscribe/${self.meta.id}` 57 | : `/api/player/unsubscribe/${self.meta.id}` 58 | ); 59 | var data = response.data; 60 | 61 | if (data.success) { 62 | self.meta.subscribed = subscribed; 63 | } 64 | } 65 | 66 | @action toggleLoading(show = !self.loading) { 67 | self.loading = show; 68 | } 69 | 70 | @action toggleSearch(show = !self.searching) { 71 | self.searching = show; 72 | } 73 | 74 | @action doFilter(text) { 75 | var songs = []; 76 | 77 | // Convert text to chinese pinyin 78 | text = han.letter(text.trim()); 79 | 80 | songs = self.songs.filter(e => { 81 | return false 82 | // Fuzzy match the song name 83 | || han.letter(e.name).indexOf(text) > -1 84 | // Fuzzy match the album name 85 | || han.letter(e.album.name).indexOf(text) > -1 86 | // Mathc the artist name 87 | || e.artists.findIndex(e => han.letter(e.name).indexOf(text) > -1) !== -1 88 | ; 89 | }); 90 | 91 | self.keywords = text; 92 | self.filtered = songs; 93 | } 94 | 95 | filter(text = '') { 96 | clearTimeout(self.timer); 97 | self.timer = setTimeout(() => self.doFilter(text), 50); 98 | } 99 | } 100 | 101 | const self = new Player(); 102 | export default self; 103 | -------------------------------------------------------------------------------- /src/js/stores/playing.js: -------------------------------------------------------------------------------- 1 | 2 | import { observable, action } from 'mobx'; 3 | import han from 'han'; 4 | 5 | import controller from './controller'; 6 | 7 | class Playing { 8 | @observable show = false; 9 | @observable filtered = []; 10 | 11 | @action toggle(show = !self.show) { 12 | self.show = show; 13 | } 14 | 15 | @action doFilter(text) { 16 | var songs = []; 17 | 18 | // Convert text to chinese pinyin 19 | text = han.letter(text.trim()); 20 | 21 | songs = controller.playlist.songs.filter(e => { 22 | return false 23 | // Fuzzy match the song name 24 | || han.letter(e.name).indexOf(text) > -1 25 | // Fuzzy match the album name 26 | || han.letter(e.album.name).indexOf(text) > -1 27 | // Mathc the artist name 28 | || e.artists.findIndex(e => han.letter(e.name).indexOf(text) > -1) !== -1 29 | ; 30 | }); 31 | 32 | self.filtered = songs; 33 | } 34 | 35 | filter(text = '') { 36 | clearTimeout(self.timer); 37 | self.timer = setTimeout(() => self.doFilter(text), 50); 38 | } 39 | } 40 | 41 | const self = new Playing(); 42 | export default self; 43 | -------------------------------------------------------------------------------- /src/js/stores/search.js: -------------------------------------------------------------------------------- 1 | 2 | import { observable, action } from 'mobx'; 3 | import axios from 'axios'; 4 | 5 | class Search { 6 | @observable loading = false; 7 | @observable playlists = []; 8 | @observable albums = []; 9 | @observable artists = []; 10 | @observable users = []; 11 | 12 | /** 13 | Search type 14 | 15 | 10: 专辑 16 | 100: 歌手 17 | 1000: 歌单 18 | 1002: 用户 19 | 1004: MV 20 | 1006: 歌词 21 | 1009: 电台 22 | * */ 23 | 24 | // URL of get more playlists 25 | nextHref4playlists = ''; 26 | nextHref4albums = ''; 27 | nextHref4artists = ''; 28 | nextHref4users = ''; 29 | 30 | @action async getPlaylists(keyword) { 31 | self.loading = true; 32 | 33 | var response = await axios.get(`/api/search/1000/0/${keyword}`); 34 | var data = response.data; 35 | 36 | self.playlists = data.playlists; 37 | self.nextHref4playlists = data.nextHref; 38 | self.loading = false; 39 | } 40 | 41 | @action async loadmorePlaylists() { 42 | if (!self.nextHref4playlists) { 43 | return; 44 | } 45 | 46 | var response = await axios.get(self.nextHref4playlists); 47 | var data = response.data; 48 | 49 | self.playlists.push(...data.playlists); 50 | self.nextHref4playlists = data.nextHref; 51 | } 52 | 53 | @action async getAlbums(keyword) { 54 | self.loading = true; 55 | 56 | var response = await axios.get(`/api/search/10/0/${keyword}`); 57 | var data = response.data; 58 | 59 | self.albums = data.albums; 60 | self.nextHref4albums = data.nextHref; 61 | self.loading = false; 62 | } 63 | 64 | @action async loadmoreAlbums() { 65 | if (!self.nextHref4albums) { 66 | return; 67 | } 68 | 69 | var response = await axios.get(self.nextHref4albums); 70 | var data = response.data; 71 | 72 | self.albums.push(...data.albums); 73 | self.nextHref4albums = data.nextHref; 74 | } 75 | 76 | @action async getArtists(keyword) { 77 | self.loading = true; 78 | 79 | var response = await axios.get(`/api/search/100/0/${keyword}`); 80 | var data = response.data; 81 | 82 | self.artists = data.artists; 83 | self.nextHref4artists = data.nextHref; 84 | self.loading = false; 85 | } 86 | 87 | @action async loadmoreArtists() { 88 | if (!self.nextHref4artists) { 89 | return; 90 | } 91 | 92 | var response = await axios.get(self.nextHref4artists); 93 | var data = response.data; 94 | 95 | self.artists.push(...data.artists); 96 | self.nextHref4artists = data.nextHref; 97 | } 98 | 99 | @action async getUsers(keyword) { 100 | self.loading = true; 101 | 102 | var response = await axios.get(`/api/search/1002/0/${keyword}`); 103 | var data = response.data; 104 | 105 | self.users = data.users; 106 | self.nextHref4users = data.nextHref; 107 | self.loading = false; 108 | } 109 | 110 | @action async loadmoreUsers() { 111 | if (!self.nextHref4users) { 112 | return; 113 | } 114 | 115 | var response = await axios.get(self.nextHref4users); 116 | var data = response.data; 117 | 118 | self.users.push(...data.users); 119 | self.nextHref4users = data.nextHref; 120 | } 121 | } 122 | 123 | const self = new Search(); 124 | export default self; 125 | -------------------------------------------------------------------------------- /src/js/stores/share.js: -------------------------------------------------------------------------------- 1 | 2 | import { observable, action } from 'mobx'; 3 | 4 | class Share { 5 | @observable show = false; 6 | 7 | @action toggle(song, show = !self.show) { 8 | self.show = show; 9 | } 10 | } 11 | 12 | const self = new Share(); 13 | export default self; 14 | -------------------------------------------------------------------------------- /src/js/stores/top.js: -------------------------------------------------------------------------------- 1 | 2 | import { observable, action } from 'mobx'; 3 | import axios from 'axios'; 4 | 5 | class Top { 6 | @observable loading = true; 7 | @observable list = []; 8 | 9 | @action async getList() { 10 | self.loading = true; 11 | 12 | var response = await axios.get('/api/top'); 13 | 14 | self.list = response.data.list; 15 | self.loading = false; 16 | } 17 | } 18 | 19 | const self = new Top(); 20 | export default self; 21 | -------------------------------------------------------------------------------- /src/js/stores/upnext.js: -------------------------------------------------------------------------------- 1 | 2 | import { observable, action } from 'mobx'; 3 | import controller from './controller'; 4 | 5 | class UpNext { 6 | @observable show = false; 7 | @observable song = { 8 | album: {}, 9 | artists: [], 10 | }; 11 | 12 | // Save the canceled song 13 | canceled = null; 14 | 15 | @action toggle(song, show = !self.show) { 16 | self.song = song; 17 | self.show = show; 18 | } 19 | 20 | @action cancel(song = controller.song) { 21 | self.canceled = song; 22 | 23 | if (song) { 24 | self.show = false; 25 | } 26 | } 27 | } 28 | 29 | const self = new UpNext(); 30 | export default self; 31 | -------------------------------------------------------------------------------- /src/js/stores/user.js: -------------------------------------------------------------------------------- 1 | 2 | import { observable, action } from 'mobx'; 3 | import axios from 'axios'; 4 | 5 | class User { 6 | @observable loading = true; 7 | @observable profile = {}; 8 | @observable playlists = []; 9 | 10 | @action async getUser(userid) { 11 | self.loading = true; 12 | 13 | var response = await axios.get(`/api/user/${userid}`); 14 | 15 | self.profile = response.data.profile; 16 | self.playlists = response.data.playlists; 17 | self.loading = false; 18 | } 19 | 20 | @action async follow(followed) { 21 | var response = await axios.get( 22 | followed 23 | ? `/api/user/unfollow/${self.profile.id}` 24 | : `/api/user/follow/${self.profile.id}` 25 | ); 26 | var data = response.data; 27 | 28 | if (data.success) { 29 | self.profile = Object.assign({}, self.profile, { 30 | followed: !followed, 31 | }); 32 | } 33 | } 34 | } 35 | 36 | const self = new User(); 37 | export default self; 38 | -------------------------------------------------------------------------------- /src/js/ui/Confirm/classes.js: -------------------------------------------------------------------------------- 1 | 2 | import colors from 'utils/colors'; 3 | 4 | export default theme => { 5 | return { 6 | container: { 7 | display: 'flex', 8 | width: '80vw', 9 | paddingTop: 16, 10 | paddingBottom: 24, 11 | justifyContent: 'center', 12 | alignItems: 'center', 13 | flexDirection: 'column', 14 | background: 'white', 15 | 16 | '& h2': { 17 | fontFamily: 'HelveticaNeue-UltraLight', 18 | fontSize: 24, 19 | letterSpacing: 2, 20 | color: 'black', 21 | }, 22 | 23 | '& p': { 24 | paddingBottom: 1, 25 | maxWidth: '80%', 26 | marginTop: 0, 27 | marginBottom: 32, 28 | fontSize: 14, 29 | color: '#333', 30 | }, 31 | }, 32 | 33 | action: { 34 | display: 'flex', 35 | justifyContent: 'space-between', 36 | alignItems: 'center', 37 | flexDirection: 'row', 38 | 39 | '& button': { 40 | padding: '8px 12px', 41 | minWidth: 70, 42 | textTransform: 'uppercase', 43 | border: 0, 44 | borderRadius: 1, 45 | background: 'none', 46 | color: 'rgb(117, 117, 117)', 47 | cursor: 'pointer', 48 | outline: 0, 49 | transition: '.2s', 50 | }, 51 | 52 | '& button:hover': { 53 | background: 'rgba(0, 0, 0, .1)', 54 | } 55 | }, 56 | 57 | ok: { 58 | marginRight: 20, 59 | 60 | '&:hover': { 61 | color: colors.pallet.google, 62 | } 63 | }, 64 | 65 | cancel: { 66 | '&:hover': { 67 | color: colors.pallet.grape, 68 | } 69 | } 70 | }; 71 | }; 72 | -------------------------------------------------------------------------------- /src/js/ui/Confirm/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import { Modal, ModalBody } from 'ui/Modal'; 4 | import injectSheet, { ThemeProvider } from 'react-jss'; 5 | import PropTypes from 'prop-types'; 6 | 7 | import classes from './classes'; 8 | import theme from 'config/theme'; 9 | 10 | class Confirm extends Component { 11 | static propTypes = { 12 | showConfirm: PropTypes.func.isRequired, 13 | title: PropTypes.string.isRequired, 14 | message: PropTypes.string, 15 | }; 16 | 17 | static defaultProps = { 18 | title: 'Are You Sure?', 19 | show: false 20 | }; 21 | 22 | state = { 23 | show: false 24 | }; 25 | 26 | // Promise resolve and reject 27 | ok; 28 | cancel 29 | 30 | show() { 31 | this.setState({ show: true }); 32 | 33 | return new Promise( 34 | (resolve, reject) => { 35 | var close = (returned) => { 36 | this.setState({ show: false }); 37 | resolve(returned); 38 | }; 39 | 40 | this.ok = () => close(true); 41 | this.cancel = () => close(false); 42 | } 43 | ); 44 | } 45 | 46 | componentDidMount() { 47 | this.props.showConfirm( 48 | () => this.show() 49 | ); 50 | } 51 | 52 | renderContent() { 53 | var { classes, title, message } = this.props; 54 | 55 | return ( 56 |
    57 |

    58 | {title} 59 |

    60 | 61 | { 62 | message 63 | ? (

    {message}

    ) 64 | : false 65 | } 66 | 67 |
    68 | 74 | 75 | 81 |
    82 |
    83 | ); 84 | } 85 | 86 | render() { 87 | return ( 88 | 91 | 94 | 95 | 96 | { 97 | this.renderContent() 98 | } 99 | 100 | 101 | 102 | ); 103 | } 104 | } 105 | 106 | export default injectSheet(classes)(Confirm); 107 | -------------------------------------------------------------------------------- /src/js/ui/FadeImage/classes.js: -------------------------------------------------------------------------------- 1 | 2 | export default theme => ({ 3 | fade: { 4 | opacity: 1, 5 | transition: '.2s', 6 | }, 7 | 8 | fadein: { 9 | opacity: 0 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /src/js/ui/FadeImage/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import injectSheet from 'react-jss'; 5 | import clazz from 'classname'; 6 | 7 | import classes from './classes'; 8 | 9 | class FadeImage extends Component { 10 | static propTypes = { 11 | src: PropTypes.string, 12 | fallback: PropTypes.string, 13 | }; 14 | 15 | static defaultProps = { 16 | fallback: 'https://source.unsplash.com/random', 17 | }; 18 | 19 | componentWillReceiveProps(nextProps) { 20 | var ele = this.image; 21 | 22 | if (ele 23 | && this.props.src !== nextProps.src) { 24 | ele.classList.add(nextProps.classes.fadein); 25 | } 26 | } 27 | 28 | handleError(e) { 29 | e.target.src = this.props.fallback; 30 | } 31 | 32 | handleLoad(e) { 33 | e.target.classList.remove(this.props.classes.fadein); 34 | } 35 | 36 | render() { 37 | var classes = this.props.classes; 38 | 39 | if (!this.props.src) return false; 40 | 41 | return ( 42 | (this.image = ele) 45 | } 46 | src={this.props.src} 47 | className={clazz(classes.fade, classes.fadein, this.props.className)} 48 | onLoad={e => this.handleLoad(e)} 49 | onError={e => this.handleError(e)} /> 50 | ); 51 | } 52 | } 53 | 54 | export default injectSheet(classes)(FadeImage); 55 | -------------------------------------------------------------------------------- /src/js/ui/Indicator/classes.js: -------------------------------------------------------------------------------- 1 | 2 | import helper from 'utils/helper'; 3 | 4 | export default theme => { 5 | var animationLine1 = helper.randomName(); 6 | var animationLine2 = helper.randomName(); 7 | var animationLine3 = helper.randomName(); 8 | var animationLine4 = helper.randomName(); 9 | 10 | return { 11 | container: { 12 | position: 'relative', 13 | width: 12, 14 | height: 12, 15 | display: 'inline-block', 16 | overflow: 'hidden', 17 | 18 | '& span': { 19 | position: 'absolute', 20 | background: 'rgb(92, 188, 125)', 21 | height: 12, 22 | left: 1, 23 | width: 2, 24 | borderRadius: '25%', 25 | }, 26 | 27 | '& span:nth-child(1)': { 28 | left: 1, 29 | animationName: animationLine1, 30 | animationDuration: '2s', 31 | animationIterationCount: 'infinite', 32 | }, 33 | 34 | '& span:nth-child(2)': { 35 | left: 4, 36 | animationName: animationLine2, 37 | animationDuration: '1.4s', 38 | animationIterationCount: 'infinite', 39 | }, 40 | 41 | '& span:nth-child(3)': { 42 | left: 7, 43 | animationName: animationLine3, 44 | animationDuration: '1.8s', 45 | animationIterationCount: 'infinite', 46 | }, 47 | 48 | '& span:nth-child(4)': { 49 | left: 10, 50 | animationName: animationLine4, 51 | animationDuration: '1s', 52 | animationIterationCount: 'infinite', 53 | } 54 | }, 55 | 56 | [`@keyframes ${animationLine1}`]: { 57 | '0%': { 58 | transform: 'translateY(0) translateZ(0)', 59 | }, 60 | 61 | '50%': { 62 | transform: 'translateY(10px) translateZ(0)', 63 | }, 64 | 65 | '100%': { 66 | transform: 'translateY(0) translateZ(0)', 67 | }, 68 | }, 69 | 70 | [`@keyframes ${animationLine2}`]: { 71 | '0%': { 72 | transform: 'translateY(10px) translateZ(0)', 73 | }, 74 | 75 | '50%': { 76 | transform: 'translateY(0) translateZ(0)', 77 | }, 78 | 79 | '100%': { 80 | transform: 'translateY(10px) translateZ(0)', 81 | }, 82 | }, 83 | 84 | [`@keyframes ${animationLine3}`]: { 85 | '0%': { 86 | transform: 'translateY(10px) translateZ(0)', 87 | }, 88 | 89 | '50%': { 90 | transform: 'translateY(0) translateZ(0)', 91 | }, 92 | 93 | '100%': { 94 | transform: 'translateY(10px) translateZ(0)', 95 | }, 96 | }, 97 | 98 | [`@keyframes ${animationLine4}`]: { 99 | '0%': { 100 | transform: 'translateY(10px) translateZ(0)', 101 | }, 102 | 103 | '50%': { 104 | transform: 'translateY(0) translateZ(0)', 105 | }, 106 | 107 | '100%': { 108 | transform: 'translateY(10px) translateZ(0)', 109 | }, 110 | }, 111 | }; 112 | }; 113 | -------------------------------------------------------------------------------- /src/js/ui/Indicator/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import injectSheet from 'react-jss'; 4 | import clazz from 'classname'; 5 | 6 | import classes from './classes'; 7 | 8 | class Indicator extends Component { 9 | render() { 10 | var { classes, className, style } = this.props; 11 | 12 | return ( 13 |
    19 | 20 | 21 | 22 | 23 |
    24 | ); 25 | } 26 | } 27 | 28 | export default injectSheet(classes)(Indicator); 29 | -------------------------------------------------------------------------------- /src/js/ui/Loader/classes.js: -------------------------------------------------------------------------------- 1 | 2 | import helper from 'utils/helper'; 3 | import colors from 'utils/colors'; 4 | 5 | export default theme => { 6 | var animationLoader = helper.randomName(); 7 | var animationInner = helper.randomName(); 8 | 9 | return { 10 | container: { 11 | position: 'fixed', 12 | top: 0, 13 | left: 0, 14 | width: '100vw', 15 | height: '100vh', 16 | background: 'rgba(255, 255, 255, .4)', 17 | opacity: 0, 18 | visibility: 'hidden', 19 | transition: '.2s', 20 | }, 21 | 22 | show: { 23 | opacity: 1, 24 | visibility: 'visible', 25 | zIndex: 1000, 26 | }, 27 | 28 | loader: { 29 | position: 'absolute', 30 | top: '54%', 31 | left: '50%', 32 | height: 30, 33 | width: 30, 34 | marginLeft: -19, 35 | marginTop: -19, 36 | border: '4px solid #000', 37 | animationName: animationLoader, 38 | animationDuration: '2s', 39 | animationIterationCount: 'infinite', 40 | animationTimingFunction: 'ease', 41 | }, 42 | 43 | inner: { 44 | verticalAlign: 'top', 45 | display: 'inline-block', 46 | width: '100%', 47 | backgroundColor: colors.randomColor(), 48 | animation: `${animationInner} 2s infinite ease-in;`, 49 | }, 50 | 51 | [`@keyframes ${animationLoader}`]: { 52 | '0%': { 53 | transform: 'rotate(0deg) translateZ(0)', 54 | }, 55 | 56 | '25%': { 57 | transform: 'rotate(180deg) translateZ(0)', 58 | }, 59 | 60 | '50%': { 61 | transform: 'rotate(180deg) translateZ(0)', 62 | }, 63 | 64 | '75%': { 65 | transform: 'rotate(360deg) translateZ(0)', 66 | }, 67 | 68 | '100%': { 69 | transform: 'rotate(360deg) translateZ(0)', 70 | }, 71 | }, 72 | 73 | [`@keyframes ${animationInner}`]: { 74 | '0%': { 75 | height: '0%', 76 | transform: 'translateZ(0)', 77 | }, 78 | 79 | '25%': { 80 | height: '0%', 81 | transform: 'translateZ(0)', 82 | }, 83 | 84 | '50%': { 85 | height: '100%', 86 | transform: 'translateZ(0)', 87 | }, 88 | 89 | '75%': { 90 | height: '100%', 91 | transform: 'translateZ(0)', 92 | }, 93 | 94 | '100%': { 95 | height: '0%', 96 | transform: 'translateZ(0)', 97 | }, 98 | }, 99 | }; 100 | }; 101 | -------------------------------------------------------------------------------- /src/js/ui/Loader/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import injectSheet from 'react-jss'; 5 | import clazz from 'classname'; 6 | 7 | import colors from 'utils/colors'; 8 | import classes from './classes'; 9 | 10 | class Loader extends Component { 11 | static propTypes = { 12 | show: PropTypes.bool, 13 | }; 14 | 15 | static defaultProps = { 16 | show: false, 17 | }; 18 | 19 | render() { 20 | var classes = this.props.classes; 21 | 22 | if (!this.props.show) { 23 | return false; 24 | } 25 | 26 | return ( 27 |
    32 | { 33 | /** 34 | Square loader 35 | https://codepen.io/tashfene/pen/raEqrJ?editors=1100 36 | * */ 37 | } 38 |
    43 | { 46 | e.target.style.backgroundColor = colors.randomColor(); 47 | } 48 | } 49 | className={ 50 | clazz(classes.inner, classes.animationInner) 51 | } 52 | /> 53 |
    54 |
    55 | ); 56 | } 57 | } 58 | 59 | export default injectSheet(classes)(Loader); 60 | -------------------------------------------------------------------------------- /src/js/ui/Modal/classes.css: -------------------------------------------------------------------------------- 1 | 2 | .Modal-overlay { 3 | position: absolute; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | background: rgba(0,0,0,.7); 9 | z-index: 999; 10 | } 11 | 12 | .Modal-content { 13 | position: absolute; 14 | top: 50%; 15 | left: 50%; 16 | z-index: 999; 17 | transform: translate(-50%, -50%); 18 | } 19 | 20 | .Modal-overlay-enter { 21 | opacity: 0; 22 | visibility: hidden; 23 | transition: .2s; 24 | } 25 | 26 | .Modal-overlay-enter.Modal-overlay-enter-active { 27 | opacity: 1; 28 | visibility: visible; 29 | } 30 | 31 | .Modal-overlay-leave { 32 | opacity: 1; 33 | visibility: visible; 34 | transition: .3s; 35 | } 36 | 37 | .Modal-overlay-leave.Modal-overlay-leave-active { 38 | opacity: 0; 39 | visibility: hidden; 40 | } 41 | 42 | .Modal-body-enter { 43 | transform: translate(-50%, -40%); 44 | opacity: 0; 45 | transition: .2s; 46 | } 47 | 48 | .Modal-body-enter.Modal-body-enter-active { 49 | transform: translate(-50%, -50%); 50 | opacity: 1; 51 | } 52 | 53 | .Modal-body-leave { 54 | transform: translate(-50%, -50%); 55 | opacity: 1; 56 | transition: .3s; 57 | } 58 | 59 | .Modal-body-leave.Modal-body-leave-active { 60 | transform: translate(-50%, -60%); 61 | opacity: 0; 62 | } 63 | -------------------------------------------------------------------------------- /src/js/ui/Modal/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import ReactDOM from 'react-dom'; 5 | import clazz from 'classname'; 6 | import Transition from 'react-transition-group/CSSTransitionGroup'; 7 | 8 | import './classes.css'; 9 | 10 | function on(el, events, fn) { 11 | (el && events && fn) 12 | && events.split().forEach(e => el.addEventListener(e, fn, false)); 13 | } 14 | 15 | function off(el, events, fn) { 16 | (el && events && fn) 17 | && events.split().forEach(e => el.removeEventListener(e, fn, false)); 18 | } 19 | 20 | class TransitionPortal extends Component { 21 | ele; 22 | 23 | componentDidMount() { 24 | this.ele = document.createElement('div'); 25 | document.body.appendChild(this.ele); 26 | this.componentDidUpdate(); 27 | } 28 | 29 | componentDidUpdate() { 30 | ReactDOM.render( 31 | 32 | { 33 | this.props.children 34 | } 35 | , 36 | this.ele 37 | ); 38 | } 39 | 40 | componentWillUnmount() { 41 | document.body.removeChild(this.ele); 42 | } 43 | 44 | render() { 45 | return null; 46 | } 47 | } 48 | 49 | class ModalBody extends Component { 50 | render() { 51 | return ( 52 | 57 |
    58 | {this.props.children} 59 |
    60 |
    61 | ); 62 | } 63 | }; 64 | 65 | class ModalHeader extends Component { 66 | render() { 67 | return ( 68 |
    69 | {this.props.children} 70 |
    71 | ); 72 | } 73 | } 74 | 75 | class Modal extends Component { 76 | static propTypes = { 77 | show: PropTypes.bool, 78 | onCancel: PropTypes.func, 79 | }; 80 | 81 | static defaultProps = { 82 | onCancel: () => {} 83 | }; 84 | 85 | renderOverlay() { 86 | if (!this.props.show) { 87 | return; 88 | } 89 | 90 | return ( 91 |
    95 | ); 96 | } 97 | 98 | renderBody() { 99 | if (!this.props.show) { 100 | return; 101 | } 102 | 103 | return ( 104 |
    105 | {this.props.children} 106 |
    107 | ); 108 | } 109 | 110 | handleEscKey(e) { 111 | if (e.keyCode === 27 && this.props.show) { 112 | this.props.onCancel(); 113 | } 114 | } 115 | 116 | componentWillUnmount() { 117 | off(document, 'keydown', this.handleEscKey); 118 | } 119 | 120 | componentDidMount() { 121 | this.handleEscKey = this.handleEscKey.bind(this); 122 | on(document, 'keydown', this.handleEscKey); 123 | } 124 | 125 | render() { 126 | document.body.style.overflow = this.props.show ? 'hidden' : null; 127 | 128 | return ( 129 |
    130 | 135 | {this.renderOverlay()} 136 | 137 | 138 | 143 | {this.renderBody()} 144 | 145 |
    146 | ); 147 | } 148 | }; 149 | 150 | export { Modal, ModalBody, ModalHeader }; 151 | -------------------------------------------------------------------------------- /src/js/ui/Offline/classes.js: -------------------------------------------------------------------------------- 1 | 2 | import colors from 'utils/colors'; 3 | 4 | export default theme => { 5 | return { 6 | container: { 7 | position: 'fixed', 8 | top: 0, 9 | left: 0, 10 | display: 'flex', 11 | width: '100vw', 12 | height: '100vh', 13 | justifyContent: 'center', 14 | flexDirection: 'column', 15 | alignItems: 'center', 16 | background: 'white', 17 | fontFamily: 'HelveticaNeue-UltraLight', 18 | 19 | '& h1': { 20 | fontSize: 40, 21 | fontWeight: '200', 22 | color: colors.pallet.dribbble 23 | }, 24 | 25 | '& button': { 26 | marginTop: 20, 27 | padding: '8px 12px', 28 | border: 0, 29 | fontSize: 14, 30 | fontWeight: 'lighter', 31 | color: 'dimgray', 32 | background: 'white', 33 | letterSpacing: .5, 34 | textTransform: 'uppercase', 35 | borderBottom: 'thin solid dimgray', 36 | outline: 0, 37 | } 38 | }, 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /src/js/ui/Offline/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import injectSheet from 'react-jss'; 5 | 6 | import classes from './classes'; 7 | class Offline extends Component { 8 | static propTypes = { 9 | show: PropTypes.bool.isRequired, 10 | }; 11 | state = { 12 | offline: false, 13 | }; 14 | componentDidMount() { 15 | window.addEventListener('offline', () => { 16 | this.setState({ 17 | offline: true, 18 | }); 19 | }); 20 | 21 | window.addEventListener('online', () => { 22 | this.setState({ 23 | offline: false, 24 | }); 25 | }); 26 | } 27 | render() { 28 | var { classes, show } = this.props; 29 | if (!show) { 30 | return false; 31 | } 32 | if (!this.state.offline) { 33 | return ( 34 |
    35 |

    Opps, seems like you are offline...

    36 |
    ); 37 | } 38 | } 39 | } 40 | 41 | export default injectSheet(classes)(Offline); 42 | -------------------------------------------------------------------------------- /src/js/ui/ProgressImage/classes.js: -------------------------------------------------------------------------------- 1 | 2 | export default theme => ({ 3 | container: { 4 | position: 'relative', 5 | padding: 0, 6 | margin: 0, 7 | background: '#ddd', 8 | overflow: 'hidden', 9 | 10 | '& img': { 11 | height: 'auto', 12 | pointerEvents: 'none', 13 | }, 14 | }, 15 | 16 | main: { 17 | display: 'block', 18 | opacity: 0, 19 | transition: 'opacity 0.5s ease-out', 20 | }, 21 | 22 | thumb: { 23 | '& img': { 24 | position: 'absolute', 25 | top: 0, 26 | left: 0, 27 | transition: 'opacity 0.3s ease-out', 28 | filter: 'blur(30px)', 29 | }, 30 | 31 | '&$loaded img': { 32 | opacity: 1, 33 | }, 34 | }, 35 | 36 | loaded: { 37 | '& $main': { 38 | opacity: 1, 39 | }, 40 | 41 | '& $thumb img': { 42 | opacity: 0, 43 | } 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /src/js/ui/ProgressImage/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import injectSheet from 'react-jss'; 5 | import clazz from 'classname'; 6 | 7 | import classes from './classes'; 8 | 9 | class ProgressImage extends Component { 10 | static propTypes = { 11 | src: PropTypes.string, 12 | thumb: PropTypes.string, 13 | height: PropTypes.number, 14 | width: PropTypes.number, 15 | fallback: PropTypes.string, 16 | }; 17 | 18 | static defaultProps = { 19 | fallback: 'https://source.unsplash.com/random', 20 | }; 21 | 22 | componentWillReceiveProps(nextProps) { 23 | var container = this.container; 24 | if (true 25 | && container 26 | && nextProps.src !== this.props.src) { 27 | // Immediate render the new image 28 | container.classList.remove(this.props.classes.loaded); 29 | } 30 | } 31 | 32 | handleError(e) { 33 | e.target.src = this.props.fallback; 34 | } 35 | 36 | handleLoad(e) { 37 | var ele = this.container; 38 | this.thumb.style.paddingBottom = '0%'; 39 | 40 | // Fix bug, sometiems this dom has been destroyed 41 | if (ele) { 42 | setTimeout(() => { 43 | ele.classList.add(this.props.classes.loaded); 44 | }, 50); 45 | } 46 | } 47 | 48 | render() { 49 | var { classes, className, style, src, thumb, height, width } = this.props; 50 | 51 | if (!src) return false; 52 | 53 | if (!thumb) { 54 | // Get the thumb image src 55 | thumb = src.replace(/\?.*$/, '') + '?param=20y20'; 56 | } 57 | 58 | return ( 59 |
    (this.container = ele) 63 | } 64 | style={ 65 | Object.assign( 66 | { 67 | height, 68 | width, 69 | }, 70 | style 71 | ) 72 | } 73 | > 74 | this.handleError(e)} 77 | onLoad={e => this.handleLoad(e)} 78 | src={this.props.src} 79 | style={{ 80 | height, 81 | width, 82 | }} 83 | /> 84 | 85 |
    (this.thumb = ele) 89 | } 90 | style={{ 91 | // Use as placeholder, anti reflow 92 | paddingBottom: (height / width) * 100 || 0, 93 | }} 94 | > 95 | 108 |
    109 |
    110 | ); 111 | } 112 | } 113 | 114 | export default injectSheet(classes)(ProgressImage); 115 | -------------------------------------------------------------------------------- /src/js/ui/Switch/classes.js: -------------------------------------------------------------------------------- 1 | 2 | export default theme => ({ 3 | container: { 4 | display: 'inline-block', 5 | margin: 0, 6 | padding: 0, 7 | cursor: 'pointer', 8 | 9 | '& input': { 10 | display: 'none', 11 | 12 | '&:checked + $fake:before': { 13 | background: 'rgba(157, 166, 216, 1)', 14 | }, 15 | 16 | '&:checked + $fake:after': { 17 | left: 'auto', 18 | right: 0, 19 | background: '#3f51b5', 20 | } 21 | }, 22 | 23 | '& input:disabled + $fake:before': { 24 | background: 'rgba(0, 0, 0, .12)', 25 | }, 26 | 27 | '& input:disabled + $fake:after': { 28 | left: 'auto', 29 | right: 0, 30 | background: '#bdbdbd', 31 | } 32 | }, 33 | 34 | fake: { 35 | position: 'relative', 36 | display: 'inline-block', 37 | width: 35, 38 | height: 20, 39 | 40 | '&:before, &:after': { 41 | content: '""', 42 | }, 43 | 44 | '&:before': { 45 | display: 'block', 46 | width: 35, 47 | height: 14, 48 | marginTop: 3, 49 | background: 'rgba(0, 0, 0, .26)', 50 | borderRadius: 14, 51 | transition: '.5s ease-in-out', 52 | }, 53 | 54 | '&:after': { 55 | position: 'absolute', 56 | left: 0, 57 | top: 0, 58 | width: 20, 59 | height: 20, 60 | borderRadius: 20, 61 | background: '#fff', 62 | boxSizing: 'border-box', 63 | boxShadow: '0 3px 4px 0 rgba(0, 0, 0, .5)', 64 | transition: '.2s', 65 | } 66 | }, 67 | }); 68 | -------------------------------------------------------------------------------- /src/js/ui/Switch/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import injectSheet from 'react-jss'; 4 | 5 | import classes from './classes'; 6 | 7 | class Switch extends Component { 8 | render() { 9 | var { classes, id, defaultChecked, onChange } = this.props; 10 | 11 | return ( 12 | 13 | 18 | 19 | 20 | ); 21 | } 22 | } 23 | 24 | export default injectSheet(classes)(Switch); 25 | -------------------------------------------------------------------------------- /src/js/utils/colors.js: -------------------------------------------------------------------------------- 1 | 2 | const pallet = { 3 | primary: '#6496f0', 4 | sunflower: '#ffce54', 5 | grape: '#e0245e', 6 | coral: '#ff6470', 7 | mint: '#48cfad', 8 | dribbble: '#ea4c89', 9 | twitter: '#55acee', 10 | google: '#5090fb', 11 | }; 12 | 13 | const gradients = [ 14 | 'linear-gradient(to right, #0099f7, #f11712)', 15 | 'linear-gradient(to bottom, #834d9b, #d04ed6)', 16 | 'linear-gradient(to left, #4da0b0, #d39d38)', 17 | 'linear-gradient(to left, #5614b0, #dbd65c)', 18 | 'linear-gradient(to right, #114357, #f29492)', 19 | 'linear-gradient(to right, #fd746c, #ff9068)', 20 | 'linear-gradient(to left, #6a3093, #a044ff)', 21 | 'linear-gradient(to left, #b24592, #f15f79)', 22 | 'linear-gradient(to top, #403a3e, #be5869)', 23 | 'linear-gradient(to right, #c2e59c, #64b3f4)', 24 | 'linear-gradient(to right, #00c9ff, #92fe9d)', 25 | 'linear-gradient(to right, #e53935, #e35d5b)', 26 | 'linear-gradient(to right, #fc00ff, #00dbde)', 27 | 'linear-gradient(to right, #7b4397, #dc2430)', 28 | ]; 29 | 30 | export default { 31 | pallet, 32 | 33 | randomGradient() { 34 | return gradients[Math.floor(Math.random() * gradients.length)]; 35 | }, 36 | 37 | randomColor() { 38 | var colors = []; 39 | 40 | Object.keys(pallet).map( 41 | e => colors.push(pallet[e]) 42 | ); 43 | 44 | gradients.map( 45 | e => { 46 | var matched = e.match(/#(?:[0-9a-fA-F]{3}){1,2}/g); 47 | 48 | if (matched) { 49 | colors = [ 50 | ...matched, 51 | ...colors, 52 | ]; 53 | } 54 | } 55 | ); 56 | 57 | return colors[Math.floor(Math.random() * colors.length)]; 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /src/js/utils/helper.js: -------------------------------------------------------------------------------- 1 | 2 | import uuid from 'uuid'; 3 | import { parsePhoneNumberFromString } from 'libphonenumber-js'; 4 | 5 | const helper = { 6 | pad(number) { 7 | return ('0' + number).slice(-2); 8 | }, 9 | 10 | getTime(duration) { 11 | var minutes = Math.floor(duration / 1000 / 60); 12 | var second = Math.floor(duration / 1000 - minutes * 60); 13 | 14 | return `${this.pad(minutes)}:${this.pad(second)}`; 15 | }, 16 | 17 | getPallet(image) { 18 | return new Promise((resolve, reject) => { 19 | image = image.replace(/\?.*$/, '') + '?param=20y20'; 20 | 21 | new window.AlbumColors(image).getColors((colors, err) => { 22 | if (err) { 23 | resolve([ 24 | [0, 0, 0], 25 | [0, 0, 0], 26 | [0, 0, 0], 27 | ]); 28 | } else { 29 | resolve(colors); 30 | } 31 | }); 32 | }); 33 | }, 34 | 35 | getLyricsKey(times, lyrics) { 36 | var keys = Object.keys(lyrics); 37 | 38 | return keys.find((e, index) => { 39 | return times > +e && index < keys.length - 1 && times < +keys[index + 1]; 40 | }); 41 | }, 42 | 43 | formatPhone(phone) { 44 | if (!phone.startsWith('+86') 45 | && /1[34578][012356789]\d{8}|134[012345678]\d{7}/.test(phone)) { 46 | return { 47 | code: '86', 48 | phone, 49 | }; 50 | } 51 | 52 | var parsed = parsePhoneNumberFromString(phone); 53 | 54 | if (!parsed) { 55 | return {}; 56 | } 57 | 58 | return { 59 | code: parsed.countryCallingCode, 60 | phone: parsed.nationalNumber, 61 | }; 62 | }, 63 | 64 | pureColor(colors = []) { 65 | var rgb = colors[1] || [255, 255, 255]; 66 | 67 | return ` 68 | rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${Math.random()}) 69 | `; 70 | }, 71 | 72 | genColor(colors = []) { 73 | var r = colors[0] || [255, 255, 255]; 74 | var b = colors[1] || [255, 255, 255]; 75 | var g = colors[2] || [255, 255, 255]; 76 | 77 | return ` 78 | linear-gradient(${Math.random() * 100}deg, 79 | rgba(${r[0]}, ${r[1]}, ${r[2]}, ${Math.random()}) ${Math.random() * 33}%, 80 | rgba(${g[0]}, ${g[1]}, ${g[2]}, ${Math.random()}) ${Math.random() * 66}%, 81 | rgba(${b[0]}, ${b[1]}, ${b[2]}, ${Math.random()}) 100% 82 | ) 83 | `; 84 | }, 85 | 86 | randomName(prefix = 'animation') { 87 | return `${prefix}-${uuid.v4()}`; 88 | }, 89 | 90 | formatNumber(number = 0) { 91 | return number.toString().split('').reverse().reduce((prev, next, index) => { 92 | return ((index % 3) ? next : (next + ',')) + prev; 93 | }); 94 | }, 95 | 96 | humanNumber(number) { 97 | if (number > 1000 * 1000) { 98 | return (number / 1000 / 1000).toFixed(2) + 'M'; 99 | } 100 | 101 | if (number > 1000) { 102 | return (number / 1000).toFixed(2) + 'K'; 103 | } 104 | 105 | return number; 106 | }, 107 | 108 | clearWith(name, args) { 109 | var clear = (token) => { 110 | var index = name.indexOf(token); 111 | 112 | if (index !== -1) { 113 | name = name.substring(0, index); 114 | } 115 | 116 | return name; 117 | }; 118 | 119 | args.map(e => { 120 | name = clear(e); 121 | }); 122 | 123 | return name; 124 | }, 125 | 126 | getRate(song) { 127 | if (!song.data || !song.data.bitRate) { 128 | return 'Unknow kbps'; 129 | } 130 | 131 | if (song.data.isFlac) { 132 | return 'SQ'; 133 | } 134 | 135 | return `${song.data.bitRate / 1000} Kbps`; 136 | } 137 | }; 138 | 139 | export default helper; 140 | -------------------------------------------------------------------------------- /src/js/utils/lastfm.js: -------------------------------------------------------------------------------- 1 | 2 | import API from 'simple-lastfm'; 3 | 4 | const API_KEY = 'c1f9a819c03a17083eba0fe9ee41119e'; 5 | const SECRET = '3742198243c490bf333d0aa615e5d117'; 6 | 7 | var lastfm; 8 | 9 | async function getSession() { 10 | var success = await new Promise((resolve, reject) => { 11 | if (!lastfm) { 12 | resolve(false); 13 | return; 14 | } 15 | 16 | lastfm.getSessionKey(result => { 17 | resolve(result.success); 18 | }); 19 | }); 20 | 21 | if (!success) { 22 | return; 23 | } 24 | 25 | return lastfm; 26 | } 27 | 28 | async function initialize(username, password) { 29 | if (!username || !password) { 30 | return; 31 | } 32 | 33 | lastfm = new API({ 34 | api_key: API_KEY, 35 | api_secret: SECRET, 36 | username, 37 | password, 38 | }); 39 | 40 | return getSession(); 41 | } 42 | 43 | async function scrobble(song) { 44 | var session = await getSession(); 45 | 46 | if (!session) { 47 | return; 48 | } 49 | 50 | return new Promise((resolve, reject) => { 51 | session.scrobbleTrack({ 52 | artist: song.artists.map(e => e.name).join(','), 53 | track: song.name, 54 | callback: (result) => resolve(result) 55 | }); 56 | }); 57 | } 58 | 59 | async function playing(song) { 60 | var session = await getSession(); 61 | 62 | if (!session) { 63 | return; 64 | } 65 | 66 | return new Promise((resolve, reject) => { 67 | session.scrobbleNowPlayingTrack({ 68 | artist: song.artists.map(e => e.name).join(','), 69 | track: song.name, 70 | callback: (result) => resolve(result) 71 | }); 72 | }); 73 | } 74 | 75 | async function love(song) { 76 | var session = await getSession(); 77 | 78 | if (!session) { 79 | return; 80 | } 81 | 82 | return new Promise((resolve, reject) => { 83 | session.loveTrack({ 84 | artist: song.artists.map(e => e.name).join(','), 85 | track: song.name, 86 | callback: (result) => resolve(result) 87 | }); 88 | }); 89 | } 90 | 91 | async function unlove(song) { 92 | var session = await getSession(); 93 | 94 | if (!session) { 95 | return; 96 | } 97 | 98 | return new Promise((resolve, reject) => { 99 | session.unloveTrack({ 100 | artist: song.artists.map(e => e.name).join(','), 101 | track: song.name, 102 | callback: (result) => resolve(result) 103 | }); 104 | }); 105 | } 106 | 107 | export default { 108 | initialize, 109 | scrobble, 110 | playing, 111 | love, 112 | unlove, 113 | }; 114 | -------------------------------------------------------------------------------- /submodules/downloader/viewport/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Downloader 7 | 8 | 9 | 10 |
    11 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /submodules/downloader/viewport/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | import { HashRouter, Switch, Route } from 'react-router-dom'; 5 | import { Provider } from 'mobx-react'; 6 | import { ThemeProvider } from 'react-jss'; 7 | import 'ionicons201/css/ionicons.css'; 8 | 9 | import 'app/global.css'; 10 | import theme from 'config/theme'; 11 | import stores from './stores'; 12 | import Downloader from './views/Downloader'; 13 | import List from './views/List'; 14 | 15 | /* eslint-disable */ 16 | render( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | , 27 | document.getElementById('root') 28 | ); 29 | /* eslint-enable */ 30 | -------------------------------------------------------------------------------- /submodules/downloader/viewport/stores.js: -------------------------------------------------------------------------------- 1 | 2 | import { observable, action, transaction } from 'mobx'; 3 | import storage from 'common/storage'; 4 | import { ipcRenderer, remote } from 'electron'; 5 | 6 | const KEY = 'downloaded'; 7 | 8 | class Stores { 9 | @observable tasks = new Map(); 10 | @observable playlist = []; 11 | 12 | @action.bound 13 | load = async() => { 14 | try { 15 | var persistence = await storage.get(KEY); 16 | 17 | transaction(() => { 18 | Object.keys(persistence).map( 19 | e => this.tasks.set(e, persistence[e]) 20 | ); 21 | }); 22 | } catch (ex) { 23 | storage.remove(KEY); 24 | this.tasks.clear(); 25 | } 26 | } 27 | 28 | save = () => { 29 | var persistence = {}; 30 | var items = Array.from(this.tasks.entries()); 31 | 32 | items.map( 33 | ([key, value]) => { 34 | if (value.progress === 1 || value.success === false) { 35 | persistence[key] = value; 36 | } 37 | } 38 | ); 39 | storage.set(KEY, persistence); 40 | } 41 | 42 | isPersistence = (task) => { 43 | return (this.tasks.get(task.id) || {}).success; 44 | } 45 | 46 | @action.bound 47 | updateTask = (task) => { 48 | // You can not repeat an inprogress task 49 | var exists = this.tasks.get(task.id); 50 | if (exists && task.waiting === true) { 51 | return; 52 | } 53 | this.tasks.set(task.id, task); 54 | } 55 | 56 | @action.bound 57 | batchTask = (tasks) => { 58 | transaction(() => { 59 | tasks.map( 60 | task => this.updateTask(task) 61 | ); 62 | }); 63 | } 64 | 65 | @action.bound 66 | doneTask = (task) => { 67 | this.updateTask(task); 68 | this.save(); 69 | } 70 | 71 | @action.bound 72 | removeTasks = (items) => { 73 | items = Array.isArray(items) ? items : [items]; 74 | 75 | items.forEach( 76 | e => { 77 | this.tasks.delete(e.id); 78 | } 79 | ); 80 | this.save(); 81 | } 82 | 83 | @action.bound 84 | getPlaylist = () => { 85 | return new Promise( 86 | (resolve, reject) => { 87 | var timer = setTimeout( 88 | () => resolve(false), 89 | 5000 90 | ); 91 | 92 | ipcRenderer.once('response-playlist', (e, data) => { 93 | clearTimeout(timer); 94 | data = JSON.parse(data); 95 | this.playlist = data.songs; 96 | resolve(true); 97 | }); 98 | 99 | remote.getGlobal('mainWindow').webContents.send('request-playlist'); 100 | } 101 | ); 102 | } 103 | }; 104 | 105 | export default new Stores(); 106 | -------------------------------------------------------------------------------- /submodules/downloader/viewport/views/List/classes.js: -------------------------------------------------------------------------------- 1 | 2 | import colors from 'utils/colors'; 3 | 4 | export default theme => ({ 5 | container: { 6 | height: '100vh', 7 | backgroundColor: '#f5f5f7', 8 | 9 | '& nav': { 10 | position: 'relative', 11 | display: 'flex', 12 | height: 38, 13 | paddingLeft: 100, 14 | flexDirection: 'row', 15 | justifyContent: 'space-around', 16 | alignItems: 'center', 17 | color: 'black', 18 | backgroundColor: 'white', 19 | fontFamily: 'Roboto', 20 | fontSize: 14, 21 | fontWeight: '500', 22 | boxShadow: '0 -6px 24px rgba(0, 0, 0, .1)', 23 | }, 24 | 25 | '& section': { 26 | height: 'calc(100vh - 38px - 40px)', 27 | overflowY: 'auto', 28 | overflowX: 'hidden', 29 | }, 30 | 31 | '& aside': { 32 | position: 'relative', 33 | display: 'flex', 34 | width: 235, 35 | justifyContent: 'space-around', 36 | flexDirection: 'column', 37 | height: 48, 38 | paddingLeft: 24, 39 | }, 40 | 41 | '& small': { 42 | color: '#666', 43 | }, 44 | 45 | '& small, $title': { 46 | maxWidth: 140, 47 | overflow: 'hidden', 48 | textOverflow: 'ellipsis', 49 | whiteSpace: 'nowrap', 50 | }, 51 | 52 | '& footer': { 53 | position: 'absolute', 54 | bottom: 0, 55 | left: 0, 56 | display: 'flex', 57 | flexDirection: 'row', 58 | alignItems: 'center', 59 | justifyContent: 'space-between', 60 | width: 'calc(100vw - 24px)', 61 | height: 40, 62 | paddingLeft: 12, 63 | paddingRight: 12, 64 | backgroundColor: 'white', 65 | boxShadow: '0 6px 24px rgba(0, 0, 0, .1)', 66 | }, 67 | 68 | '& button': { 69 | display: 'flex', 70 | flexDirection: 'row', 71 | justifyContent: 'center', 72 | alignItems: 'center', 73 | fontSize: 12, 74 | color: '#333', 75 | padding: '4px 12px', 76 | margin: 0, 77 | border: 0, 78 | background: '#efefef', 79 | textTransform: 'uppercase', 80 | transition: '.2s', 81 | }, 82 | 83 | '& button i': { 84 | marginRight: 4, 85 | }, 86 | 87 | '& button:disabled': { 88 | color: '#999', 89 | }, 90 | 91 | '& button:hover:not(:disabled)': { 92 | color: colors.pallet.google, 93 | }, 94 | 95 | '& button:last-child:not(:disabled), & $checked': { 96 | color: '#fff', 97 | backgroundColor: colors.pallet.google, 98 | } 99 | }, 100 | 101 | checked: {}, 102 | 103 | item: { 104 | position: 'relative', 105 | display: 'flex', 106 | flexDirection: 'row', 107 | alignItems: 'center', 108 | padding: 24, 109 | paddingRight: 12, 110 | paddingTop: 6, 111 | paddingBottom: 6, 112 | 113 | '& i': { 114 | position: 'absolute', 115 | right: 24, 116 | top: '50%', 117 | fontSize: 18, 118 | color: '#333', 119 | transform: 'translateY(-50%)', 120 | }, 121 | 122 | '& i.ion-ios-checkmark, & i.ion-ios-cloud-download': { 123 | color: colors.pallet.google, 124 | } 125 | }, 126 | 127 | cover: { 128 | width: 32, 129 | height: 32, 130 | boxShadow: '0 0 24px rgba(0, 0, 0, .3)', 131 | }, 132 | 133 | title: { 134 | margin: 0, 135 | color: '#333', 136 | }, 137 | 138 | nothing: { 139 | display: 'flex', 140 | height: '100%', 141 | justifyContent: 'center', 142 | alignItems: 'center', 143 | fontFamily: 'HelveticaNeue-UltraLight', 144 | fontSize: 32, 145 | letterSpacing: 1, 146 | wordSpacing: 3, 147 | color: '#333', 148 | }, 149 | 150 | close: { 151 | position: 'absolute', 152 | right: 6, 153 | top: '50%', 154 | width: 32, 155 | fontSize: 24, 156 | textAlign: 'center', 157 | transform: 'translateY(-45%)', 158 | cursor: 'pointer', 159 | }, 160 | }); 161 | -------------------------------------------------------------------------------- /submodules/index.js: -------------------------------------------------------------------------------- 1 | 2 | import updater from './updater'; 3 | import downloader from './downloader'; 4 | 5 | export { 6 | updater, 7 | downloader, 8 | }; 9 | -------------------------------------------------------------------------------- /submodules/updater.js: -------------------------------------------------------------------------------- 1 | 2 | import { dialog } from 'electron'; 3 | import { autoUpdater } from 'electron-updater'; 4 | import _debug from 'debug'; 5 | import pkg from '../package.json'; 6 | 7 | let debug = _debug('dev:submodules:updater'); 8 | let error = _debug('dev:submodules:updater:error'); 9 | 10 | let downloading = false; 11 | let alreadyAutoupdate = false; 12 | 13 | function checkForUpdates(autoupdate = false) { 14 | if (downloading) { 15 | dialog.showMessageBox({ 16 | type: 'info', 17 | buttons: ['OK'], 18 | title: pkg.name, 19 | message: `Downloading...`, 20 | detail: `Please leave the app open, the new version is downloading. You'll receive a new dialog when downloading is finished.` 21 | }); 22 | 23 | return; 24 | } 25 | 26 | if (autoupdate || alreadyAutoupdate) { 27 | autoUpdater.checkForUpdates(); 28 | } else { 29 | alreadyAutoupdate = true; 30 | } 31 | } 32 | 33 | function installAutoUpdater(done) { 34 | autoUpdater.on('checking-for-update', () => { 35 | debug('Checking for update...'); 36 | }); 37 | 38 | autoUpdater.on('update-not-available', e => { 39 | debug('Update not available.'); 40 | 41 | // Do'nt show the message on app launched 42 | if (!alreadyAutoupdate) { 43 | alreadyAutoupdate = true; 44 | return; 45 | } 46 | 47 | dialog.showMessageBox({ 48 | type: 'info', 49 | buttons: ['OK'], 50 | title: pkg.name, 51 | message: `${pkg.name} is up to date :)`, 52 | detail: `${pkg.name} ${pkg.version} is currently the newest version available, It looks like you're already rocking the latest version!` 53 | }); 54 | }); 55 | 56 | autoUpdater.on('update-available', e => { 57 | debug('Update available.'); 58 | downloading = true; 59 | checkForUpdates(); 60 | }); 61 | 62 | autoUpdater.on('error', err => { 63 | dialog.showMessageBox({ 64 | type: 'error', 65 | buttons: ['Cancel update'], 66 | title: pkg.name, 67 | message: `Failed to update ${pkg.name} :(`, 68 | detail: `An error occurred in retrieving update information, Please try again later.`, 69 | }); 70 | 71 | downloading = false; 72 | error(err); 73 | }); 74 | 75 | autoUpdater.on('update-downloaded', info => { 76 | var { releaseNotes, releaseName } = info; 77 | var index = dialog.showMessageBox({ 78 | type: 'info', 79 | buttons: ['Restart', 'Later'], 80 | title: pkg.name, 81 | message: `The new version has been downloaded. Please restart the application to apply the updates.`, 82 | detail: `${releaseName}\n\n${releaseNotes}` 83 | }); 84 | downloading = false; 85 | 86 | if (index === 1) { 87 | return; 88 | } 89 | 90 | autoUpdater.quitAndInstall(); 91 | setTimeout(() => done()); 92 | }); 93 | } 94 | 95 | export default { 96 | installAutoUpdater, 97 | checkForUpdates, 98 | }; 99 | --------------------------------------------------------------------------------