├── .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 |
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 |
74 |
75 |
101 |
102 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------