├── secrets └── .gitkeep ├── tests └── unit │ └── example.spec.js ├── .browserslistrc ├── public ├── favicon.png ├── font │ ├── Lato-Regular.ttf │ └── Roboto-Regular.ttf ├── css │ └── index.css └── index.html ├── src ├── assets │ ├── logo.png │ └── image │ │ ├── album.png │ │ ├── star.png │ │ ├── Arcucy.png │ │ ├── podcast.png │ │ ├── record.png │ │ ├── single.png │ │ ├── ArcLight.png │ │ ├── bandcamp.png │ │ ├── soundcloud.png │ │ ├── soundeffect.png │ │ ├── cover_placeholder.png │ │ ├── neteasecloudmusic.png │ │ └── paymentCompleted.png ├── icons │ ├── filter │ │ └── .gitkeep │ ├── svg │ │ ├── close-thick.svg │ │ └── artist.svg │ ├── doc.md │ ├── index.js │ └── svgo.yml ├── config │ ├── index.js │ └── blocked.json ├── plugins │ ├── element.js │ └── vuetify.js ├── util │ ├── momentFun.js │ ├── hash.js │ ├── file.js │ ├── string.js │ ├── decode.js │ ├── jwk.js │ ├── cookie.js │ ├── global.js │ ├── encrypt.js │ ├── cache.js │ └── audio.js ├── pages │ ├── User │ │ ├── Single.vue │ │ ├── Podcast.vue │ │ ├── Sound.vue │ │ ├── Album.vue │ │ └── _id.vue │ ├── Playlist │ │ └── index.vue │ ├── Podcast │ │ └── Index.vue │ ├── Sound │ │ └── Index.vue │ ├── Landing.vue │ ├── About.vue │ ├── Songs │ │ ├── Singles.vue │ │ ├── Albums.vue │ │ └── Index.vue │ ├── Library.vue │ └── Upload │ │ └── Index.vue ├── api │ └── api.js ├── element-variables.scss ├── locales │ ├── index.js │ └── zh-cn.js ├── components │ ├── Layout │ │ ├── AppStyle.less │ │ ├── Space.vue │ │ └── Space.css │ ├── SvgIcon │ │ └── index.vue │ ├── Song │ │ ├── LoadCard.vue │ │ ├── GetAudioInfo.vue │ │ ├── GenreFilter.vue │ │ ├── SingleCard.vue │ │ └── AlbumCard.vue │ ├── imgUpload │ │ ├── index.less │ │ └── cropper.less │ ├── User │ │ ├── MiniAvatar.vue │ │ ├── Avatar.vue │ │ ├── UserCard.vue │ │ ├── UserAllSellings.vue │ │ └── UserInfo.vue │ ├── GenreSelect.vue │ ├── CategoryNav.vue │ ├── PodcastCategorySelect.vue │ ├── uploadPriceReceipt.vue │ ├── Search.vue │ ├── KeyReader.vue │ ├── ScrollXBox.vue │ ├── PlayerPlaylist.vue │ ├── Album │ │ └── AlbumInfo.vue │ └── Playlist │ │ └── PlaylistInfo.vue ├── main.js ├── App.vue └── router │ └── index.js ├── babel.config.js ├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ ├── node.dev.yml │ ├── node.prod.yml │ ├── node.prerelease.yml │ └── azure-static-web-apps-deploy.yml ├── vue.config.js ├── LICENSE ├── package.json ├── .gitignore ├── doc └── zh-cn.md └── README.md /secrets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/example.spec.js: -------------------------------------------------------------------------------- 1 | // CI 2 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | 5 | not ie <= 10 -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcucy/ArcLight/HEAD/public/favicon.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcucy/ArcLight/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/image/album.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcucy/ArcLight/HEAD/src/assets/image/album.png -------------------------------------------------------------------------------- /src/assets/image/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcucy/ArcLight/HEAD/src/assets/image/star.png -------------------------------------------------------------------------------- /src/icons/filter/.gitkeep: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file !.gitkeep -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /public/font/Lato-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcucy/ArcLight/HEAD/public/font/Lato-Regular.ttf -------------------------------------------------------------------------------- /src/assets/image/Arcucy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcucy/ArcLight/HEAD/src/assets/image/Arcucy.png -------------------------------------------------------------------------------- /src/assets/image/podcast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcucy/ArcLight/HEAD/src/assets/image/podcast.png -------------------------------------------------------------------------------- /src/assets/image/record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcucy/ArcLight/HEAD/src/assets/image/record.png -------------------------------------------------------------------------------- /src/assets/image/single.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcucy/ArcLight/HEAD/src/assets/image/single.png -------------------------------------------------------------------------------- /public/font/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcucy/ArcLight/HEAD/public/font/Roboto-Regular.ttf -------------------------------------------------------------------------------- /src/assets/image/ArcLight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcucy/ArcLight/HEAD/src/assets/image/ArcLight.png -------------------------------------------------------------------------------- /src/assets/image/bandcamp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcucy/ArcLight/HEAD/src/assets/image/bandcamp.png -------------------------------------------------------------------------------- /src/assets/image/soundcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcucy/ArcLight/HEAD/src/assets/image/soundcloud.png -------------------------------------------------------------------------------- /src/assets/image/soundeffect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcucy/ArcLight/HEAD/src/assets/image/soundeffect.png -------------------------------------------------------------------------------- /src/assets/image/cover_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcucy/ArcLight/HEAD/src/assets/image/cover_placeholder.png -------------------------------------------------------------------------------- /src/assets/image/neteasecloudmusic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcucy/ArcLight/HEAD/src/assets/image/neteasecloudmusic.png -------------------------------------------------------------------------------- /src/assets/image/paymentCompleted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcucy/ArcLight/HEAD/src/assets/image/paymentCompleted.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | dev: 'A4LCIVue3lxOR1ua_P2zMs_0B9Evsaypk3iNjsft8m0', 3 | community: 'nxZu3_PWyO_4w2Q7mX2NxpBmEFp5a_Q2bv8CWfDfOjo' 4 | } 5 | 6 | module.exports = config 7 | -------------------------------------------------------------------------------- /src/icons/svg/close-thick.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugins/element.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Element from 'element-ui' 3 | import '../element-variables.scss' 4 | 5 | Vue.use(Element) 6 | Vue.prototype.$messsage = Element.Message 7 | Vue.prototype.$notify = Element.Notification 8 | -------------------------------------------------------------------------------- /src/icons/doc.md: -------------------------------------------------------------------------------- 1 | # svg 使用 2 | 3 | 1. 将单色(如果是多色的icon 就不要过滤了)需要使用的svg icon放入filter文件夹 4 | 2. 执行命令 5 | ```js 6 | npm run svgo || yarn svgo 7 | ``` 8 | 3. 复制icon文件到svg文件夹内使用 9 | 4. 使用⬇️⬇️⬇️⬇️方法 10 | ```html 11 | 12 | ``` 13 | -------------------------------------------------------------------------------- /src/util/momentFun.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | 3 | export const isNDaysAgo = (n, time) => { 4 | const nowTime = moment().subtract(n, 'days').format('YYYY-MM-DD') 5 | const timeFormat = moment(time).format('YYYY-MM-DD') 6 | return moment(nowTime).isAfter(timeFormat) 7 | } 8 | -------------------------------------------------------------------------------- /public/css/index.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "lato"; 3 | font-style: normal; 4 | src: url(../font/Lato-Regular.ttf) format("ttf"); 5 | } 6 | 7 | @font-face { 8 | font-family: "Roboto"; 9 | font-style: normal; 10 | src: url(../font/Roboto-Regular.ttf) format("ttf"); 11 | } -------------------------------------------------------------------------------- /src/pages/User/Single.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /src/pages/User/Podcast.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /src/pages/User/Sound.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /src/icons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SvgIcon from '@/components/SvgIcon'// svg组件 3 | 4 | // register globally 5 | Vue.component('svg-icon', SvgIcon) 6 | 7 | const requireAll = requireContext => requireContext.keys().map(requireContext) 8 | const req = require.context('./svg', false, /\.svg$/) 9 | requireAll(req) 10 | -------------------------------------------------------------------------------- /src/pages/User/Album.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | -------------------------------------------------------------------------------- /src/util/hash.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | 3 | function sha256 (string) { 4 | let str = string 5 | for (let i = 0; i < 4; i++) { 6 | str += Math.floor(Math.random() * Math.floor(9)) 7 | } 8 | const hash = crypto.createHash('sha256').update(str, 'utf8').digest('hex') 9 | return hash 10 | } 11 | 12 | exports.sha256 = sha256 13 | -------------------------------------------------------------------------------- /src/icons/svgo.yml: -------------------------------------------------------------------------------- 1 | # replace default config 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | 8 | # - name 9 | # 10 | # or: 11 | # - name: false 12 | # - name: true 13 | # 14 | # or: 15 | # - name: 16 | # param1: 1 17 | # param2: 2 18 | 19 | - removeAttrs: 20 | attrs: 21 | - 'fill' 22 | - 'fill-rule' -------------------------------------------------------------------------------- /src/api/api.js: -------------------------------------------------------------------------------- 1 | const arweave = require('./arweave').default 2 | const Ar = require('arweave').default 3 | 4 | const Arweave = Ar.init({ 5 | host: 'arweave.arcucy.io', 6 | port: 443, 7 | protocol: 'https', 8 | timeout: 20000, 9 | logging: false 10 | }) 11 | 12 | const API = { 13 | arweave: arweave, 14 | Arweave: Arweave 15 | } 16 | 17 | export default API 18 | -------------------------------------------------------------------------------- /src/util/file.js: -------------------------------------------------------------------------------- 1 | import API from '@/api/api' 2 | 3 | class FileUtil { 4 | // 检查是否是Arweave的key文件 5 | static async isValidKeyFile (content) { 6 | let shouldContinue = true 7 | await API.arweave.getAddress(content).catch(() => { // 提前检查是否是Arweave的key 8 | shouldContinue = false 9 | }) 10 | return shouldContinue 11 | } 12 | } 13 | 14 | export { 15 | FileUtil 16 | } 17 | -------------------------------------------------------------------------------- /src/element-variables.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Write your variables here. All available variables can be 3 | found in element-ui/packages/theme-chalk/src/common/var.scss. 4 | For example, to overwrite the theme color: 5 | */ 6 | $--color-primary: #E56D9B; 7 | 8 | /* icon font path, required */ 9 | $--font-path: '~element-ui/lib/theme-chalk/fonts'; 10 | 11 | @import "~element-ui/packages/theme-chalk/src/index"; 12 | -------------------------------------------------------------------------------- /src/locales/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | 4 | import en from './en' 5 | import zhTW from './zh-tw' 6 | import zhCN from './zh-cn' 7 | import jaJP from './ja-jp' 8 | 9 | Vue.use(VueI18n) 10 | 11 | export default new VueI18n({ 12 | locale: 'en', 13 | fallbackLocale: 'en', 14 | messages: { 15 | en, 16 | zhCN, 17 | zhTW, 18 | jaJP 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /src/icons/svg/artist.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify' 3 | import 'vuetify/dist/vuetify.min.css' 4 | 5 | Vue.use(Vuetify) 6 | 7 | export default new Vuetify({ 8 | theme: { 9 | options: { 10 | customProperties: true 11 | }, 12 | themes: { 13 | light: { 14 | primary: '#ee44aa', 15 | secondary: '#424242', 16 | accent: '#82B1FF', 17 | error: '#FF5252', 18 | info: '#2196F3', 19 | success: '#4CAF50', 20 | warning: '#FFC107' 21 | } 22 | } 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /src/components/Layout/AppStyle.less: -------------------------------------------------------------------------------- 1 | .appstyle { 2 | /deep/ .v-autocomplete__content { 3 | background: #333333; 4 | .v-list-item__title { 5 | color: white; 6 | } 7 | .theme--light.v-list { 8 | background: #333333; 9 | } 10 | .v-list-item__mask { 11 | color: white !important; 12 | background-color: #E56D9B !important; 13 | } 14 | .v-list-item__title { 15 | text-align: left; 16 | } 17 | } 18 | 19 | /deep/ .v-autocomplete:not(.v-input--is-disabled).v-select.v-text-field input { 20 | color: white; 21 | } 22 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'plugin:vue/essential', 8 | '@vue/standard' 9 | ], 10 | parserOptions: { 11 | parser: 'babel-eslint' 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 16 | }, 17 | overrides: [ 18 | { 19 | files: [ 20 | '**/__tests__/*.{j,t}s?(x)', 21 | '**/tests/unit/**/*.spec.{j,t}s?(x)' 22 | ], 23 | env: { 24 | mocha: true 25 | } 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/config/blocked.json: -------------------------------------------------------------------------------- 1 | { 2 | "single": [ 3 | "W_8i6MH0Z7G_c3HrGlDBKj-VRYLef_pDvrark1gHOVs", 4 | "vcts__2y35pRQ1KYpj7u0sipyXcDq1vhuZ0sCdGTjmo", 5 | "RqFvk2tk84pPg1_j47irPEkY_gxKF54uE8zDX-pzNCs", 6 | "_K_08jNJU9s_wjQo0LRjQfrGXl-wuFrmwX7zspITxSU", 7 | "lADrt0AfXIWn5vsxm3gFKIHbjzemEJx89uqTI3r7wRU", 8 | "oIldrw-feUyic3tJE_5tBKjiRFDWmDinmLeXI2uTEMI" 9 | ], 10 | "album": [ 11 | "jjVVER5cb6ghdmUi6smkrOVYO4RGE1GCJVLlAN2EQsg", 12 | "MQ3WiXu6tmbDU5alxXpTZEOUp7tza7sqhXaeoXM_oNM", 13 | "LtFnsTycD33AgtyLPXd16G22d8FsZib6DopN-RPtRVI" 14 | ], 15 | "podcast": [ 16 | 17 | ], 18 | "soundEffect": [ 19 | 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import vuetify from './plugins/vuetify' 6 | import Aplayer from 'vue-aplayer' 7 | import i18n from '@/locales' 8 | import moment from 'moment' 9 | 10 | import '@babel/polyfill' 11 | import 'roboto-fontface/css/roboto/roboto-fontface.css' 12 | import '@mdi/font/css/materialdesignicons.css' 13 | import './plugins/element.js' 14 | import '@/icons' 15 | 16 | Vue.component('aplayer', Aplayer) 17 | 18 | Vue.config.productionTip = false 19 | 20 | moment.locale('en-US') 21 | Vue.prototype.$moment = moment 22 | 23 | new Vue({ 24 | router, 25 | store, 26 | vuetify, 27 | i18n, 28 | render: h => h(App) 29 | }).$mount('#app') 30 | -------------------------------------------------------------------------------- /src/pages/User/_id.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 27 | 28 | 39 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 33 | 34 | 43 | -------------------------------------------------------------------------------- /src/util/string.js: -------------------------------------------------------------------------------- 1 | import blockList from '../config/blocked.json' 2 | 3 | export default class StringUtil { 4 | /** 5 | * 计算 Utf8 字符串的体积 6 | * @param {String} str 需要计算的字符串 7 | */ 8 | static lengthInUtf8Bytes (str) { 9 | const m = encodeURIComponent(str).match(/%[89ABab]/g) 10 | return str.length + (m ? m.length : 0) 11 | } 12 | 13 | static toPlainString (num) { 14 | return ('' + num).replace(/(-?)(\d*)\.?(\d+)e([+-]\d+)/, 15 | function (a, b, c, d, e) { 16 | return e < 0 17 | ? b + '0.' + Array(1 - e - c.length).join(0) + c + d 18 | : b + c + d + Array(e - d.length + 1).join(0) 19 | }) 20 | } 21 | 22 | static getBlockedArray (array, type) { 23 | let res = [] 24 | const filterList = [...blockList[type]] 25 | res = array.filter(item => !filterList.find(elem => elem === item)) 26 | 27 | return res 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ArcLight 8 | 9 | 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/components/Song/LoadCard.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 31 | 32 | 45 | -------------------------------------------------------------------------------- /.github/workflows/node.dev.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node Build Test 5 | 6 | on: 7 | pull_request: 8 | branches: [ main ] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [12.x] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | env: 24 | ENCRYPT_CODE: ${{ secrets.ENCRYPT_CODE }} 25 | ENCRYPT_IV: ${{ secrets.ENCRYPT_IV }} 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: touch secrets/encrypt.json && printf '{"code":"%s", "iv":"%s"}' "$ENCRYPT_CODE" "$ENCRYPT_IV" > secrets/encrypt.json 29 | - run: npm install -g yarn 30 | - run: yarn install --frozen-lockfile 31 | - run: yarn build 32 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | productionSourceMap: false, 3 | configureWebpack: { 4 | performance: { 5 | hints: 'warning', 6 | // 入口起点的最大体积 7 | maxEntrypointSize: 50000000, 8 | // 生成文件的最大体积 9 | maxAssetSize: 30000000, 10 | // 只给出 js 文件的性能提示 11 | assetFilter: function (assetFilename) { 12 | return assetFilename.endsWith('.js') 13 | } 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(svg)(\?.*)?$/, 19 | use: [ 20 | { 21 | loader: 'svg-sprite-loader', 22 | options: { 23 | limit: 10000, 24 | name: 'assets/img/[name].[hash:7].[ext]', 25 | symbolId: 'icon-[name]' 26 | } 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | }, 33 | publicPath: process.env.NODE_ENV === 'production' 34 | ? '././' 35 | : './', 36 | chainWebpack: config => { 37 | config.module 38 | .rule('svg') 39 | .test(() => false) 40 | .use('file-loader') 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/node.prod.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Production CI Build Test 5 | 6 | on: 7 | pull_request: 8 | branches: [ production ] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [12.x] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | env: 24 | ENCRYPT_CODE: ${{ secrets.ENCRYPT_CODE }} 25 | ENCRYPT_IV: ${{ secrets.ENCRYPT_IV }} 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: touch secrets/encrypt.json && printf '{"code":"%s", "iv":"%s"}' "$ENCRYPT_CODE" "$ENCRYPT_IV" > secrets/encrypt.json 29 | - run: npm install -g yarn 30 | - run: yarn install --frozen-lockfile 31 | - run: yarn build 32 | -------------------------------------------------------------------------------- /src/util/decode.js: -------------------------------------------------------------------------------- 1 | const Decode = { 2 | /** 3 | * 将 Uint8Array 类型转换成 String 4 | * @param {Uint8Array} array 需要转换的数组 5 | */ 6 | uint8ArrayToString (array) { 7 | var out, i, len, c 8 | var char2, char3 9 | out = '' 10 | len = array.length 11 | i = 0 12 | while (i < len) { 13 | c = array[i++] 14 | switch (c >> 4) { 15 | case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: 16 | // 0xxxxxxx 17 | out += String.fromCharCode(c) 18 | break 19 | case 12: case 13: 20 | // 110x xxxx 10xx xxxx 21 | char2 = array[i++] 22 | out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F)) 23 | break 24 | case 14: 25 | // 1110 xxxx 10xx xxxx 10xx xxxx 26 | char2 = array[i++] 27 | char3 = array[i++] 28 | out += String.fromCharCode(((c & 0x0F) << 12) | 29 | ((char2 & 0x3F) << 6) | 30 | ((char3 & 0x3F) << 0)) 31 | break 32 | } 33 | } 34 | return out 35 | } 36 | } 37 | 38 | module.exports = Decode 39 | -------------------------------------------------------------------------------- /.github/workflows/node.prerelease.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Pre-Release CI Build Test 5 | 6 | on: 7 | pull_request: 8 | branches: [ pre-release ] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [12.x] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | env: 24 | ENCRYPT_CODE: ${{ secrets.ENCRYPT_CODE }} 25 | ENCRYPT_IV: ${{ secrets.ENCRYPT_IV }} 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: touch secrets/encrypt.json && printf '{"code":"%s", "iv":"%s"}' "$ENCRYPT_CODE" "$ENCRYPT_IV" > secrets/encrypt.json 29 | - run: npm install -g yarn 30 | - run: yarn install --frozen-lockfile 31 | - run: yarn build 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ayaka Neko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/util/jwk.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | const jwkToPem = require('jwk-to-pem') 3 | 4 | /** 5 | * 用来签名的小工具 6 | */ 7 | class JwkUtil { 8 | /** 9 | * 将 jwk 转换为 PEM 格式的 RSA 密钥 10 | * @param {Object} input jwk 对象 11 | * @param {*} mode 输出模式:['private': 私钥, 'public': 公钥] 12 | */ 13 | static async toPem (input, mode) { 14 | let key = '' 15 | if (mode === 'private') { 16 | key = jwkToPem(input, { private: true }) 17 | } else if (mode === 'public') { 18 | key = jwkToPem(input) 19 | } 20 | return key 21 | } 22 | 23 | /** 24 | * 对数据签名,输入私钥,客户端 25 | * @param {String} pri - 私钥 26 | * @param {String} data - 需要签名的数据 27 | */ 28 | static async signMessage (pri, data) { 29 | // 创建签名 30 | const sign = crypto.createSign('sha384') 31 | sign.update(data) 32 | const signature = sign.sign(pri, 'hex') 33 | return signature 34 | } 35 | 36 | /** 37 | * 验证签名,输入公钥,服务端验证 38 | * @param {String} pub - 公钥 39 | * @param {String} signature - signMessage 获得的签名数据 40 | * @param {String} data - 之前签名的数据 41 | */ 42 | static async verifyMessage (pub, signature, data) { 43 | const verify = crypto.createVerify('sha384') 44 | verify.update(data) 45 | return verify.verify(pub, signature, 'hex') 46 | } 47 | } 48 | 49 | module.exports = JwkUtil 50 | -------------------------------------------------------------------------------- /src/components/imgUpload/index.less: -------------------------------------------------------------------------------- 1 | @import './cropper.less'; 2 | 3 | // 覆盖图像上传modal样式 无法使用scoped 4 | .img-upload-modal { 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | .ivu-modal{ 10 | top: 0; 11 | } 12 | 13 | .ivu-modal-header { 14 | border-bottom: none; 15 | } 16 | .modal-header { 17 | text-align: center; 18 | &-title { 19 | margin-top: 30px; 20 | font-size: 18px; 21 | font-weight: bold; 22 | color: #1a1a1a; 23 | text-align: center; 24 | } 25 | &-subtitle { 26 | margin-top: 4px; 27 | font-size: 14px; 28 | color: #8590a6; 29 | text-align: center; 30 | } 31 | } 32 | .ivu-modal-content { 33 | overflow: hidden; 34 | } 35 | .ivu-modal-body { 36 | margin: 0 40px; 37 | padding: 0; 38 | } 39 | .modal-content { 40 | width: 240px; 41 | height: 240px; 42 | margin: 4px auto 40px; 43 | } 44 | 45 | .ivu-modal-footer { 46 | border-top: none; 47 | } 48 | 49 | .save-button { 50 | display: block; 51 | margin: 0 auto 20px; 52 | text-align: center; 53 | width: 220px; 54 | background-color: #E56D9B; 55 | border-color: #E56D9B; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/util/cookie.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the Cookie associate with current window's Origin based on given Cookie name 3 | * 根据给定的 Cookie 名称获取当前 Window 的域名归属 Cookie 4 | * @param {String} cname - Cookie's name Cookie 的名字 5 | */ 6 | export function getCookie (cname) { 7 | const name = cname + '=' 8 | const decodedCookie = decodeURIComponent(document.cookie) 9 | const ca = decodedCookie.split(';') 10 | for (let i = 0; i < ca.length; i++) { 11 | let c = ca[i] 12 | while (c.charAt(0) === ' ') { 13 | c = c.substring(1) 14 | } 15 | if (c.indexOf(name) === 0) { 16 | return c.substring(name.length, c.length) 17 | } 18 | } 19 | return '' 20 | } 21 | 22 | /** 23 | * Set the Cookie associate with current window's origin based on given name and token 24 | * 根据给定的 Cookie 名字和 Cookie Token 设置到当前窗口域名归属的 Cookie 25 | * @param {String} cname - Cookie's name Cookie 的名字 26 | * @param {String} token - Cookie 的存储值 27 | * @param {Number} exp - Cookie 失效的时间,顺延时间,以 天数 为单位 28 | */ 29 | export function setCookie (cname, token, exp) { 30 | const date = new Date(Date.now()) 31 | date.setDate(date.getDate() + exp) 32 | 33 | document.cookie = cname + '=' + token + '; expires=' + date.toUTCString() 34 | } 35 | 36 | /** 37 | * Clear the Cookie based on the given name 38 | * 清除指定 的 Cookie 39 | * @param {String} cname - Cookie's name Cookie 的名字 40 | */ 41 | export function clearCookie (cname) { 42 | document.cookie = `${cname}=; expires=Thu, 01 Jan 1970 00:00:01 GMT` 43 | } 44 | -------------------------------------------------------------------------------- /src/util/global.js: -------------------------------------------------------------------------------- 1 | // Local 2 | const Log = require('./log') 3 | 4 | // Pre-defined value 5 | const ignoraceNotice = 'Ignore this message if this is working as intended.' 6 | 7 | // Global Data 8 | const Storage = new Map() 9 | 10 | // Global Control 11 | function Add (name, reference) { 12 | // Check eval 13 | if (Storage.has(name)) { 14 | Log.warning('Global Variable Control: Variable is been set twice, ignored change.' + ignoraceNotice) 15 | return false 16 | } 17 | 18 | Storage.set(name, reference) 19 | return true 20 | } 21 | 22 | function Remove (name) { 23 | if (!Storage.has(name)) { 24 | Log.warning('Global Variable Control: Variable removal is terminated because variable is not exist, ignore change.' + ignoraceNotice) 25 | return false 26 | } 27 | 28 | Storage.delete(name) 29 | return true 30 | } 31 | 32 | function Read (name) { 33 | if (!Storage.has(name)) { 34 | Log.warning('Global Variable Control: Variable read failed because variable is not exist, ignore action.' + ignoraceNotice) 35 | return undefined 36 | } 37 | 38 | return Storage.get(name) 39 | } 40 | 41 | function Write (name, reference) { 42 | if (!Storage.has(name)) { 43 | Log.warning('Global Variable Control: Variable write failed because variable is not exist, ignore action.' + ignoraceNotice) 44 | return false 45 | } 46 | 47 | Storage.set(name, reference) 48 | return true 49 | } 50 | 51 | module.exports = { 52 | Add, 53 | Remove, 54 | Read, 55 | Write 56 | } 57 | -------------------------------------------------------------------------------- /src/util/encrypt.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | 3 | const passcode = require('../../secrets/encrypt.json') 4 | 5 | const algorithm = 'aes-256-ctr' 6 | 7 | const key = passcode.code 8 | let iv = passcode.iv 9 | 10 | function encryptText (text) { 11 | const cipher = crypto.createCipheriv(algorithm, Buffer.from(key), iv) 12 | let encrypted = cipher.update(text) 13 | encrypted = Buffer.concat([encrypted, cipher.final()]) 14 | return encrypted.toString('hex') 15 | } 16 | 17 | function decryptText (text) { 18 | iv = Buffer.from(iv) 19 | const encryptedText = Buffer.from(text, 'hex') 20 | const decipher = crypto.createDecipheriv(algorithm, Buffer.from(key), iv) 21 | let decrypted = decipher.update(encryptedText) 22 | decrypted = Buffer.concat([decrypted, decipher.final()]) 23 | return decrypted.toString() 24 | } 25 | 26 | function encryptBuffer (audioBuffer) { 27 | const cipher = crypto.createCipheriv(algorithm, Buffer.from(key), iv) 28 | let encrypted = cipher.update(audioBuffer) 29 | encrypted = Buffer.concat([encrypted, cipher.final()]) 30 | return encrypted 31 | } 32 | 33 | function decryptBuffer (audioBuffer) { 34 | iv = Buffer.from(iv) 35 | const encryptedContent = audioBuffer 36 | const decipher = crypto.createDecipheriv(algorithm, Buffer.from(key), iv) 37 | let decrypted = decipher.update(encryptedContent) 38 | decrypted = Buffer.concat([decrypted, decipher.final()]) 39 | return decrypted 40 | } 41 | 42 | module.exports = { 43 | encryptText, 44 | decryptText, 45 | encryptBuffer, 46 | decryptBuffer 47 | } 48 | -------------------------------------------------------------------------------- /src/components/User/MiniAvatar.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 44 | 45 | 70 | -------------------------------------------------------------------------------- /src/components/User/Avatar.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 40 | 41 | 71 | -------------------------------------------------------------------------------- /src/components/Song/GetAudioInfo.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 67 | 68 | 73 | -------------------------------------------------------------------------------- /src/components/GenreSelect.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 72 | 73 | 88 | -------------------------------------------------------------------------------- /src/components/Song/GenreFilter.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 53 | 54 | 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arclight", 3 | "version": "1.2.0", 4 | "detailVersion": "(c5b4568)", 5 | "description": "A Arweave Storage App", 6 | "author": "Ayaka Neko ", 7 | "scripts": { 8 | "dev": "vue-cli-service serve", 9 | "build": "vue-cli-service build", 10 | "test:unit": "vue-cli-service test:unit", 11 | "lint": "vue-cli-service lint", 12 | "svgo": "svgo -f src/icons/filter --config=src/icons/svgo.yml" 13 | }, 14 | "dependencies": { 15 | "@babel/polyfill": "^7.4.4", 16 | "@mdi/font": "^3.6.95", 17 | "arweave": "^1.10.0", 18 | "axios": "^0.21.0", 19 | "community-js": "^1.1.6", 20 | "compressorjs": "^1.0.6", 21 | "core-js": "^3.6.5", 22 | "cropperjs": "^1.5.9", 23 | "element-ui": "^2.14.1", 24 | "eslint-plugin-vue": "^7.1.0", 25 | "hls.js": "^0.14.16", 26 | "jszip": "^3.5.0", 27 | "jwk-to-pem": "^2.0.4", 28 | "localforage": "^1.9.0", 29 | "moment": "^2.29.1", 30 | "musicgenres-json": "1.0.2", 31 | "roboto-fontface": "*", 32 | "svgo": "^1.3.2", 33 | "vue": "^2.6.11", 34 | "vue-aplayer": "^1.6.1", 35 | "vue-i18n": "^8.22.2", 36 | "vue-router": "^3.2.0", 37 | "vue-upload-component": "^2.8.20", 38 | "vuetify": "^2.2.11", 39 | "vuex": "^3.4.0" 40 | }, 41 | "devDependencies": { 42 | "@vue/cli-plugin-babel": "~4.5.0", 43 | "@vue/cli-plugin-eslint": "~4.5.0", 44 | "@vue/cli-plugin-router": "~4.5.0", 45 | "@vue/cli-plugin-unit-mocha": "~4.5.0", 46 | "@vue/cli-plugin-vuex": "~4.5.0", 47 | "@vue/cli-service": "~4.5.0", 48 | "@vue/eslint-config-standard": "^5.1.2", 49 | "@vue/test-utils": "^1.0.3", 50 | "babel-eslint": "^10.1.0", 51 | "chai": "^4.1.2", 52 | "eslint": "^6.7.2", 53 | "eslint-plugin-import": "^2.20.2", 54 | "eslint-plugin-node": "^11.1.0", 55 | "eslint-plugin-promise": "^4.2.1", 56 | "eslint-plugin-standard": "^4.0.0", 57 | "less": "^3.0.4", 58 | "less-loader": "^5.0.0", 59 | "node-sass": "^4.9.2", 60 | "sass-loader": "^7.0.3", 61 | "svg-sprite-loader": "^5.0.0", 62 | "vue-cli-plugin-element": "~1.0.1", 63 | "vue-cli-plugin-vuetify": "~2.0.7", 64 | "vue-template-compiler": "^2.6.11" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/components/Layout/Space.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 51 | 52 | 98 | -------------------------------------------------------------------------------- /src/components/CategoryNav.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 50 | 51 | 104 | -------------------------------------------------------------------------------- /.github/workflows/azure-static-web-apps-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Azure Static Web Apps CI/CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - production 7 | pull_request: 8 | types: [opened, synchronize, reopened, closed] 9 | branches: 10 | - production 11 | - pre-release 12 | 13 | jobs: 14 | build_and_deploy_job: 15 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') 16 | runs-on: ubuntu-latest 17 | name: Build and Deploy Job 18 | steps: 19 | - uses: actions/checkout@v2 20 | with: 21 | submodules: true 22 | - name: Build And Deploy 23 | id: builddeploy 24 | uses: Azure/static-web-apps-deploy@v1 25 | env: 26 | ENCRYPT_CODE: ${{ secrets.ENCRYPT_CODE }} 27 | ENCRYPT_IV: ${{ secrets.ENCRYPT_IV }} 28 | with: 29 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_WONDERFUL_BUSH_029EA0B00 }} 30 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 31 | action: "upload" 32 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### 33 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig 34 | app_location: "/" # App source code path 35 | api_location: "" # Api source code path - optional 36 | output_location: "dist" # Built app content directory - optional 37 | app_build_command: rm -rf secrets/encrypt.json && touch secrets/encrypt.json && printf '{"code":"%s", "iv":"%s"}' "$ENCRYPT_CODE" "$ENCRYPT_IV" > secrets/encrypt.json && yarn run build 38 | ###### End of Repository/Build Configurations ###### 39 | 40 | close_pull_request_job: 41 | if: github.event_name == 'pull_request' && github.event.action == 'closed' 42 | runs-on: ubuntu-latest 43 | name: Close Pull Request Job 44 | steps: 45 | - name: Close Pull Request 46 | id: closepullrequest 47 | uses: Azure/static-web-apps-deploy@v1 48 | with: 49 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_WONDERFUL_BUSH_029EA0B00 }} 50 | action: "close" 51 | -------------------------------------------------------------------------------- /src/util/cache.js: -------------------------------------------------------------------------------- 1 | import API from '@/api/api' 2 | import decode from '@/util/decode' 3 | import localforage from 'localforage' 4 | 5 | class LocalCache { 6 | /** 7 | * 获取缓存的key 8 | * @param txid 交易地址 9 | * @returns {Promise}如果有,返回key,如果没有,返回undefined 10 | */ 11 | async findKeyByTxid (txid) { 12 | let result 13 | const keys = await localforage.keys() 14 | for (const index in keys) { 15 | if (keys[index].endsWith(txid)) { 16 | result = keys[index] 17 | break 18 | } 19 | } 20 | return result 21 | } 22 | 23 | /** 24 | * 根据交易地址获取信息,如果有缓存则直接从缓存里读 25 | * 如果没有再从Arweave读并缓存进去 26 | * @param txid 交易地址 27 | * @returns 各种info 28 | */ 29 | async getInfoByTxid (txid) { 30 | const keyName = await this.findKeyByTxid(txid) 31 | if (keyName) { 32 | const cached = await localforage.getItem(keyName) 33 | if (cached) { 34 | return JSON.parse(cached) 35 | } 36 | } 37 | const transaction = await API.arweave.getTransactionDetail(txid) 38 | if (transaction) { 39 | const data = JSON.parse(decode.uint8ArrayToString(transaction.data)) 40 | const tags = API.arweave.getTagsByTransaction(transaction) 41 | const type = tags.Type 42 | const needToCache = { 43 | txid, 44 | authorAddress: tags['Author-Address'], 45 | authorUsername: tags['Author-Username'], 46 | type: tags.Type, 47 | unixTime: Number(tags['Unix-Time']), 48 | title: data.title, 49 | desp: data.desp, 50 | price: data.price, 51 | duration: data.duration, 52 | coverTxid: data.cover, 53 | tags 54 | } 55 | switch (type) { 56 | case 'single-info': 57 | needToCache.musicTxid = data.music 58 | needToCache.genre = data.genre 59 | break 60 | case 'album-info': 61 | needToCache.music = data.music 62 | needToCache.genre = data.genre 63 | break 64 | case 'soundeffect-info': 65 | needToCache.audio = data.audio 66 | break 67 | case 'podcast-info': 68 | needToCache.category = data.category 69 | needToCache.podcast = data.podcast 70 | break 71 | case 'playlist-info': 72 | break 73 | } 74 | const keyName = type + ':' + Date.now() + ':' + txid 75 | await localforage.setItem(keyName, JSON.stringify(needToCache)) 76 | return needToCache 77 | } 78 | } 79 | } 80 | 81 | export const localCache = new LocalCache() 82 | -------------------------------------------------------------------------------- /src/util/audio.js: -------------------------------------------------------------------------------- 1 | const AudioUtil = { 2 | /** 3 | * 剪切音频 4 | * @param {AudioBuffer} audioBuffer 需要剪切的音频 5 | * @param {Number} start 开始位置(单位秒) 6 | * @param {Number} last 持续时间(单位秒) 7 | */ 8 | cutAudio (audioBuffer, start, last) { 9 | // 声道数量和采样率 10 | const channels = audioBuffer.numberOfChannels 11 | const rate = audioBuffer.sampleRate 12 | // 裁切后的采样长度 13 | const lastOffset = rate * last 14 | // 创建同样采用率、同样声道数量的空的AudioBuffer 15 | const newAudioBuffer = new AudioContext().createBuffer(channels, lastOffset, rate) 16 | // 创建临时的Array存放复制的buffer数据 17 | const anotherArray = new Float32Array(lastOffset) 18 | // 声道的数据的复制和写入 19 | const offset = 0 20 | for (var channel = 0; channel < channels; channel++) { 21 | audioBuffer.copyFromChannel(anotherArray, channel, start) 22 | newAudioBuffer.copyToChannel(anotherArray, channel, offset) 23 | } 24 | return newAudioBuffer 25 | }, 26 | 27 | /** 28 | * 将 AudioBuffer 转换成 Wav 格式的文件 29 | * @param {AudioBuffer} abuffer 需要转换的 AudioBuffer 30 | */ 31 | audioBufferToWave (abuffer) { 32 | const numOfChan = abuffer.numberOfChannels 33 | const length = abuffer.length * numOfChan * 2 + 44 34 | const buffer = new ArrayBuffer(length) 35 | const view = new DataView(buffer) 36 | const channels = [] 37 | let i 38 | let sample 39 | let offset = 0 40 | let pos = 0 41 | 42 | const setUint16 = (data) => { 43 | view.setUint16(pos, data, true) 44 | pos += 2 45 | } 46 | 47 | const setUint32 = (data) => { 48 | view.setUint32(pos, data, true) 49 | pos += 4 50 | } 51 | 52 | setUint32(0x46464952) 53 | setUint32(length - 8) 54 | setUint32(0x45564157) 55 | setUint32(0x20746d66) 56 | setUint32(16) 57 | setUint16(1) 58 | setUint16(numOfChan) 59 | setUint32(abuffer.sampleRate) 60 | setUint32(abuffer.sampleRate * 2 * numOfChan) 61 | setUint16(numOfChan * 2) 62 | setUint16(16) 63 | setUint32(0x61746164) 64 | setUint32(length - pos - 4) 65 | 66 | for (i = 0; i < abuffer.numberOfChannels; i++) channels.push(abuffer.getChannelData(i)) 67 | while (pos < length) { 68 | for (i = 0; i < numOfChan; i++) { 69 | sample = Math.max(-1, Math.min(1, channels[i][offset])) 70 | sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767) | 0 71 | view.setInt16(pos, sample, true) 72 | pos += 2 73 | } 74 | offset++ 75 | } 76 | 77 | return new Blob([buffer], { type: 'audio/wav' }) 78 | } 79 | } 80 | 81 | module.exports = AudioUtil 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | # Logs 26 | logs 27 | *.log 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | lerna-debug.log* 32 | 33 | # Diagnostic reports (https://nodejs.org/api/report.html) 34 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 35 | 36 | # Runtime data 37 | pids 38 | *.pid 39 | *.seed 40 | *.pid.lock 41 | 42 | # Directory for instrumented libs generated by jscoverage/JSCover 43 | lib-cov 44 | 45 | # Coverage directory used by tools like istanbul 46 | coverage 47 | *.lcov 48 | 49 | # nyc test coverage 50 | .nyc_output 51 | 52 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 53 | .grunt 54 | 55 | # Bower dependency directory (https://bower.io/) 56 | bower_components 57 | 58 | # node-waf configuration 59 | .lock-wscript 60 | 61 | # Compiled binary addons (https://nodejs.org/api/addons.html) 62 | build/Release 63 | 64 | # Dependency directories 65 | node_modules/ 66 | jspm_packages/ 67 | 68 | # TypeScript v1 declaration files 69 | typings/ 70 | 71 | # TypeScript cache 72 | *.tsbuildinfo 73 | 74 | # Optional npm cache directory 75 | .npm 76 | 77 | # Optional eslint cache 78 | .eslintcache 79 | 80 | # Microbundle cache 81 | .rpt2_cache/ 82 | .rts2_cache_cjs/ 83 | .rts2_cache_es/ 84 | .rts2_cache_umd/ 85 | 86 | # Optional REPL history 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | *.tgz 91 | 92 | # Yarn Integrity file 93 | .yarn-integrity 94 | 95 | # dotenv environment variables file 96 | .env 97 | .env.test 98 | 99 | # parcel-bundler cache (https://parceljs.org/) 100 | .cache 101 | 102 | # Next.js build output 103 | .next 104 | 105 | # Nuxt.js build / generate output 106 | .nuxt 107 | dist 108 | 109 | # Gatsby files 110 | .cache/ 111 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 112 | # https://nextjs.org/blog/next-9-1#public-directory-support 113 | # public 114 | 115 | # vuepress build output 116 | .vuepress/dist 117 | 118 | # Serverless directories 119 | .serverless/ 120 | 121 | # FuseBox cache 122 | .fusebox/ 123 | 124 | # DynamoDB Local files 125 | .dynamodb/ 126 | 127 | # TernJS port file 128 | .tern-port 129 | 130 | # User Keys 131 | secrets/ 132 | 133 | # Vue 134 | .DS_Store 135 | node_modules/ 136 | /dist/ 137 | npm-debug.log* 138 | yarn-debug.log* 139 | yarn-error.log* 140 | /test/unit/coverage/ 141 | 142 | # Editor directories and files 143 | .idea 144 | .vscode 145 | *.suo 146 | *.ntvs* 147 | *.njsproj 148 | *.sln 149 | -------------------------------------------------------------------------------- /doc/zh-cn.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

ArcLight

5 |

6 | 一个艺术家可以获得所有权和合理收益的艺术品发布平台
7 | 感谢 ❤️ @LittleSound ❤️ 为本项目作出的贡献 8 |

9 |

10 | 永存网在线演示 ArcLight
11 | 现在 1.2.0 在线 12 |

13 |

14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 |

23 | 24 | 25 | 26 | --- 27 | 28 | [English](https://github.com/Arcucy/ArcLight/blob/master/README.md) 29 | 30 | --- 31 | 32 | ## **基于永存网的艺术品发布平台** 33 | > ArcLight 的目标旨在创造一个完全公平自由的艺术品发布平台 34 | 35 | 我们使用的是 Arweave(区块纺)提供的永存网,这是一种采用区块链技术构建的永久性和分散式的数据存储类型。在您上传您的作品前,我们会对您提交到浏览器的作品进行加密,使其成为在区块链上不可直接读取的文件类型,您将获得知识产权的完全所有权。作为艺术家,您甚至可以自定义属于你的价格来鼓励用户通过购买来支持您。 36 | 37 | 在 ArcLight 平台上,我们可以省去中间人和经纪人带来的抽成,以此来把收益的所有权还给作者。 38 | 39 | ## **多种作品类型的支持** 40 | > 我们支持单曲、专辑、播客、音效,以及任意的绘画作品 41 | 42 | 上传多种不同格式的作品!ArcLight 支持从单曲到博客,甚至是您录制的音效,以及精美的电子绘画作品,只要是能够授予(不包含版权)用户拥有的电子媒体格式通通都可以被支持。在最适合您的媒体中表达您的创造力。ArcLight 将为您提供存储创作并从中获得收益的办法! 43 | 44 | ## **数据安全与补偿** 45 | > 储存作品的安全方案 46 | 47 | 通过加密和 Arweave 的分散储存机制,我们可以限制用户通过区块链数据访问来获得需要付费的项目,获得原文件的唯一方法是通过付费来完成。 48 | 49 | 交易都必须通过 AR 代币或是将来支持的内部代币进行,AR 代币是 Arweave 永存网的流通货币。在用户付款后,我们将生成一个特定的收据来证明你拥有这个作品,包括 UNIX 时间戳,已经支付给您的价格,购买的目标作品以及您的钱包地址。 50 | 51 | **感谢您选择 ArcLight** 52 | 53 | ## 感谢 54 | 55 | [@LittleSound](https://github.com/LittleSound) - 主要开发者 56 | 57 | [@Garfield550](https://github.com/Garfield550) - 主要开发者 58 | 59 | [@KagurazakaIzumi](https://github.com/KagurazakaIzumi) - 简体中文、繁体中文、日本語的本地化作者 60 | 61 | 62 | ### 与 Arweave 交互 63 | ``` 64 | yarn dev 65 | ``` 66 | 67 | ### 使用钱包密钥进行部署操作 68 | ``` 69 | arweave deploy-dir ./dist --key-file ./secrets/key.json 70 | ``` 71 | 72 | ### 引用 73 | [arweave interface](https://www.arweave.org/build) 74 | [Arweave.js](https://github.com/ArweaveTeam/arweave-js): A library for interacting with the Arweave network from web applications and node.js programs. 75 | [Arweave Deploy](https://github.com/ArweaveTeam/arweave-deploy): A simple command line tool for deploying web apps, pages, and other files to the permaweb. 76 | 77 | ## 构建步骤 78 | 79 | ``` bash 80 | # 安装依赖 81 | yarn 82 | 83 | # 使用热加载来同步开发 localhost:8090 84 | yarn dev 85 | 86 | # 构建优化后的实例 87 | yarn build 88 | 89 | # 构建实例并查看构建分析 90 | yarn build --report 91 | ``` 92 | -------------------------------------------------------------------------------- /src/components/User/UserCard.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 75 | 76 | 118 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 106 | 107 | 116 | -------------------------------------------------------------------------------- /src/components/PodcastCategorySelect.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 89 | 90 | 105 | -------------------------------------------------------------------------------- /src/components/Layout/Space.css: -------------------------------------------------------------------------------- 1 | #stars { 2 | object-fit: cover; 3 | width: 100vw; 4 | height: 100vh; 5 | } 6 | 7 | .layout-bg { 8 | position: fixed; 9 | z-index: -10; 10 | height: 100%; 11 | width: 100%; 12 | font-family: "lato", sans-serif; 13 | color: #FFF; 14 | background: radial-gradient(ellipse at bottom, #0c1116 0%, #090a0f 100%); 15 | } 16 | 17 | #horizon { 18 | position: absolute; 19 | width: 160%; 20 | height: 70%; 21 | border-radius: 100%/100%; 22 | background: #51AFFF; 23 | filter: blur(30px); 24 | -webkit-filter: blur(30px); 25 | left: 50%; 26 | bottom: -40%; 27 | margin-left: -80%; 28 | } 29 | 30 | #horizon:before { 31 | content: " "; 32 | position: absolute; 33 | width: 81.25%; 34 | height: 70%; 35 | border-radius: 100%/100%; 36 | background: #51AFFF; 37 | filter: blur(30px); 38 | -webkit-filter: blur(30px); 39 | opacity: 0.6; 40 | margin-left: 9.375%; 41 | left: 0; 42 | } 43 | 44 | #horizon:after { 45 | content: " "; 46 | position: absolute; 47 | width: 32%; 48 | height: 20%; 49 | border-radius: 650px/350px; 50 | background: #215496; 51 | filter: blur(30px); 52 | -webkit-filter: blur(30px); 53 | opacity: 0.5; 54 | margin-left: 34%; 55 | left: 0; 56 | } 57 | 58 | #horizon .glow { 59 | position: absolute; 60 | width: 100%; 61 | height: 100%; 62 | border-radius: 100%/100%; 63 | background: #FF92BC; 64 | filter: blur(30px); 65 | -webkit-filter: blur(30px); 66 | opacity: 0.7; 67 | top: -10%; 68 | } 69 | 70 | #earth { 71 | position: absolute; 72 | width: 200%; 73 | height: 100%; 74 | background: black; 75 | border-radius: 100%/100%; 76 | left: 50%; 77 | bottom: -70%; 78 | margin-left: -100%; 79 | } 80 | 81 | #earth:before { 82 | left: 0; 83 | content: " "; 84 | position: absolute; 85 | width: 100%; 86 | height: 100%; 87 | border-radius: 100%/100%; 88 | box-shadow: inset 0px 0px 62px 20px rgba(60, 105, 138, 0.85); 89 | } 90 | 91 | #earth:after { 92 | left: 0; 93 | *zoom: 1; 94 | filter: progid:DXImageTransform.Microsoft.gradient(gradientType=1, startColorstr='#FF000000', endColorstr='#FF000000'); 95 | background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuMCIgeTE9IjAuNSIgeDI9IjEuMCIgeTI9IjAuNSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzAwMDAwMCIvPjxzdG9wIG9mZnNldD0iNTAlIiBzdG9wLWNvbG9yPSIjMDAwMDAwIiBzdG9wLW9wYWNpdHk9IjAuMCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzAwMDAwMCIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); 96 | background-size: 100%; 97 | background-image: -webkit-gradient(linear, 0% 50%, 100% 50%, color-stop(0%, #000000), color-stop(50%, rgba(0, 0, 0, 0)), color-stop(100%, #000000)); 98 | background-image: -moz-linear-gradient(left, #000000 0%, rgba(0, 0, 0, 0) 50%, #000000 100%); 99 | background-image: -webkit-linear-gradient(left, #000000 0%, rgba(0, 0, 0, 0) 50%, #000000 100%); 100 | background-image: linear-gradient(to right, #000000 0%, rgba(0, 0, 0, 0) 50%, #000000 100%); 101 | content: " "; 102 | position: absolute; 103 | width: 100%; 104 | height: 100%; 105 | border-radius: 100%/100%; 106 | } 107 | 108 | a { 109 | text-decoration: none; 110 | color: #E56D9B !important; 111 | } 112 | 113 | @keyframes fadeInOpacity { 114 | 0% { 115 | opacity: 0; 116 | } 117 | 118 | 100% { 119 | opacity: 1; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

ArcLight

5 |

6 | An artwork distribution platform where artists are given ownership and fair compensation
7 | Great Thanks to ❤️ @LittleSound ❤️ for contributing this project 8 |

9 |

10 | Live Permaweb for ArcLight
11 | Now 1.2.0 Online 12 |

13 |

14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 |

23 | 24 | 25 | --- 26 | 27 | [简体中文](https://github.com/Arcucy/ArcLight/blob/master/doc/zh-cn.md) 28 | 29 | --- 30 | 31 | ## **A Peer-to-Peer Artwork Distribution Platform Built on the Permaweb** 32 | > the goal of project ArcLight is to create a truly free artwork distribution platform. 33 | 34 | We use __[Arweave’s](https://www.arweave.org/) permaweb__, a permanent and decentralized type of data storage built with blockchain technology. By encrypting your artworks to an unreadable source on the Arweave network, you are given full ownership of your intellectual property. Artists define the price for downloading their work, and users are able to support them directly. 35 | 36 | With ArcLight, we cut out the middleman so that we can give the power back to the artists. 37 | 38 | ## **Wide Range of Content** 39 | > we support singles, albums, podcasts, and sound effects, event paintings 40 | 41 | Upload your artwork in multiple different formats! ArcLight supports singles, albums, podcasts, sound effects, and paintings in digital format 42 | Express your creativity in whichever medium suits you best. ArcLight will give you the means to store and monetize your creations. 43 | 44 | ## **Data Security and Compensation** 45 | > a secure way to store your artwork 46 | 47 | Through encryption and the decentralized storage mechanism of Arweave, the only way your follower has access to the source file is by payment. 48 | Payments are facilitated in Ar token, the native currency of the permaweb. Once the payment is made, we will generate a receipt including the UNIX timestamp, the price paid to you, the target artwork purchased, and your address. 49 | 50 | **Thank you for choosing ArcLight!** 51 | 52 | ## Credits 53 | 54 | [@LittleSound](https://github.com/LittleSound) - Major Developer 55 | 56 | [@Garfield550](https://github.com/Garfield550) - Major Developer 57 | 58 | [@LemonNeko](https://github.com/LemonNekoGH) - Major Developer 59 | 60 | [@KagurazakaIzumi](https://github.com/KagurazakaIzumi) - Localization for Simplified/Traditional Chinese, Japanese 61 | 62 | 63 | ### Interact with Arweave 64 | ``` 65 | yarn dev 66 | ``` 67 | 68 | ### Deploy using a key 69 | ``` 70 | arweave deploy-dir ./dist --key-file ./secrets/key.json 71 | ``` 72 | 73 | ### Reference 74 | [arweave interface](https://www.arweave.org/build) 75 | [Arweave.js](https://github.com/ArweaveTeam/arweave-js): A library for interacting with the Arweave network from web applications and node.js programs. 76 | [Arweave Deploy](https://github.com/ArweaveTeam/arweave-deploy): A simple command line tool for deploying web apps, pages, and other files to the permaweb. 77 | ## Build Setup 78 | 79 | ``` bash 80 | # install dependencies 81 | yarn 82 | 83 | # serve with hot reload at localhost:8080 84 | yarn dev 85 | 86 | # build for production with minification 87 | yarn build 88 | 89 | # build for production and view the bundle analyzer report 90 | yarn build --report 91 | 92 | # #FB5B9E 93 | ``` 94 | -------------------------------------------------------------------------------- /src/pages/Playlist/index.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 101 | 102 | 149 | -------------------------------------------------------------------------------- /src/pages/Podcast/Index.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 101 | 102 | 149 | -------------------------------------------------------------------------------- /src/pages/Sound/Index.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 107 | 108 | 155 | -------------------------------------------------------------------------------- /src/components/uploadPriceReceipt.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 120 | 121 | 165 | -------------------------------------------------------------------------------- /src/pages/Landing.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 32 | 33 | 225 | -------------------------------------------------------------------------------- /src/components/Search.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 122 | 123 | 147 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | import Landing from '../pages/Landing.vue' 5 | import Music from '../pages/Music/_id.vue' 6 | import Album from '../pages/Album/_id.vue' 7 | 8 | import Songs from '../pages/Songs/Index' 9 | import SongsSingles from '../pages/Songs/Singles' 10 | import SongsAlbums from '../pages/Songs/Albums' 11 | import Sound from '../pages/Sound/Index' 12 | import Podcast from '../pages/Podcast/Index' 13 | import Playlist from '../pages/Playlist/index' 14 | import PlaylistItems from '../pages/Playlist/_id' 15 | 16 | import User from '../pages/User/_id' 17 | import UserIndex from '../pages/User/index' 18 | 19 | import UserSingle from '../pages/User/Single' 20 | import UserAlbum from '../pages/User/Album' 21 | import UserSound from '../pages/User/Sound' 22 | import UserPodcast from '../pages/User/Podcast' 23 | 24 | import Edit from '../pages/User/Edit.vue' 25 | import Library from '../pages/Library' 26 | 27 | Vue.use(VueRouter) 28 | 29 | const routes = [ 30 | { 31 | path: '/', 32 | name: 'Landing', 33 | component: Landing 34 | }, 35 | { 36 | path: '/about', 37 | name: 'About', 38 | component: () => import(/* webpackChunkName: "about" */ '../pages/About.vue') 39 | }, 40 | { 41 | path: '/upload', 42 | name: 'Upload', 43 | component: () => import(/* webpackChunkName: "upload" */ '../pages/Upload/Index.vue') 44 | }, 45 | { 46 | path: '/upload/single', 47 | name: 'uploadSingle', 48 | component: () => import(/* webpackChunkName: "upload" */ '../pages/Upload/Single.vue') 49 | }, 50 | { 51 | path: '/review/single', 52 | name: 'ReviewSingle', 53 | component: () => import(/* webpackChunkName: "upload" */ '../pages/Upload/ReviewSingle.vue') 54 | }, 55 | { 56 | path: '/upload/album', 57 | name: 'uploadAlbum', 58 | component: () => import(/* webpackChunkName: "upload" */ '../pages/Upload/Album.vue') 59 | }, 60 | { 61 | path: '/review/album', 62 | name: 'ReviewAlbum', 63 | component: () => import(/* webpackChunkName: "upload" */ '../pages/Upload/ReviewAlbum.vue') 64 | }, 65 | { 66 | path: '/upload/podcast', 67 | name: 'uploadPodcast', 68 | component: () => import(/* webpackChunkName: "upload" */ '../pages/Upload/Podcast.vue') 69 | }, 70 | { 71 | path: '/review/podcast', 72 | name: 'ReviewPodcast', 73 | component: () => import(/* webpackChunkName: "upload" */ '../pages/Upload/ReviewPodcast.vue') 74 | }, 75 | { 76 | path: '/upload/soundeffect', 77 | name: 'uploadSoundEffect', 78 | component: () => import(/* webpackChunkName: "upload" */ '../pages/Upload/SoundEffect.vue') 79 | }, 80 | { 81 | path: '/review/soundeffect', 82 | name: 'ReviewSoundEffect', 83 | component: () => import(/* webpackChunkName: "upload" */ '../pages/Upload/ReviewSoundEffect.vue') 84 | }, 85 | { 86 | path: '/music/:id', 87 | name: 'Music', 88 | component: Music, 89 | props: true 90 | }, 91 | { 92 | path: '/album/:id', 93 | name: 'Album', 94 | component: Album, 95 | props: true 96 | }, 97 | { 98 | path: '/songs', 99 | name: 'Songs', 100 | component: Songs 101 | }, 102 | { 103 | path: '/songs/singles', 104 | name: 'SongsSingles', 105 | component: SongsSingles 106 | }, 107 | { 108 | path: '/songs/albums', 109 | name: 'SongsAlbums', 110 | component: SongsAlbums 111 | }, 112 | { 113 | path: '/sound', 114 | name: 'Sound', 115 | component: Sound 116 | }, 117 | { 118 | path: '/podcast', 119 | name: 'Podcast', 120 | component: Podcast 121 | }, 122 | { 123 | path: '/playlist', 124 | name: 'Playlist', 125 | component: Playlist 126 | }, 127 | { 128 | path: '/playlist/:id', 129 | name: 'PlaylistItem', 130 | component: PlaylistItems, 131 | props: true 132 | }, 133 | { 134 | path: '/user/:id', 135 | component: User, 136 | children: [ 137 | { 138 | path: '', 139 | name: 'User', 140 | component: UserIndex, 141 | props: true 142 | }, 143 | { 144 | path: 'single', 145 | name: 'UserSingle', 146 | component: UserSingle, 147 | props: true 148 | }, 149 | { 150 | path: 'album', 151 | name: 'UserAlbum', 152 | component: UserAlbum, 153 | props: true 154 | }, 155 | { 156 | path: 'sound', 157 | name: 'UserSound', 158 | component: UserSound, 159 | props: true 160 | }, 161 | { 162 | path: 'podcast', 163 | name: 'UserPodcast', 164 | component: UserPodcast, 165 | props: true 166 | } 167 | ] 168 | }, 169 | { 170 | path: '/user/:id/edit', 171 | name: 'Edit', 172 | component: Edit 173 | }, 174 | { 175 | path: '/library', 176 | name: 'Library', 177 | component: Library 178 | } 179 | ] 180 | 181 | const router = new VueRouter({ 182 | routes 183 | }) 184 | 185 | export default router 186 | -------------------------------------------------------------------------------- /src/pages/About.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 88 | 89 | 188 | -------------------------------------------------------------------------------- /src/components/Song/SingleCard.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 100 | 101 | 204 | -------------------------------------------------------------------------------- /src/pages/Songs/Singles.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 117 | 118 | 188 | -------------------------------------------------------------------------------- /src/pages/Songs/Albums.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 118 | 119 | 189 | -------------------------------------------------------------------------------- /src/pages/Library.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 144 | 145 | 211 | -------------------------------------------------------------------------------- /src/components/Song/AlbumCard.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 104 | 105 | 229 | -------------------------------------------------------------------------------- /src/components/KeyReader.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 151 | 152 | 197 | -------------------------------------------------------------------------------- /src/components/ScrollXBox.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 146 | 147 | 212 | -------------------------------------------------------------------------------- /src/components/User/UserAllSellings.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 145 | 146 | 214 | -------------------------------------------------------------------------------- /src/components/PlayerPlaylist.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 82 | 83 | 262 | -------------------------------------------------------------------------------- /src/components/imgUpload/cropper.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Cropper.js v1.5.1 3 | * https://fengyuanchen.github.io/cropperjs 4 | * 5 | * Copyright 2015-present Chen Fengyuan 6 | * Released under the MIT license 7 | * 8 | * Date: 2019-03-10T09:55:50.492Z 9 | */ 10 | 11 | .cropper-container { 12 | direction: ltr; 13 | font-size: 0; 14 | line-height: 0; 15 | position: relative; 16 | -ms-touch-action: none; 17 | touch-action: none; 18 | -webkit-user-select: none; 19 | -moz-user-select: none; 20 | -ms-user-select: none; 21 | user-select: none; 22 | } 23 | 24 | .cropper-container img { 25 | display: block; 26 | height: 100%; 27 | image-orientation: 0deg; 28 | max-height: none !important; 29 | max-width: none !important; 30 | min-height: 0 !important; 31 | min-width: 0 !important; 32 | width: 100%; 33 | } 34 | 35 | .cropper-wrap-box, 36 | .cropper-canvas, 37 | .cropper-drag-box, 38 | .cropper-crop-box, 39 | .cropper-modal { 40 | bottom: 0; 41 | left: 0; 42 | position: absolute; 43 | right: 0; 44 | top: 0; 45 | } 46 | 47 | .cropper-wrap-box, 48 | .cropper-canvas { 49 | overflow: hidden; 50 | } 51 | 52 | .cropper-drag-box { 53 | background-color: #fff; 54 | opacity: 0; 55 | } 56 | 57 | .cropper-modal { 58 | background-color: #000; 59 | opacity: 0.5; 60 | } 61 | 62 | .cropper-view-box { 63 | display: block; 64 | height: 100%; 65 | outline: 1px solid #39f; 66 | outline-color: rgba(51, 153, 255, 0.75); 67 | overflow: hidden; 68 | width: 100%; 69 | } 70 | 71 | .cropper-dashed { 72 | border: 0 dashed #eee; 73 | display: block; 74 | opacity: 0.5; 75 | position: absolute; 76 | } 77 | 78 | .cropper-dashed.dashed-h { 79 | border-bottom-width: 1px; 80 | border-top-width: 1px; 81 | height: calc(100% / 3); 82 | left: 0; 83 | top: calc(100% / 3); 84 | width: 100%; 85 | } 86 | 87 | .cropper-dashed.dashed-v { 88 | border-left-width: 1px; 89 | border-right-width: 1px; 90 | height: 100%; 91 | left: calc(100% / 3); 92 | top: 0; 93 | width: calc(100% / 3); 94 | } 95 | 96 | .cropper-center { 97 | display: block; 98 | height: 0; 99 | left: 50%; 100 | opacity: 0.75; 101 | position: absolute; 102 | top: 50%; 103 | width: 0; 104 | } 105 | 106 | .cropper-center::before, 107 | .cropper-center::after { 108 | background-color: #eee; 109 | content: ' '; 110 | display: block; 111 | position: absolute; 112 | } 113 | 114 | .cropper-center::before { 115 | height: 1px; 116 | left: -3px; 117 | top: 0; 118 | width: 7px; 119 | } 120 | 121 | .cropper-center::after { 122 | height: 7px; 123 | left: 0; 124 | top: -3px; 125 | width: 1px; 126 | } 127 | 128 | .cropper-face, 129 | .cropper-line, 130 | .cropper-point { 131 | display: block; 132 | height: 100%; 133 | opacity: 0.1; 134 | position: absolute; 135 | width: 100%; 136 | } 137 | 138 | .cropper-face { 139 | background-color: #fff; 140 | left: 0; 141 | top: 0; 142 | } 143 | 144 | .cropper-line { 145 | background-color: #39f; 146 | } 147 | 148 | .cropper-line.line-e { 149 | cursor: ew-resize; 150 | right: -3px; 151 | top: 0; 152 | width: 5px; 153 | } 154 | 155 | .cropper-line.line-n { 156 | cursor: ns-resize; 157 | height: 5px; 158 | left: 0; 159 | top: -3px; 160 | } 161 | 162 | .cropper-line.line-w { 163 | cursor: ew-resize; 164 | left: -3px; 165 | top: 0; 166 | width: 5px; 167 | } 168 | 169 | .cropper-line.line-s { 170 | bottom: -3px; 171 | cursor: ns-resize; 172 | height: 5px; 173 | left: 0; 174 | } 175 | 176 | .cropper-point { 177 | background-color: #39f; 178 | height: 5px; 179 | opacity: 0.75; 180 | width: 5px; 181 | } 182 | 183 | .cropper-point.point-e { 184 | cursor: ew-resize; 185 | margin-top: -3px; 186 | right: -3px; 187 | top: 50%; 188 | } 189 | 190 | .cropper-point.point-n { 191 | cursor: ns-resize; 192 | left: 50%; 193 | margin-left: -3px; 194 | top: -3px; 195 | } 196 | 197 | .cropper-point.point-w { 198 | cursor: ew-resize; 199 | left: -3px; 200 | margin-top: -3px; 201 | top: 50%; 202 | } 203 | 204 | .cropper-point.point-s { 205 | bottom: -3px; 206 | cursor: s-resize; 207 | left: 50%; 208 | margin-left: -3px; 209 | } 210 | 211 | .cropper-point.point-ne { 212 | cursor: nesw-resize; 213 | right: -3px; 214 | top: -3px; 215 | } 216 | 217 | .cropper-point.point-nw { 218 | cursor: nwse-resize; 219 | left: -3px; 220 | top: -3px; 221 | } 222 | 223 | .cropper-point.point-sw { 224 | bottom: -3px; 225 | cursor: nesw-resize; 226 | left: -3px; 227 | } 228 | 229 | .cropper-point.point-se { 230 | bottom: -3px; 231 | cursor: nwse-resize; 232 | height: 20px; 233 | opacity: 1; 234 | right: -3px; 235 | width: 20px; 236 | } 237 | 238 | @media (min-width: 768px) { 239 | .cropper-point.point-se { 240 | height: 15px; 241 | width: 15px; 242 | } 243 | } 244 | 245 | @media (min-width: 992px) { 246 | .cropper-point.point-se { 247 | height: 10px; 248 | width: 10px; 249 | } 250 | } 251 | 252 | @media (min-width: 1200px) { 253 | .cropper-point.point-se { 254 | height: 5px; 255 | opacity: 0.75; 256 | width: 5px; 257 | } 258 | } 259 | 260 | .cropper-point.point-se::before { 261 | background-color: #39f; 262 | bottom: -50%; 263 | content: ' '; 264 | display: block; 265 | height: 200%; 266 | opacity: 0; 267 | position: absolute; 268 | right: -50%; 269 | width: 200%; 270 | } 271 | 272 | .cropper-invisible { 273 | opacity: 0; 274 | } 275 | 276 | .cropper-bg { 277 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC'); 278 | } 279 | 280 | .cropper-hide { 281 | display: block; 282 | height: 0; 283 | position: absolute; 284 | width: 0; 285 | } 286 | 287 | .cropper-hidden { 288 | display: none !important; 289 | } 290 | 291 | .cropper-move { 292 | cursor: move; 293 | } 294 | 295 | .cropper-crop { 296 | cursor: crosshair; 297 | } 298 | 299 | .cropper-disabled .cropper-drag-box, 300 | .cropper-disabled .cropper-face, 301 | .cropper-disabled .cropper-line, 302 | .cropper-disabled .cropper-point { 303 | cursor: not-allowed; 304 | } 305 | -------------------------------------------------------------------------------- /src/pages/Songs/Index.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 148 | 149 | 236 | -------------------------------------------------------------------------------- /src/pages/Upload/Index.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 187 | 188 | 250 | -------------------------------------------------------------------------------- /src/components/User/UserInfo.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 180 | 181 | 282 | -------------------------------------------------------------------------------- /src/components/Album/AlbumInfo.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 115 | 116 | 291 | -------------------------------------------------------------------------------- /src/components/Playlist/PlaylistInfo.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 115 | 116 | 291 | -------------------------------------------------------------------------------- /src/locales/zh-cn.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // landing 3 | startBrowsing: '开始浏览', 4 | whyArcLight: '为什么选择 ArcLight', 5 | developedBy: '开发团队', 6 | forProject: '为', 7 | developedApp: '计划而开发', 8 | arclight: 'ArcLight', 9 | // about 10 | arcLightLocalized: 'ArcLight (弧光)', 11 | title1: '基于永存网的艺术品发布平台', 12 | subtitle1: '我们旨在让', 13 | subtitle1part2: '成为一个完全公平自由的艺术品发布平台', 14 | desp1part1: '我们使用的是 Arweave(区块纺)提供的永存网,这是一种采用区块链技术构建的永久性和分散式的数据存储类型。在您上传您的作品前,我们会对您提交到浏览器的作品进行加密,使其成为在区块链上不可直接读取的文件类型,您将获得知识产权的完全所有权。作为艺术家,您甚至可以自定义属于你的价格来鼓励用户通过购买来支持您。', 15 | desp1part2: '在 ArcLight 平台上,我们可以省去中间人和经纪人带来的抽成,以此来把收益的所有权还给作者。', 16 | 17 | title2: '多种作品类型的支持', 18 | desp2part1: '上传多种不同格式的作品!ArcLight 支持从单曲到播客,甚至是您录制的音效,以及精美的电子绘画作品,只要是能够授予(不包含版权)用户拥有的电子媒体格式通通都可以被支持。在最适合您的媒体中表达您的创造力。ArcLight 将为您提供存储创作并从中获得收益的办法!', 19 | weSupport: '我们支持', 20 | singleMusic: '单曲', 21 | albums: '专辑', 22 | podcasts: '播客', 23 | andWithComma: ',以及 ', 24 | soundEffects: '音效', 25 | paintings: '绘画作品', 26 | 27 | title3: '数据安全与补偿', 28 | subtitle3: '', 29 | desp3part1: '通过加密和 Arweave 的分散储存机制,我们可以限制用户通过区块链数据访问来获得需要付费的项目,获得原文件的唯一方法是通过付费来完成。交易都必须通过 AR 代币或是将来支持的内部代币进行,AR 代币是 Arweave 永存网的流通货币。在用户付款后,我们将生成一个特定的收据来证明你拥有这个作品,包括 UNIX 时间戳,已经支付给您的价格,购买的目标作品以及您的钱包地址。', 30 | thankYouForChoosingArcLight: '感谢您选择 ArcLight!', 31 | secureWay: '安全地', 32 | toStoreYourArtworks: '储存您的作品', 33 | // header 34 | music: '音乐', 35 | about: '关于', 36 | login: '登录', 37 | uploadYourKey: '提供你的私钥', 38 | insertYourKey: '选择你的私钥文件', 39 | saveYourKeyInCookie: '7 天内使用保存的私钥自动登录', 40 | accountHasNoBalance: '账户余额为零,请使用另一个账户再试一次', 41 | accountHasErroredTx: '你的账户中存在错误的交易,请检查你的余额再试一次', 42 | uploadMusic: '上传音乐', 43 | searchPlaceholder: '搜索 地址 / 用户 / 音乐', 44 | // account menu 45 | myProfile: '个人主页', 46 | myLibrary: '我的音乐库', 47 | signOut: '登出', 48 | // file message 49 | fileReadSuccess: '文件读取成功', 50 | fileReadFail: '文件读取失败,请再试一次', 51 | imageReadSuccess: '图片读取成功', 52 | // page title 53 | mainTitle: '去中心化音乐平台', 54 | uploadNewSingle: '上传新单曲', 55 | uploadNewAlbum: '上传新专辑', 56 | uploadNewPodcast: '上传新播客', 57 | uploadNewSoundEffect: '上传新音效', 58 | reviewYourUpload: '检查你的上传信息', 59 | browseAllAlbum: '所有专辑', 60 | browseAllSingle: '所有单曲', 61 | browseAllMusic: '所有曲目', 62 | browseAllPodcast: '所有播客', 63 | browseAllSound: '所有音效', 64 | browseAllPlaylist: '所有歌单', 65 | profile: '个人主页', 66 | profileOf: '的个人主页', 67 | // general 68 | cacheLogin: '登录', 69 | cacheUpload: '本地保存', 70 | upload: '上传', 71 | download: '下载', 72 | close: '关闭', 73 | back: '返回', 74 | search: '搜索', 75 | noData: '无数据', 76 | single: '单曲', 77 | album: '专辑', 78 | podcast: '播客', 79 | soundEffect: '音效', 80 | loading: '加载中...', 81 | artistLoading: '加载作曲者中...', 82 | genre: '音乐流派', 83 | category: '类别', 84 | price: '价格', 85 | fee: '手续费', 86 | tip: '额外支付', 87 | review: '检查上传表单', 88 | verify: '验证上传文件', 89 | transaction: '交易', 90 | confirm: '确定', 91 | submit: '提交', 92 | done: '完成', 93 | avatar: '头像', 94 | username: '用户名', 95 | save: '保存', 96 | free: '免费', 97 | pay: '支付', 98 | pleaseWait: '请等待. . . ', 99 | time: '时间', 100 | // payment 101 | buy: '购买', 102 | paymentOf: '购买', 103 | pleaseInsertYourWalletKey: '请将您的钱包拖到此框中或是点击选中钱包以完成付款。', 104 | uploadKey: '上传私钥', 105 | orderReviewWarningPart1: '请仔细查看有关您订单的以下信息,当您准备好付款后,请点击', 106 | orderReviewWarningPart2: '按钮', 107 | toDeveloper: '给开发者', 108 | toCommunity: '给社区', 109 | paymentTotal: '总计', 110 | // songs 111 | newSingleSelling: '最新发布单曲', 112 | allSelling: '所有作品', 113 | newAlbumSelling: '最新发布专辑', 114 | newPodcastSelling: '最新发布播客', 115 | newSoundEffectSelling: '最新发布音效', 116 | navSONG: '音乐', 117 | navSOUND: '音效', 118 | navPODCAST: '播客', 119 | navPLAYLIST: '歌单', 120 | podcastSelling: '所有播客作品', 121 | soundSelling: '所有音效作品', 122 | playlistOnline: '线上的歌单', 123 | // music 124 | titleLoading: '名称加载中...', 125 | awaitData: '等待数据...', 126 | musicLoading: '音乐加载中...', 127 | genreFilter: '音乐流派过滤', 128 | genreFilterEmpty: '过滤器无数据', 129 | viewSimilarArtwork: '查看相似歌曲', 130 | thereIsNoDemoVersionOfThisArtwork: '此作品没有试听版本', 131 | waitingForBlockConfirm: '等待交易合并到新区块中,这可能需要几分钟。您可以离开此页面浏览其他内容', 132 | succeedToUnlockMusic: '成功解锁音乐!', 133 | backToMusicPlayer: '返回音乐播放器', 134 | // profile 135 | similarAuthors: '相似作曲家', 136 | editProfile: '编辑个人主页', 137 | noIntroductionYet: '暂无个人简介', 138 | accountInvalid: '账号无效', 139 | // profile edit 140 | backToProfile: '返回个人主页', 141 | avatarEditInfo1: '很抱歉,我们在这里不提供任何头像存储的功能', 142 | avatarEditInfo2: '要变更你的头像,请前往 ', 143 | usernameEditInfo1: '很抱歉,我们在这里不提供任何用户名存储的功能', 144 | usernameEditInfo2: '要变更你的用户名,请前往 ', 145 | location: '地点', 146 | whereDoYouLive: '你居住在哪里?', 147 | officialWebsite: '个人或乐队网站', 148 | websiteAddress: '网站地址...', 149 | introduction: '个人简介', 150 | howDoYouDescribeYourSelf: '你怎么描述你自己?', 151 | neteaseCloudMusic: '网易云音乐', 152 | digitsOfYourId: '你的 id 就是数字串', 153 | usernameOfSoundCloud: '用户名跟随在 soundcloud.com/ 之后', 154 | usernameOfBandcamp: '用户名就在 用户名.bandcamp.com', 155 | pleaseLoginFirst: '请先登录', 156 | locationUpdateSuccess: '地点更新成功', 157 | websiteUpdateSuccess: '网站更新成功', 158 | introductionUpdateSuccess: '个人简介更新成功', 159 | neteaseIdUpdateSuccess: '网易云音乐 ID 更新成功', 160 | soundCloudIdUpdateSuccess: 'SoundCloud ID 更新成功', 161 | bandcampIdUpdateSuccess: 'Bandcamp ID 更新成功', 162 | websiteUrlInvalid: '无效的网站 URL 地址', 163 | introductionHasLimit: '个人简介不得超出 1000 个字符', 164 | neteaseCloudMusicIdInvalid: '无效的网易云音乐用户 ID', 165 | // library 166 | from: '来自', 167 | txPendingPleaseWait: '交易正在等待被确认,请等待...', 168 | // upload 169 | chooseType: '选择你想要上传的类型', 170 | backToSelection: '返回选择菜单', 171 | singleCover: '单曲封面', 172 | albumCover: '专辑封面', 173 | podcastCover: '播客封面', 174 | soundEffectCover: '音效封面', 175 | musicName: '单曲名称', 176 | podcastTitle: '播客名称 (更新到老的播客请使用同一个名称)', 177 | programTitle: '节目名称', 178 | soundEffectName: '音效名称', 179 | enterYourMusicTitle: '请输入你的单曲名称...', 180 | enterYourAlbumTitle: '请输入你的专辑名称...', 181 | enterYourPodcastTitle: '请输入你的播客名称...', 182 | enterYourProgramTitle: '请输入你的节目名称...', 183 | enterYourSoundEffectTitle: '请输入你的音效名称...', 184 | uploadDescription: '简介 (使用 \\n 来换行)', 185 | yourSingleDescription: '请输入你的单曲简介...', 186 | yourAlbumDescritption: '请输入你的专辑简介...', 187 | yourPodcastDescription: '请输入你的节目简介...', 188 | yourSoundEffectDescription: '请输入你的音效简介...', 189 | selectYourFile: '选择你的文件', 190 | demoDuration: '试听长度', 191 | pleaseUploadYourArtwork: '请上传你的作品...', 192 | loginIsRequiredToUpload: '必须登录才能上传作品', 193 | usernameIsRequiredToUpload: '必须设置用户名后才能上传作品', 194 | selectDemoDuration: '选择试听长度', 195 | searchGenre: '搜索音乐流派', 196 | genreIsRequiredToUpload: '请为你的作品选择你的音乐流派 (None 为留空)', 197 | demoDurationIsRequiredToUpload: '请选择试听长度', 198 | priceMustBeNumber: '价格必须设置为数字', 199 | priceCantBeNegative: '价格不可以为负值', 200 | demoCantBeSetToFreeMusic: '你不可以给免费音乐设置试听时长', 201 | // upload fee 202 | feeToUpload: '上传所需费用', 203 | audio: '音频', 204 | cover: '封面', 205 | txInfo: '交易信息', 206 | // image upload 207 | editCover: '编辑封面', 208 | adjustSizeAndPositionOfImage: '调整图片的缩放大小和位置', 209 | selectImage: '选择图片', 210 | imageTooBig: '图片大小太大', 211 | autoCompressImageFail: '自动压缩图片失败,再试一次', 212 | imageUploadFail: '上传失败', 213 | // single 214 | singleCoverIsRequiredToUpload: '需要设置封面才能发布单曲', 215 | singleTitleIsRequiredToUpload: '需要填写标题才能发布单曲', 216 | singleDespIsRequiredToUpload: '需要填写简介才能发布单曲', 217 | singleSourceFileIsRequiredToUpload: '需要选中上传文件才能发布单曲', 218 | // album 219 | albumCoverIsRequiredToUpload: '需要设置封面才能发布专辑', 220 | albumTitleIsRequiredToUpload: '需要填写标题才能发布专辑', 221 | albumDespIsRequiredToUpload: '需要填写简介才能发布专辑', 222 | albumPleaseUploadAtLeast: '请上传至少 ', 223 | albumPleaseUploadAtLeastFile: ' 个文件', 224 | albumSongShouldHavePriceOrFree: '专辑内单曲只能一一填写价格或完全免费', 225 | albumSourceFileIsRequiredToUpload: '专辑内缺失了应包含的曲目源文件', 226 | albumMusicTitleIsRequiredToUpload: '音乐必须具有标题给 #', 227 | albumPriceWillAlwaysBe80Percent: '专辑总价格将会永远是你的所有单曲总价格的 80%', 228 | // podcast 229 | podcastCoverIsRequiredToUpload: '需要设置封面才能发布播客', 230 | podcastTitleIsRequiredToUpload: '需要填写播客标题才能发布播客', 231 | podcastProgramTitleIsRequiredToUpload: '需要填写节目名称才能发布播客', 232 | podcastDespIsRequiredToUpload: '需要填写简介才能发布播客', 233 | searchCategory: '搜索类别', 234 | podcastCategoryIsRequiredToUpload: '请为你的播客节目选择类别 (None 为留空)', 235 | podcastSourceFileIsRequiredToUpload: '需要选中上传文件才能发布播客', 236 | // sound Effect 237 | soundEffectCoverIsRequiredToUpload: '需要设置封面才能发布音效', 238 | soundEffectTitleIsRequiredToUpload: '需要填写标题才能发布音效', 239 | soundEffectDespIsRequiredToUpload: '需要填写简介才能发布音效', 240 | soundEffectSourceFileIsRequiredToUpload: '需要选中上传文件才能发布音效', 241 | // review 242 | backToUpload: '返回上传表单', 243 | singleWarning: '请在此仔细检查你的单曲上传内容,', 244 | albumWarning: '请在此仔细检查你的专辑上传内容,', 245 | podcastWarning: '请在此仔细检查你的播客上传内容,', 246 | soundEffectWarning: '请在此仔细检查你的音效上传内容,', 247 | reviewWarning: '如果没有任何问题,你就可以提交你的上传请求了', 248 | priceCost: '价格消耗', 249 | uploadingCover: '正在上传封面...', 250 | uploadingMusic: '正在上传曲目...', 251 | uploadSuccess: '上传成功!', 252 | uploadPending: '上传正在等待确定', 253 | uploadPendingInfo: '你的作品已经被提交至 Arweave Permaweb Storage(永存网存储中心)。你的作品在上传结束之后可能不会立即可用,数据需要区块链上的矿工进行数据处理后帮你存储至下一个区块。请耐心等待,你的作品将会被永远存储起来!', 254 | unknownErrorOccurred: '发生了未知错误', 255 | play: '播放', 256 | playDemo: '试听', 257 | clear: '清除', 258 | playlist: '播放列表', 259 | // login 260 | thisIsNotArweaveKey: '这不是正确的Arweave密钥,请重新上传一个正确的密钥', 261 | loginConnectionTimeout: '登录时连接超时,请检查您的网络连接,然后重试', 262 | gettingAvatarTimeout: '获取头像时出错' 263 | } 264 | --------------------------------------------------------------------------------