├── .buckconfig ├── .eslintignore ├── .eslintrc.js ├── .flowconfig ├── .gitattributes ├── .github ├── scripts │ ├── decrypt_secret.sh │ └── my-release-key.keystore.gpg └── workflows │ └── build.yml ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── .watchmanconfig ├── App.js ├── LICENSE ├── README.md ├── Routes.js ├── __tests__ └── App-test.js ├── android ├── app │ ├── BUCK │ ├── build.gradle │ ├── build_defs.bzl │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── ic_launcher-web.png │ │ ├── java │ │ └── com │ │ │ └── listen1 │ │ │ └── app │ │ │ ├── MainActivity.java │ │ │ └── MainApplication.java │ │ └── res │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── keystores │ ├── BUCK │ └── debug.keystore.properties └── settings.gradle ├── app.json ├── babel.config.js ├── fastlane └── metadata │ └── android │ ├── en-US │ ├── full_description.txt │ ├── images │ │ └── phoneScreenshots │ │ │ └── 1.png │ ├── short_description.txt │ └── title.txt │ └── zh-CN │ ├── full_description.txt │ └── short_description.txt ├── index.js ├── ios ├── Listen1-tvOS │ └── Info.plist ├── Listen1-tvOSTests │ └── Info.plist ├── Listen1.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ ├── Listen1-tvOS.xcscheme │ │ └── Listen1.xcscheme ├── Listen1 │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Base.lproj │ │ └── LaunchScreen.xib │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── 1024.png │ │ │ ├── 114.png │ │ │ ├── 120.png │ │ │ ├── 180.png │ │ │ ├── 29.png │ │ │ ├── 40.png │ │ │ ├── 57.png │ │ │ ├── 58.png │ │ │ ├── 60.png │ │ │ ├── 80.png │ │ │ ├── 87.png │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ └── main.m └── Listen1Tests │ ├── Info.plist │ └── Listen1Tests.m ├── metro.config.js ├── package.json ├── src ├── api │ ├── client.js │ └── provider │ │ ├── kugou.js │ │ ├── kuwo.js │ │ ├── netease.js │ │ ├── qq.js │ │ └── xiami.js ├── assets │ └── images │ │ └── logo.png ├── components │ ├── flex.component.js │ ├── image.component.js │ ├── index.js │ ├── list-editor.component.js │ ├── modal-lite.component.js │ ├── option-row.component.js │ ├── playlist-row.component.js │ ├── table-cell-row.component.js │ ├── text.component.js │ ├── touchable.component.js │ └── track-row.component.js ├── config │ ├── colors.js │ ├── settings.js │ └── theme.js ├── modules │ ├── crypto.js │ ├── encrypt_test.js │ ├── state-json-convert.js │ └── toast.js ├── redux │ ├── actions.js │ ├── myplaylist.reducer.js │ ├── player.reducer.js │ └── reducer.js ├── utils │ └── kugouUtils.js └── views │ ├── dev │ ├── modal-playground.screen.js │ ├── network-playground.screen.js │ ├── sort-playground.screen.js │ └── toast-playground.screen.js │ ├── myplaylist │ ├── create-playlist.screen.js │ ├── import-playlist.screen.js │ ├── myplaylist-list.screen.js │ └── reorder.screen.js │ ├── player │ ├── background-player.screen.js │ ├── mini-player.screen.js │ ├── modal-lite-container.screen.js │ ├── modal-player-container.screen.js │ ├── modal-player.screen.js │ ├── player-control.screen.js │ ├── player-info.screen.js │ └── player-nav.screen.js │ ├── playlist │ ├── add-to-playlist-popup.screen.js │ ├── nav-header.screen.js │ ├── playlist-grid.screen.js │ ├── playlist-popup.screen.js │ ├── playlist-tabs.screen.js │ ├── playlist.screen.js │ ├── search.screen.js │ └── track-popup.screen.js │ └── setting │ ├── about.screen.js │ ├── export-local.screen.js │ ├── import-local.screen.js │ └── setting.screen.js ├── vendor ├── cryptojs_aes.js ├── jsencrypt.js └── react-native-super-grid │ ├── .eslintrc.json │ ├── FlatGrid.js │ ├── LICENSE │ ├── README.md │ ├── SectionGrid.js │ ├── index.d.ts │ ├── index.js │ ├── package.json │ └── utils.js └── yarn.lock /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | __tests__/** 2 | testenv.js 3 | coverage/** 4 | vendor/** -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'eslint-config-airbnb', 3 | plugins: ['jsx-a11y', 'import', 'react', 'react-native', 'flowtype'], 4 | parser: 'babel-eslint', 5 | env: { 6 | browser: true, 7 | node: true, 8 | es6: true, 9 | }, 10 | parserOptions: { 11 | ecmaFeatures: { 12 | jsx: true, 13 | }, 14 | }, 15 | globals: { 16 | __DEV__: true, 17 | }, 18 | settings: { 19 | 'import/resolver': { 20 | 'babel-module': { 21 | root: ['./src'], 22 | alias: { 23 | testData: './__tests__/data', 24 | }, 25 | }, 26 | }, 27 | }, 28 | rules: { 29 | 'global-require': 'off', 30 | 'import/prefer-default-export': 'off', 31 | 'jsx-a11y/anchor-has-content': 'off', 32 | 'jsx-a11y/no-noninteractive-element-interactions': 'off', 33 | 'jsx-a11y/no-static-element-interactions': 'off', 34 | 'arrow-body-style': 'off', 35 | 'arrow-parens': ['error', 'as-needed'], 36 | 'comma-dangle': ['error', 'always-multiline'], 37 | indent: 'off', 38 | 'padding-line-between-statements': [ 39 | 2, 40 | { blankLine: 'always', prev: '*', next: 'return' }, 41 | { blankLine: 'always', prev: ['var', 'let', 'const'], next: '*' }, 42 | { 43 | blankLine: 'any', 44 | prev: ['var', 'let', 'const'], 45 | next: ['var', 'let', 'const'], 46 | }, 47 | ], 48 | 'newline-per-chained-call': 'off', 49 | 'no-confusing-arrow': 'off', 50 | 'no-else-return': [ 51 | 'error', 52 | { 53 | allowElseIf: true, 54 | }, 55 | ], 56 | 'no-mixed-operators': [ 57 | 'error', 58 | { 59 | groups: [ 60 | ['&', '|', '^', '~', '<<', '>>', '>>>'], 61 | ['==', '!=', '===', '!==', '>', '>=', '<', '<='], 62 | ['&&', '||'], 63 | ['in', 'instanceof'], 64 | ], 65 | allowSamePrecedence: true, 66 | }, 67 | ], 68 | 'no-underscore-dangle': 'off', 69 | 'max-len': 'off', 70 | 'no-plusplus': [ 71 | 'error', 72 | { 73 | allowForLoopAfterthoughts: true, 74 | }, 75 | ], 76 | 'space-before-function-paren': [ 77 | 'error', 78 | { 79 | anonymous: 'never', 80 | named: 'never', 81 | asyncArrow: 'always', 82 | }, 83 | ], 84 | 'wrap-iife': [ 85 | 'error', 86 | 'inside', 87 | { 88 | functionPrototypeMethods: false, 89 | }, 90 | ], 91 | 'flowtype/define-flow-type': 1, 92 | 'react/jsx-wrap-multilines': 'off', 93 | 'react/jsx-closing-bracket-location': 'off', 94 | 'react/jsx-curly-spacing': [ 95 | 'error', 96 | 'never', 97 | { 98 | allowMultiline: true, 99 | }, 100 | ], 101 | 'react/jsx-filename-extension': [ 102 | 'error', 103 | { 104 | extensions: ['.js', '.jsx'], 105 | }, 106 | ], 107 | 'react/jsx-indent': 'off', 108 | 'react/jsx-indent-props': 'off', 109 | 'react/jsx-no-bind': 'error', 110 | 'react/no-multi-comp': 'off', 111 | 'react/prefer-stateless-function': 'off', 112 | 'react/sort-comp': [ 113 | 'error', 114 | { 115 | order: [ 116 | 'static-methods', 117 | 'lifecycle', 118 | '/^on.+$/', 119 | '/^(get|set)(?!(InitialState$|DefaultProps$|ChildContext$)).+$/', 120 | 'everything-else', 121 | '/^render.+$/', 122 | 'render', 123 | ], 124 | groups: { 125 | lifecycle: [ 126 | 'displayName', 127 | 'props', 128 | 'propTypes', 129 | 'contextTypes', 130 | 'childContextTypes', 131 | 'mixins', 132 | 'statics', 133 | 'defaultProps', 134 | 'state', 135 | 'constructor', 136 | 'getDefaultProps', 137 | 'getInitialState', 138 | 'getChildContext', 139 | 'componentWillMount', 140 | 'componentDidMount', 141 | 'componentWillReceiveProps', 142 | 'shouldComponentUpdate', 143 | 'componentWillUpdate', 144 | 'componentDidUpdate', 145 | 'componentWillUnmount', 146 | ], 147 | }, 148 | }, 149 | ], 150 | // disable temporarily in order to not modify current codes 151 | 'function-paren-newline': 'off', 152 | 'implicit-arrow-linebreak': 'off', 153 | 'lines-between-class-members': 'off', 154 | 'object-curly-newline': 'off', 155 | 'operator-linebreak': 'off', 156 | 'prefer-destructuring': 'off', 157 | 'import/named': 'off', 158 | 'import/no-cycle': 'off', 159 | 'jsx-a11y/anchor-is-valid': 'off', 160 | 'no-restricted-globals': 'off', 161 | 'react/default-props-match-prop-types': 'off', 162 | 'react/destructuring-assignment': 'off', 163 | 'react/jsx-one-expression-per-line': 'off', 164 | 'react/no-access-state-in-setstate': 'off', 165 | 'react/no-this-in-sfc': 'off', 166 | 'react/no-unused-state': 'off', 167 | 'react/require-default-props': 'off', 168 | }, 169 | }; 170 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore unexpected extra "@providesModule" 9 | .*/node_modules/.*/node_modules/fbjs/.* 10 | 11 | ; Ignore duplicate module providers 12 | ; For RN Apps installed via npm, "Libraries" folder is inside 13 | ; "node_modules/react-native" but in the source repo it is in the root 14 | .*/Libraries/react-native/React.js 15 | 16 | ; Ignore polyfills 17 | .*/Libraries/polyfills/.* 18 | 19 | ; Ignore metro 20 | .*/node_modules/metro/.* 21 | 22 | [include] 23 | 24 | [libs] 25 | node_modules/react-native/Libraries/react-native/react-native-interface.js 26 | node_modules/react-native/flow/ 27 | node_modules/react-native/flow-github/ 28 | 29 | [options] 30 | module.system.node.resolve_dirname=node_modules 31 | module.system.node.resolve_dirname=src 32 | module.name_mapper='^package.json$' -> '/package.json' 33 | emoji=true 34 | 35 | esproposal.optional_chaining=enable 36 | esproposal.nullish_coalescing=enable 37 | 38 | module.system=haste 39 | module.system.haste.use_name_reducers=true 40 | # get basename 41 | module.system.haste.name_reducers='^.*/\([a-zA-Z0-9$_.-]+\.js\(\.flow\)?\)$' -> '\1' 42 | # strip .js or .js.flow suffix 43 | module.system.haste.name_reducers='^\(.*\)\.js\(\.flow\)?$' -> '\1' 44 | # strip .ios suffix 45 | module.system.haste.name_reducers='^\(.*\)\.ios$' -> '\1' 46 | module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1' 47 | module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1' 48 | module.system.haste.paths.blacklist=.*/__tests__/.* 49 | module.system.haste.paths.blacklist=.*/__mocks__/.* 50 | module.system.haste.paths.blacklist=/node_modules/react-native/Libraries/Animated/src/polyfills/.* 51 | module.system.haste.paths.whitelist=/node_modules/react-native/Libraries/.* 52 | 53 | munge_underscores=true 54 | 55 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 56 | 57 | module.file_ext=.js 58 | module.file_ext=.jsx 59 | module.file_ext=.json 60 | module.file_ext=.native.js 61 | 62 | suppress_type=$FlowIssue 63 | suppress_type=$FlowFixMe 64 | suppress_type=$FlowFixMeProps 65 | suppress_type=$FlowFixMeState 66 | 67 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 68 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 69 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 70 | 71 | [untyped] 72 | .*/node_modules/**/.* 73 | 74 | [version] 75 | ^0.102.0 76 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /.github/scripts/decrypt_secret.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # --batch to prevent interactive command 4 | # --yes to assume "yes" for questions 5 | gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" \ 6 | --output $GITHUB_WORKSPACE/android/app/my-release-key.keystore $GITHUB_WORKSPACE/.github/scripts/my-release-key.keystore.gpg -------------------------------------------------------------------------------- /.github/scripts/my-release-key.keystore.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/.github/scripts/my-release-key.keystore.gpg -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: react-native-android-build-apk 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | install-and-test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Install npm dependencies 12 | run: | 13 | npm install 14 | build-android: 15 | needs: install-and-test 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Decrypt large secret 20 | run: ./.github/scripts/decrypt_secret.sh 21 | env: 22 | LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} 23 | - name: Install npm dependencies 24 | run: | 25 | npm install 26 | - name: Build Android Release 27 | run: | 28 | cd android && chmod +x ./gradlew && ./gradlew assembleRelease 29 | - name: Upload Artifact 30 | uses: actions/upload-artifact@v1 31 | with: 32 | name: app-release.apk 33 | path: android/app/build/outputs/apk/release/ 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | 33 | # node.js 34 | # 35 | node_modules/ 36 | npm-debug.log 37 | yarn-error.log 38 | 39 | # BUCK 40 | buck-out/ 41 | \.buckd/ 42 | *.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | */fastlane/report.xml 52 | */fastlane/Preview.html 53 | */fastlane/screenshots 54 | 55 | # Bundle artifact 56 | *.jsbundle 57 | 58 | # font resource(link will update) 59 | android/app/src/main/assets/fonts/* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | printWidth: 80 2 | singleQuote: true 3 | trailingComma: es5 4 | bracketSpacing: true 5 | semi: true 6 | useTabs: false 7 | tabWidth: 2 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.eslintIntegration": true, 3 | "editor.formatOnSave": true, 4 | "files.exclude": { 5 | "**/*.git": true, 6 | "node_modules/": true 7 | }, 8 | "javascript.validate.enable": false 9 | } -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { createStore } from 'redux'; 4 | import { persistStore, persistReducer } from 'redux-persist'; 5 | import { Text } from 'react-native'; 6 | import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web 7 | import { PersistGate } from 'redux-persist/integration/react'; 8 | 9 | import Routes from './Routes'; 10 | import reducer from './src/redux/reducer'; 11 | 12 | // Note: Playground provide quick environment for testing component 13 | // replace `` with component for testing 14 | 15 | // import SortPlayground from './src/views/dev/sort-playground.screen'; 16 | // import ModalPlayground from './src/views/dev/modal-playground.screen'; 17 | // import NetworkPlayground from './src/views/dev/network-playground.screen'; 18 | // import ToastPlayground from './src/views/dev/toast-playground.screen'; 19 | 20 | // TODO: timeout setting not working in debug mode 21 | const persistConfig = { 22 | key: 'root', 23 | storage, 24 | blacklist: ['searchState', 'modalState', 'playerState'], 25 | timeout: null, 26 | }; 27 | 28 | const persistedReducer = persistReducer(persistConfig, reducer); 29 | 30 | const store = createStore(persistedReducer); 31 | const persistor = persistStore(store); 32 | 33 | // config Text not changed by system font scale 34 | Text.defaultProps = Text.defaultProps || {}; 35 | Text.defaultProps.allowFontScaling = false; 36 | 37 | export default class App extends React.Component { 38 | render() { 39 | return ( 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019- Listen 1 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Listen1 Mobile V0.8.1 2 | 3 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) 4 | 5 | ## 简介 6 | 7 | 一款支持多平台音乐播放和搜索的移动音乐 App。现有版本已支持网易云音乐,QQ 音乐,虾米音乐。还有丰富的歌单管理功能。使用 React Native 开发,基于 MIT 协议开源免费。 8 | 9 | **支持 iOS 和 Android 平台** 10 | 11 | [![imgur](https://i.imgur.com/zYyaK92.png)]() 12 | 13 | ## 特性 14 | 15 | - 一个 App 播放多平台的音乐 16 | - 搜索多平台音乐 17 | - 浏览,播放多平台歌单 18 | - 收藏音乐到自建歌单 19 | - 夜间模式 20 | - 备份,恢复(支持从`Listen1 chrome extension`导入数据) 21 | 22 | ## 下载 23 | 24 | Github 主页下载: https://listen1.github.io/listen1 25 | 26 | [Get it on F-Droid](https://f-droid.org/packages/com.listen1.app) 29 | 30 | ## 安装 31 | 32 | ### iOS 33 | 34 | iOS 只支持编译安装,请拥有开发者证书的开发者连接 iPhone 后,将项目文件中的证书换成自己的证书,然后执行命令安装。 35 | 36 | ### Andriod 37 | 38 | 下载 apk 安装, apk 下载地址请访问[项目 release 页面](https://github.com/listen1/listen1_mobile/releases) 39 | 40 | ## 编译 41 | 42 | 开发环境 43 | 44 | - Java 8 JDK (更高版本需更新默认 gradle 版本) 45 | - Nodejs 8 (版本>12.10.0 可能遇到 metro 一个关于正则表达式的 bug 导致的启动失败) 46 | - Android Studio (Android SDK 版本 v28) 47 | 48 | 编译步骤 49 | 50 | - Clone 或下载本项目代码 51 | - `yarn` 安装依赖 52 | - `yarn run link` 链接 React Native 的依赖库 53 | - `yarn start:ios` 将在 iOS 模拟器上运行项目 54 | - `yarn start:android` 将在安卓真机或模拟器(取决于手机是否连接)运行项目 55 | 56 | Apk 打包 57 | 58 | ``` 59 | cd .\android\ 60 | ./gradlew assembleRelease 61 | react-native run-android --variant=release 62 | 63 | ``` 64 | 65 | 更详细的打包信息(包括生成 keystone) 66 | 67 | https://reactnative.cn/docs/signed-apk-android 68 | 69 | ## 代码基本结构 70 | 71 | - api: 音乐平台相关资源 API 72 | - asset: 图片等资源 73 | - components: 可复用的组件 74 | - views: 业务相关的 screen 组件 75 | - modules: 组件使用的自定义函数库 76 | - redux: redux 需要的 action 和 reducer 函数 77 | 78 | ## 鸣谢 79 | 80 | - [git-point](https://github.com/gitpoint/git-point): github 的 RN 客户端,提供本项目开发环境搭建的结构支持。 81 | - [Binaryify/NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi): 网易 API,参考了网络协议部分代码。 82 | - [yezihaohao/NeteaseCloudMusic](https://github.com/yezihaohao/NeteaseCloudMusic): 网易音乐 RN 端,参考了 RN 导航,播放部分的库代码实现。 83 | - [soimort/you-get](https://github.com/soimort/you-get): 音乐下载命令行,参考了协议和法律相关声明(如下)。 84 | 85 | 开发过程还有很多开源软件提供了各种问题的解决方案,详见代码注释,篇幅原因不一一列出,感谢开源社区的各位开发者。 86 | 87 | ## 更新日志 88 | 89 | `2020-10-31` 90 | 91 | - 修复网易云歌单只有 10 首歌的 bug (感谢 @eatenid 的提交) 92 | - 修复虾米云音乐歌单只有 30 首的 bug 93 | - 优化过长歌曲名或标题的显示 94 | - 优化下侧播放控制栏的弹窗性能 95 | - 修正了点击暂停按钮时导致闪退的 bug 96 | - 支持 GitHub action 在线打包 97 | 98 | `2019-11-27` 99 | 100 | - 修复 qq 音乐因 user-agent 无法访问的 bug 101 | 102 | `2019-08-09` 103 | 104 | - 修复网易云音乐无法访问的 bug 105 | 106 | `2019-07-31` 107 | 108 | - 首次发布 109 | 110 | ## 法律相关 111 | 112 | This software is distributed under the MIT license 113 | 114 | In particular, please be aware that 115 | 116 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 117 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 118 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 119 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 120 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 121 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 122 | > SOFTWARE. 123 | 124 | Translated to human words: 125 | 126 | _In case your use of the software forms the basis of copyright infringement, or you use the software for any other illegal purposes, the authors cannot take any responsibility for you._ 127 | 128 | We only ship the code here, and how you are going to use it is left to your own discretion. 129 | -------------------------------------------------------------------------------- /Routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { createStackNavigator, createAppContainer } from 'react-navigation'; 4 | import { StatusBar } from 'react-native'; 5 | import { fromRight } from 'react-navigation-transitions'; 6 | import { withTheme, ThemeProvider } from 'styled-components'; 7 | import { animationSetting } from './src/config/settings'; 8 | 9 | import { blackTheme, whiteTheme } from './src/config/theme'; 10 | 11 | import { ThemeFlex } from './src/components'; 12 | 13 | /* screens */ 14 | import PlaylistTabs from './src/views/playlist/playlist-tabs.screen'; 15 | import Playlist from './src/views/playlist/playlist.screen'; 16 | import BackgroundPlayer from './src/views/player/background-player.screen'; 17 | import ModalPlayerContainer from './src/views/player/modal-player-container.screen'; 18 | import MiniPlayer from './src/views/player/mini-player.screen'; 19 | 20 | import Setting from './src/views/setting/setting.screen'; 21 | import CreatePlaylist from './src/views/myplaylist/create-playlist.screen'; 22 | import ReOrder from './src/views/myplaylist/reorder.screen'; 23 | import ModalLiteContainer from './src/views/player/modal-lite-container.screen'; 24 | import ImportPlaylist from './src/views/myplaylist/import-playlist.screen'; 25 | import About from './src/views/setting/about.screen'; 26 | import ImportLocal from './src/views/setting/import-local.screen'; 27 | import ExportLocal from './src/views/setting/export-local.screen'; 28 | 29 | const MainStack = createStackNavigator( 30 | { 31 | Home: { 32 | screen: PlaylistTabs, 33 | }, 34 | Details: { 35 | screen: Playlist, 36 | }, 37 | Setting: { 38 | screen: Setting, 39 | }, 40 | CreatePlaylist: { 41 | screen: CreatePlaylist, 42 | }, 43 | ImportPlaylist: { 44 | screen: ImportPlaylist, 45 | }, 46 | ReOrder: { 47 | screen: ReOrder, 48 | }, 49 | About: { 50 | screen: About, 51 | }, 52 | ImportLocal: { 53 | screen: ImportLocal, 54 | }, 55 | ExportLocal: { 56 | screen: ExportLocal, 57 | }, 58 | }, 59 | { 60 | initialRouteName: 'Home', 61 | // force navigation animation window from right to left, set animation time in milliseconds 62 | transitionConfig: () => fromRight(animationSetting.transitionTime), 63 | defaultNavigationOptions: { 64 | headerBackTitle: null, 65 | }, 66 | } 67 | ); 68 | 69 | const AppContainer = createAppContainer(MainStack); 70 | const ThemeAppContainer = withTheme(({ theme }) => { 71 | return ; 72 | }); 73 | 74 | const ThemeStatusBar = withTheme(({ theme }) => { 75 | return ( 76 | 77 | ); 78 | }); 79 | 80 | class App extends React.Component { 81 | props: { 82 | settingState: Object, 83 | }; 84 | render() { 85 | return ( 86 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | ); 101 | } 102 | } 103 | const mapStateToProps = ({ settingState }) => ({ 104 | settingState, 105 | }); 106 | 107 | export default connect(mapStateToProps)(App); 108 | -------------------------------------------------------------------------------- /__tests__/App-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import 'react-native'; 6 | import React from 'react'; 7 | import App from '../App'; 8 | 9 | // Note: test renderer must be required after react-native. 10 | import renderer from 'react-test-renderer'; 11 | 12 | it('renders correctly', () => { 13 | renderer.create(); 14 | }); 15 | -------------------------------------------------------------------------------- /android/app/BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets") 12 | 13 | lib_deps = [] 14 | 15 | create_aar_targets(glob(["libs/*.aar"])) 16 | 17 | create_jar_targets(glob(["libs/*.jar"])) 18 | 19 | android_library( 20 | name = "all-libs", 21 | exported_deps = lib_deps, 22 | ) 23 | 24 | android_library( 25 | name = "app-code", 26 | srcs = glob([ 27 | "src/main/java/**/*.java", 28 | ]), 29 | deps = [ 30 | ":all-libs", 31 | ":build_config", 32 | ":res", 33 | ], 34 | ) 35 | 36 | android_build_config( 37 | name = "build_config", 38 | package = "com.listen1.app", 39 | ) 40 | 41 | android_resource( 42 | name = "res", 43 | package = "com.listen1.app", 44 | res = "src/main/res", 45 | ) 46 | 47 | android_binary( 48 | name = "app", 49 | keystore = "//android/keystores:debug", 50 | manifest = "src/main/AndroidManifest.xml", 51 | package_type = "debug", 52 | deps = [ 53 | ":app-code", 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /android/app/build_defs.bzl: -------------------------------------------------------------------------------- 1 | """Helper definitions to glob .aar and .jar targets""" 2 | 3 | def create_aar_targets(aarfiles): 4 | for aarfile in aarfiles: 5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] 6 | lib_deps.append(":" + name) 7 | android_prebuilt_aar( 8 | name = name, 9 | aar = aarfile, 10 | ) 11 | 12 | def create_jar_targets(jarfiles): 13 | for jarfile in jarfiles: 14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] 15 | lib_deps.append(":" + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 16 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /android/app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/android/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /android/app/src/main/java/com/listen1/app/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.listen1.app; 2 | 3 | import com.facebook.react.ReactActivity; 4 | import com.facebook.react.ReactActivityDelegate; 5 | import com.facebook.react.ReactRootView; 6 | import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView; 7 | 8 | public class MainActivity extends ReactActivity { 9 | 10 | /** 11 | * Returns the name of the main component registered from JavaScript. 12 | * This is used to schedule rendering of the component. 13 | */ 14 | @Override 15 | protected String getMainComponentName() { 16 | return "Listen1"; 17 | } 18 | @Override 19 | protected ReactActivityDelegate createReactActivityDelegate() { 20 | return new ReactActivityDelegate(this, getMainComponentName()) { 21 | @Override 22 | protected ReactRootView createRootView() { 23 | return new RNGestureHandlerEnabledRootView(MainActivity.this); 24 | } 25 | }; 26 | } 27 | @Override 28 | public void invokeDefaultOnBackPressed() { 29 | // do not call super. invokeDefaultOnBackPressed() as it will close the app. Instead lets just put it in the background. 30 | moveTaskToBack(true); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/listen1/app/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.listen1.app; 2 | 3 | import android.app.Application; 4 | 5 | import com.facebook.react.ReactApplication; 6 | import com.reactnativecommunity.asyncstorage.AsyncStoragePackage; 7 | import com.tanguyantoine.react.MusicControl; 8 | import com.oblador.vectoricons.VectorIconsPackage; 9 | import com.swmansion.gesturehandler.react.RNGestureHandlerPackage; 10 | import com.brentvatne.react.ReactVideoPackage; 11 | import com.facebook.react.ReactNativeHost; 12 | import com.facebook.react.ReactPackage; 13 | import com.facebook.react.shell.MainReactPackage; 14 | import com.facebook.soloader.SoLoader; 15 | import com.facebook.react.bridge.ReadableNativeArray; 16 | import com.facebook.react.bridge.ReadableNativeMap; 17 | 18 | import java.util.Arrays; 19 | import java.util.List; 20 | 21 | public class MainApplication extends Application implements ReactApplication { 22 | 23 | private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { 24 | @Override 25 | public boolean getUseDeveloperSupport() { 26 | return BuildConfig.DEBUG; 27 | } 28 | 29 | @Override 30 | protected List getPackages() { 31 | return Arrays.asList( 32 | new MainReactPackage(), 33 | new AsyncStoragePackage(), 34 | new MusicControl(), 35 | new VectorIconsPackage(), 36 | new RNGestureHandlerPackage(), 37 | new ReactVideoPackage() 38 | ); 39 | } 40 | 41 | @Override 42 | protected String getJSMainModuleName() { 43 | return "index"; 44 | } 45 | }; 46 | 47 | @Override 48 | public ReactNativeHost getReactNativeHost() { 49 | return mReactNativeHost; 50 | } 51 | 52 | @Override 53 | public void onCreate() { 54 | super.onCreate(); 55 | SoLoader.init(this, /* native exopackage */ false); 56 | 57 | // resolve fetch result with same header name is merged 58 | // https://github.com/facebook/react-native/issues/21795#issuecomment-430384534 59 | ReadableNativeArray.setUseNativeAccessor(true); 60 | ReadableNativeMap.setUseNativeAccessor(true); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Listen 1 3 | 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "28.0.3" 6 | minSdkVersion = 16 7 | compileSdkVersion = 28 8 | targetSdkVersion = 28 9 | supportLibVersion = "28.0.0" 10 | } 11 | repositories { 12 | google() 13 | jcenter() 14 | } 15 | dependencies { 16 | classpath("com.android.tools.build:gradle:3.4.0") 17 | 18 | // NOTE: Do not place your application dependencies here; they belong 19 | // in the individual module build.gradle files 20 | } 21 | } 22 | 23 | allprojects { 24 | repositories { 25 | mavenLocal() 26 | google() 27 | jcenter() 28 | maven { 29 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 30 | url "$rootDir/../node_modules/react-native/android" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | MYAPP_UPLOAD_STORE_FILE=my-release-key.keystore 20 | MYAPP_UPLOAD_KEY_ALIAS=my-key-alias 21 | MYAPP_UPLOAD_STORE_PASSWORD=lovemusic 22 | MYAPP_UPLOAD_KEY_PASSWORD=lovemusic -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem http://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /android/keystores/BUCK: -------------------------------------------------------------------------------- 1 | keystore( 2 | name = "debug", 3 | properties = "debug.keystore.properties", 4 | store = "debug.keystore", 5 | visibility = [ 6 | "PUBLIC", 7 | ], 8 | ) 9 | -------------------------------------------------------------------------------- /android/keystores/debug.keystore.properties: -------------------------------------------------------------------------------- 1 | key.store=debug.keystore 2 | key.alias=androiddebugkey 3 | key.store.password=android 4 | key.alias.password=android 5 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'Listen1' 2 | include ':@react-native-community_async-storage' 3 | project(':@react-native-community_async-storage').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/async-storage/android') 4 | include ':react-native-music-control' 5 | project(':react-native-music-control').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-music-control/android') 6 | include ':react-native-vector-icons' 7 | project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android') 8 | include ':react-native-gesture-handler' 9 | project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android') 10 | include ':react-native-video' 11 | project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android-exoplayer') 12 | 13 | include ':app' 14 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Listen1", 3 | "displayName": "Listen 1" 4 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const presets = ['module:metro-react-native-babel-preset']; 2 | const plugins = []; 3 | 4 | if (process.env.ENV === 'prod') { 5 | plugins.push('transform-remove-console'); 6 | } 7 | 8 | module.exports = { presets, plugins }; 9 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Mobile music app for multiple music platforms with play and search support. Current version supports Netease Music, QQ Music, and Xiami Music. Playlist management is also supported. Developed with React Native. Distributed under MIT license. 2 | 3 | Features: 4 | 5 | * One app for multiple music platforms 6 | * Search songs from multiple music platforms 7 | * Browse and play playlist on multiple music platforms 8 | * Add your favorite music to your own playlist 9 | * Night mode 10 | * Backup and restore (importing data from Listen1 chrome extension supported) 11 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Mobile music app for multiple music platforms with play and search support 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | Listen1 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/full_description.txt: -------------------------------------------------------------------------------- 1 | 一款支持多平台音乐播放和搜索的移动音乐 App。现有版本已支持网易云音乐,QQ 音乐,虾米音乐。还有丰富的歌单管理功能。使用 React Native 开发,基于 MIT 协议开源免费。 2 | 3 | 特性 4 | 5 | * 一个 App 播放多平台的音乐 6 | * 搜索多平台音乐 7 | * 浏览,播放多平台歌单 8 | * 收藏音乐到自建歌单 9 | * 夜间模式 10 | * 备份,恢复(支持从 Listen1 chrome extension 导入数据) 11 | -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/short_description.txt: -------------------------------------------------------------------------------- 1 | 一款支持多平台音乐播放和搜索的移动音乐 App 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import { AppRegistry } from 'react-native'; 6 | import App from './App'; 7 | import { name as appName } from './app.json'; 8 | 9 | AppRegistry.registerComponent(appName, () => App); 10 | -------------------------------------------------------------------------------- /ios/Listen1-tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UIViewControllerBasedStatusBarAppearance 38 | 39 | NSLocationWhenInUseUsageDescription 40 | 41 | NSAppTransportSecurity 42 | 43 | 44 | NSExceptionDomains 45 | 46 | localhost 47 | 48 | NSExceptionAllowsInsecureHTTPLoads 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /ios/Listen1-tvOSTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ios/Listen1.xcodeproj/xcshareddata/xcschemes/Listen1-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 77 | 78 | 79 | 80 | 81 | 82 | 92 | 94 | 100 | 101 | 102 | 103 | 104 | 105 | 111 | 113 | 119 | 120 | 121 | 122 | 124 | 125 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /ios/Listen1.xcodeproj/xcshareddata/xcschemes/Listen1.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 77 | 78 | 79 | 80 | 81 | 82 | 92 | 94 | 100 | 101 | 102 | 103 | 104 | 105 | 111 | 113 | 119 | 120 | 121 | 122 | 124 | 125 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /ios/Listen1/AppDelegate.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (nonatomic, strong) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /ios/Listen1/AppDelegate.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import "AppDelegate.h" 9 | 10 | #import 11 | #import 12 | #import 13 | 14 | @implementation AppDelegate 15 | 16 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 17 | { 18 | // allow play next song in background mode, refer to https://stackoverflow.com/questions/9660488/ios-avaudioplayer-doesnt-continue-to-next-song-while-in-background 19 | [[UIApplication sharedApplication] beginReceivingRemoteControlEvents]; 20 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; 21 | RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge 22 | moduleName:@"Listen1" 23 | initialProperties:nil]; 24 | 25 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; 26 | 27 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 28 | UIViewController *rootViewController = [UIViewController new]; 29 | rootViewController.view = rootView; 30 | self.window.rootViewController = rootViewController; 31 | [self.window makeKeyAndVisible]; 32 | return YES; 33 | } 34 | 35 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 36 | { 37 | #if DEBUG 38 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; 39 | #else 40 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 41 | #endif 42 | } 43 | 44 | @end 45 | -------------------------------------------------------------------------------- /ios/Listen1/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /ios/Listen1/Images.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/ios/Listen1/Images.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /ios/Listen1/Images.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/ios/Listen1/Images.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /ios/Listen1/Images.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/ios/Listen1/Images.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /ios/Listen1/Images.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/ios/Listen1/Images.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /ios/Listen1/Images.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/ios/Listen1/Images.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /ios/Listen1/Images.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/ios/Listen1/Images.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /ios/Listen1/Images.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/ios/Listen1/Images.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /ios/Listen1/Images.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/ios/Listen1/Images.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /ios/Listen1/Images.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/ios/Listen1/Images.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /ios/Listen1/Images.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/ios/Listen1/Images.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /ios/Listen1/Images.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/ios/Listen1/Images.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /ios/Listen1/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "40.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "60.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "29.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "58.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "87.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "80.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "120.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "57x57", 47 | "idiom" : "iphone", 48 | "filename" : "57.png", 49 | "scale" : "1x" 50 | }, 51 | { 52 | "size" : "57x57", 53 | "idiom" : "iphone", 54 | "filename" : "114.png", 55 | "scale" : "2x" 56 | }, 57 | { 58 | "size" : "60x60", 59 | "idiom" : "iphone", 60 | "filename" : "120.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "60x60", 65 | "idiom" : "iphone", 66 | "filename" : "180.png", 67 | "scale" : "3x" 68 | }, 69 | { 70 | "size" : "1024x1024", 71 | "idiom" : "ios-marketing", 72 | "filename" : "1024.png", 73 | "scale" : "1x" 74 | } 75 | ], 76 | "info" : { 77 | "version" : 1, 78 | "author" : "xcode" 79 | } 80 | } -------------------------------------------------------------------------------- /ios/Listen1/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ios/Listen1/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Listen 1 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | NSExceptionDomains 32 | 33 | localhost 34 | 35 | NSExceptionAllowsInsecureHTTPLoads 36 | 37 | 38 | 39 | 40 | NSLocationWhenInUseUsageDescription 41 | 42 | UIAppFonts 43 | 44 | AntDesign.ttf 45 | Entypo.ttf 46 | EvilIcons.ttf 47 | Feather.ttf 48 | FontAwesome.ttf 49 | FontAwesome5_Brands.ttf 50 | FontAwesome5_Regular.ttf 51 | FontAwesome5_Solid.ttf 52 | Fontisto.ttf 53 | Foundation.ttf 54 | Ionicons.ttf 55 | MaterialCommunityIcons.ttf 56 | MaterialIcons.ttf 57 | Octicons.ttf 58 | SimpleLineIcons.ttf 59 | Zocial.ttf 60 | 61 | UIBackgroundModes 62 | 63 | audio 64 | 65 | UILaunchStoryboardName 66 | LaunchScreen 67 | UIRequiredDeviceCapabilities 68 | 69 | armv7 70 | 71 | UISupportedInterfaceOrientations 72 | 73 | UIInterfaceOrientationPortrait 74 | UIInterfaceOrientationLandscapeLeft 75 | UIInterfaceOrientationLandscapeRight 76 | 77 | UIViewControllerBasedStatusBarAppearance 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /ios/Listen1/main.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ios/Listen1Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ios/Listen1Tests/Listen1Tests.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | #import 10 | 11 | #import 12 | #import 13 | 14 | #define TIMEOUT_SECONDS 600 15 | #define TEXT_TO_LOOK_FOR @"Welcome to React Native!" 16 | 17 | @interface Listen1Tests : XCTestCase 18 | 19 | @end 20 | 21 | @implementation Listen1Tests 22 | 23 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 24 | { 25 | if (test(view)) { 26 | return YES; 27 | } 28 | for (UIView *subview in [view subviews]) { 29 | if ([self findSubviewInView:subview matching:test]) { 30 | return YES; 31 | } 32 | } 33 | return NO; 34 | } 35 | 36 | - (void)testRendersWelcomeScreen 37 | { 38 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 39 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 40 | BOOL foundElement = NO; 41 | 42 | __block NSString *redboxError = nil; 43 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 44 | if (level >= RCTLogLevelError) { 45 | redboxError = message; 46 | } 47 | }); 48 | 49 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 50 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 51 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 52 | 53 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 54 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 55 | return YES; 56 | } 57 | return NO; 58 | }]; 59 | } 60 | 61 | RCTSetLogFunction(RCTDefaultLogFunction); 62 | 63 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 64 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 65 | } 66 | 67 | 68 | @end 69 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "listen1-mobile", 3 | "version": "0.8.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "react-native start", 7 | "start:android": "concurrently -r \"react-native start\" \"yarn start:android:no-packager\"", 8 | "start:android:no-packager": "react-native run-android --no-packager", 9 | "start:ios": "concurrently -r 'react-native start' 'yarn start:ios:no-packager'", 10 | "start:ios:no-packager": "react-native run-ios --no-packager", 11 | "link": "react-native link", 12 | "eslint": "eslint .", 13 | "test": "jest", 14 | "release": "react-native run-android --variant=release" 15 | }, 16 | "dependencies": { 17 | "@react-native-community/async-storage": "^1.12.1", 18 | "@react-native-community/toolbar-android": "^0.1.0-rc.2", 19 | "deprecated-react-native-listview": "^0.0.5", 20 | "md5": "^2.2.1", 21 | "query-string": "^6.8.1", 22 | "react": "16.8.3", 23 | "react-native": "0.59.9", 24 | "react-native-elements": "^1.1.0", 25 | "react-native-gesture-handler": "1.3.0", 26 | "react-native-iphone-x-helper": "^1.2.1", 27 | "react-native-music-control": "0.10.4", 28 | "react-native-root-toast": "^3.1.2", 29 | "react-native-screens": "^2.12.0", 30 | "react-native-scrollable-tab-view": "^0.10.0", 31 | "react-native-slider": "^0.11.0", 32 | "react-native-sortable-listview": "^0.2.8", 33 | "react-native-text-ticker": "^1.10.0", 34 | "react-native-vector-icons": "^6.6.0", 35 | "react-native-video": "^4.4.4", 36 | "react-navigation": "^3.11.0", 37 | "react-navigation-transitions": "^1.0.11", 38 | "react-redux": "^7.1.0", 39 | "redux": "^4.0.1", 40 | "redux-persist": "^5.10.0", 41 | "styled-components": "^4.3.2" 42 | }, 43 | "devDependencies": { 44 | "@babel/core": "^7.5.0", 45 | "@babel/runtime": "^7.5.0", 46 | "babel-eslint": "^10.0.1", 47 | "babel-jest": "^24.8.0", 48 | "babel-plugin-module-resolver": "^3.0.0", 49 | "babel-plugin-transform-remove-console": "^6.9.4", 50 | "concurrently": "^4.1.1", 51 | "eslint": "^5.16.0", 52 | "eslint-config-airbnb": "^17.1.0", 53 | "eslint-import-resolver-babel-module": "^5.1.0", 54 | "eslint-plugin-flowtype": "^3.10.3", 55 | "eslint-plugin-import": "^2.17.3", 56 | "eslint-plugin-jsx-a11y": "^6.2.1", 57 | "eslint-plugin-react": "^7.13.0", 58 | "eslint-plugin-react-native": "^3.7.0", 59 | "flow-bin": "^0.102.0", 60 | "jest": "^24.8.0", 61 | "metro-react-native-babel-preset": "^0.55.0", 62 | "react-test-renderer": "16.8.3" 63 | }, 64 | "jest": { 65 | "preset": "react-native" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/api/client.js: -------------------------------------------------------------------------------- 1 | import qq from './provider/qq'; 2 | import netease from './provider/netease'; 3 | import kugou from './provider/kugou'; 4 | import kuwo from './provider/kuwo'; 5 | // import bilibili from './provider/bilibili'; 6 | // import migu from './provider/migu'; 7 | 8 | const availableProvider = [netease, kugou, kuwo, qq]; 9 | 10 | const enabledProvider = availableProvider; 11 | 12 | function getPlatformArray() { 13 | return enabledProvider.map((provider) => provider.meta); 14 | } 15 | 16 | const prefix2provider = {}; 17 | const enName2NameDict = {}; 18 | 19 | enabledProvider.forEach((provider) => { 20 | prefix2provider[provider.meta.platformId] = provider; 21 | enName2NameDict[provider.meta.enName] = provider.meta.name; 22 | }); 23 | 24 | function getProviderByItemId(itemId) { 25 | const prefix = itemId.slice(0, 2); 26 | 27 | return prefix2provider[prefix]; 28 | } 29 | function getProviderName(enName) { 30 | return enName2NameDict[enName] || '暂未支持的平台'; 31 | } 32 | 33 | export default class Client { 34 | static getPlatformArray = getPlatformArray; 35 | static getProviderName = getProviderName; 36 | static showPlaylist(offset, platformId) { 37 | const provider = getProviderByItemId(platformId); 38 | 39 | return provider.showPlaylist(offset); 40 | } 41 | 42 | static search(keyword, page, platformId) { 43 | const provider = getProviderByItemId(platformId); 44 | 45 | return provider.search(keyword, page); 46 | } 47 | 48 | static getPlaylist(playlistId) { 49 | const provider = getProviderByItemId(playlistId); 50 | 51 | return provider.getPlaylist(playlistId); 52 | } 53 | 54 | static bootstrapTrack(trackId) { 55 | const provider = getProviderByItemId(trackId); 56 | 57 | if (provider === undefined) { 58 | return new Promise(() => { 59 | return ''; 60 | }); 61 | } 62 | 63 | return provider.bootstrapTrack(trackId); 64 | } 65 | 66 | static parseUrl(url) { 67 | let result = null; 68 | 69 | // eslint-disable-next-line consistent-return 70 | enabledProvider.forEach((provider) => { 71 | const r = provider.parseUrl(url); 72 | 73 | if (r !== null) { 74 | result = r; 75 | } 76 | }); 77 | 78 | return result; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/listen1/listen1_mobile/01ddf8ba1f39fda44320e777f39c83861a0a8dd2/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/components/flex.component.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | // notice: react native flex vs css flex is DIFFERENT for flex-grow, 4 | // flex-shrink default value 5 | // refer to: 6 | // https://github.com/styled-components/styled-components/issues/465 7 | export const Flex = styled.View` 8 | flex: 1 0; 9 | `; 10 | export const RowFlex = styled.View` 11 | flex: 1 0; 12 | flex-direction: row; 13 | `; 14 | export const ColumnFlex = styled.View` 15 | flex: 1 0; 16 | flex-direction: column; 17 | `; 18 | export const ThemeFlex = styled(Flex)` 19 | background-color: ${props => props.theme.backgroundColor}; 20 | `; 21 | -------------------------------------------------------------------------------- /src/components/image.component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Image } from 'react-native'; 3 | 4 | function parseImageUrl(url) { 5 | if (url.startsWith('http')) { 6 | return { uri: url }; 7 | } 8 | const d = { 9 | './assets/images/logo.png': require('../assets/images/logo.png'), 10 | }; 11 | 12 | return d[url]; 13 | } 14 | export class AImage extends React.PureComponent { 15 | props: { 16 | source: String, 17 | }; 18 | render() { 19 | const { source, ...props } = this.props; 20 | 21 | return ; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export * from './flex.component'; 2 | export * from './text.component'; 3 | export * from './touchable.component'; 4 | export * from './track-row.component'; 5 | export * from './modal-lite.component'; 6 | export * from './playlist-row.component'; 7 | export * from './image.component'; 8 | export * from './list-editor.component'; 9 | export * from './option-row.component'; 10 | export * from './table-cell-row.component'; 11 | -------------------------------------------------------------------------------- /src/components/modal-lite.component.js: -------------------------------------------------------------------------------- 1 | import { View, Animated, TouchableOpacity, Platform } from 'react-native'; 2 | import React from 'react'; 3 | 4 | export class ModalLite extends React.PureComponent { 5 | props: { 6 | modalHeight: Number, 7 | duration: Number, 8 | backgroundColor: String, 9 | children: Object, 10 | isVisible: Boolean, 11 | onOpened: Function, 12 | onClose: Function, 13 | }; 14 | state = { 15 | bounceValue: new Animated.Value(this.props.modalHeight), 16 | fadeAnim: new Animated.Value(0), 17 | isHidden: true, 18 | }; 19 | constructor(props) { 20 | super(props); 21 | if (this.props.isVisible) { 22 | this.state = { 23 | ...this.state, 24 | isHidden: false, 25 | }; 26 | } 27 | } 28 | 29 | componentDidMount() { 30 | if (!this.state.isHidden) { 31 | this.open(); 32 | } 33 | } 34 | componentDidUpdate(prevProps) { 35 | // On modal open request, we slide the view up and fade in the backdrop 36 | if (this.props.isVisible && !prevProps.isVisible) { 37 | this.open(); 38 | } else if (!this.props.isVisible && prevProps.isVisible) { 39 | // On modal close request, we slide the view down and fade out the backdrop 40 | this.close(); 41 | } 42 | } 43 | 44 | open() { 45 | let toPositionY = this.props.modalHeight; 46 | let toOpacity = 0; 47 | 48 | toPositionY = 0; 49 | toOpacity = 0.6; 50 | 51 | Animated.parallel([ 52 | Animated.timing(this.state.bounceValue, { 53 | toValue: toPositionY, 54 | duration: this.props.duration, 55 | // velocity: 3, 56 | // tension: 2, 57 | // friction: 8, 58 | }), 59 | Animated.timing(this.state.fadeAnim, { 60 | toValue: toOpacity, 61 | duration: this.props.duration, 62 | }), 63 | ]).start(() => { 64 | // trigger opened when open animation finished 65 | this.props.onOpened(); 66 | }); 67 | this.setState({ isHidden: false }); 68 | } 69 | 70 | close() { 71 | const toPositionY = this.props.modalHeight; 72 | const toOpacity = 0; 73 | 74 | Animated.parallel([ 75 | Animated.timing(this.state.bounceValue, { 76 | toValue: toPositionY, 77 | duration: this.props.duration, 78 | // velocity: 3, 79 | // tension: 2, 80 | // friction: 8, 81 | }), 82 | Animated.timing(this.state.fadeAnim, { 83 | toValue: toOpacity, 84 | duration: this.props.duration, 85 | }), 86 | ]).start(); 87 | this.setState({ isHidden: true }); 88 | } 89 | render() { 90 | return ( 91 | 102 | this.close()} 105 | style={{ 106 | position: 'absolute', 107 | top: 0, 108 | bottom: 0, 109 | left: 0, 110 | right: 0, 111 | backgroundColor: '#000', 112 | opacity: this.state.fadeAnim, 113 | elevation: Platform.OS === 'android' ? 50 : 0, 114 | }} 115 | > 116 | {this.state.isHidden ? null : ( 117 | { 120 | this.props.onClose(); 121 | }} 122 | style={{ flex: 1 }} 123 | /> 124 | )} 125 | 126 | 127 | 142 | {this.props.children} 143 | 144 | 145 | ); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/components/option-row.component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from 'react-native-vector-icons/MaterialIcons'; 3 | import styled, { withTheme } from 'styled-components'; 4 | import { PrimaryText } from './text.component'; 5 | import Client from '../api/client'; 6 | 7 | const ControlRow = styled.TouchableOpacity` 8 | flex: 1; 9 | flex-direction: row; 10 | border-top-width: 1px; 11 | border-top-color: ${props => props.theme.borderColor}; 12 | height: 50px; 13 | align-items: center; 14 | `; 15 | 16 | class OptionRowClass extends React.PureComponent { 17 | props: { 18 | item: Object, 19 | option: Object, 20 | onPress: Function, 21 | theme: Object, 22 | }; 23 | constructor(props) { 24 | super(props); 25 | this.onPress = this.onPress.bind(this); 26 | } 27 | onPress = () => { 28 | this.props.onPress({ action: this.props.option, item: this.props.item }); 29 | }; 30 | 31 | render() { 32 | // console.log(`render ${this.constructor.name}`); 33 | const { option } = this.props; 34 | 35 | return ( 36 | 37 | 43 | 44 | {option.title}{' '} 45 | {option.key === 'nav_artist' ? ` : ${this.props.item.artist}` : ''} 46 | {option.key === 'nav_album' ? ` : ${this.props.item.album}` : ''} 47 | {option.key === 'nav_source' 48 | ? ` : ${Client.getProviderName(this.props.item.source)}` 49 | : ''} 50 | 51 | 52 | ); 53 | } 54 | } 55 | export const OptionRow = withTheme(OptionRowClass); 56 | -------------------------------------------------------------------------------- /src/components/playlist-row.component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TouchableOpacity } from 'react-native'; 3 | import styled from 'styled-components'; 4 | import { RowFlex, ColumnFlex } from './flex.component'; 5 | import { PrimaryText } from './text.component'; 6 | import { AImage } from './image.component'; 7 | 8 | const MyPlaylistRow = styled(RowFlex)` 9 | height: 65; 10 | align-items: center; 11 | padding: 0 10px; 12 | `; 13 | const MyPlaylistCover = styled(AImage)` 14 | width: 50px; 15 | height: 50px; 16 | margin: 0 10px 0 5px; 17 | border-radius: 5px; 18 | `; 19 | const MyPlaylistTitle = styled(PrimaryText)` 20 | margin-bottom: 5px; 21 | `; 22 | 23 | export class PlaylistRow extends React.PureComponent { 24 | props: { 25 | item: Object, 26 | onPress: Function, 27 | }; 28 | constructor(props) { 29 | super(props); 30 | this.onPress = this.onPress.bind(this); 31 | } 32 | onPress = () => { 33 | this.props.onPress(this.props.item); 34 | }; 35 | render() { 36 | return ( 37 | 38 | 39 | 40 | 41 | {this.props.item.title} 42 | {/* 0首 */} 43 | 44 | 45 | 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/table-cell-row.component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TouchableOpacity } from 'react-native'; 3 | import styled, { withTheme } from 'styled-components'; 4 | import Icon from 'react-native-vector-icons/MaterialIcons'; 5 | import { RowFlex } from './flex.component'; 6 | import { PrimaryText } from './text.component'; 7 | 8 | const KeyField = styled(PrimaryText)` 9 | text-align: left; 10 | `; 11 | 12 | const SettingTouchableRow = styled(RowFlex)` 13 | height: 60px; 14 | justify-content: space-between; 15 | padding: 0 20px; 16 | align-items: center; 17 | border-bottom-width: 1px; 18 | border-bottom-color: ${props => props.theme.borderColor}; 19 | `; 20 | 21 | class TableCellRowClass extends React.PureComponent { 22 | props: { 23 | onPress: Function, 24 | title: String, 25 | theme: Object, 26 | style: Object, 27 | }; 28 | render() { 29 | return ( 30 | 34 | 35 | {this.props.title} 36 | 37 | 42 | 43 | 44 | ); 45 | } 46 | } 47 | 48 | export const TableCellRow = withTheme(TableCellRowClass); 49 | -------------------------------------------------------------------------------- /src/components/text.component.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const PrimaryText = styled.Text` 4 | color: ${props => props.theme.primaryColor}; 5 | `; 6 | 7 | export const SecondaryText = styled.Text` 8 | color: ${props => props.theme.secondaryColor}; 9 | `; 10 | -------------------------------------------------------------------------------- /src/components/touchable.component.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const CloseTouchable = styled.TouchableOpacity` 4 | background-color: transparent; 5 | flex: 0 50px; 6 | align-items: center; 7 | justify-content: center; 8 | border-top-width: 1px; 9 | border-top-color: ${props => props.theme.borderColor}; 10 | `; 11 | -------------------------------------------------------------------------------- /src/components/track-row.component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TouchableOpacity } from 'react-native'; 3 | import styled, { withTheme } from 'styled-components'; 4 | import Icon from 'react-native-vector-icons/MaterialIcons'; 5 | 6 | const PlaylistItem = styled(TouchableOpacity)` 7 | padding-left: 10; 8 | padding-right: 10; 9 | padding-top: 7; 10 | padding-bottom: 7; 11 | flex-direction: row; 12 | height: 50px; 13 | `; 14 | const PlaylistInfo = styled.View` 15 | flex-direction: column; 16 | flex: 1; 17 | margin-left: 10px; 18 | `; 19 | const PlaylistControl = styled.TouchableOpacity` 20 | flex: 0 40px; 21 | align-items: center; 22 | justify-content: center; 23 | `; 24 | const PlaylistStatus = styled.TouchableOpacity` 25 | flex: 0 20px; 26 | align-items: center; 27 | justify-content: center; 28 | `; 29 | 30 | const PlaylistItemSongTitle = styled.Text` 31 | font-size: 14; 32 | color: ${(props) => props.theme.primaryColor}; 33 | overflow: hidden; 34 | `; 35 | const PlaylistItemSongInfo = styled.Text` 36 | font-size: 12; 37 | color: ${(props) => props.theme.secondaryColor}; 38 | overflow: hidden; 39 | `; 40 | 41 | export class TrackRowClass extends React.PureComponent { 42 | props: { 43 | item: Object, 44 | onPress: Function, 45 | theme: Object, 46 | iconType: String, 47 | onPressIcon: Function, 48 | isPlaying: Boolean, 49 | }; 50 | constructor(props) { 51 | super(props); 52 | this.onPress = this.onPress.bind(this); 53 | this.onPressIcon = this.onPressIcon.bind(this); 54 | } 55 | onPress = () => { 56 | this.props.onPress(this.props.item); 57 | }; 58 | onPressIcon = () => { 59 | this.props.onPressIcon(this.props.item); 60 | }; 61 | getStyle = () => { 62 | if (this.props.isPlaying) { 63 | return { color: this.props.theme.playingColor }; 64 | } 65 | if (this.props.item.disabled) { 66 | return { color: this.props.theme.disableColor }; 67 | } 68 | 69 | return {}; 70 | }; 71 | render() { 72 | const textStyle = this.getStyle(); 73 | 74 | return ( 75 | 76 | {this.props.isPlaying ? ( 77 | 78 | 83 | 84 | ) : null} 85 | 86 | 87 | {this.props.item.title} 88 | 89 | 90 | {this.props.item.artist} - {this.props.item.album} 91 | 92 | 93 | 94 | 99 | 100 | 101 | ); 102 | } 103 | } 104 | export const TrackRow = withTheme(TrackRowClass); 105 | -------------------------------------------------------------------------------- /src/config/colors.js: -------------------------------------------------------------------------------- 1 | export const colors = { 2 | lightBlue: '#3498db', 3 | blue: '#0366d6', 4 | black: '#000000', 5 | primaryDark: '#262626', 6 | grey: '#bbb', 7 | greyDarkest: '#808080', 8 | greyBlue: '#6a737d', 9 | greyDark: '#999', 10 | greyMid: '#d1d5da', 11 | greyLight: '#f1f1f1', 12 | greyMidLight: '#f6f8fa', 13 | greyVeryLight: '#fbfbfb', 14 | lightGreen: '#00FF00', 15 | lighterBoldGreen: '#359d68', 16 | green: '#2cbe4e', 17 | darkGreen: '#27ae60', 18 | red: '#ee0701', 19 | darkRed: '#e74c3c', 20 | darkerRed: '#da0000', 21 | white: '#ffffff', 22 | transparent: 'transparent', 23 | codeChunkBlue: '#f8f8ff', 24 | codeChunkLineNumberBlue: '#f3f3ff', 25 | addCodeGreen: '#eaffea', 26 | addCodeLineNumberGreen: '#DBFFD6', 27 | delCodeRed: '#ffecec', 28 | delCodeLineNumberRed: '#ffdddd', 29 | lightPurple: '#bf54eb', 30 | purple: '#8e44ad', 31 | orange: '#e67e22', 32 | githubDark: '#1f2327', 33 | alabaster: '#f7f7f7', 34 | topicLightBlue: '#f1f8ff', 35 | theme: '#D43C33', 36 | nicewhite: '#f2f2f2', 37 | niceblack: '#666666', 38 | backgroundBlack: '#111111', 39 | windowBlack: '#171717', 40 | primaryBlack: '#7E7E7E', 41 | secondaryBlack: '#5a5a5a', 42 | inactiveGrey: '#393939', 43 | disableGreyInWhite: '#cfcfcf', 44 | secondaryWhite: '#666666', 45 | disableGreyInBlack: '#202020', 46 | borderWhite: '#eaeaea', 47 | borderBlack: '#2b2b2b', 48 | heartRed: '#cd2929', 49 | nowPlayingHighlight: '#ff4444', 50 | thirdWhite: '#afafaf', 51 | thirdBlack: '#555555', 52 | }; 53 | -------------------------------------------------------------------------------- /src/config/settings.js: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native'; 2 | import { 3 | getBottomSpace, 4 | getStatusBarHeight, 5 | } from 'react-native-iphone-x-helper'; 6 | import { colors } from './colors'; 7 | 8 | const defaultFontSize = 14; 9 | const defaultFontColor = colors.black; 10 | 11 | export const miniPlayerSetting = { 12 | height: 50, 13 | paddingBottom: getBottomSpace(), 14 | titleFontSize: defaultFontSize, 15 | subtitleFontSize: 12, 16 | }; 17 | 18 | export const playlistSetting = { 19 | briefTitleFontSize: 12, 20 | briefTitleColor: defaultFontColor, 21 | }; 22 | 23 | export const modalPlayerSetting = { 24 | paddingTop: Platform.OS === 'ios' ? getStatusBarHeight() : 0, 25 | paddingBottom: getBottomSpace(), 26 | }; 27 | 28 | export const animationSetting = { 29 | transitionTime: 200, 30 | }; 31 | -------------------------------------------------------------------------------- /src/config/theme.js: -------------------------------------------------------------------------------- 1 | import { colors } from './colors'; 2 | 3 | export const whiteTheme = { 4 | barStyle: 'dark-content', 5 | backgroundColor: colors.white, 6 | windowColor: colors.nicewhite, 7 | primaryColor: colors.black, 8 | secondaryColor: colors.secondaryWhite, 9 | thirdColor: colors.thirdWhite, 10 | inactiveColor: colors.inactiveGrey, 11 | disableColor: colors.disableGreyInWhite, 12 | borderColor: colors.borderWhite, 13 | playingColor: colors.nowPlayingHighlight, 14 | }; 15 | 16 | export const blackTheme = { 17 | barStyle: 'light-content', 18 | backgroundColor: colors.backgroundBlack, 19 | windowColor: colors.windowBlack, 20 | primaryColor: colors.primaryBlack, 21 | secondaryColor: colors.secondaryBlack, 22 | thirdColor: colors.thirdBlack, 23 | inactiveColor: colors.inactiveGrey, 24 | disableColor: colors.disableGreyInBlack, 25 | borderColor: colors.borderBlack, 26 | playingColor: colors.nowPlayingHighlight, 27 | }; 28 | -------------------------------------------------------------------------------- /src/modules/crypto.js: -------------------------------------------------------------------------------- 1 | import CryptoJS from '../../vendor/cryptojs_aes'; 2 | import JSEncrypt from '../../vendor/jsencrypt'; 3 | 4 | const aesEncrypt = (text, key, iv) => { 5 | const keyutf = CryptoJS.enc.Utf8.parse(key); 6 | const ivBin = CryptoJS.enc.Utf8.parse(iv); 7 | const enc = CryptoJS.AES.encrypt(text, keyutf, { 8 | iv: ivBin, 9 | mode: CryptoJS.mode.CBC, 10 | }); 11 | const encStr = enc.toString(); 12 | 13 | return encStr; 14 | // return CryptoJS.enc.Hex.stringify(CryptoJS.enc.Base64.parse(encStr)); 15 | }; 16 | 17 | const rsaEncrypt = (text, key) => { 18 | const rsaEncryptObject = new JSEncrypt({ padding: 'RSA_ZERO_PADDING' }); 19 | 20 | rsaEncryptObject.setPublicKey(key); 21 | const secKey = rsaEncryptObject.encrypt(text); 22 | const buf = CryptoJS.enc.Base64.parse(secKey); 23 | 24 | return CryptoJS.enc.Hex.stringify(buf); 25 | }; 26 | 27 | function getRandomBase62(length) { 28 | const chars = 29 | 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 30 | let result = ''; 31 | 32 | for (let i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)]; 33 | 34 | return result; 35 | } 36 | function reverseString(str) { 37 | return str 38 | .split('') 39 | .reverse() 40 | .join(''); 41 | } 42 | 43 | export const weapi = object => { 44 | const iv = '0102030405060708'; 45 | const presetKey = '0CoJUm6Qyw8W8jud'; 46 | const publicKey = 47 | '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\n-----END PUBLIC KEY-----'; 48 | 49 | const text = JSON.stringify(object); 50 | const secretKey = getRandomBase62(16); 51 | 52 | // const secretKey = "1234123412341234"; 53 | return { 54 | params: aesEncrypt(aesEncrypt(text, presetKey, iv), secretKey, iv), 55 | encSecKey: rsaEncrypt(reverseString(secretKey), publicKey), 56 | }; 57 | }; 58 | // module.exports = weapi; 59 | // console.log(weapi({name:'secret'})) 60 | -------------------------------------------------------------------------------- /src/modules/encrypt_test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable no-shadow */ 3 | /* eslint-disable no-param-reassign */ 4 | /* eslint-disable no-unused-vars */ 5 | const crypto = require('crypto'); 6 | 7 | const iv = Buffer.from('0102030405060708'); 8 | const presetKey = Buffer.from('0CoJUm6Qyw8W8jud'); 9 | const linuxapiKey = Buffer.from('rFgB&h#%2?^eDg:Q'); 10 | const base62 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 11 | const publicKey = 12 | '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\n-----END PUBLIC KEY-----'; 13 | const eapiKey = 'e82ckenh8dichen8'; 14 | 15 | const aesEncrypt = (buffer, mode, key, iv) => { 16 | const cipher = crypto.createCipheriv(`aes-128-${mode}`, key, iv); 17 | 18 | return Buffer.concat([cipher.update(buffer), cipher.final()]); 19 | }; 20 | 21 | const rsaEncrypt = (buffer, key) => { 22 | buffer = Buffer.concat([Buffer.alloc(128 - buffer.length), buffer]); 23 | 24 | return crypto.publicEncrypt( 25 | { key, padding: crypto.constants.RSA_NO_PADDING }, 26 | buffer 27 | ); 28 | }; 29 | 30 | const weapi = object => { 31 | const text = JSON.stringify(object); 32 | const secretKey = crypto 33 | .randomBytes(16) 34 | .map(n => base62.charAt(n % 62).charCodeAt()); 35 | 36 | // const secretKey = Buffer.from('1234123412341234'); 37 | return { 38 | params: aesEncrypt( 39 | Buffer.from( 40 | aesEncrypt(Buffer.from(text), 'cbc', presetKey, iv).toString('base64') 41 | ), 42 | 'cbc', 43 | secretKey, 44 | iv 45 | ).toString('base64'), 46 | encSecKey: rsaEncrypt(secretKey.reverse(), publicKey).toString('hex'), 47 | }; 48 | }; 49 | 50 | function f1(text) { 51 | // console.log(weapi({name: 'secret'})) 52 | const result = aesEncrypt(Buffer.from(text), 'cbc', presetKey, iv).toString( 53 | 'hex' 54 | ); 55 | 56 | console.log(result); 57 | 58 | return result; 59 | } 60 | 61 | function f2(text) { 62 | const CryptoJS = require('../../vendor/cryptojs_aes'); 63 | const key = '0CoJUm6Qyw8W8jud'; 64 | const keyutf = CryptoJS.enc.Utf8.parse(key); 65 | const iv = CryptoJS.enc.Utf8.parse('0102030405060708'); 66 | const enc = CryptoJS.AES.encrypt(text, keyutf, { 67 | iv, 68 | mode: CryptoJS.mode.CBC, 69 | }); 70 | const encStr = enc.toString(); 71 | 72 | console.log(CryptoJS.enc.Hex.stringify(CryptoJS.enc.Base64.parse(encStr))); 73 | } 74 | 75 | function f11(text) { 76 | const result = rsaEncrypt(Buffer.from(text), publicKey).toString('hex'); 77 | 78 | console.log(result); 79 | } 80 | 81 | function f22(text) { 82 | const CryptoJS = require('../../vendor/cryptojs_aes'); 83 | const JSEncrypt = require('../../vendor/jsencrypt'); 84 | const k = text; 85 | const rsaEncrypt = new JSEncrypt({ padding: 'RSA_ZERO_PADDING' }); 86 | const publicKey = 87 | '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\n-----END PUBLIC KEY-----'; 88 | 89 | rsaEncrypt.setPublicKey(publicKey); 90 | const secKey = rsaEncrypt.encrypt(k); 91 | // console.log(secKey); 92 | const buf = CryptoJS.enc.Base64.parse(secKey); 93 | 94 | console.log(CryptoJS.enc.Hex.stringify(buf)); 95 | } 96 | 97 | // f1('test'); 98 | // f2('test'); 99 | // f11('test'); 100 | // f22('test'); 101 | console.log('weapi'); 102 | console.log(weapi({ name: 'secret' })); 103 | const weapi2 = require('./crypto'); 104 | 105 | console.log('weapi2'); 106 | console.log(weapi2({ name: 'secret' })); 107 | -------------------------------------------------------------------------------- /src/modules/state-json-convert.js: -------------------------------------------------------------------------------- 1 | import { defaultPlayerState } from '../redux/player.reducer'; 2 | import { defaultmyPlaylistState } from '../redux/myplaylist.reducer'; 3 | 4 | function deepCopy(oldObject) { 5 | return JSON.parse(JSON.stringify(oldObject)); 6 | } 7 | 8 | export default class StateJsonConvert { 9 | static getJson(state) { 10 | const { playerState, myPlaylistState } = state; 11 | 12 | const nowPlayingTrackId = 13 | playerState.nowPlayingTrack === undefined 14 | ? '-1' 15 | : playerState.nowPlayingTrack.id; 16 | const result = { 17 | // TODO: support language setting 18 | language: 'zh_CN', 19 | 'player-settings': { 20 | playmode: playerState.playMode, 21 | nowplaying_track_id: nowPlayingTrackId, 22 | volume: 100, 23 | }, 24 | 'current-playing': playerState.tracks, 25 | playerlists: myPlaylistState.playlists.map(i => i.id), 26 | }; 27 | 28 | myPlaylistState.playlists.forEach(i => { 29 | const playlist = myPlaylistState.myPlaylistDict[i.id]; 30 | 31 | // fix local image url 32 | if (!playlist.info.cover_img_url.startsWith('http')) { 33 | playlist.info.cover_img_url = 'images/mycover.jpg'; 34 | } 35 | result[i.id] = { ...playlist, is_mine: 1 }; 36 | }); 37 | 38 | return result; 39 | } 40 | static getState(jsonData) { 41 | const playerState = deepCopy(defaultPlayerState); 42 | const myPlaylistState = deepCopy(defaultmyPlaylistState); 43 | 44 | playerState.tracks = jsonData['current-playing']; 45 | 46 | jsonData.playerlists.forEach(playlistId => { 47 | const playlist = jsonData[playlistId]; 48 | 49 | // fix local image url 50 | if (!playlist.info.cover_img_url.startsWith('http')) { 51 | playlist.info.cover_img_url = './assets/images/logo.png'; 52 | } 53 | if (playlistId !== 'myplaylist_favorite') { 54 | myPlaylistState.playlists.push(playlist.info); 55 | } else { 56 | // load favorite ids 57 | jsonData[playlistId].tracks.forEach(track => { 58 | myPlaylistState.myFavoriteIds[track.id] = 1; 59 | }); 60 | } 61 | myPlaylistState.myPlaylistDict[playlistId] = playlist; 62 | }); 63 | 64 | return { playerState, myPlaylistState }; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/modules/toast.js: -------------------------------------------------------------------------------- 1 | import Toast from 'react-native-root-toast'; 2 | 3 | export function showToast(message) { 4 | Toast.show(message, { 5 | duration: Toast.durations.SHORT, 6 | position: Toast.positions.CENTER, 7 | shadow: true, 8 | animation: true, 9 | hideOnPress: true, 10 | delay: 0, 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/redux/actions.js: -------------------------------------------------------------------------------- 1 | export const TYPE = { 2 | PLAY_TRACK: 'PLAY_TRACK', 3 | TOGGLE_PLAY: 'TOGGLE_PLAY', 4 | UPDATE_PLAYER: 'UPDATE_PLAYER', 5 | TOGGLE_MODAL: 'TOGGLE_MODAL', 6 | OPEN_MODEL_LITE: 'OPEN_MODEL_LITE', 7 | CLOSE_MODEL_LITE: 'CLOSE_MODEL_LITE', 8 | PREV_TRACK: 'PREV_TRACK', 9 | NEXT_TRACK: 'NEXT_TRACK', 10 | PLAY_TRACKS: 'PLAY_TRACKS', 11 | PLAY_TRACK_IN_PLAYLIST: 'PLAY_TRACK_IN_PLAYLIST', 12 | ADD_NEXT_TRACK: 'ADD_NEXT_TRACK', 13 | LOAD_FAIL: 'LOAD_FAIL', 14 | REMOVE_TRACK: 'REMOVE_TRACK', 15 | SEARCH: 'SEARCH', 16 | PLAY: 'PLAY', 17 | PAUSE: 'PAUSE', 18 | CHANGE_THEME: 'CHANGE_THEME', 19 | CHANGE_PLAY_MODE: 'CHANGE_PLAY_MODE', 20 | CREATE_MY_PLAYLIST: 'CREATE_MY_PLAYLIST', 21 | SAVE_MY_PLAYLISTS: 'SAVE_MY_PLAYLISTS', 22 | EDIT_MY_PLAYLIST: 'EDIT_MY_PLAYLIST', 23 | ADD_TO_MY_PLAYLIST: 'ADD_TO_MY_PLAYLIST', 24 | ADD_TO_MY_FAVORITE: 'ADD_TO_MY_FAVORITE', 25 | REMOVE_FROM_MY_FAVORITE: 'REMOVE_FROM_MY_FAVORITE', 26 | RECOVER_DATA: 'RECOVER_DATA', 27 | }; 28 | 29 | export const playTrack = track => ({ 30 | type: TYPE.PLAY_TRACK, 31 | track, 32 | }); 33 | 34 | export const togglePlay = () => ({ 35 | type: TYPE.TOGGLE_PLAY, 36 | }); 37 | 38 | export const play = () => ({ 39 | type: TYPE.PLAY, 40 | }); 41 | 42 | export const pause = () => ({ 43 | type: TYPE.PAUSE, 44 | }); 45 | 46 | export const updatePlayer = next => ({ 47 | type: TYPE.UPDATE_PLAYER, 48 | next, 49 | }); 50 | 51 | export const toggleModal = () => ({ 52 | type: TYPE.TOGGLE_MODAL, 53 | }); 54 | 55 | export const openModalLite = payload => ({ 56 | type: TYPE.OPEN_MODEL_LITE, 57 | payload, 58 | }); 59 | 60 | export const closeModalLite = () => ({ 61 | type: TYPE.CLOSE_MODEL_LITE, 62 | }); 63 | 64 | export const prevTrack = () => ({ 65 | type: TYPE.PREV_TRACK, 66 | }); 67 | 68 | export const nextTrack = () => ({ 69 | type: TYPE.NEXT_TRACK, 70 | }); 71 | 72 | export const addNextTrack = payload => ({ 73 | type: TYPE.ADD_NEXT_TRACK, 74 | payload, 75 | }); 76 | export const removeTrack = payload => ({ 77 | type: TYPE.REMOVE_TRACK, 78 | payload, 79 | }); 80 | export const playTracks = tracks => ({ 81 | type: TYPE.PLAY_TRACKS, 82 | tracks, 83 | }); 84 | export const playTrackInPlaylist = payload => ({ 85 | type: TYPE.PLAY_TRACK_IN_PLAYLIST, 86 | payload, 87 | }); 88 | export const loadFail = () => ({ 89 | type: TYPE.LOAD_FAIL, 90 | }); 91 | export const search = text => ({ 92 | type: TYPE.SEARCH, 93 | text, 94 | }); 95 | export const changeTheme = theme => ({ 96 | type: TYPE.CHANGE_THEME, 97 | theme, 98 | }); 99 | export const changePlayMode = () => ({ 100 | type: TYPE.CHANGE_PLAY_MODE, 101 | }); 102 | export const createMyPlaylist = playlist => ({ 103 | type: TYPE.CREATE_MY_PLAYLIST, 104 | playlist, 105 | }); 106 | export const saveMyPlaylists = playlists => ({ 107 | type: TYPE.SAVE_MY_PLAYLISTS, 108 | playlists, 109 | }); 110 | export const editMyPlaylist = playlist => ({ 111 | type: TYPE.EDIT_MY_PLAYLIST, 112 | playlist, 113 | }); 114 | export const addToMyPlaylist = payload => ({ 115 | type: TYPE.ADD_TO_MY_PLAYLIST, 116 | payload, 117 | }); 118 | export const addToMyFavorite = payload => ({ 119 | type: TYPE.ADD_TO_MY_FAVORITE, 120 | payload, 121 | }); 122 | export const removeFromMyFavorite = payload => ({ 123 | type: TYPE.REMOVE_FROM_MY_FAVORITE, 124 | payload, 125 | }); 126 | export const recoverData = payload => ({ 127 | type: TYPE.RECOVER_DATA, 128 | payload, 129 | }); 130 | -------------------------------------------------------------------------------- /src/redux/myplaylist.reducer.js: -------------------------------------------------------------------------------- 1 | import { TYPE } from './actions'; 2 | 3 | function guid() { 4 | function s4() { 5 | return Math.floor((1 + Math.random()) * 0x10000) 6 | .toString(16) 7 | .substring(1); 8 | } 9 | 10 | return `${s4() + s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; 11 | } 12 | 13 | const myFavoritePlaylistInfo = { 14 | id: 'myplaylist_favorite', 15 | title: '我喜欢的音乐', 16 | cover_img_url: './assets/images/logo.png', 17 | }; 18 | 19 | const myFavoritePlaylist = { 20 | info: myFavoritePlaylistInfo, 21 | tracks: [], 22 | }; 23 | 24 | function handleAddToMyPlaylist(state, action) { 25 | let newTracks = []; 26 | 27 | if ( 28 | state.myPlaylistDict[action.payload.playlist.id].tracks.filter( 29 | i => i.id === action.payload.track.id 30 | ).length > 0 31 | ) { 32 | return state; 33 | } 34 | newTracks = [ 35 | ...state.myPlaylistDict[action.payload.playlist.id].tracks, 36 | action.payload.track, 37 | ]; 38 | 39 | if (action.payload.playlist.id === myFavoritePlaylistInfo.id) { 40 | return { 41 | ...state, 42 | myFavoriteIds: { ...state.myFavoriteIds, [action.payload.track.id]: 1 }, 43 | myPlaylistDict: { 44 | ...state.myPlaylistDict, 45 | [action.payload.playlist.id]: { 46 | ...state.myPlaylistDict[action.payload.playlist.id], 47 | tracks: newTracks, 48 | }, 49 | }, 50 | }; 51 | } 52 | 53 | return { 54 | ...state, 55 | myPlaylistDict: { 56 | ...state.myPlaylistDict, 57 | [action.payload.playlist.id]: { 58 | ...state.myPlaylistDict[action.payload.playlist.id], 59 | tracks: newTracks, 60 | }, 61 | }, 62 | }; 63 | } 64 | 65 | function handleAddToMyFavorite(state, action) { 66 | const { payload: track } = action; 67 | 68 | // console.log(state.myFavoriteIds, track); 69 | 70 | if (state.myFavoriteIds[track.id] !== undefined) { 71 | // track already in favorite 72 | return state; 73 | } 74 | 75 | if (state.myPlaylistDict[myFavoritePlaylistInfo.id] === undefined) { 76 | // new created my favorite 77 | return { 78 | ...state, 79 | myFavoriteIds: { [track.id]: 1 }, 80 | playlists: [myFavoritePlaylistInfo, ...state.playlists], 81 | myPlaylistDict: { 82 | ...state.myPlaylistDict, 83 | [myFavoritePlaylistInfo.id]: { 84 | info: myFavoritePlaylistInfo, 85 | tracks: [track], 86 | }, 87 | }, 88 | }; 89 | } 90 | 91 | return handleAddToMyPlaylist(state, { 92 | payload: { track, playlist: { id: myFavoritePlaylistInfo.id } }, 93 | }); 94 | } 95 | 96 | function handleRemoveFromMyFavorite(state, action) { 97 | const { payload: track } = action; 98 | 99 | if (state.myFavoriteIds[track.id] === undefined) { 100 | // track already not in favorite 101 | return state; 102 | } 103 | 104 | const newTracks = state.myPlaylistDict[ 105 | myFavoritePlaylist.info.id 106 | ].tracks.filter(i => i.id !== track.id); 107 | 108 | return { 109 | ...state, 110 | myFavoriteIds: { ...state.myFavoriteIds, [track.id]: undefined }, 111 | myPlaylistDict: { 112 | ...state.myPlaylistDict, 113 | [myFavoritePlaylist.info.id]: { 114 | info: myFavoritePlaylist.info, 115 | tracks: newTracks, 116 | }, 117 | }, 118 | }; 119 | } 120 | 121 | export const defaultmyPlaylistState = { 122 | myFavoriteIds: {}, 123 | playlists: [myFavoritePlaylistInfo], 124 | myPlaylistDict: { 125 | [myFavoritePlaylist.info.id]: myFavoritePlaylist, 126 | }, 127 | }; 128 | 129 | export const myPlaylistReducer = (state = defaultmyPlaylistState, action) => { 130 | let newPlaylist; 131 | let currentKeys; 132 | let newMyPlaylistDict; 133 | let newMyFavDict; 134 | 135 | switch (action.type) { 136 | case TYPE.CREATE_MY_PLAYLIST: 137 | newPlaylist = { ...action.playlist }; 138 | 139 | newPlaylist.info.id = `myplaylist_${guid()}`; 140 | 141 | return { 142 | ...state, 143 | playlists: [...state.playlists, newPlaylist.info], 144 | myPlaylistDict: { 145 | ...state.myPlaylistDict, 146 | [newPlaylist.info.id]: newPlaylist, 147 | }, 148 | }; 149 | case TYPE.SAVE_MY_PLAYLISTS: 150 | currentKeys = action.playlists.map(i => i.id); 151 | 152 | newMyPlaylistDict = { 153 | [myFavoritePlaylist.info.id]: 154 | state.myPlaylistDict[myFavoritePlaylist.info.id], 155 | }; 156 | currentKeys.forEach(k => { 157 | newMyPlaylistDict[k] = state.myPlaylistDict[k]; 158 | }); 159 | 160 | return { 161 | ...state, 162 | myPlaylistDict: newMyPlaylistDict, 163 | playlists: [ 164 | state.myPlaylistDict[myFavoritePlaylist.info.id].info, 165 | ...action.playlists, 166 | ], 167 | }; 168 | case TYPE.EDIT_MY_PLAYLIST: 169 | if (action.playlist.info.id === myFavoritePlaylistInfo.id) { 170 | currentKeys = action.playlist.tracks.map(i => i.id); 171 | newMyFavDict = {}; 172 | currentKeys.forEach(k => { 173 | newMyFavDict[k] = 1; 174 | }); 175 | } else { 176 | newMyFavDict = state.myFavoriteIds; 177 | } 178 | 179 | return { 180 | ...state, 181 | myFavoriteIds: newMyFavDict, 182 | myPlaylistDict: { 183 | ...state.myPlaylistDict, 184 | [action.playlist.info.id]: { ...action.playlist }, 185 | }, 186 | }; 187 | case TYPE.ADD_TO_MY_PLAYLIST: 188 | return handleAddToMyPlaylist(state, action); 189 | case TYPE.ADD_TO_MY_FAVORITE: 190 | return handleAddToMyFavorite(state, action); 191 | case TYPE.REMOVE_FROM_MY_FAVORITE: 192 | return handleRemoveFromMyFavorite(state, action); 193 | case TYPE.RECOVER_DATA: 194 | return { ...action.payload.myPlaylistState }; 195 | default: 196 | return state; 197 | } 198 | }; 199 | -------------------------------------------------------------------------------- /src/redux/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import { TYPE } from './actions'; 4 | import { myPlaylistReducer } from './myplaylist.reducer'; 5 | import { playerReducer } from './player.reducer'; 6 | 7 | const modalReducer = ( 8 | state = { 9 | isOpen: false, 10 | isLiteOpen: false, 11 | liteHeight: 500, 12 | liteType: '', 13 | item: {}, 14 | }, 15 | action 16 | ) => { 17 | switch (action.type) { 18 | case TYPE.TOGGLE_MODAL: 19 | return { ...state, isOpen: !state.isOpen }; 20 | case TYPE.OPEN_MODEL_LITE: 21 | return { 22 | ...state, 23 | isLiteOpen: true, 24 | liteHeight: action.payload.height || 500, 25 | liteType: action.payload.type || '', 26 | item: action.payload.item || {}, 27 | }; 28 | case TYPE.CLOSE_MODEL_LITE: 29 | return { ...state, isLiteOpen: false }; 30 | default: 31 | return state; 32 | } 33 | }; 34 | 35 | const settingReducer = ( 36 | state = { language: 'zh_CN', theme: 'white' }, 37 | action 38 | ) => { 39 | switch (action.type) { 40 | case TYPE.CHANGE_THEME: 41 | return { ...state, theme: action.theme }; 42 | default: 43 | return state; 44 | } 45 | }; 46 | 47 | const searchReducer = (state = { text: '' }, action) => { 48 | switch (action.type) { 49 | case TYPE.SEARCH: 50 | return { ...state, text: action.text }; 51 | default: 52 | return state; 53 | } 54 | }; 55 | 56 | export default combineReducers({ 57 | playerState: playerReducer, 58 | settingState: settingReducer, 59 | searchState: searchReducer, 60 | modalState: modalReducer, 61 | myPlaylistState: myPlaylistReducer, 62 | }); 63 | -------------------------------------------------------------------------------- /src/utils/kugouUtils.js: -------------------------------------------------------------------------------- 1 | import md5 from 'md5'; 2 | import CryptoJS from '../../vendor/cryptojs_aes'; 3 | 4 | const reg = new RegExp('-', 'g'); 5 | 6 | export function getConnectUrl() { 7 | var e = h(), 8 | t = 4, 9 | n = parseInt(new Date().getTime() / 1e3), 10 | i = S(), 11 | r = 0, 12 | a = guid().replace(reg, ''), 13 | o = '', 14 | s = '', 15 | c = { 16 | appid: e, 17 | platid: t, 18 | clientver: 0, 19 | clienttime: n, 20 | signature: '', 21 | mid: i, 22 | uuid: a, 23 | userid: r, 24 | dfid: o, 25 | 'p.token': s, 26 | }; 27 | c.signature = signatureParam(c, e); 28 | const bodyParam = JSON.stringify({uuid: a}); 29 | const d = CryptoJS.enc.Utf8.parse(bodyParam); 30 | const u = CryptoJS.enc.Base64.stringify(d); 31 | return { 32 | url: 33 | 'https://userservice.kugou.com/risk/v1/r_query_collect?appid=' + 34 | c.appid + 35 | '&platid=' + 36 | c.platid + 37 | '&clientver=' + 38 | c.clientver + 39 | '&clienttime=' + 40 | c.clienttime + 41 | '&signature=' + 42 | c.signature + 43 | '&mid=' + 44 | c.mid + 45 | '&userid=' + 46 | c.userid + 47 | '&uuid=' + 48 | c.uuid + 49 | '&dfid=' + 50 | c.dfid + 51 | '&p.token=' + 52 | c['p.token'], 53 | body: u, 54 | }; 55 | } 56 | export function getTokenUrl() { 57 | const n = h(), 58 | i = 4, 59 | r = parseInt(new Date().getTime() / 1e3), 60 | a = S(), 61 | o = 0, 62 | c = ''; 63 | //kg_mid a 64 | //kg_dfid 65 | //kg_dfid_collect md5(r) 66 | const u = { 67 | appid: n, 68 | platid: i, 69 | clientver: 0, 70 | clienttime: r, 71 | signature: '', 72 | mid: a, 73 | uuid: guid().replace(reg, ''), 74 | userid: o, 75 | 'p.token': c, 76 | }; 77 | u.signature = signatureParam(u, n); 78 | return { 79 | collect: md5(r), 80 | mid: a, 81 | url: 82 | 'https://userservice.kugou.com/risk/v1/r_register_dev?appid=' + 83 | u.appid + 84 | '&platid=' + 85 | u.platid + 86 | '&clientver=' + 87 | u.clientver + 88 | '&clienttime=' + 89 | u.clienttime + 90 | '&signature=' + 91 | u.signature + 92 | '&mid=' + 93 | u.mid + 94 | '&userid=' + 95 | u.userid + 96 | '&uuid=' + 97 | u.uuid + 98 | '&p.token=' + 99 | u['p.token'], 100 | }; 101 | } 102 | 103 | function h() { 104 | // const e = document.getElementsByTagName('script'); 105 | // if (e && e.length > 0) { 106 | // for (var t = 0, n = e.length; t < n; t++) { 107 | // var i = e[t].src; 108 | // if (i.indexOf('verify/static/js/registerDev1.min.js?appid=') != -1) { 109 | // var r = {}, 110 | // a = (i = i.split('?')[1]).split('&'); 111 | // for (t = 0; t < a.length; t++) { 112 | // r[a[t].split('=')[0]] = unescape(a[t].split('=')[1]); 113 | // } 114 | // return r.appid; 115 | // } 116 | // } 117 | // } 118 | return 1014; 119 | } 120 | 121 | function signatureParam(e, t) { 122 | const n = new Array(); 123 | for (const i in e) { 124 | e.hasOwnProperty(i) && i != 'signature' && n.push(e[i]); 125 | } 126 | let a = ''; 127 | for (let r = n.sort(), o = 0, s = r.length; o < s; o++) { 128 | a += r[o]; 129 | } 130 | return md5(t + a + t); 131 | } 132 | 133 | function S() { 134 | const e = guid(); 135 | return md5(e); 136 | // try { 137 | // Cookie.write('kg_mid', md5(e), 864e6, '/', 'kugou.com'); 138 | // } catch (e) {} 139 | // return md5(e); 140 | } 141 | function guid() { 142 | function s4() { 143 | return Math.floor((1 + Math.random()) * 0x10000) 144 | .toString(16) 145 | .substring(1); 146 | } 147 | 148 | return `${s4() + s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; 149 | } 150 | -------------------------------------------------------------------------------- /src/views/dev/modal-playground.screen.js: -------------------------------------------------------------------------------- 1 | import { View, Text, Button } from 'react-native'; 2 | import React from 'react'; 3 | 4 | import { ModalLite } from '../../components'; 5 | 6 | export default class ModalPlayground extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.onPress = this.onPress.bind(this); 10 | } 11 | onPress() { 12 | this._myRef.toggle(); 13 | } 14 | render() { 15 | return ( 16 | 17 |