Merge pull request 'rebase' (#1) from RemixDev/deemix-webui:main into main

Reviewed-on: https://codeberg.org/wolfwork/deemix-webui/pulls/1
This commit is contained in:
wolfwork 2020-09-23 09:11:21 +02:00
commit d886194d6f
63 changed files with 2766 additions and 1974 deletions

View File

@ -34,12 +34,14 @@ By simply running
$ npm run dev
```
P.S.: You need to be inside the [deemix-pyweb](https://codeberg.org/RemixDev/deemix-pyweb) repo to have the server working correctly.
you will have 3 tasks running at the same time:
- the [Python](https://www.python.org/) server
- the server
- the [rollup](https://rollupjs.org/guide/en/) watcher pointing to the configured `.js` file and ready to re-bundle
- the [SASS](https://sass-lang.com/) compiler watching for `.scss` files changes
Note that in development mode 2 more files, `bundle.js.map` and `style.css.map`, will be created in the public folder. These files will be deleted when running the build command, so you don't need to worry about them.
Note that in development mode 2 more files (`bundle.js.map` and `style.css.map`) will be created in the public folder. These files will be deleted when running the build command, so you don't need to worry about them.
**You can now go to http://127.0.0.1:6595 and see the app running.**
@ -51,7 +53,7 @@ However, if you need to edit the `public/index.html` file you'll have to kill th
### Adding files
If you want to add new super-awesome `.js` files, just add them. Deemix uses ES6 synthax, so you'll probably need to export some functions or variables from your new file. Files that will export and import nothing will be ignored by the bundler (rollup).
If you want to add new super-awesome `.js` or `.vue` files, just add them. Deemix uses ES6 synthax, so you'll probably need to export some functions or variables from your new file. Files that will export and import nothing will be ignored by the bundler (rollup).
If you want to add new mega-awesome `.scss` (style) files, you need to import them in the main `style.scss` file. The `.scss` files **must** all start with an underscore _, except for the `style.scss` file.

View File

@ -1,38 +1,15 @@
# deemix-webui
This is just the WebUI for deemix, it should be used with deemix-pyweb or something like that
This is just the WebUI for deemix, it should be used with deemix-pyweb or something like that.
If you are a web developer and want to contribute to this project, please read the [COMPILE-UI](COMPILE-UI.md) file.
## What's left to do?
## Related projects
- [ ] Use Vue app-wide
- [X] First step: rewrite the app in Single File Components way
- [ ] Second step: Implement routing for the whole app using Vue Router ⚒
- [ ] Third step: Remove jQuery
- [ ] Implement custom contextmenu ⚒
- [X] Copy and paste functions
- [X] Copy Link where possible
- [X] Download Quality
- [X] Copy Image URL where possible
- [ ] Resolve cut/copy/paste compatibility issues
- [ ] Make i18n async (https://kazupon.github.io/vue-i18n/guide/lazy-loading.html)
- Use ES2020 async imports, if possible
- [ ] Make the UI look coherent
- [ ] Style buttons
- [ ] Style text inputs
- [ ] Style checkboxes
- [ ] Search tab
- [ ] Better placeholer before search
- [ ] Link Analyzer
- [ ] Better placeholer before analyzing and error feedback
- [ ] Settings tab
- [ ] Variable selector near template inputs
- Maybe tabbing the section for easy navigation
- Could use a carousel, but it's not worth adding a new dep
- [ ] Block selection where it's not needed (keep only titles artists albums labels and useful data)
- There's a SASS mixin for this. Need to use it in the proper classes
- [ ] Better feedback for socket.io possible errors
- [X] Remove images size limit and add warning if > 1200
- ?
You can find more informations about deemix at https://deemix.app/
- [deemix](https://codeberg.org/RemixDev/deemix)
- [deemix-pyweb](https://codeberg.org/RemixDev/deemix-pyweb)
- [deemix-tools](https://codeberg.org/RemixDev/deemix-tools)
# License

26
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "deemix",
"version": "1.0.16",
"name": "deemix-webui",
"version": "0.2.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -1241,11 +1241,6 @@
}
}
},
"jquery": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
},
"js-base64": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.2.tgz",
@ -2795,9 +2790,9 @@
}
},
"vue": {
"version": "2.6.11",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.11.tgz",
"integrity": "sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ=="
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
},
"vue-i18n": {
"version": "8.18.2",
@ -2810,9 +2805,9 @@
"integrity": "sha512-SdKRBeoXUjaZ9R/8AyxsdTqkOfMcI5tWxPZOUX5Ie1BTL5rPSZ0O++pbiZCeYeythiZIdLEfkDiQPKIaWk5hDg=="
},
"vue-template-compiler": {
"version": "2.6.11",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.11.tgz",
"integrity": "sha512-KIq15bvQDrcCjpGjrAhx4mUlyyHfdmTaoNfeoATHLAiWB+MU3cx4lOzMwrnUh9cCxy0Lt1T11hAFY6TQgroUAA==",
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.12.tgz",
"integrity": "sha512-OzzZ52zS41YUbkCBfdXShQTe69j1gQDZ9HIX8miuC9C3rBCk9wIRjLiZZLrmX9V+Ftq/YEyv1JaVr5Y/hNtByg==",
"dev": true,
"requires": {
"de-indent": "^1.0.2",
@ -2825,6 +2820,11 @@
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
"dev": true
},
"vuex": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.5.1.tgz",
"integrity": "sha512-w7oJzmHQs0FM9LXodfskhw9wgKBiaB+totOdb8sNzbTB2KDCEEwEs29NzBZFh/lmEK1t5tDmM1vtsO7ubG1DFw=="
},
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",

View File

@ -1,6 +1,6 @@
{
"name": "deemix",
"version": "1.0.16",
"name": "deemix-webui",
"version": "0.2.0",
"homepage": "https://codeberg.org/RemixDev/deemix-webui/src/master/README.md",
"repository": {
"type": "git",
@ -13,17 +13,19 @@
"build:js": "rollup -c",
"watch:js": "rollup -c -w",
"serve": "python ../server.py",
"serve:gui": "python ../deemix-pyweb.py --dev",
"dev": "npm-run-all --parallel serve watch:js watch:styles",
"dev:gui": "npm-run-all --parallel serve:gui watch:js watch:styles",
"build": "npm-run-all --sequential clean build:js build:styles"
},
"dependencies": {
"jquery": "^3.5.1",
"lodash-es": "^4.17.15",
"svg-country-flags": "^1.2.7",
"toastify-js": "^1.8.0",
"vue": "^2.6.11",
"vue": "^2.6.12",
"vue-i18n": "^8.18.2",
"vue-router": "^3.3.4"
"vue-router": "^3.3.4",
"vuex": "^3.5.1"
},
"devDependencies": {
"@rollup/plugin-alias": "^3.1.0",
@ -39,6 +41,6 @@
"rollup-plugin-terser": "^6.1.0",
"rollup-plugin-vue": "^4.2.0",
"sass": "^1.26.8",
"vue-template-compiler": "^2.6.11"
"vue-template-compiler": "^2.6.12"
}
}

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,7 @@
font-style: italic;
font-weight: 300;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKWyV9hmIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKWyV9hmIqOjjg.woff2') format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@ -13,7 +13,7 @@
font-style: italic;
font-weight: 300;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKWyV9hvIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKWyV9hvIqOjjg.woff2') format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@ -22,7 +22,7 @@
font-style: italic;
font-weight: 300;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKWyV9hnIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKWyV9hnIqOjjg.woff2') format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@ -31,7 +31,7 @@
font-style: italic;
font-weight: 300;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKWyV9hoIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKWyV9hoIqOjjg.woff2') format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@ -40,7 +40,7 @@
font-style: italic;
font-weight: 300;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKWyV9hkIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKWyV9hkIqOjjg.woff2') format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@ -49,7 +49,7 @@
font-style: italic;
font-weight: 300;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKWyV9hlIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKWyV9hlIqOjjg.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@ -58,7 +58,7 @@
font-style: italic;
font-weight: 300;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKWyV9hrIqM.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKWyV9hrIqM.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@ -67,7 +67,7 @@
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('/public/fonts/OpenSans/mem6YaGs126MiZpBA-UFUK0Udc1UAw.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem6YaGs126MiZpBA-UFUK0Udc1UAw.woff2') format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@ -76,7 +76,7 @@
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('/public/fonts/OpenSans/mem6YaGs126MiZpBA-UFUK0ddc1UAw.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem6YaGs126MiZpBA-UFUK0ddc1UAw.woff2') format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@ -85,7 +85,7 @@
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('/public/fonts/OpenSans/mem6YaGs126MiZpBA-UFUK0Vdc1UAw.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem6YaGs126MiZpBA-UFUK0Vdc1UAw.woff2') format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@ -94,7 +94,7 @@
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('/public/fonts/OpenSans/mem6YaGs126MiZpBA-UFUK0adc1UAw.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem6YaGs126MiZpBA-UFUK0adc1UAw.woff2') format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@ -103,7 +103,7 @@
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('/public/fonts/OpenSans/mem6YaGs126MiZpBA-UFUK0Wdc1UAw.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem6YaGs126MiZpBA-UFUK0Wdc1UAw.woff2') format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@ -112,7 +112,7 @@
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('/public/fonts/OpenSans/mem6YaGs126MiZpBA-UFUK0Xdc1UAw.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem6YaGs126MiZpBA-UFUK0Xdc1UAw.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@ -121,7 +121,7 @@
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('/public/fonts/OpenSans/mem6YaGs126MiZpBA-UFUK0Zdc0.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem6YaGs126MiZpBA-UFUK0Zdc0.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@ -130,7 +130,7 @@
font-style: italic;
font-weight: 600;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKXGUdhmIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKXGUdhmIqOjjg.woff2') format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@ -139,7 +139,7 @@
font-style: italic;
font-weight: 600;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKXGUdhvIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKXGUdhvIqOjjg.woff2') format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@ -148,7 +148,7 @@
font-style: italic;
font-weight: 600;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKXGUdhnIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKXGUdhnIqOjjg.woff2') format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@ -157,7 +157,7 @@
font-style: italic;
font-weight: 600;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKXGUdhoIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKXGUdhoIqOjjg.woff2') format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@ -166,7 +166,7 @@
font-style: italic;
font-weight: 600;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKXGUdhkIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKXGUdhkIqOjjg.woff2') format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@ -175,7 +175,7 @@
font-style: italic;
font-weight: 600;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKXGUdhlIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKXGUdhlIqOjjg.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@ -184,7 +184,7 @@
font-style: italic;
font-weight: 600;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKXGUdhrIqM.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKXGUdhrIqM.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@ -193,7 +193,7 @@
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKWiUNhmIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKWiUNhmIqOjjg.woff2') format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@ -202,7 +202,7 @@
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKWiUNhvIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKWiUNhvIqOjjg.woff2') format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@ -211,7 +211,7 @@
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKWiUNhnIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKWiUNhnIqOjjg.woff2') format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@ -220,7 +220,7 @@
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKWiUNhoIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKWiUNhoIqOjjg.woff2') format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@ -229,7 +229,7 @@
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKWiUNhkIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKWiUNhkIqOjjg.woff2') format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@ -238,7 +238,7 @@
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKWiUNhlIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKWiUNhlIqOjjg.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@ -247,7 +247,7 @@
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKWiUNhrIqM.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKWiUNhrIqM.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@ -256,7 +256,7 @@
font-style: italic;
font-weight: 800;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKW-U9hmIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKW-U9hmIqOjjg.woff2') format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@ -265,7 +265,7 @@
font-style: italic;
font-weight: 800;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKW-U9hvIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKW-U9hvIqOjjg.woff2') format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@ -274,7 +274,7 @@
font-style: italic;
font-weight: 800;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKW-U9hnIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKW-U9hnIqOjjg.woff2') format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@ -283,7 +283,7 @@
font-style: italic;
font-weight: 800;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKW-U9hoIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKW-U9hoIqOjjg.woff2') format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@ -292,7 +292,7 @@
font-style: italic;
font-weight: 800;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKW-U9hkIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKW-U9hkIqOjjg.woff2') format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@ -301,7 +301,7 @@
font-style: italic;
font-weight: 800;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKW-U9hlIqOjjg.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKW-U9hlIqOjjg.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@ -310,7 +310,7 @@
font-style: italic;
font-weight: 800;
font-display: swap;
src: url('/public/fonts/OpenSans/memnYaGs126MiZpBA-UFUKW-U9hrIqM.woff2') format('woff2');
src: url('../../fonts/OpenSans/memnYaGs126MiZpBA-UFUKW-U9hrIqM.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@ -319,7 +319,7 @@
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UN_r8OX-hpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UN_r8OX-hpOqc.woff2') format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@ -328,7 +328,7 @@
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UN_r8OVuhpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UN_r8OVuhpOqc.woff2') format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@ -337,7 +337,7 @@
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UN_r8OXuhpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UN_r8OXuhpOqc.woff2') format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@ -346,7 +346,7 @@
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UN_r8OUehpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UN_r8OUehpOqc.woff2') format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@ -355,7 +355,7 @@
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UN_r8OXehpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UN_r8OXehpOqc.woff2') format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@ -364,7 +364,7 @@
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UN_r8OXOhpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UN_r8OXOhpOqc.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@ -373,7 +373,7 @@
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UN_r8OUuhp.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UN_r8OUuhp.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@ -382,7 +382,7 @@
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/public/fonts/OpenSans/mem8YaGs126MiZpBA-UFWJ0bbck.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem8YaGs126MiZpBA-UFWJ0bbck.woff2') format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@ -391,7 +391,7 @@
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/public/fonts/OpenSans/mem8YaGs126MiZpBA-UFUZ0bbck.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem8YaGs126MiZpBA-UFUZ0bbck.woff2') format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@ -400,7 +400,7 @@
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/public/fonts/OpenSans/mem8YaGs126MiZpBA-UFWZ0bbck.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem8YaGs126MiZpBA-UFWZ0bbck.woff2') format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@ -409,7 +409,7 @@
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/public/fonts/OpenSans/mem8YaGs126MiZpBA-UFVp0bbck.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem8YaGs126MiZpBA-UFVp0bbck.woff2') format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@ -418,7 +418,7 @@
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/public/fonts/OpenSans/mem8YaGs126MiZpBA-UFWp0bbck.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem8YaGs126MiZpBA-UFWp0bbck.woff2') format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@ -427,7 +427,7 @@
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/public/fonts/OpenSans/mem8YaGs126MiZpBA-UFW50bbck.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem8YaGs126MiZpBA-UFW50bbck.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@ -436,7 +436,7 @@
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/public/fonts/OpenSans/mem8YaGs126MiZpBA-UFVZ0b.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem8YaGs126MiZpBA-UFVZ0b.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@ -445,7 +445,7 @@
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UNirkOX-hpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UNirkOX-hpOqc.woff2') format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@ -454,7 +454,7 @@
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UNirkOVuhpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UNirkOVuhpOqc.woff2') format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@ -463,7 +463,7 @@
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UNirkOXuhpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UNirkOXuhpOqc.woff2') format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@ -472,7 +472,7 @@
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UNirkOUehpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UNirkOUehpOqc.woff2') format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@ -481,7 +481,7 @@
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UNirkOXehpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UNirkOXehpOqc.woff2') format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@ -490,7 +490,7 @@
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UNirkOXOhpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UNirkOXOhpOqc.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@ -499,7 +499,7 @@
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UNirkOUuhp.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UNirkOUuhp.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@ -508,7 +508,7 @@
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UN7rgOX-hpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UN7rgOX-hpOqc.woff2') format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@ -517,7 +517,7 @@
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UN7rgOVuhpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UN7rgOVuhpOqc.woff2') format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@ -526,7 +526,7 @@
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UN7rgOXuhpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UN7rgOXuhpOqc.woff2') format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@ -535,7 +535,7 @@
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UN7rgOUehpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UN7rgOUehpOqc.woff2') format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@ -544,7 +544,7 @@
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UN7rgOXehpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UN7rgOXehpOqc.woff2') format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@ -553,7 +553,7 @@
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UN7rgOXOhpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UN7rgOXOhpOqc.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@ -562,7 +562,7 @@
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UN7rgOUuhp.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UN7rgOUuhp.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@ -571,7 +571,7 @@
font-style: normal;
font-weight: 800;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UN8rsOX-hpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UN8rsOX-hpOqc.woff2') format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@ -580,7 +580,7 @@
font-style: normal;
font-weight: 800;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UN8rsOVuhpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UN8rsOVuhpOqc.woff2') format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@ -589,7 +589,7 @@
font-style: normal;
font-weight: 800;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UN8rsOXuhpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UN8rsOXuhpOqc.woff2') format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@ -598,7 +598,7 @@
font-style: normal;
font-weight: 800;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UN8rsOUehpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UN8rsOUehpOqc.woff2') format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@ -607,7 +607,7 @@
font-style: normal;
font-weight: 800;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UN8rsOXehpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UN8rsOXehpOqc.woff2') format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@ -616,7 +616,7 @@
font-style: normal;
font-weight: 800;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UN8rsOXOhpOqc.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UN8rsOXOhpOqc.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@ -625,6 +625,6 @@
font-style: normal;
font-weight: 800;
font-display: swap;
src: url('/public/fonts/OpenSans/mem5YaGs126MiZpBA-UN8rsOUuhp.woff2') format('woff2');
src: url('../../fonts/OpenSans/mem5YaGs126MiZpBA-UN8rsOUuhp.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

View File

@ -4,12 +4,12 @@
<head>
<meta charset="utf-8">
<title>deemix</title>
<link rel="stylesheet" type="text/css" href="/public/css/vendor/animate.css">
<link rel="stylesheet" type="text/css" href="/public/css/vendor/material-icons.css">
<link rel="stylesheet" type="text/css" href="/public/css/vendor/OpenSans.css">
<link rel="stylesheet" type="text/css" href="/public/css/vendor/toastify.css">
<link rel="stylesheet" type="text/css" href="/public/css/style.css">
<link rel="shortcut icon" href="/public/favicon.ico">
<link rel="stylesheet" type="text/css" href="/css/vendor/animate.css">
<link rel="stylesheet" type="text/css" href="/css/vendor/material-icons.css">
<link rel="stylesheet" type="text/css" href="/css/vendor/OpenSans.css">
<link rel="stylesheet" type="text/css" href="/css/vendor/toastify.css">
<link rel="stylesheet" type="text/css" href="/css/style.css">
<link rel="shortcut icon" href="/favicon.ico">
<meta name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=0">
<script>
@ -23,8 +23,8 @@
<div id="app"></div>
</body>
<script type="text/javascript" src="/public/js/vendor/socket.io.js"></script>
<script type="text/javascript" src="/js/vendor/socket.io.js"></script>
<script src="/public/js/bundle.js"></script>
<script src="/js/bundle.js"></script>
</html>

File diff suppressed because one or more lines are too long

View File

@ -7,9 +7,9 @@ window.vol = {
import App from '@components/App.vue'
import i18n from '@/plugins/i18n'
// import router from '@/plugins/router'
import router from '@/router'
import store from '@/store'
import $ from 'jquery'
import { socket } from '@/utils/socket'
import { toast } from '@/utils/toasts'
import { init as initTabs } from '@js/tabs.js'
@ -27,17 +27,15 @@ function startApp() {
function mountApp() {
new Vue({
// router,
store,
router,
i18n,
render: h => h(App)
}).$mount('#app')
}
function initClient() {
window.clientMode = true
document.querySelector(`#open_downloads_folder`).classList.remove('hide')
document.querySelector(`#select_downloads_folder`).classList.remove('hide')
document.querySelector(`#settings_btn_applogin`).classList.remove('hide')
store.dispatch('setClientMode', true)
}
document.addEventListener('DOMContentLoaded', startApp)
@ -45,25 +43,18 @@ window.addEventListener('pywebviewready', initClient)
/* ===== Global shortcuts ===== */
document.addEventListener('keyup', keyEvent => {
if (keyEvent.key == "Backspace" && keyEvent.ctrlKey){
let searchbar = document.querySelector('#searchbar')
searchbar.value = ""
searchbar.focus()
}
})
document.addEventListener('paste', pasteEvent => {
pasteText = pasteEvent.clipboardData.getData('Text')
if (pasteEvent.target.localName != "input"){
if (isValidURL(pasteText)){
if (main_selected === 'analyzer_tab') {
let pasteText = pasteEvent.clipboardData.getData('Text')
if (pasteEvent.target.localName != 'input') {
if (isValidURL(pasteText)) {
if (window.main_selected === 'analyzer_tab') {
EventBus.$emit('linkAnalyzerTab:reset')
socket.emit('analyzeLink', pasteText)
} else {
Downloads.sendAddToQueue(pasteText)
}
}else{
} else {
let searchbar = document.querySelector('#searchbar')
searchbar.select()
searchbar.setSelectionRange(0, 99999)
@ -85,8 +76,10 @@ socket.on('logging_in', function() {
socket.on('init_autologin', function() {
let arl = localStorage.getItem('arl')
let accountNum = localStorage.getItem('accountNum')
if (arl) {
arl = arl.trim()
if (accountNum != 0) {
socket.emit('login', arl, true, accountNum)
} else {
@ -96,62 +89,42 @@ socket.on('init_autologin', function() {
})
socket.on('logged_in', function(data) {
switch (data.status) {
const { status, user } = data
switch (status) {
case 1:
case 3:
// Login ok
toast(i18n.t('toasts.loggedIn'), 'done', true, 'login-toast')
if (data.arl) {
localStorage.setItem('arl', data.arl)
$('#login_input_arl').val(data.arl)
}
$('#open_login_prompt').hide()
if (data.user) {
$('#settings_username').text(data.user.name)
$('#settings_picture').attr(
'src',
`https://e-cdns-images.dzcdn.net/images/user/${data.user.picture}/125x125-000000-80-0-0.jpg`
)
// $('#logged_in_info').show()
document.getElementById('logged_in_info').classList.remove('hide')
}
document.getElementById('home_not_logged_in').classList.add('hide')
store.dispatch('login', data)
break
case 2:
// Already logged in
toast(i18n.t('toasts.alreadyLogged'), 'done', true, 'login-toast')
if (data.user) {
$('#settings_username').text(data.user.name)
$('#settings_picture').attr(
'src',
`https://e-cdns-images.dzcdn.net/images/user/${data.user.picture}/125x125-000000-80-0-0.jpg`
)
// $('#logged_in_info').show()
document.getElementById('logged_in_info').classList.remove('hide')
}
document.getElementById('home_not_logged_in').classList.add('hide')
store.dispatch('setUser', user)
break
case 0:
// Login failed
toast(i18n.t('toasts.loginFailed'), 'close', true, 'login-toast')
localStorage.removeItem('arl')
$('#login_input_arl').val('')
store.dispatch('removeARL')
break
case -1:
toast(i18n.t('toasts.deezerNotAvailable'), 'close', true, 'login-toast')
$('#open_login_prompt').show()
document.getElementById('logged_in_info').classList.add('hide')
// $('#logged_in_info').hide()
$('#settings_username').text('Not Logged')
$('#settings_picture').attr('src', `https://e-cdns-images.dzcdn.net/images/user/125x125-000000-80-0-0.jpg`)
document.getElementById('home_not_logged_in').classList.remove('hide')
break
}
})
socket.on('logged_out', function() {
toast(i18n.t('toasts.loggedOut'), 'done', true, 'login-toast')
localStorage.removeItem('arl')
$('#login_input_arl').val('')
$('#open_login_prompt').show()
document.getElementById('logged_in_info').classList.add('hide')
$('#settings_username').text('Not Logged')
$('#settings_picture').attr('src', `https://e-cdns-images.dzcdn.net/images/user/125x125-000000-80-0-0.jpg`)
document.getElementById('home_not_logged_in').classList.remove('hide')
store.dispatch('logout')
})
socket.on('restoringQueue', function() {
@ -167,11 +140,11 @@ socket.on('currentItemCancelled', function(uuid) {
})
socket.on('startAddingArtist', function(data) {
toast(i18n.t('toasts.startAddingArtist', {artist: data.name}), 'loading', false, 'artist_' + data.id)
toast(i18n.t('toasts.startAddingArtist', { artist: data.name }), 'loading', false, 'artist_' + data.id)
})
socket.on('finishAddingArtist', function(data) {
toast(i18n.t('toasts.finishAddingArtist', {artist: data.name}), 'done', true, 'artist_' + data.id)
toast(i18n.t('toasts.finishAddingArtist', { artist: data.name }), 'done', true, 'artist_' + data.id)
})
socket.on('startConvertingSpotifyPlaylist', function(id) {
@ -187,12 +160,15 @@ socket.on('errorMessage', function(error) {
})
socket.on('queueError', function(queueItem) {
if (queueItem.errid) toast(i18n.t(`errors.ids.${queueItem.errid}`), 'error')
else toast(queueItem.error, 'error')
if (queueItem.errid) {
toast(i18n.t(`errors.ids.${queueItem.errid}`), 'error')
} else {
toast(queueItem.error, 'error')
}
})
socket.on('alreadyInQueue', function(data) {
toast(i18n.t('toasts.alreadyInQueue', {item: data.title}), 'playlist_add_check')
toast(i18n.t('toasts.alreadyInQueue', { item: data.title }), 'playlist_add_check')
})
socket.on('loginNeededToDownload', function(data) {

View File

@ -1,9 +1,16 @@
<template>
<div style="height: inherit;">
<BaseLoadingPlaceholder id="start_app_placeholder" text="Connecting to the server..." />
<div>
<TheSidebar />
<TheMainContent />
<div class="app-container">
<div class="content-container">
<TheSearchBar />
<TheMiddleSection />
</div>
<TheDownloadTab class="downlaods" />
</div>
<BaseLoadingPlaceholder id="start_app_placeholder" text="Connecting to the server..." />
<TheTrackPreview />
<TheQualityModal />
@ -12,18 +19,39 @@
</div>
</template>
<style lang="scss">
.app-container {
display: flex;
}
.content-container {
width: 100%;
display: flex;
flex-direction: column;
margin-left: 48px;
}
.downlaods {
flex-basis: 32px;
}
</style>
<script>
import TheSidebar from '@components/TheSidebar.vue'
import TheMainContent from '@components/TheMainContent.vue'
import TheMiddleSection from '@components/TheMiddleSection.vue'
import TheTrackPreview from '@components/TheTrackPreview.vue'
import TheQualityModal from '@components/TheQualityModal.vue'
import BaseLoadingPlaceholder from '@components/BaseLoadingPlaceholder.vue'
import TheContextMenu from '@components/TheContextMenu.vue'
import TheDownloadTab from '@components/TheDownloadTab.vue'
import TheSearchBar from '@components/TheSearchBar.vue'
export default {
components: {
TheSidebar,
TheMainContent,
TheSearchBar,
TheMiddleSection,
TheDownloadTab,
TheTrackPreview,
TheQualityModal,
BaseLoadingPlaceholder,

View File

@ -1,5 +1,5 @@
<template>
<div id="artist_tab" class="main_tabcontent fixed_footer image_header">
<div id="artist_tab" class="main_tabcontent fixed_footer image_header" ref="root">
<header
class="inline-flex"
:style="{
@ -51,29 +51,25 @@
<img
class="rounded coverart"
:src="release.cover_small"
style="margin-right: 16px; width: 56px; height: 56px;"
style="margin-right: 16px; width: 56px; height: 56px"
/>
<i v-if="release.explicit_lyrics" class="material-icons explicit_icon">
explicit
</i>
<i v-if="release.explicit_lyrics" class="material-icons explicit_icon"> explicit </i>
{{ release.title }}
<i v-if="checkNewRelease(release.release_date)" class="material-icons" style="color: #ff7300;">
<i v-if="checkNewRelease(release.release_date)" class="material-icons" style="color: #ff7300">
fiber_new
</i>
</td>
<td>{{ release.release_date }}</td>
<td>{{ release.nb_song }}</td>
<td @click.stop="addToQueue" :data-link="release.link" class="clickable">
<i class="material-icons" :title="$t('globals.download_hint')">
file_download
</i>
<i class="material-icons" :title="$t('globals.download_hint')"> file_download </i>
</td>
</tr>
</tbody>
</table>
<footer>
<button class="back-button">{{ $t('globals.back') }}</button>
<button class="back-button" @click="backTab">{{ $t('globals.back') }}</button>
</footer>
</div>
</template>
@ -82,7 +78,7 @@
import { isEmpty, orderBy } from 'lodash-es'
import { socket } from '@/utils/socket'
import Downloads from '@/utils/downloads'
import { showView } from '@js/tabs'
import { showView, backTab } from '@js/tabs'
import EventBus from '@/utils/EventBus'
export default {
@ -101,6 +97,7 @@ export default {
}
},
methods: {
backTab,
albumView: showView.bind(null, 'album'),
reset() {
this.title = 'Loading...'
@ -143,6 +140,8 @@ export default {
return g1.getTime() <= g2.getTime()
},
showArtist(data) {
this.reset()
const { name, picture_xl, id, releases } = data
this.title = name
@ -183,7 +182,6 @@ export default {
mounted() {
socket.on('show_artist', this.showArtist)
EventBus.$on('artistTab:reset', this.reset)
EventBus.$on('artistTab:updateSelected', this.updateSelected)
EventBus.$on('artistTab:changeTab', this.changeTab)
}

View File

@ -1,6 +1,15 @@
<template>
<div id="about_tab" class="main_tabcontent">
<div id="about_tab" class="main_tabcontent" ref="root">
<h2 class="page_heading">{{ $t('sidebar.about') }}</h2>
<ul>
<li>
{{ $t('about.updates.currentVersion') }}:
<span>{{ current || $t('about.updates.versionNotAvailable') }}</span>
</li>
<li>{{ $t('about.updates.deemixVersion') }}: {{ deemixVersion }}</li>
<li v-if="updateAvailable && latest">{{ $t('about.updates.updateAvailable', { version: latest }) }}</li>
</ul>
<ul>
<li v-html="$t('about.usesLibrary')"></li>
<li v-html="$t('about.thanks')"></li>
@ -93,7 +102,7 @@
<a rel="license" href="https://www.gnu.org/licenses/gpl-3.0.en.html" target="_blank">
<img
alt="GNU General Public License"
style="border-width: 0;"
style="border-width: 0"
src="https://www.gnu.org/graphics/gplv3-127x51.png"
/>
</a>
@ -101,6 +110,7 @@
<p v-html="$t('about.lincensedUnder')"></p>
</div>
</template>
<style lang="scss" scoped>
li,
p,
@ -190,14 +200,36 @@ ul {
}
}
</style>
<script>
import { socket } from '@/utils/socket'
import paypal from '@/assets/paypal.svg'
import ethereum from '@/assets/ethereum.svg'
import { mapGetters } from 'vuex'
export default {
data: () => ({
paypal,
ethereum
})
ethereum,
current: null,
latest: null,
updateAvailable: false,
deemixVersion: null
}),
computed: {
...mapGetters(['getAboutInfo'])
},
methods: {
initUpdate(data) {
const { currentCommit, latestCommit, updateAvailable, deemixVersion } = data
this.current = currentCommit
this.latest = latestCommit
this.updateAvailable = updateAvailable
this.deemixVersion = deemixVersion
}
},
mounted() {
this.initUpdate(this.getAboutInfo)
}
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<div id="charts_tab" class="main_tabcontent">
<div id="charts_tab" class="main_tabcontent" ref="root">
<h2 class="page_heading">{{ $t('charts.title') }}</h2>
<div v-if="country === ''" id="charts_selection">
<div class="release_grid charts_grid">
@ -35,7 +35,7 @@
</div>
</div>
<div v-else id="charts_table">
<button @click="changeCountry">{{ $t('charts.changeCountry') }}</button>
<button @click="onChangeCountry">{{ $t('charts.changeCountry') }}</button>
<button @click.stop="addToQueue" :data-link="'https://www.deezer.com/playlist/' + id">
{{ $t('charts.download') }}
</button>
@ -105,15 +105,17 @@
</template>
<script>
import { mapGetters } from 'vuex'
import { socket } from '@/utils/socket'
import { showView } from '@js/tabs.js'
import Downloads from '@/utils/downloads'
import Utils from '@/utils/utils'
import { sendAddToQueue } from '@/utils/downloads'
import { convertDuration } from '@/utils/utils'
import { getChartsData } from '@/data/charts'
import EventBus from '@/utils/EventBus'
export default {
name: 'the-charts-tab',
data() {
return {
country: '',
@ -122,7 +124,18 @@ export default {
chart: []
}
},
async created() {
socket.on('setChartTracks', this.setTracklist)
this.$on('hook:destroyed', () => {
socket.off('setChartTracks')
})
const chartsData = await getChartsData()
this.initCharts(chartsData)
},
methods: {
convertDuration,
artistView: showView.bind(null, 'artist'),
albumView: showView.bind(null, 'album'),
playPausePreview(e) {
@ -134,10 +147,9 @@ export default {
previewMouseLeave(e) {
EventBus.$emit('trackPreview:previewMouseLeave', e)
},
convertDuration: Utils.convertDuration,
addToQueue(e) {
e.stopPropagation()
Downloads.sendAddToQueue(e.currentTarget.dataset.link)
sendAddToQueue(e.currentTarget.dataset.link)
},
getTrackList(event) {
document.getElementById('content').scrollTo(0, 0)
@ -159,12 +171,12 @@ export default {
setTracklist(data) {
this.chart = data
},
changeCountry() {
onChangeCountry() {
this.country = ''
this.id = 0
},
initCharts(data) {
this.countries = data
initCharts(chartsData) {
this.countries = chartsData
this.country = localStorage.getItem('chart') || ''
if (!this.country) return
@ -182,10 +194,6 @@ export default {
localStorage.setItem('chart', this.country)
}
}
},
mounted() {
socket.on('init_charts', this.initCharts)
socket.on('setChartTracks', this.setTracklist)
}
}
</script>

View File

@ -1,72 +1,87 @@
<template>
<section id="content" @scroll="handleContentScroll" ref="content">
<section id="content" @scroll="$route.name === 'Search' ? handleContentScroll($event) : null" ref="content">
<div id="container">
<ArtistTab />
<TheChartsTab />
<TheFavoritesTab />
<TheErrorsTab />
<TheHomeTab />
<TheLinkAnalyzerTab />
<TheAboutTab />
<TheSettingsTab />
<TheMainSearch :scrolled-search-type="newScrolled" />
<TracklistTab />
<BaseLoadingPlaceholder id="search_placeholder" text="Searching..." :hidden="!loading" />
<keep-alive>
<router-view
v-if="!$route.meta.notKeepAlive"
v-show="!loading"
:key="$route.fullPath"
:perform-scrolled-search="performScrolledSearch"
exclude=""
></router-view>
</keep-alive>
<router-view
v-if="$route.meta.notKeepAlive"
v-show="!loading"
:key="$route.fullPath"
:perform-scrolled-search="performScrolledSearch"
exclude=""
></router-view>
</div>
</section>
</template>
<style lang="scss">
#container {
margin: 0 auto;
max-width: 1280px;
width: var(--container-width);
}
#content {
background-color: var(--main-background);
width: 100%;
height: calc(100vh - 93px);
overflow-y: scroll;
overflow-x: hidden;
&::-webkit-scrollbar {
width: 10px;
}
&::-webkit-scrollbar-track {
background: var(--main-background);
}
&::-webkit-scrollbar-thumb {
background: var(--main-scroll);
border-radius: 4px;
width: 6px;
padding: 0px 2px;
}
}
</style>
<script>
import ArtistTab from '@components/ArtistTab.vue'
import TracklistTab from '@components/TracklistTab.vue'
import TheChartsTab from '@components/TheChartsTab.vue'
import TheFavoritesTab from '@components/TheFavoritesTab.vue'
import TheErrorsTab from '@components/TheErrorsTab.vue'
import TheHomeTab from '@components/TheHomeTab.vue'
import TheLinkAnalyzerTab from '@components/TheLinkAnalyzerTab.vue'
import TheAboutTab from '@components/TheAboutTab.vue'
import TheSettingsTab from '@components/TheSettingsTab.vue'
import TheMainSearch from '@components/TheMainSearch.vue'
import { debounce } from '@/utils/utils'
import EventBus from '@/utils/EventBus.js'
import BaseLoadingPlaceholder from '@components/BaseLoadingPlaceholder.vue'
export default {
components: {
ArtistTab,
TheChartsTab,
TheFavoritesTab,
TheErrorsTab,
TheHomeTab,
TheLinkAnalyzerTab,
TheAboutTab,
TheSettingsTab,
TheMainSearch,
TracklistTab
BaseLoadingPlaceholder
},
data: () => ({
newScrolled: null
performScrolledSearch: false,
loading: false
}),
mounted() {
this.$root.$on('updateSearchLoadingState', loading => {
this.loading = loading
})
},
methods: {
handleContentScroll: debounce(async function() {
handleContentScroll: debounce(async function () {
if (this.$refs.content.scrollTop + this.$refs.content.clientHeight < this.$refs.content.scrollHeight) return
if (
main_selected !== 'search_tab' ||
['track_search', 'album_search', 'artist_search', 'playlist_search'].indexOf(window.search_selected) === -1
) {
return
}
this.newScrolled = window.search_selected.split('_')[0]
this.performScrolledSearch = true
await this.$nextTick()
this.newScrolled = null
this.performScrolledSearch = false
}, 100)
}
}
</script>
<style>
</style>

View File

@ -15,16 +15,54 @@
:title="$t('globals.toggle_download_tab_hint')"
></i>
<div id="queue_buttons">
<i id="open_downloads_folder" class="material-icons download_bar_icon hide" :title="$t('globals.open_downloads_folder')" @click="openDownloadsFolder">folder_open</i>
<i id="clean_queue" class="material-icons download_bar_icon" @click="cleanQueue" :title="$t('globals.clean_queue_hint')">clear_all</i>
<i id="cancel_queue" class="material-icons download_bar_icon" @click="cancelQueue" :title="$t('globals.cancel_queue_hint')">delete_sweep</i>
<i
id="open_downloads_folder"
v-if="clientMode"
class="material-icons download_bar_icon"
:title="$t('globals.open_downloads_folder')"
@click="openDownloadsFolder"
>
folder_open
</i>
<i
id="clean_queue"
class="material-icons download_bar_icon"
@click="cleanQueue"
:title="$t('globals.clean_queue_hint')"
>
clear_all
</i>
<i
id="cancel_queue"
class="material-icons download_bar_icon"
@click="cancelQueue"
:title="$t('globals.cancel_queue_hint')"
>
delete_sweep
</i>
</div>
<div id="download_list" ref="list">
<QueueItem
v-for="item in queueList"
:queue-item="item"
:key="item.uuid"
@show-errors="showErrorsTab"
@remove-item="onRemoveItem"
/>
</div>
<div id="download_list" @click="handleListClick" ref="list"></div>
</div>
</template>
<style lang="scss" scoped>
#download_tab_container {
height: 100vh;
}
</style>
<script>
import $ from 'jquery'
import { mapActions, mapGetters } from 'vuex'
import QueueItem from '@components/downloads/QueueItem.vue'
import { socket } from '@/utils/socket'
import { toast } from '@/utils/toasts'
@ -32,12 +70,23 @@ const tabMinWidth = 250
const tabMaxWidth = 500
export default {
data: () => ({
cachedTabWidth: parseInt(localStorage.getItem('downloadTabWidth')) || 300,
queue: [],
queueList: {},
queueComplete: []
}),
components: {
QueueItem
},
data() {
return {
cachedTabWidth: parseInt(localStorage.getItem('downloadTabWidth')) || 300,
queue: [],
queueList: {},
queueComplete: []
// clientMode: window.clientMode
}
},
computed: {
...mapGetters({
clientMode: 'getClientMode'
})
},
mounted() {
socket.on('startDownload', this.startDownload)
socket.on('startConversion', this.startConversion)
@ -69,6 +118,10 @@ export default {
})
},
methods: {
...mapActions(['setErrors']),
onRemoveItem(uuid) {
socket.emit('removeFromQueue', uuid)
},
setTabWidth(newWidth) {
if (undefined === newWidth) {
this.$refs.container.style.width = ''
@ -78,25 +131,14 @@ export default {
this.$refs.list.style.width = newWidth + 'px'
}
},
handleListClick(event) {
const { target } = event
if (!target.matches('.queue_icon[data-uuid]')) {
return
}
let icon = target.innerText
let uuid = $(target).data('uuid')
switch (icon) {
case 'remove':
socket.emit('removeFromQueue', uuid)
break
default:
}
},
initQueue(data) {
const { queue: initQueue, queueComplete: initQueueComplete, currentItem, queueList: initQueueList, restored } = data
const {
queue: initQueue,
queueComplete: initQueueComplete,
currentItem,
queueList: initQueueList,
restored
} = data
if (initQueueComplete.length) {
initQueueComplete.forEach(item => {
@ -115,97 +157,57 @@ export default {
this.addToQueue(initQueueList[item])
})
if (restored){
if (restored) {
toast(this.$t('toasts.queueRestored'), 'done', true, 'restoring_queue')
socket.emit('queueRestored')
}
},
addToQueue(queueItem, current = false) {
if (Array.isArray(queueItem)){
if (queueItem.length > 1){
if (Array.isArray(queueItem)) {
if (queueItem.length > 1) {
queueItem.forEach((item, i) => {
item.silent = true
this.addToQueue(item)
});
toast(this.$t('toasts.addedMoreToQueue', {n: queueItem.length}), 'playlist_add_check')
})
toast(this.$t('toasts.addedMoreToQueue', { n: queueItem.length }), 'playlist_add_check')
return
}else{
} else {
queueItem = queueItem[0]
}
}
this.queueList[queueItem.uuid] = queueItem
if (queueItem.downloaded + queueItem.failed == queueItem.size) {
if (this.queueComplete.indexOf(queueItem.uuid) == -1) {
// * Here we have only queueItem objects
this.$set(queueItem, 'current', current)
this.$set(this.queueList, queueItem.uuid, queueItem)
// * Used when opening the app in another tab
const itemIsAlreadyDownloaded = queueItem.downloaded + queueItem.failed == queueItem.size
if (itemIsAlreadyDownloaded) {
const itemIsNotInCompletedQueue = this.queueComplete.indexOf(queueItem.uuid) == -1
this.$set(this.queueList[queueItem.uuid], 'status', 'download finished')
if (itemIsNotInCompletedQueue) {
// * Add it
this.queueComplete.push(queueItem.uuid)
}
} else {
if (this.queue.indexOf(queueItem.uuid) == -1) {
const itemIsNotInQueue = this.queue.indexOf(queueItem.uuid) == -1
if (itemIsNotInQueue) {
this.queue.push(queueItem.uuid)
}
}
let queueDOM = document.getElementById('download_' + queueItem.uuid)
const needToStartDownload = (queueItem.progress > 0 && queueItem.progress < 100) || current
if (typeof queueDOM == 'undefined' || queueDOM == null) {
$(this.$refs.list).append(
`<div class="download_object" id="download_${queueItem.uuid}" data-deezerid="${queueItem.id}">
<div class="download_info">
<img width="75px" class="rounded coverart" src="${queueItem.cover}" alt="Cover ${queueItem.title}"/>
<div class="download_info_data">
<span class="download_line">${queueItem.title}</span> <span class="download_slim_separator"> - </span>
<span class="secondary-text">${queueItem.artist}</span>
</div>
<div class="download_info_status">
<span class="download_line"><span class="queue_downloaded">${queueItem.downloaded + queueItem.failed}</span>/${
queueItem.size
}</span>
</div>
</div>
<div class="download_bar">
<div class="progress"><div id="bar_${queueItem.uuid}" class="indeterminate"></div></div>
<i class="material-icons queue_icon" data-uuid="${queueItem.uuid}">remove</i>
</div>
</div>`
)
}
if (queueItem.progress > 0 || current) {
if (needToStartDownload) {
this.startDownload(queueItem.uuid)
}
$('#bar_' + queueItem.uuid).css('width', queueItem.progress + '%')
if (queueItem.failed >= 1 && $('#download_' + queueItem.uuid + ' .queue_failed').length == 0) {
$('#download_' + queueItem.uuid + ' .download_info_status').append(
`<span class="secondary-text inline-flex"><span class="download_slim_separator">(</span><span class="queue_failed_button inline-flex"><span class="queue_failed">${queueItem.failed}</span><i class="material-icons">error_outline</i></span><span class="download_slim_separator">)</span></span>`
)
}
if (queueItem.downloaded + queueItem.failed == queueItem.size) {
let resultIcon = $('#download_' + queueItem.uuid).find('.queue_icon')
if (queueItem.failed == 0) {
resultIcon.text('done')
} else {
let failedButton = $('#download_' + queueItem.uuid).find('.queue_failed_button')
resultIcon.addClass('clickable')
failedButton.addClass('clickable')
resultIcon.bind('click', { item: queueItem }, this.showErrorsTab)
failedButton.bind('click', { item: queueItem }, this.showErrorsTab)
if (queueItem.failed >= queueItem.size) {
resultIcon.text('error')
} else {
resultIcon.text('warning')
}
}
}
if (!queueItem.silent) {
toast(this.$t('toasts.addedToQueue', {item: queueItem.title}), 'playlist_add_check')
toast(this.$t('toasts.addedToQueue', { item: queueItem.title }), 'playlist_add_check')
}
},
updateQueue(update) {
@ -215,34 +217,19 @@ export default {
if (uuid && this.queue.indexOf(uuid) > -1) {
if (downloaded) {
this.queueList[uuid].downloaded++
$('#download_' + uuid + ' .queue_downloaded').text(
this.queueList[uuid].downloaded + this.queueList[uuid].failed
)
}
if (failed) {
this.queueList[uuid].failed++
$('#download_' + uuid + ' .queue_downloaded').text(
this.queueList[uuid].downloaded + this.queueList[uuid].failed
)
if (this.queueList[uuid].failed == 1 && $('#download_' + uuid + ' .queue_failed').length == 0) {
$('#download_' + uuid + ' .download_info_status').append(
`<span class="secondary-text inline-flex"><span class="download_slim_separator">(</span><span class="queue_failed_button inline-flex"><span class="queue_failed">1</span> <i class="material-icons">error_outline</i></span><span class="download_slim_separator">)</span></span>`
)
} else {
$('#download_' + uuid + ' .queue_failed').text(this.queueList[uuid].failed)
}
this.queueList[uuid].errors.push({ message: error, data: data, errid: errid })
}
if (progress) {
this.queueList[uuid].progress = progress
$('#bar_' + uuid).css('width', progress + '%')
}
if (conversion) {
$('#bar_' + uuid).css('width', (100-conversion) + '%')
this.queueList[uuid].conversion = conversion
}
}
},
@ -250,32 +237,28 @@ export default {
let index = this.queue.indexOf(uuid)
if (index > -1) {
this.queue.splice(index, 1)
$(`#download_${this.queueList[uuid].uuid}`).remove()
delete this.queueList[uuid]
this.$delete(this.queue, index)
this.$delete(this.queueList, uuid)
}
},
removeAllDownloads(currentItem) {
this.queueComplete = []
if (currentItem == '') {
if (!currentItem) {
this.queue = []
this.queueList = {}
$(listEl).html('')
} else {
this.queue = [currentItem]
let tempQueueItem = this.queueList[currentItem]
this.queueList = {}
this.queueList[currentItem] = tempQueueItem
$('.download_object').each(function(index) {
if ($(this).attr('id') != 'download_' + currentItem) $(this).remove()
})
}
},
removedFinishedDownloads() {
this.queueComplete.forEach(item => {
$('#download_' + item).remove()
this.queueComplete.forEach(uuid => {
this.$delete(this.queueList, uuid)
})
this.queueComplete = []
@ -300,47 +283,10 @@ export default {
cancelQueue() {
socket.emit('cancelAllDownloads')
},
finishDownload(uuid) {
if (this.queue.indexOf(uuid) > -1) {
toast(this.$t('toasts.finishDownload', {item: this.queueList[uuid].title}), 'done')
$('#bar_' + uuid).css('width', '100%')
let resultIcon = $('#download_' + uuid).find('.queue_icon')
if (this.queueList[uuid].failed == 0) {
resultIcon.text('done')
} else {
let failedButton = $('#download_' + uuid).find('.queue_failed_button')
resultIcon.addClass('clickable')
failedButton.addClass('clickable')
resultIcon.bind('click', { item: this.queueList[uuid] }, this.showErrorsTab)
failedButton.bind('click', { item: this.queueList[uuid] }, this.showErrorsTab)
if (this.queueList[uuid].failed >= this.queueList[uuid].size) {
resultIcon.text('error')
} else {
resultIcon.text('warning')
}
}
let index = this.queue.indexOf(uuid)
if (index > -1) {
this.queue.splice(index, 1)
this.queueComplete.push(uuid)
}
if (this.queue.length <= 0) {
toast(this.$t('toasts.allDownloaded'), 'done_all')
}
}
},
openDownloadsFolder() {
if (window.clientMode) {
socket.emit('openDownloadsFolder')
}
// if (this.clientMode) {
socket.emit('openDownloadsFolder')
// }
},
handleDrag(event) {
let newWidth = window.innerWidth - event.pageX + 2
@ -358,24 +304,37 @@ export default {
document.addEventListener('mousemove', this.handleDrag)
},
startDownload(uuid) {
$('#bar_' + uuid)
.removeClass('converting')
.removeClass('indeterminate')
.addClass('determinate')
this.$set(this.queueList[uuid], 'status', 'downloading')
},
finishDownload(uuid) {
let isInQueue = this.queue.indexOf(uuid) > -1
if (!isInQueue) return
this.$set(this.queueList[uuid], 'status', 'download finished')
toast(this.$t('toasts.finishDownload', { item: this.queueList[uuid].title }), 'done')
let index = this.queue.indexOf(uuid)
if (index > -1) {
this.queue.splice(index, 1)
this.queueComplete.push(uuid)
}
if (this.queue.length <= 0) {
toast(this.$t('toasts.allDownloaded'), 'done_all')
}
},
startConversion(uuid) {
$('#bar_' + uuid)
.addClass('converting')
.removeClass('indeterminate')
.addClass('determinate')
.css('width', '100%')
this.$set(this.queueList[uuid], 'status', 'converting')
this.$set(this.queueList[uuid], 'conversion', 0)
},
showErrorsTab(clickEvent) {
this.$root.$emit('showTabErrors', clickEvent.data.item, clickEvent.target)
async showErrorsTab(item) {
await this.setErrors(item)
this.$router.push({ name: 'Errors' })
}
}
}
</script>
<style>
</style>

View File

@ -1,6 +1,7 @@
<template>
<div id="errors_tab" class="main_tabcontent">
<h1>{{ $t('errors.title', {name: title}) }}</h1>
<h1>{{ $t('errors.title', { name: title }) }}</h1>
<table class="table table--tracklist">
<tr>
<th>ID</th>
@ -19,31 +20,17 @@
</template>
<script>
import { changeTab } from '@js/tabs.js'
import EventBus from '@/utils/EventBus'
import { mapGetters } from 'vuex'
export default {
name: 'the-errors-tab',
data: () => ({
title: '',
errors: []
}),
methods: {
reset() {
this.title = ''
this.errors = []
computed: {
...mapGetters(['getErrors']),
title() {
return `${this.getErrors.artist} - ${this.getErrors.title}`
},
showErrors(data, eventTarget) {
this.title = data.artist + ' - ' + data.title
this.errors = data.errors
changeTab(eventTarget, 'main', 'errors_tab')
errors() {
return this.getErrors.errors
}
},
mounted() {
EventBus.$on('showTabErrors', this.showErrors)
this.$root.$on('showTabErrors', this.showErrors)
}
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<div id="favorites_tab" class="main_tabcontent" @click="handleFavoritesTabClick">
<div id="favorites_tab" class="main_tabcontent">
<h2 class="page_heading">
{{ $t('favorites.title') }}
<div
@ -13,21 +13,18 @@
</div>
</h2>
<div class="section-tabs">
<div class="section-tabs__tab favorites_tablinks" id="favorites_playlist_tab">
{{ $tc('globals.listTabs.playlist', 2) }}
</div>
<div class="section-tabs__tab favorites_tablinks" id="favorites_album_tab">
{{ $tc('globals.listTabs.album', 2) }}
</div>
<div class="section-tabs__tab favorites_tablinks" id="favorites_artist_tab">
{{ $tc('globals.listTabs.artist', 2) }}
</div>
<div class="section-tabs__tab favorites_tablinks" id="favorites_track_tab">
{{ $tc('globals.listTabs.track', 2) }}
<div
class="section-tabs__tab favorites_tablinks"
:class="{ active: activeTab === tab }"
@click="activeTab = tab"
v-for="tab in tabs"
:key="tab"
>
{{ $tc(`globals.listTabs.${tab}`, 2) }}
</div>
</div>
<div id="playlist_favorites" class="favorites_tabcontent">
<div class="favorites_tabcontent" :class="{ 'favorites_tabcontent--active': activeTab === 'playlist' }">
<div v-if="playlists.length == 0">
<h1>{{ $t('favorites.noPlaylists') }}</h1>
</div>
@ -47,7 +44,12 @@
</div>
<p class="primary-text">{{ release.title }}</p>
<p class="secondary-text">
{{ `${$t('globals.by', {artist: release.creator.name})} - ${$tc('globals.listTabs.trackN', release.nb_tracks)}` }}
{{
`${$t('globals.by', { artist: release.creator.name })} - ${$tc(
'globals.listTabs.trackN',
release.nb_tracks
)}`
}}
</p>
</div>
<div
@ -70,12 +72,18 @@
</div>
<p class="primary-text">{{ release.title }}</p>
<p class="secondary-text">
{{ `${$t('globals.by', {artist: release.creator.name})} - ${$tc('globals.listTabs.trackN', release.nb_tracks)}` }}
{{
`${$t('globals.by', { artist: release.creator.name })} - ${$tc(
'globals.listTabs.trackN',
release.nb_tracks
)}`
}}
</p>
</div>
</div>
</div>
<div id="album_favorites" class="favorites_tabcontent">
<div class="favorites_tabcontent" :class="{ 'favorites_tabcontent--active': activeTab === 'album' }">
<div v-if="albums.length == 0">
<h1>{{ $t('favorites.noAlbums') }}</h1>
</div>
@ -94,11 +102,12 @@
</div>
</div>
<p class="primary-text">{{ release.title }}</p>
<p class="secondary-text">{{ `${$t('globals.by', {artist: release.artist.name})}` }}</p>
<p class="secondary-text">{{ `${$t('globals.by', { artist: release.artist.name })}` }}</p>
</div>
</div>
</div>
<div id="artist_favorites" class="favorites_tabcontent">
<div class="favorites_tabcontent" :class="{ 'favorites_tabcontent--active': activeTab === 'artist' }">
<div v-if="artists.length == 0">
<h1>{{ $t('favorites.noArtists') }}</h1>
</div>
@ -120,7 +129,8 @@
</div>
</div>
</div>
<div id="track_favorites" class="favorites_tabcontent">
<div class="favorites_tabcontent" :class="{ 'favorites_tabcontent--active': activeTab === 'track' }">
<div v-if="tracks.length == 0">
<h1>{{ $t('favorites.noTracks') }}</h1>
</div>
@ -189,24 +199,63 @@
</div>
</template>
<style lang="scss">
.favorites_tabcontent {
display: none;
&--active {
display: block;
}
}
</style>
<script>
import { showView } from '@js/tabs'
import { socket } from '@/utils/socket'
import { showView, changeTab } from '@js/tabs.js'
import Downloads from '@/utils/downloads'
import Utils from '@/utils/utils'
import { sendAddToQueue } from '@/utils/downloads'
import { convertDuration } from '@/utils/utils'
import { toast } from '@/utils/toasts'
import { getFavoritesData } from '@/data/favorites'
export default {
name: 'the-favorites-tab',
data() {
return {
tracks: [],
albums: [],
artists: [],
playlists: [],
spotifyPlaylists: []
spotifyPlaylists: [],
activeTab: 'playlist',
tabs: ['playlist', 'album', 'artist', 'track']
}
},
async created() {
const favoritesData = await getFavoritesData()
// TODO Change with isLoggedIn vuex getter
if (Object.entries(favoritesData).length === 0) return
this.setFavorites(favoritesData)
},
mounted() {
socket.on('updated_userFavorites', this.updated_userFavorites)
socket.on('updated_userSpotifyPlaylists', this.updated_userSpotifyPlaylists)
socket.on('updated_userPlaylists', this.updated_userPlaylists)
socket.on('updated_userAlbums', this.updated_userAlbums)
socket.on('updated_userArtist', this.updated_userArtist)
socket.on('updated_userTracks', this.updated_userTracks)
this.$on('hook:destroyed', () => {
socket.off('updated_userFavorites')
socket.off('updated_userSpotifyPlaylists')
socket.off('updated_userPlaylists')
socket.off('updated_userAlbums')
socket.off('updated_userArtist')
socket.off('updated_userTracks')
})
},
methods: {
artistView: showView.bind(null, 'artist'),
albumView: showView.bind(null, 'album'),
@ -221,39 +270,9 @@ export default {
previewMouseLeave(e) {
EventBus.$emit('trackPreview:previewMouseLeave', e)
},
convertDuration: Utils.convertDuration,
handleFavoritesTabClick(event) {
const {
target,
target: { id }
} = event
let selectedTab = null
switch (id) {
case 'favorites_playlist_tab':
selectedTab = 'playlist_favorites'
break
case 'favorites_album_tab':
selectedTab = 'album_favorites'
break
case 'favorites_artist_tab':
selectedTab = 'artist_favorites'
break
case 'favorites_track_tab':
selectedTab = 'track_favorites'
break
default:
break
}
if (!selectedTab) return
changeTab(target, 'favorites', selectedTab)
},
convertDuration,
addToQueue(e) {
e.stopPropagation()
Downloads.sendAddToQueue(e.currentTarget.dataset.link)
sendAddToQueue(e.currentTarget.dataset.link)
},
updated_userSpotifyPlaylists(data) {
this.spotifyPlaylists = data
@ -272,16 +291,15 @@ export default {
},
reloadTabs() {
this.$refs.reloadButton.classList.add('spin')
socket.emit('update_userFavorites')
if (localStorage.getItem('spotifyUser'))
if (localStorage.getItem('spotifyUser')) {
socket.emit('update_userSpotifyPlaylists', localStorage.getItem('spotifyUser'))
}
},
updated_userFavorites(data) {
const { tracks, albums, artists, playlists } = data
this.tracks = tracks
this.albums = albums
this.artists = artists
this.playlists = playlists
this.setFavorites(data)
// Removing animation class only when the animation has completed an iteration
// Prevents animation ugly stutter
@ -294,22 +312,14 @@ export default {
{ once: true }
)
},
initFavorites(data) {
this.updated_userFavorites(data)
document.getElementById('favorites_playlist_tab').click()
setFavorites(data) {
const { tracks, albums, artists, playlists } = data
this.tracks = tracks
this.albums = albums
this.artists = artists
this.playlists = playlists
}
},
mounted() {
socket.on('init_favorites', this.initFavorites)
socket.on('updated_userFavorites', this.updated_userFavorites)
socket.on('updated_userSpotifyPlaylists', this.updated_userSpotifyPlaylists)
socket.on('updated_userPlaylists', this.updated_userPlaylists)
socket.on('updated_userAlbums', this.updated_userAlbums)
socket.on('updated_userArtist', this.updated_userArtist)
socket.on('updated_userTracks', this.updated_userTracks)
}
}
</script>
<style>
</style>

View File

@ -1,10 +1,15 @@
<template>
<div id="home_tab" class="main_tabcontent">
<div id="home_tab" class="main_tabcontent" ref="root">
<h2 class="page_heading">{{ $t('globals.welcome') }}</h2>
<section id="home_not_logged_in" class="home_section" ref="notLogged">
<section class="home_section" ref="notLogged" v-if="!isLoggedIn">
<p id="home_not_logged_text">{{ $t('home.needTologin') }}</p>
<button type="button" name="button" @click="openSettings">{{ $t('home.openSettings') }}</button>
<!-- <button type="button" name="button" @click="openSettings">{{ $t('home.openSettings') }}</button> -->
<router-link tag="button" name="button" :to="{ name: 'Settings' }">
{{ $t('home.openSettings') }}
</router-link>
</section>
<section v-if="playlists.length" class="home_section">
<h3 class="section_heading">{{ $t('home.sections.popularPlaylists') }}</h3>
<div class="release_grid">
@ -29,11 +34,17 @@
</div>
<p class="primary-text">{{ release.title }}</p>
<p class="secondary-text">
{{ `${$t('globals.by', {artist: release.user.name})} - ${$tc('globals.listTabs.trackN', release.nb_tracks)}` }}
{{
`${$t('globals.by', { artist: release.user.name })} - ${$tc(
'globals.listTabs.trackN',
release.nb_tracks
)}`
}}
</p>
</div>
</div>
</section>
<section v-if="albums.length" class="home_section">
<h3 class="section_heading">{{ $t('home.sections.popularAlbums') }}</h3>
<div class="release_grid">
@ -57,7 +68,7 @@
</div>
</div>
<p class="primary-text">{{ release.title }}</p>
<p class="secondary-text">{{ `${$t('globals.by', {artist: release.artist.name})}` }}</p>
<p class="secondary-text">{{ `${$t('globals.by', { artist: release.artist.name })}` }}</p>
</div>
</div>
</section>
@ -65,27 +76,36 @@
</template>
<script>
import { socket } from '@/utils/socket'
import { showView } from '@js/tabs.js'
import Downloads from '@/utils/downloads'
import { mapGetters } from 'vuex'
import { showView } from '@js/tabs'
import { sendAddToQueue } from '@/utils/downloads'
import { getHomeData } from '@/data/home'
export default {
name: 'the-home-tab',
data() {
return {
playlists: [],
albums: []
}
},
async created() {
const homeData = await getHomeData()
this.initHome(homeData)
},
computed: {
...mapGetters(['isLoggedIn']),
needToWait() {
return this.getHomeData.albums.data.length === 0 && this.getHomeData.playlists.data.length === 0
}
},
methods: {
artistView: showView.bind(null, 'artist'),
albumView: showView.bind(null, 'album'),
playlistView: showView.bind(null, 'playlist'),
openSettings() {
document.getElementById('main_settings_tablink').click()
},
addToQueue(e) {
Downloads.sendAddToQueue(e.currentTarget.dataset.link)
sendAddToQueue(e.currentTarget.dataset.link)
},
initHome(data) {
const {
@ -96,13 +116,6 @@ export default {
this.playlists = playlistData
this.albums = albumData
}
},
mounted() {
if (localStorage.getItem('arl')) {
this.$refs.notLogged.classList.add('hide')
}
socket.on('init_home', this.initHome)
}
}
</script>

View File

@ -1,7 +1,8 @@
<template>
<div id="analyzer_tab" class="main_tabcontent image_header">
<div id="analyzer_tab" class="main_tabcontent image_header" ref="root">
<h2 class="page_heading page_heading--capitalize">{{ $t('sidebar.linkAnalyzer') }}</h2>
<div v-if="link == ''">
<div v-if="link === ''">
<p>
{{ $t('linkAnalyzer.info') }}
</p>
@ -9,10 +10,11 @@
{{ $t('linkAnalyzer.useful') }}
</p>
</div>
<div v-else-if="link == 'error'">
<div v-else-if="link === 'error'">
<h2>{{ $t('linkAnalyzer.linkNotSupported') }}</h2>
<p>{{ $t('linkAnalyzer.linkNotSupportedYet') }}</p>
</div>
<div v-else>
<header
class="inline-flex"
@ -115,21 +117,21 @@
<script>
import { socket } from '@/utils/socket'
import { showView } from '@js/tabs.js'
import Utils from '@/utils/utils'
import { showView } from '@js/tabs'
import { convertDuration } from '@/utils/utils'
import { COUNTRIES } from '@/utils/countries'
import EventBus from '@/utils/EventBus'
import Downloads from '@/utils/downloads'
import { sendAddToQueue } from '@/utils/downloads'
export default {
name: 'the-link-analyzer-tab',
data() {
return {
link: '',
title: '',
subtitle: '',
image: '',
data: {},
type: '',
link: '',
id: '0',
countries: []
}
@ -137,7 +139,7 @@ export default {
methods: {
artistView: showView.bind(null, 'artist'),
albumView: showView.bind(null, 'album'),
convertDuration: Utils.convertDuration,
convertDuration,
reset() {
this.title = 'Loading...'
this.subtitle = ''
@ -148,6 +150,7 @@ export default {
this.countries = []
},
showTrack(data) {
this.reset()
const {
title,
title_version,
@ -167,13 +170,14 @@ export default {
let temp = []
let chars = [...cc].map(c => c.charCodeAt() + 127397)
temp.push(String.fromCodePoint(...chars))
temp.push(Utils.COUNTRIES[cc])
temp.push(COUNTRIES[cc])
this.countries.push(temp)
})
this.data = data
},
showAlbum(data) {
this.reset()
const { title, cover_xl, link, id } = data
this.title = title
@ -187,7 +191,7 @@ export default {
this.link = 'error'
},
addToQueue(e) {
Downloads.sendAddToQueue(e.currentTarget.dataset.link)
sendAddToQueue(e.currentTarget.dataset.link)
}
},
mounted() {

View File

@ -1,23 +0,0 @@
<template>
<main id="main_content">
<!-- <router-link to="/tracklist/132">Go to Foo</router-link> -->
<!-- <router-view></router-view> -->
<TheMiddleSection />
<TheDownloadTab />
</main>
</template>
<script>
import TheMiddleSection from '@components/TheMiddleSection.vue'
import TheDownloadTab from '@components/TheDownloadTab.vue'
export default {
components: {
TheMiddleSection,
TheDownloadTab
}
}
</script>
<style>
</style>

View File

@ -1,455 +1,92 @@
<template>
<div id="search_tab" class="main_tabcontent" @click="handleSearchTabClick">
<div :class="{ hide: results.query != '' }">
<div id="search_tab" class="main_tabcontent" ref="root">
<div v-show="!showSearchTab">
<h2>{{ $t('search.startSearching') }}</h2>
<p>{{ $t('search.description') }}</p>
</div>
<div v-show="results.query !== ''">
<div v-show="showSearchTab">
<ul class="section-tabs">
<li class="section-tabs__tab search_tablinks" id="search_all_tab">{{ $t('globals.listTabs.all') }}</li>
<li class="section-tabs__tab search_tablinks" id="search_track_tab">{{ $tc('globals.listTabs.track', 2) }}</li>
<li class="section-tabs__tab search_tablinks" id="search_album_tab">{{ $tc('globals.listTabs.album', 2) }}</li>
<li class="section-tabs__tab search_tablinks" id="search_artist_tab">
{{ $tc('globals.listTabs.artist', 2) }}
</li>
<li class="section-tabs__tab search_tablinks" id="search_playlist_tab">
{{ $tc('globals.listTabs.playlist', 2) }}
<li
class="section-tabs__tab"
v-for="tab in tabs"
:key="tab.name"
@click="currentTab = tab"
:class="{ active: currentTab.name === tab.name }"
>
{{ tab.name }}
</li>
</ul>
<div id="search_tab_content">
<!-- ### Main Search Tab ### -->
<div id="main_search" class="search_tabcontent">
<template v-for="section in results.allTab.ORDER">
<section
v-if="
(section != 'TOP_RESULT' && results.allTab[section].data.length > 0) ||
results.allTab[section].length > 0
"
class="search_section"
>
<h2
@click="changeSearchTab(section)"
class="search_header"
:class="{ top_result_header: section === 'TOP_RESULT' }"
>
{{ $tc(`globals.listTabs.${section.toLowerCase()}`, 2) }}
</h2>
<!-- Top result -->
<div
v-if="section == 'TOP_RESULT'"
class="top_result clickable"
@click="handleClickTopResult"
:data-id="results.allTab.TOP_RESULT[0].id"
>
<div class="cover_container">
<img
aria-hidden="true"
:src="results.allTab.TOP_RESULT[0].picture"
:class="(results.allTab.TOP_RESULT[0].type == 'artist' ? 'circle' : 'rounded') + ' coverart'"
/>
<div
role="button"
aria-label="download"
@click.stop="addToQueue"
:data-link="results.allTab.TOP_RESULT[0].link"
class="download_overlay"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</div>
</div>
<div class="info_box">
<p class="primary-text">{{ results.allTab.TOP_RESULT[0].title }}</p>
<p class="secondary-text">
{{
results.allTab.TOP_RESULT[0].type == 'artist'
? $t('search.fans', {n: $n(results.allTab.TOP_RESULT[0].nb_fan)})
: $t('globals.by', {artist: results.allTab.TOP_RESULT[0].artist}) +
' - ' +
$tc('globals.listTabs.trackN', results.allTab.TOP_RESULT[0].nb_song)
}}
</p>
<span class="tag">{{ $tc(`globals.listTabs.${results.allTab.TOP_RESULT[0].type}`, 1) }}</span>
</div>
</div>
<div v-else-if="section == 'TRACK'">
<table class="table table--tracks">
<tbody>
<tr v-for="track in results.allTab.TRACK.data.slice(0, 6)">
<td class="table__icon" aria-hidden="true">
<img
class="rounded coverart"
:src="
'https://e-cdns-images.dzcdn.net/images/cover/' +
track.ALB_PICTURE +
'/32x32-000000-80-0-0.jpg'
"
/>
</td>
<td class="table__cell table__cell--large breakline">
<div class="table__cell-content table__cell-content--vertical-center">
<i v-if="track.EXPLICIT_LYRICS == 1" class="material-icons explicit_icon">
explicit
</i>
{{ track.SNG_TITLE + (track.VERSION ? ' ' + track.VERSION : '') }}
</div>
</td>
<td class="table__cell table__cell--medium table__cell--center breakline">
<span
class="clickable"
@click="artistView"
:data-id="artist.ART_ID"
v-for="artist in track.ARTISTS"
>{{ artist.ART_NAME }}
</span>
</td>
<td
class="table__cell--medium table__cell--center breakline clickable"
@click="albumView"
:data-id="track.ALB_ID"
>
{{ track.ALB_TITLE }}
</td>
<td class="table__cell table__cell--center">
{{ convertDuration(track.DURATION) }}
</td>
<td
class="table__cell--download table__cell--center clickable"
@click.stop="addToQueue"
:data-link="'https://www.deezer.com/track/' + track.SNG_ID"
role="button"
aria-label="download"
>
<i class="material-icons" :title="$t('globals.download_hint')">
get_app
</i>
</td>
</tr>
</tbody>
</table>
</div>
<div v-else-if="section == 'ARTIST'" class="release_grid firstrow_only">
<div
v-for="release in results.allTab.ARTIST.data.slice(0, 10)"
class="release clickable"
@click="artistView"
:data-id="release.ART_ID"
>
<div class="cover_container">
<img
aria-hidden="true"
class="circle coverart"
:src="
'https://e-cdns-images.dzcdn.net/images/artist/' +
release.ART_PICTURE +
'/156x156-000000-80-0-0.jpg'
"
/>
<div
role="button"
aria-label="download"
@click.stop="addToQueue"
:data-link="'https://deezer.com/artist/' + release.ART_ID"
class="download_overlay"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</div>
</div>
<p class="primary-text">{{ release.ART_NAME }}</p>
<p class="secondary-text">{{ $t('search.fans', {n: $n(release.NB_FAN)}) }}</p>
</div>
</div>
<div v-else-if="section == 'ALBUM'" class="release_grid firstrow_only">
<div
v-for="release in results.allTab.ALBUM.data.slice(0, 10)"
class="release clickable"
@click="albumView"
:data-id="release.ALB_ID"
>
<div class="cover_container">
<img
aria-hidden="true"
class="rounded coverart"
:src="
'https://e-cdns-images.dzcdn.net/images/cover/' +
release.ALB_PICTURE +
'/156x156-000000-80-0-0.jpg'
"
/>
<div
role="button"
aria-label="download"
@click.stop="addToQueue"
:data-link="'https://deezer.com/album/' + release.ALB_ID"
class="download_overlay"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</div>
</div>
<p class="primary-text inline-flex">
<i
v-if="[1, 4].indexOf(release.EXPLICIT_ALBUM_CONTENT.EXPLICIT_LYRICS_STATUS) != -1"
class="material-icons explicit_icon"
>explicit</i
>
{{ release.ALB_TITLE }}
</p>
<p class="secondary-text">
{{ release.ART_NAME + ' - ' + $tc('globals.listTabs.trackN', release.NUMBER_TRACK) }}
</p>
</div>
</div>
<div v-else-if="section == 'PLAYLIST'" class="release_grid firstrow_only">
<div
v-for="release in results.allTab.PLAYLIST.data.slice(0, 10)"
class="release clickable"
@click="playlistView"
:data-id="release.PLAYLIST_ID"
>
<div class="cover_container">
<img
aria-hidden="true"
class="rounded coverart"
:src="
'https://e-cdns-images.dzcdn.net/images/' +
release.PICTURE_TYPE +
'/' +
release.PLAYLIST_PICTURE +
'/156x156-000000-80-0-0.jpg'
"
/>
<div
role="button"
aria-label="download"
@click.stop="addToQueue"
:data-link="'https://deezer.com/playlist/' + release.PLAYLIST_ID"
class="download_overlay"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</div>
</div>
<p class="primary-text">{{ release.TITLE }}</p>
<p class="secondary-text">{{ $tc('globals.listTabs.trackN', release.NB_SONG) }}</p>
</div>
</div>
</section>
</template>
<div
v-if="
results.allTab.ORDER.every(section =>
section == 'TOP_RESULT' ? results.allTab[section].length == 0 : results.allTab[section].data.length == 0
)
"
>
<h1>{{ $t('search.noResults') }}</h1>
</div>
</div>
<!-- ### Track Search Tab ### -->
<div id="track_search" class="search_tabcontent">
<base-loading-placeholder v-if="!results.trackTab.loaded"></base-loading-placeholder>
<div v-else-if="results.trackTab.data.length == 0">
<h1>{{ $t('search.noResultsTrack') }}</h1>
</div>
<table class="table table--tracks" v-if="results.trackTab.data.length > 0">
<thead>
<tr>
<th colspan="2">{{ $tc('globals.listTabs.title', 1) }}</th>
<th>{{ $tc('globals.listTabs.artist', 1) }}</th>
<th>{{ $tc('globals.listTabs.album', 1) }}</th>
<th>
<i class="material-icons">
timer
</i>
</th>
<th style="width: 56px;"></th>
</tr>
</thead>
<tbody>
<tr v-for="track in results.trackTab.data">
<td class="table__icon table__icon--big">
<a
href="#"
@click="playPausePreview"
:class="'rounded' + (track.preview ? ' single-cover' : '')"
:data-preview="track.preview"
>
<i
@mouseenter="previewMouseEnter"
@mouseleave="previewMouseLeave"
v-if="track.preview"
class="material-icons preview_controls"
:title="$t('globals.play_hint')"
>
play_arrow
</i>
<img class="rounded coverart" :src="track.album.cover_small" />
</a>
</td>
<td class="table__cell table__cell--large breakline">
<div class="table__cell-content table__cell-content--vertical-center">
<i v-if="track.explicit_lyrics" class="material-icons explicit_icon">
explicit
</i>
{{
track.title +
(track.title_version && track.title.indexOf(track.title_version) == -1
? ' ' + track.title_version
: '')
}}
</div>
</td>
<td
class="table__cell table__cell--medium table__cell--center breakline clickable"
@click="artistView"
:data-id="track.artist.id"
>
{{ track.artist.name }}
</td>
<td
class="table__cell table__cell--medium table__cell--center breakline clickable"
@click="albumView"
:data-id="track.album.id"
>
{{ track.album.title }}
</td>
<td class="table__cell table__cell--small table__cell--center">
{{ convertDuration(track.duration) }}
</td>
<td
class="table__cell--download table__cell--center clickable"
@click.stop="addToQueue"
:data-link="track.link"
role="button"
aria-label="download"
>
<i class="material-icons" :title="$t('globals.download_hint')">
get_app
</i>
</td>
</tr>
</tbody>
</table>
</div>
<!-- ### Album Search Tab ### -->
<div id="album_search" class="search_tabcontent">
<base-loading-placeholder v-if="!results.albumTab.loaded"></base-loading-placeholder>
<div v-else-if="results.albumTab.data.length == 0">
<h1>{{ $t('search.noResultsAlbum') }}</h1>
</div>
<div class="release_grid" v-if="results.albumTab.data.length > 0">
<div
v-for="release in results.albumTab.data"
class="release clickable"
@click="albumView"
:data-id="release.id"
>
<div class="cover_container">
<img aria-hidden="true" class="rounded coverart" :src="release.cover_medium" />
<div
role="button"
aria-label="download"
@click.stop="addToQueue"
:data-link="release.link"
class="download_overlay"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</div>
</div>
<p class="primary-text inline-flex">
<i v-if="release.explicit_lyrics" class="material-icons explicit_icon">explicit</i>
{{ release.title }}
</p>
<p class="secondary-text">
{{
$t('globals.by', {artist: release.artist.name}) + ' - ' + $tc('globals.listTabs.trackN', release.nb_tracks)
}}
</p>
</div>
</div>
</div>
<!-- ### Artist Search Tab ### -->
<div id="artist_search" class="search_tabcontent">
<base-loading-placeholder v-if="!results.artistTab.loaded"></base-loading-placeholder>
<div v-else-if="results.artistTab.data.length == 0">
<h1>{{ $t('search.noResultsArtist') }}</h1>
</div>
<div class="release_grid" v-if="results.artistTab.data.length > 0">
<div
v-for="release in results.artistTab.data"
class="release clickable"
@click="artistView"
:data-id="release.id"
>
<div class="cover_container">
<img aria-hidden="true" class="circle coverart" :src="release.picture_medium" />
<div
role="button"
aria-label="download"
@click.stop="addToQueue"
:data-link="release.link"
class="download_overlay"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</div>
</div>
<p class="primary-text">{{ release.name }}</p>
<p class="secondary-text">{{ $tc('globals.listTabs.releaseN', release.nb_album) }}</p>
</div>
</div>
</div>
<!-- ### Playlist Search Tab ### -->
<div id="playlist_search" class="search_tabcontent">
<base-loading-placeholder v-if="!results.playlistTab.loaded"></base-loading-placeholder>
<div v-else-if="results.playlistTab.data.length == 0">
<h1>{{ $t('search.noResultsPlaylist') }}</h1>
</div>
<div class="release_grid" v-if="results.playlistTab.data.length > 0">
<div
v-for="release in results.playlistTab.data"
class="release clickable"
@click="playlistView"
:data-id="release.id"
>
<div class="cover_container">
<img aria-hidden="true" class="rounded coverart" :src="release.picture_medium" />
<div
role="button"
aria-label="download"
@click.stop="addToQueue"
:data-link="release.link"
class="download_overlay"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</div>
</div>
<p class="primary-text">{{ release.title }}</p>
<p class="secondary-text">
{{ `${$t('globals.by', {artist: release.user.name})} - ${$tc('globals.listTabs.trackN', release.nb_tracks)}` }}
</p>
</div>
</div>
</div>
</div>
<keep-alive>
<component
:is="currentTab.component"
:results="results"
@add-to-queue="addToQueue"
@artist-view="artistView"
@album-view="albumView"
@playlist-view="playlistView"
@change-search-tab="changeSearchTab"
></component>
</keep-alive>
</div>
</div>
</template>
<script>
import { socket } from '@/utils/socket'
import { showView } from '@js/tabs.js'
import Downloads from '@/utils/downloads'
import Utils from '@/utils/utils'
import BaseLoadingPlaceholder from '@components/BaseLoadingPlaceholder.vue'
import ResultsAll from '@components/search/ResultsAll.vue'
import ResultsAlbums from '@components/search/ResultsAlbums.vue'
import ResultsArtists from '@components/search/ResultsArtists.vue'
import ResultsPlaylists from '@components/search/ResultsPlaylists.vue'
import ResultsTracks from '@components/search/ResultsTracks.vue'
import { changeTab } from '@js/tabs.js'
import EventBus from '@/utils/EventBus.js'
import { socket } from '@/utils/socket'
import { showView } from '@js/tabs'
import { sendAddToQueue } from '@/utils/downloads'
import { numberWithDots, convertDuration } from '@/utils/utils'
import EventBus from '@/utils/EventBus'
export default {
name: 'the-main-search-tab',
components: {
BaseLoadingPlaceholder
},
data() {
const $t = this.$t.bind(this)
const $tc = this.$tc.bind(this)
return {
currentTab: {
name: '',
component: {}
},
tabs: [
{
name: $t('globals.listTabs.all'),
searchType: 'all',
component: ResultsAll
},
{
name: $tc('globals.listTabs.track', 2),
searchType: 'track',
component: ResultsTracks
},
{
name: $tc('globals.listTabs.album', 2),
searchType: 'album',
component: ResultsAlbums
},
{
name: $tc('globals.listTabs.artist', 2),
searchType: 'artist',
component: ResultsArtists
},
{
name: $tc('globals.listTabs.playlist', 2),
searchType: 'playlist',
component: ResultsPlaylists
}
],
results: {
query: '',
allTab: {
@ -487,16 +124,39 @@ export default {
}
}
},
computed: {
showSearchTab() {
return this.results.query !== ''
},
loadedTabs() {
const loaded = []
for (const resultKey in this.results) {
if (this.results.hasOwnProperty(resultKey)) {
const result = this.results[resultKey]
if (result.loaded) {
loaded.push(resultKey.replace(/Tab/g, ''))
}
}
}
return loaded
}
},
props: {
scrolledSearchType: {
type: String,
performScrolledSearch: {
type: Boolean,
required: false
}
},
created() {
this.currentTab = this.tabs[0]
},
mounted() {
EventBus.$on('mainSearch:checkLoadMoreContent', this.checkLoadMoreContent)
this.$root.$on('mainSearch:showNewResults', this.checkIfShowNewResults)
this.$root.$on('mainSearch:showNewResults', this.showNewResults)
socket.on('mainSearch', this.handleMainSearch)
socket.on('search', this.handleSearch)
},
@ -504,138 +164,65 @@ export default {
artistView: showView.bind(null, 'artist'),
albumView: showView.bind(null, 'album'),
playlistView: showView.bind(null, 'playlist'),
playPausePreview(e) {
EventBus.$emit('trackPreview:playPausePreview', e)
},
previewMouseEnter(e) {
EventBus.$emit('trackPreview:previewMouseEnter', e)
},
previewMouseLeave(e) {
EventBus.$emit('trackPreview:previewMouseLeave', e)
},
handleSearchTabClick(event) {
const {
target,
target: { id }
} = event
let selectedTab = null
changeSearchTab(sectionName) {
sectionName = sectionName.toLowerCase()
switch (id) {
case 'search_all_tab':
selectedTab = 'main_search'
break
case 'search_track_tab':
selectedTab = 'track_search'
break
case 'search_album_tab':
selectedTab = 'album_search'
break
case 'search_artist_tab':
selectedTab = 'artist_search'
break
case 'search_playlist_tab':
selectedTab = 'playlist_search'
break
let newTab = this.tabs.find(tab => {
return tab.searchType === sectionName
})
default:
break
if (!newTab) {
console.error(`No tab ${sectionName} found`)
return
}
if (!selectedTab) return
changeTab(target, 'search', selectedTab)
window.scrollTo(0, 0)
this.currentTab = newTab
},
handleClickTopResult(event) {
let topResultType = this.results.allTab.TOP_RESULT[0].type
checkIfShowNewResults(term, mainSelected) {
let needToPerformNewSearch = term !== this.results.query || mainSelected == 'search_tab'
switch (topResultType) {
case 'artist':
this.artistView(event)
break
case 'album':
this.albumView(event)
break
case 'playlist':
this.playlistView(event)
break
default:
break
if (needToPerformNewSearch) {
this.showNewResults(term)
}
},
showNewResults(term, mainSelected) {
if (term !== this.results.query || mainSelected == 'search_tab') {
document.getElementById('search_tab_content').style.display = 'none'
socket.emit('mainSearch', { term })
showNewResults(term) {
socket.emit('mainSearch', { term })
// Showing loading placeholder
document.getElementById('content').style.display = 'none'
document.getElementById('search_placeholder').classList.toggle('loading_placeholder--hidden')
} else {
document.getElementById('search_tab_content').style.display = 'block'
document.getElementById('main_search_tablink').click()
}
// Showing loading placeholder
this.$root.$emit('updateSearchLoadingState', true)
this.currentTab = this.tabs[0]
},
checkLoadMoreContent(searchSelected) {
if (this.results[searchSelected.split('_')[0] + 'Tab'].data.length !== 0) return
this.search(searchSelected.split('_')[0])
},
changeSearchTab(section) {
if (section === 'TOP_RESULT') return
let tabID
// Using the switch beacuse it's tricky to find refernces of the belo IDs
switch (section) {
case 'TRACK':
tabID = 'search_track_tab'
break
case 'ALBUM':
tabID = 'search_album_tab'
break
case 'ARTIST':
tabID = 'search_artist_tab'
break
case 'PLAYLIST':
tabID = 'search_playlist_tab'
break
default:
break
}
document.getElementById(tabID).click()
},
addToQueue(e) {
Downloads.sendAddToQueue(e.currentTarget.dataset.link)
sendAddToQueue(e.currentTarget.dataset.link)
},
numberWithDots: Utils.numberWithDots,
convertDuration: Utils.convertDuration,
numberWithDots,
convertDuration,
search(type) {
socket.emit('search', {
term: this.results.query,
type: type,
start: this.results[type + 'Tab'].next,
type,
start: this.results[`${type}Tab`].next,
nb: 30
})
},
scrolledSearch(type) {
let currentTab = type + 'Tab'
scrolledSearch() {
if (this.currentTab.searchType === 'all') return
let currentTab = `${this.currentTab.searchType}Tab`
if (this.results[currentTab].next < this.results[currentTab].total) {
socket.emit('search', {
term: this.results.query,
type: type,
start: this.results[currentTab].next,
nb: 30
})
this.search(this.currentTab.searchType)
}
},
handleMainSearch(result) {
// Hiding loading placeholder
document.getElementById('content').style.display = ''
document.getElementById('search_placeholder').classList.toggle('loading_placeholder--hidden')
this.$root.$emit('updateSearchLoadingState', false)
let resetObj = { data: [], next: 0, total: 0, loaded: false }
@ -644,12 +231,7 @@ export default {
this.results.albumTab = { ...resetObj }
this.results.artistTab = { ...resetObj }
this.results.playlistTab = { ...resetObj }
if (this.results.query == '') document.getElementById('search_all_tab').click()
this.results.query = result.QUERY
document.getElementById('search_tab_content').style.display = 'block'
document.getElementById('main_search_tablink').click()
},
handleSearch(result) {
const { next: nextResult, total, type, data } = result
@ -673,13 +255,21 @@ export default {
}
this.results[currentTab].loaded = true
},
isTabLoaded(tab) {
return this.loadedTabs.indexOf(tab.searchType) !== -1 || tab.searchType === 'all'
}
},
watch: {
scrolledSearchType(newType) {
if (!newType) return
performScrolledSearch(needToSearch) {
if (!needToSearch) return
this.scrolledSearch(newType)
this.scrolledSearch(needToSearch)
},
currentTab(newTab) {
if (this.isTabLoaded(newTab)) return
this.search(newTab.searchType)
}
}
}

View File

@ -1,20 +1,29 @@
<template>
<div id="middle_section">
<TheSearchBar />
<main id="main_content">
<TheContent />
<BaseLoadingPlaceholder id="search_placeholder" text="Searching..." :hidden="true" />
</div>
<!-- <BaseLoadingPlaceholder id="search_placeholder" text="Searching..." :hidden="true" /> -->
</main>
</template>
<style lang="scss" scoped>
#main_content {
background-color: var(--main-background);
min-width: 10px;
// margin-left: 48px; // $sidebar-width
// width: calc(100% - #{$sidebar-width});
// flex: 1;
width: 100%;
height: 100%;
}
</style>
<script>
import TheContent from '@components/TheContent.vue'
import TheSearchBar from '@components/TheSearchBar.vue'
import BaseLoadingPlaceholder from '@components/BaseLoadingPlaceholder.vue'
export default {
components: {
TheContent,
TheSearchBar,
BaseLoadingPlaceholder
}
}

View File

@ -14,6 +14,8 @@
ref="searchbar"
@keyup="handleSearchBarKeyup($event)"
/>
<!-- @keyup.enter.exact="onEnter"
@keyup.ctrl.enter="onCTRLEnter" -->
</header>
</template>
@ -25,28 +27,68 @@ import EventBus from '@/utils/EventBus.js'
import { socket } from '@/utils/socket'
export default {
data() {
return {
lastTextSearch: ''
}
},
mounted() {
document.addEventListener('keyup', keyEvent => {
if (!(keyEvent.key == 'Backspace' && keyEvent.ctrlKey)) return
this.$refs.searchbar.value = ''
this.$refs.searchbar.focus()
})
},
methods: {
handleSearchBarKeyup(keyEvent) {
// Enter key
if (keyEvent.keyCode !== 13) return
test() {
console.log('test passato')
},
async handleSearchBarKeyup(keyEvent) {
let isEnterPressed = keyEvent.keyCode === 13
// If not enter do nothing
if (!isEnterPressed) return
let term = this.$refs.searchbar.value
let isEmptySearch = term === ''
if (isValidURL(term)) {
if (keyEvent.ctrlKey) {
// If empty do nothing
if (isEmptySearch) return
let isSearchingURL = isValidURL(term)
let isCtrlPressed = keyEvent.ctrlKey
let isShowingAnalyzer = this.$route.name === 'Link Analyzer'
let isShowingSearch = this.$route.name === 'Search'
let sameAsLastSearch = term === this.lastTextSearch
if (isSearchingURL) {
if (isCtrlPressed) {
this.$root.$emit('QualityModal:open', term)
} else {
if (main_selected === 'analyzer_tab') {
EventBus.$emit('linkAnalyzerTab:reset')
if (isShowingAnalyzer) {
// EventBus.$emit('linkAnalyzerTab:reset')
socket.emit('analyzeLink', term)
} else {
// ? Open downloads tab ?
Downloads.sendAddToQueue(term)
}
}
} else {
if (term === '') return
if (isShowingSearch && sameAsLastSearch) return
this.$root.$emit('mainSearch:showNewResults', term, main_selected)
if (!isShowingSearch) {
await this.$router.push({
name: 'Search'
})
}
if (!sameAsLastSearch) {
this.$root.$emit('updateSearchLoadingState', true)
this.lastTextSearch = term
}
this.$root.$emit('mainSearch:showNewResults', term, window.main_selected)
}
}
}

View File

@ -1,15 +1,18 @@
<template>
<div id="settings_tab" class="main_tabcontent fixed_footer">
<div id="settings_tab" class="main_tabcontent fixed_footer" ref="root">
<h2 class="page_heading">{{ $t('settings.title') }}</h2>
<div id="logged_in_info" ref="loggedInInfo">
<img id="settings_picture" src="" alt="Profile Picture" ref="userpicture" class="circle" />
<div id="logged_in_info" v-if="isLoggedIn" ref="loggedInInfo">
<img id="settings_picture" :src="pictureHref" alt="Profile Picture" ref="userpicture" class="circle" />
<i18n path="settings.login.loggedIn" tag="p">
<strong place="username" id="settings_username" ref="username"></strong>
<strong place="username" id="settings_username" ref="username">{{ user.name || 'not logged' }}</strong>
</i18n>
<button id="settings_btn_logout" @click="logout">{{ $t('settings.login.logout') }}</button>
<select v-if="accounts.length" id="family_account" v-model="accountNum" @change="changeAccount">
<option v-for="(account, i) in accounts" :value="i.toString()">{{ account.BLOG_NAME }}</option>
<option v-for="(account, i) in accounts" :key="account" :value="i.toString()">{{ account.BLOG_NAME }}</option>
</select>
</div>
@ -18,7 +21,14 @@
<i class="material-icons">person</i>{{ $t('settings.login.title') }}
</h3>
<div class="inline-flex">
<input autocomplete="off" type="password" id="login_input_arl" ref="loginInput" placeholder="ARL" />
<input
autocomplete="off"
type="password"
:value="arl"
id="login_input_arl"
ref="loginInput"
placeholder="ARL"
/>
<button id="settings_btn_copyArl" class="only_icon" @click="copyARLtoClipboard">
<i class="material-icons">assignment</i>
</button>
@ -26,10 +36,10 @@
<a href="https://codeberg.org/RemixDev/deemix/wiki/Getting-your-own-ARL" target="_blank">
{{ $t('settings.login.arl.question') }}
</a>
<a id="settings_btn_applogin" class="hide" href="#" @click="applogin">
Automated login
<a id="settings_btn_applogin" v-if="clientMode" href="#" @click="appLogin">
{{ $t('settings.login.login') }}
</a>
<button id="settings_btn_updateArl" @click="login" style="width: 100%;">
<button id="settings_btn_updateArl" @click="login" style="width: 100%">
{{ $t('settings.login.arl.update') }}
</button>
</div>
@ -67,7 +77,7 @@
</h3>
<div class="inline-flex">
<input autocomplete="off" type="text" v-model="settings.downloadLocation" />
<button id="select_downloads_folder" class="only_icon hide" @click="selectDownloadFolder">
<button id="select_downloads_folder" v-if="clientMode" class="only_icon" @click="selectDownloadFolder">
<i class="material-icons">folder</i>
</button>
</div>
@ -275,13 +285,17 @@
<div class="input_group">
<p class="input_group_text">{{ $t('settings.covers.localArtworkSize') }}</p>
<input type="number" min="100" max="10000" step="100" v-model.number="settings.localArtworkSize" />
<p v-if="settings.localArtworkSize > 1200" class="input_group_text" style="opacity: 0.75; color: #ffcc22;"> {{ $t('settings.covers.imageSizeWarning') }}</p>
<p v-if="settings.localArtworkSize > 1200" class="input_group_text" style="opacity: 0.75; color: #ffcc22">
{{ $t('settings.covers.imageSizeWarning') }}
</p>
</div>
<div class="input_group">
<p class="input_group_text">{{ $t('settings.covers.embeddedArtworkSize') }}</p>
<input type="number" min="100" max="10000" step="100" v-model.number="settings.embeddedArtworkSize" />
<p v-if="settings.embeddedArtworkSize > 1200" class="input_group_text" style="opacity: 0.75; color: #ffcc22;"> {{ $t('settings.covers.imageSizeWarning') }}</p>
<p v-if="settings.embeddedArtworkSize > 1200" class="input_group_text" style="opacity: 0.75; color: #ffcc22">
{{ $t('settings.covers.imageSizeWarning') }}
</p>
</div>
<div class="input_group">
@ -297,7 +311,14 @@
<input type="checkbox" v-model="settings.embeddedArtworkPNG" />
<span class="checkbox_text">{{ $t('settings.covers.embeddedArtworkPNG') }}</span>
</label>
<p v-if="settings.embeddedArtworkPNG" style="opacity: 0.75; color: #ffcc22;"> {{ $t('settings.covers.embeddedPNGWarning') }}</p>
<p v-if="settings.embeddedArtworkPNG" style="opacity: 0.75; color: #ffcc22">
{{ $t('settings.covers.embeddedPNGWarning') }}
</p>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.coverDescriptionUTF8" />
<span class="checkbox_text">{{ $t('settings.covers.coverDescriptionUTF8') }}</span>
</label>
<div class="input_group">
<p class="input_group_text">{{ $t('settings.covers.jpegImageQuality') }}</p>
@ -307,7 +328,7 @@
<div class="settings-group">
<h3 class="settings-group__header settings-group__header--with-icon">
<i class="material-icons" style="width: 1em; height: 1em;">bookmarks</i>{{ $t('settings.tags.head') }}
<i class="material-icons" style="width: 1em; height: 1em">bookmarks</i>{{ $t('settings.tags.head') }}
</h3>
<div class="settings-container">
@ -395,6 +416,10 @@
<input type="checkbox" v-model="settings.tags.lyrics" />
<span class="checkbox_text">{{ $t('settings.tags.lyrics') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.syncedLyrics" />
<span class="checkbox_text">{{ $t('settings.tags.syncedLyrics') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.copyright" />
<span class="checkbox_text">{{ $t('settings.tags.copyright') }}</span>
@ -437,13 +462,13 @@
<option value="nothing">{{ $t('settings.other.multiArtistSeparator.nothing') }}</option>
<option value="default">{{ $t('settings.other.multiArtistSeparator.default') }}</option>
<option value="andFeat">{{ $t('settings.other.multiArtistSeparator.andFeat') }}</option>
<option value=" & ">{{ $t('settings.other.multiArtistSeparator.using', {separator: ' & '}) }}</option>
<option value=",">{{ $t('settings.other.multiArtistSeparator.using', {separator: ','}) }}</option>
<option value=", ">{{ $t('settings.other.multiArtistSeparator.using', {separator: ', '}) }}</option>
<option value="/">{{ $t('settings.other.multiArtistSeparator.using', {separator: '/'}) }}</option>
<option value=" / ">{{ $t('settings.other.multiArtistSeparator.using', {separator: ' / '}) }}</option>
<option value=";">{{ $t('settings.other.multiArtistSeparator.using', {separator: ';'}) }}</option>
<option value="; ">{{ $t('settings.other.multiArtistSeparator.using', {separator: '; '}) }}</option>
<option value=" & ">{{ $t('settings.other.multiArtistSeparator.using', { separator: ' & ' }) }}</option>
<option value=",">{{ $t('settings.other.multiArtistSeparator.using', { separator: ',' }) }}</option>
<option value=", ">{{ $t('settings.other.multiArtistSeparator.using', { separator: ', ' }) }}</option>
<option value="/">{{ $t('settings.other.multiArtistSeparator.using', { separator: '/' }) }}</option>
<option value=" / ">{{ $t('settings.other.multiArtistSeparator.using', { separator: ' / ' }) }}</option>
<option value=";">{{ $t('settings.other.multiArtistSeparator.using', { separator: ';' }) }}</option>
<option value="; ">{{ $t('settings.other.multiArtistSeparator.using', { separator: '; ' }) }}</option>
</select>
</div>
@ -470,26 +495,34 @@
<div class="input_group">
<p class="input_group_text">{{ $t('settings.other.dateFormat.title') }}</p>
<select v-model="settings.dateFormat">
<option value="Y-M-D">{{
`${$t('settings.other.dateFormat.year')}-${$t('settings.other.dateFormat.month')}-${$t(
'settings.other.dateFormat.day'
)}`
}}</option>
<option value="Y-D-M">{{
`${$t('settings.other.dateFormat.year')}-${$t('settings.other.dateFormat.day')}-${$t(
'settings.other.dateFormat.month'
)}`
}}</option>
<option value="D-M-Y">{{
`${$t('settings.other.dateFormat.day')}-${$t('settings.other.dateFormat.month')}-${$t(
'settings.other.dateFormat.year'
)}`
}}</option>
<option value="M-D-Y">{{
`${$t('settings.other.dateFormat.month')}-${$t('settings.other.dateFormat.day')}-${$t(
'settings.other.dateFormat.year'
)}`
}}</option>
<option value="Y-M-D">
{{
`${$t('settings.other.dateFormat.year')}-${$t('settings.other.dateFormat.month')}-${$t(
'settings.other.dateFormat.day'
)}`
}}
</option>
<option value="Y-D-M">
{{
`${$t('settings.other.dateFormat.year')}-${$t('settings.other.dateFormat.day')}-${$t(
'settings.other.dateFormat.month'
)}`
}}
</option>
<option value="D-M-Y">
{{
`${$t('settings.other.dateFormat.day')}-${$t('settings.other.dateFormat.month')}-${$t(
'settings.other.dateFormat.year'
)}`
}}
</option>
<option value="M-D-Y">
{{
`${$t('settings.other.dateFormat.month')}-${$t('settings.other.dateFormat.day')}-${$t(
'settings.other.dateFormat.year'
)}`
}}
</option>
<option value="Y">{{ $t('settings.other.dateFormat.year') }}</option>
</select>
</div>
@ -556,6 +589,9 @@
</svg>
{{ $t('settings.spotify.title') }}
</h3>
<a href="https://codeberg.org/RemixDev/deemix/wiki/Enabling-Spotify-Features" target="_blank">
{{ $t('settings.spotify.question') }}
</a>
<div class="input_group">
<p class="input_group_text">{{ $t('settings.spotify.clientID') }}</p>
@ -579,7 +615,16 @@
</footer>
</div>
</template>
<style lang="scss">
#logged_in_info {
height: 250px;
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
}
.locale-flag {
width: 60px;
display: inline-flex;
@ -606,30 +651,47 @@
</style>
<script>
import { mapActions, mapGetters } from 'vuex'
import { toast } from '@/utils/toasts'
import { socket } from '@/utils/socket'
import EventBus from '@/utils/EventBus'
import flags from '@/utils/flags'
import { getSettingsData } from '@/data/settings'
export default {
name: 'the-settings-tab',
data: () => ({
flags,
currentLocale: 'en',
locales: [],
settings: { tags: {} },
lastSettings: {},
spotifyFeatures: {},
lastCredentials: {},
defaultSettings: {},
lastUser: '',
spotifyUser: '',
slimDownloads: false,
previewVolume: window.vol,
accountNum: 0,
accounts: []
}),
data() {
return {
flags,
currentLocale: 'en',
locales: [],
settings: {
tags: {}
},
lastSettings: {},
spotifyFeatures: {},
lastCredentials: {},
defaultSettings: {},
lastUser: '',
spotifyUser: '',
slimDownloads: false,
previewVolume: window.vol,
accountNum: 0,
accounts: []
// clientMode: window.clientMode
}
},
computed: {
...mapGetters({
arl: 'getARL',
user: 'getUser',
isLoggedIn: 'isLoggedIn',
clientMode: 'getClientMode'
}),
needToWait() {
return Object.keys(this.getSettings).length === 0
},
changeSlimDownloads: {
get() {
return this.slimDownloads
@ -639,15 +701,22 @@ export default {
document.getElementById('download_list').classList.toggle('slim', wantSlimDownloads)
localStorage.setItem('slimDownloads', wantSlimDownloads)
}
},
pictureHref() {
// Default image: https://e-cdns-images.dzcdn.net/images/user/125x125-000000-80-0-0.jpg
return `https://e-cdns-images.dzcdn.net/images/user/${this.user.picture}/125x125-000000-80-0-0.jpg`
}
},
mounted() {
async mounted() {
this.locales = this.$i18n.availableLocales
EventBus.$on('settingsTab:revertSettings', this.revertSettings)
EventBus.$on('settingsTab:revertCredentials', this.revertCredentials)
const { settingsData, defaultSettingsData, spotifyCredentials } = await getSettingsData()
this.$refs.loggedInInfo.classList.add('hide')
this.defaultSettings = defaultSettingsData
this.initSettings(settingsData, spotifyCredentials)
// this.revertSettings()
// this.revertCredentials()
let storedLocale = localStorage.getItem('locale')
@ -656,12 +725,6 @@ export default {
this.currentLocale = storedLocale
}
let storedArl = localStorage.getItem('arl')
if (storedArl) {
this.$refs.loginInput.value = storedArl.trim()
}
let storedAccountNum = localStorage.getItem('accountNum')
if (storedAccountNum) {
@ -679,25 +742,37 @@ export default {
this.changeSlimDownloads = 'true' === localStorage.getItem('slimDownloads')
let volume = parseInt(localStorage.getItem('previewVolume'))
if (isNaN(volume)) {
volume = 80
localStorage.setItem('previewVolume', volume)
}
window.vol.preview_max_volume = volume
socket.on('init_settings', this.initSettings)
socket.on('updateSettings', this.updateSettings)
socket.on('accountChanged', this.accountChanged)
socket.on('familyAccounts', this.initAccounts)
socket.on('downloadFolderSelected', this.downloadFolderSelected)
socket.on('applogin_arl', this.setArl)
socket.on('applogin_arl', this.loggedInViaDeezer)
this.$on('hook:destroyed', () => {
socket.off('updateSettings')
socket.off('accountChanged')
socket.off('familyAccounts')
socket.off('downloadFolderSelected')
socket.off('applogin_arl')
})
},
methods: {
...mapActions({
dispatchARL: 'setARL'
}),
revertSettings() {
this.settings = { ...this.lastSettings }
this.settings = JSON.parse(JSON.stringify(this.lastSettings))
},
revertCredentials() {
this.spotifyCredentials = { ...this.lastCredentials }
this.spotifyCredentials = JSON.parse(JSON.stringify(this.lastCredentials))
this.spotifyUser = (' ' + this.lastUser).slice(1)
},
copyARLtoClipboard() {
@ -720,9 +795,11 @@ export default {
localStorage.setItem('previewVolume', this.previewVolume.preview_max_volume)
},
saveSettings() {
this.lastSettings = { ...this.settings }
this.lastCredentials = { ...this.spotifyFeatures }
this.lastSettings = JSON.parse(JSON.stringify(this.settings))
this.lastCredentials = JSON.parse(JSON.stringify(this.spotifyFeatures))
let changed = false
if (this.lastUser != this.spotifyUser) {
// force cloning without linking
this.lastUser = (' ' + this.spotifyUser).slice(1)
@ -733,34 +810,31 @@ export default {
socket.emit('saveSettings', this.lastSettings, this.lastCredentials, changed ? this.lastUser : false)
},
selectDownloadFolder() {
if (window.clientMode) socket.emit('selectDownloadFolder')
socket.emit('selectDownloadFolder')
},
downloadFolderSelected(folder){
console.log(folder)
this.settings.downloadLocation = folder
downloadFolderSelected(folder) {
this.$set(this.settings, 'downloadLocation', folder)
},
loadSettings(settings, spotifyCredentials, defaults = null) {
if (defaults) {
this.defaultSettings = { ...defaults }
}
this.lastSettings = { ...settings }
this.lastCredentials = { ...spotifyCredentials }
this.settings = settings
this.spotifyFeatures = spotifyCredentials
loadSettings(data) {
this.lastSettings = JSON.parse(JSON.stringify(data))
this.settings = JSON.parse(JSON.stringify(data))
},
loadCredentials(credentials) {
this.lastCredentials = JSON.parse(JSON.stringify(credentials))
this.spotifyFeatures = JSON.parse(JSON.stringify(credentials))
},
login() {
let arl = this.$refs.loginInput.value.trim()
if (arl != '' && arl != localStorage.getItem('arl')) {
socket.emit('login', arl, true, this.accountNum)
let newArl = this.$refs.loginInput.value.trim()
if (newArl && newArl !== this.arl) {
socket.emit('login', newArl, true, this.accountNum)
}
},
applogin(e) {
e.preventDefault()
if (window.clientMode) socket.emit('applogin')
appLogin(e) {
socket.emit('applogin')
},
setArl(arl) {
this.$refs.loginInput.value = arl
loggedInViaDeezer(arl) {
this.dispatchARL({ arl })
this.login()
},
changeAccount() {
@ -770,6 +844,7 @@ export default {
this.$refs.username.innerText = user.name
this.$refs.userpicture.src = `https://e-cdns-images.dzcdn.net/images/user/${user.picture}/125x125-000000-80-0-0.jpg`
this.accountNum = accountNum
localStorage.setItem('accountNum', this.accountNum)
},
initAccounts(accounts) {
@ -778,16 +853,21 @@ export default {
logout() {
socket.emit('logout')
},
initSettings(settings, credentials, defaults) {
this.loadSettings(settings, credentials, defaults)
initSettings(settings, credentials) {
// this.loadDefaultSettings()
this.loadSettings(settings)
this.loadCredentials(credentials)
toast(this.$t('settings.toasts.init'), 'settings')
},
updateSettings(settings, credentials) {
this.loadSettings(settings, credentials)
updateSettings(newSettings, newCredentials) {
this.loadSettings(newSettings)
this.loadCredentials(newCredentials)
toast(this.$t('settings.toasts.update'), 'settings')
},
resetSettings() {
this.settings = { ...this.defaultSettings }
this.settings = JSON.parse(JSON.stringify(this.defaultSettings))
}
}
}

View File

@ -1,40 +1,28 @@
<template>
<aside id="sidebar" role="navigation" @click="handleSidebarClick">
<span id="main_home_tablink" class="main_tablinks" role="link" aria-label="home">
<i class="material-icons side_icon">home</i>
<span class="main_tablinks_text">{{ $t('sidebar.home') }}</span>
</span>
<span id="main_search_tablink" class="main_tablinks" role="link" aria-label="search">
<i class="material-icons side_icon">search</i>
<span class="main_tablinks_text">{{ $t('sidebar.search') }}</span>
</span>
<span id="main_charts_tablink" class="main_tablinks" role="link" aria-label="charts">
<i class="material-icons side_icon">show_chart</i>
<span class="main_tablinks_text">{{ $t('sidebar.charts') }}</span>
</span>
<span id="main_favorites_tablink" class="main_tablinks" role="link" aria-label="favorites">
<i class="material-icons side_icon">star</i>
<span class="main_tablinks_text">{{ $t('sidebar.favorites') }}</span>
</span>
<span id="main_analyzer_tablink" class="main_tablinks" role="link" aria-label="link analyzer">
<i class="material-icons side_icon">link</i>
<span class="main_tablinks_text">{{ $t('sidebar.linkAnalyzer') }}</span>
</span>
<span id="main_settings_tablink" class="main_tablinks" role="link" aria-label="settings">
<i class="material-icons side_icon">settings</i>
<span class="main_tablinks_text">{{ $t('sidebar.settings') }}</span>
</span>
<span id="main_about_tablink" class="main_tablinks" role="link" aria-label="info">
<i class="material-icons side_icon">info</i>
<span class="main_tablinks_text">{{ $t('sidebar.about') }}</span>
</span>
<aside id="sidebar" role="navigation">
<router-link
v-for="link in links"
:key="link.id"
tag="span"
class="main_tablinks"
role="link"
:id="link.id"
:class="{ active: activeTablink === link.name }"
:aria-label="link.ariaLabel"
:to="{ name: link.routerName }"
@click.native="activeTablink = link.name"
>
<i class="material-icons side_icon">{{ link.icon }}</i>
<span class="main_tablinks_text">{{ link.label }}</span>
</router-link>
<span id="theme_selector" class="main_tablinks" role="link" aria-label="theme selector">
<i class="material-icons side_icon side_icon--theme">palette</i>
<div id="theme_togglers">
<div
v-for="theme of themes"
:key="theme"
class="theme_toggler "
class="theme_toggler"
:class="[{ 'theme_toggler--active': activeTheme === theme }, `theme_toggler--${theme}`]"
@click="changeTheme(theme)"
></div>
@ -77,15 +65,76 @@
</style>
<script>
import { changeTab } from '@js/tabs.js'
export default {
name: 'the-sidebar',
data: () => ({
appOnline: null,
activeTheme: 'light',
themes: ['purple', 'dark', 'light']
}),
data() {
const $t = this.$t.bind(this)
const $tc = this.$tc.bind(this)
return {
appOnline: null,
activeTheme: 'light',
themes: ['purple', 'dark', 'light'],
activeTablink: 'home',
links: [
{
id: 'main_home_tablink',
name: 'home',
ariaLabel: 'home',
routerName: 'Home',
icon: 'home',
label: $t('sidebar.home')
},
{
id: 'main_search_tablink',
name: 'search',
ariaLabel: 'search',
routerName: 'Search',
icon: 'search',
label: $t('sidebar.search')
},
{
id: 'main_charts_tablink',
name: 'charts',
ariaLabel: 'charts',
routerName: 'Charts',
icon: 'show_chart',
label: $t('sidebar.charts')
},
{
id: 'main_favorites_tablink',
name: 'favorites',
ariaLabel: 'favorites',
routerName: 'Favorites',
icon: 'star',
label: $t('sidebar.favorites')
},
{
id: 'main_analyzer_tablink',
name: 'analyzer',
ariaLabel: 'link analyzer',
routerName: 'Link Analyzer',
icon: 'link',
label: $t('sidebar.linkAnalyzer')
},
{
id: 'main_settings_tablink',
name: 'settings',
ariaLabel: 'settings',
routerName: 'Settings',
icon: 'settings',
label: $t('sidebar.settings')
},
{
id: 'main_about_tablink',
name: 'about',
ariaLabel: 'info',
routerName: 'About',
icon: 'info',
label: $t('sidebar.about')
}
]
}
},
mounted() {
/* === Online status handling === */
this.appOnline = navigator.onLine
@ -100,6 +149,14 @@ export default {
/* === Current theme handling === */
this.activeTheme = localStorage.getItem('selectedTheme') || 'light'
this.$router.afterEach((to, from) => {
const linkInSidebar = this.links.find(link => link.routerName === to.name)
if (!linkInSidebar) return
this.activeTablink = linkInSidebar.name
})
},
methods: {
changeTheme(newTheme) {
@ -110,65 +167,19 @@ export default {
localStorage.setItem('selectedTheme', newTheme)
// Animating everything to have a smoother theme switch
document.querySelectorAll('*').forEach(el => {
el.style.transition = 'all 200ms ease-in-out'
const allElements = document.querySelectorAll('*')
allElements.forEach(el => {
el.classList.add('changing-theme')
})
document.documentElement.addEventListener('transitionend', function transitionHandler() {
document.querySelectorAll('*').forEach(el => {
el.style.transition = ''
allElements.forEach(el => {
el.classList.remove('changing-theme')
})
document.documentElement.removeEventListener('transitionend', transitionHandler)
})
},
/**
* Handles click Event on the sidebar and changes tab
* according to clicked icon.
* Uses event delegation
* @param {Event} event
*/
handleSidebarClick(event) {
const { target } = event
const wantToChangeTab = target.matches('.main_tablinks') || target.parentElement.matches('.main_tablinks')
if (!wantToChangeTab) return
let sidebarEl = target.matches('.main_tablinks') ? target : target.parentElement
let targetID = sidebarEl.id
let selectedTab = null
switch (targetID) {
case 'main_search_tablink':
selectedTab = 'search_tab'
break
case 'main_home_tablink':
selectedTab = 'home_tab'
break
case 'main_charts_tablink':
selectedTab = 'charts_tab'
break
case 'main_favorites_tablink':
selectedTab = 'favorites_tab'
break
case 'main_analyzer_tablink':
selectedTab = 'analyzer_tab'
break
case 'main_settings_tablink':
selectedTab = 'settings_tab'
break
case 'main_about_tablink':
selectedTab = 'about_tab'
break
default:
break
}
if (!selectedTab) return
changeTab(sidebarEl, 'main', selectedTab)
}
}
}

View File

@ -5,9 +5,11 @@
</template>
<script>
import $ from 'jquery'
// import $ from 'jquery'
import EventBus from '@/utils/EventBus'
import { adjustVolume } from '@/utils/adjust-volume'
export default {
data: () => ({
previewStopped: false
@ -25,98 +27,113 @@ export default {
await this.$refs.preview.play()
this.previewStopped = false
$(this.$refs.preview).animate({ volume: vol.preview_max_volume / 100 }, 500)
await adjustVolume(this.$refs.preview, window.vol.preview_max_volume / 100, { duration: 500 })
},
onTimeUpdate() {
async onTimeUpdate() {
// Prevents first time entering in this function
if (isNaN(this.$refs.preview.duration)) return
let duration = this.$refs.preview.duration
if (!isFinite(duration)) duration = 30
if (!isFinite(duration)) {
duration = 30
}
if (duration - this.$refs.preview.currentTime >= 1) return
if (this.previewStopped) return
$(this.$refs.preview).animate({ volume: 0 }, 800)
await adjustVolume(this.$refs.preview, 0, { duration: 800 })
this.previewStopped = true
$('a[playing] > .preview_controls').css({ opacity: 0 })
$('*').removeAttr('playing')
$('.preview_controls').text('play_arrow')
$('.preview_playlist_controls').text('play_arrow')
document.querySelectorAll('a[playing] > .preview_controls').forEach(control => {
control.style.opacity = 0
})
document.querySelectorAll('*').forEach(el => {
el.removeAttribute('playing')
})
document.querySelectorAll('.preview_controls, .preview_playlist_controls').forEach(el => {
el.textContent = 'play_arrow'
})
},
playPausePreview(e) {
async playPausePreview(e) {
e.preventDefault()
e.stopPropagation()
const { currentTarget: obj } = event
var $icon = obj.tagName == 'I' ? $(obj) : $(obj).children('i')
var icon = obj.tagName == 'I' ? obj : obj.querySelector('i')
if ($(obj).attr('playing')) {
if (obj.hasAttribute('playing')) {
if (this.$refs.preview.paused) {
this.$refs.preview.play()
this.previewStopped = false
$icon.text('pause')
icon.innerText = 'pause'
$(this.$refs.preview).animate({ volume: vol.preview_max_volume / 100 }, 500)
await adjustVolume(this.$refs.preview, window.vol.preview_max_volume / 100, { duration: 500 })
} else {
this.previewStopped = true
$icon.text('play_arrow')
icon.innerText = 'play_arrow'
$(this.$refs.preview).animate({ volume: 0 }, 250, 'swing', () => {
this.$refs.preview.pause()
})
await adjustVolume(this.$refs.preview, 0, { duration: 250 })
this.$refs.preview.pause()
}
} else {
$('*').removeAttr('playing')
$(obj).attr('playing', true)
document.querySelectorAll('*').forEach(el => {
el.removeAttribute('playing')
})
obj.setAttribute('playing', true)
$('.preview_controls').text('play_arrow')
$('.preview_playlist_controls').text('play_arrow')
$('.preview_controls').css({ opacity: 0 })
document.querySelectorAll('.preview_controls, .preview_playlist_controls').forEach(el => {
el.textContent = 'play_arrow'
})
$icon.text('pause')
$icon.css({ opacity: 1 })
document.querySelectorAll('.preview_controls').forEach(el => {
el.style.opacity = 0
})
icon.innerText = 'pause'
icon.style.opacity = 1
this.previewStopped = false
$(this.$refs.preview).animate({ volume: 0 }, 250, 'swing', () => {
this.$refs.preview.pause()
$('#preview-track_source').prop('src', $(obj).data('preview'))
this.$refs.preview.load()
})
await adjustVolume(this.$refs.preview, 0, { duration: 250 })
this.$refs.preview.pause()
document.getElementById('preview-track_source').src = obj.getAttribute('data-preview')
this.$refs.preview.load()
}
},
stopStackedTabsPreview() {
if (
$('.preview_playlist_controls').filter(function() {
return $(this).attr('playing')
}).length > 0
) {
$(this.$refs.preview).animate({ volume: 0 }, 800)
this.previewStopped = true
$('.preview_playlist_controls').removeAttr('playing')
$('.preview_playlist_controls').text('play_arrow')
}
async stopStackedTabsPreview() {
let controls = Array.prototype.slice.call(document.querySelectorAll('.preview_playlist_controls[playing]'))
if (controls.length === 0) return
await adjustVolume(this.$refs.preview, 0, { duration: 800 })
this.previewStopped = true
controls.forEach(control => {
control.removeAttribute('playing')
control.innerText = 'play_arrow'
})
},
previewMouseEnter(e) {
$(e.currentTarget).css({ opacity: 1 })
e.currentTarget.style.opacity = 1
},
previewMouseLeave(event) {
const { currentTarget: obj } = event
const parentIsPlaying = obj.parentElement.hasAttribute('playing')
if (
($(obj)
.parent()
.attr('playing') &&
this.previewStopped) ||
!$(obj)
.parent()
.attr('playing')
) {
$(obj).css({ opacity: 0 }, 200)
if ((parentIsPlaying && this.previewStopped) || !parentIsPlaying) {
obj.style.opacity = 0
}
}
}

View File

@ -1,5 +1,5 @@
<template>
<div id="tracklist_tab" class="main_tabcontent fixed_footer image_header">
<div id="tracklist_tab" class="main_tabcontent fixed_footer image_header" ref="root">
<header
:style="{
'background-image':
@ -55,9 +55,7 @@
</td>
<td class="table__cell--large table__cell--with-icon">
<div class="table__cell-content table__cell-content--vertical-center">
<i v-if="track.explicit_lyrics" class="material-icons explicit_icon">
explicit
</i>
<i v-if="track.explicit_lyrics" class="material-icons explicit_icon"> explicit </i>
{{
track.title +
(track.title_version && track.title.indexOf(track.title_version) == -1
@ -91,9 +89,9 @@
<input class="clickable" type="checkbox" v-model="track.selected" />
</td>
</tr>
<tr v-else-if="track.type == 'disc_separator'" class="table__row-no-highlight" style="opacity: 0.54;">
<tr v-else-if="track.type == 'disc_separator'" class="table__row-no-highlight" style="opacity: 0.54">
<td>
<div class="table__cell-content table__cell-content--vertical-center" style="opacity: 0.54;">
<div class="table__cell-content table__cell-content--vertical-center" style="opacity: 0.54">
<i class="material-icons">album</i>
</div>
</td>
@ -131,15 +129,15 @@
</template>
</tbody>
</table>
<span v-if="label" style="opacity: 0.4; margin-top: 8px; display: inline-block; font-size: 13px;">{{ label }}</span>
<span v-if="label" style="opacity: 0.4; margin-top: 8px; display: inline-block; font-size: 13px">{{ label }}</span>
<footer>
<button @click.stop="addToQueue" :data-link="link">
{{ `${$t('globals.download', {thing: $tc(`globals.listTabs.${type}`, 1)})}` }}
{{ `${$t('globals.download', { thing: $tc(`globals.listTabs.${type}`, 1) })}` }}
</button>
<button class="with_icon" @click.stop="addToQueue" :data-link="selectedLinks()">
{{ $t('tracklist.downloadSelection') }}<i class="material-icons">file_download</i>
</button>
<button class="back-button">{{ $t('globals.back') }}</button>
<button class="back-button" @click="backTab">{{ $t('globals.back') }}</button>
</footer>
</div>
</template>
@ -147,7 +145,7 @@
<script>
import { isEmpty } from 'lodash-es'
import { socket } from '@/utils/socket'
import { showView } from '@js/tabs.js'
import { showView, backTab } from '@js/tabs.js'
import Downloads from '@/utils/downloads'
import Utils from '@/utils/utils'
import EventBus from '@/utils/EventBus'
@ -166,6 +164,7 @@ export default {
body: []
}),
methods: {
backTab,
artistView: showView.bind(null, 'artist'),
albumView: showView.bind(null, 'album'),
playPausePreview(e) {
@ -203,6 +202,8 @@ export default {
},
convertDuration: Utils.convertDuration,
showAlbum(data) {
this.reset()
const {
id: albumID,
title: albumTitle,
@ -231,6 +232,8 @@ export default {
}
},
showPlaylist(data) {
this.reset()
const {
id: playlistID,
title: playlistTitle,
@ -246,7 +249,10 @@ export default {
this.title = playlistTitle
this.image = playlistCover
this.release_date = creation_date.substring(0, 10)
this.metadata = `${this.$t('globals.by', {artist: creatorName})} • ${this.$tc('globals.listTabs.trackN', numberOfTracks)}`
this.metadata = `${this.$t('globals.by', { artist: creatorName })} • ${this.$tc(
'globals.listTabs.trackN',
numberOfTracks
)}`
if (isEmpty(playlistTracks)) {
this.body = null
@ -255,6 +261,8 @@ export default {
}
},
showSpotifyPlaylist(data) {
this.reset()
const {
uri: playlistURI,
name: playlistName,
@ -272,7 +280,10 @@ export default {
? images[0].url
: 'https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/1000x1000-000000-80-0-0.jpg'
this.release_date = ''
this.metadata = `${this.$t('globals.by', {artist: ownerName})} • ${this.$tc('globals.listTabs.trackN', numberOfTracks)}`
this.metadata = `${this.$t('globals.by', { artist: ownerName })} • ${this.$tc(
'globals.listTabs.trackN',
numberOfTracks
)}`
if (isEmpty(playlistTracks)) {
this.body = null
@ -285,7 +296,6 @@ export default {
}
},
mounted() {
EventBus.$on('tracklistTab:reset', this.reset)
EventBus.$on('tracklistTab:selectRow', this.selectRow)
socket.on('show_album', this.showAlbum)

View File

@ -0,0 +1,117 @@
<template>
<div class="download_object" :id="`download_${queueItem.uuid}`" :data-deezerid="queueItem.id">
<div class="download_info">
<img width="75px" class="rounded coverart" :src="queueItem.cover" :alt="`Cover ${queueItem.title}`" />
<div class="download_info_data">
<span class="download_line">{{ queueItem.title }}</span> <span class="download_slim_separator"> - </span>
<span class="secondary-text">{{ queueItem.artist }}</span>
</div>
<div class="download_info_status">
<span class="download_line">
<span class="queue_downloaded">{{ queueItem.downloaded + queueItem.failed }}</span
>/{{ queueItem.size }}
</span>
<span class="secondary-text inline-flex" v-if="queueItem.failed >= 1">
<span class="download_slim_separator">(</span>
<span
class="queue_failed_button inline-flex"
:class="{ clickable: finishedWithFails }"
@click="finishedWithFails ? $emit('show-errors', queueItem) : null"
>
<span class="queue_failed">{{ queueItem.failed }}</span>
<i class="material-icons">error_outline</i>
</span>
<span class="download_slim_separator">)</span>
</span>
</div>
</div>
<div class="download_bar">
<div class="progress">
<div :id="`bar_${queueItem.uuid}`" :class="barClass" :style="barStyle"></div>
</div>
<i
class="material-icons queue_icon"
:data-uuid="queueItem.uuid"
:class="{ clickable: finishedWithFails }"
@click="onResultIconClick"
v-if="!isLoading"
>
{{ resultIconText }}
</i>
<div v-else class="circle-loader"></div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
isLoading: false
}
},
props: {
queueItem: Object
},
computed: {
finishedWithFails() {
return this.queueItem.status === 'download finished' && this.queueItem.failed >= 1
},
barClass() {
return {
converting: this.queueItem.status === 'converting',
indeterminate: ['converting', 'downloading', 'download finished'].indexOf(this.queueItem.status) === -1,
determinate: ['converting', 'downloading', 'download finished'].indexOf(this.queueItem.status) !== -1
}
},
barStyle() {
let width = 0
if (this.queueItem.status === 'download finished') {
width = 100
}
if (this.queueItem.status === 'downloading') {
width = this.queueItem.progress
}
if (this.queueItem.status === 'converting') {
width = 100 - this.queueItem.conversion
}
return {
width: `${width}%`
}
},
resultIconText() {
let text = 'remove'
if (this.queueItem.status === 'download finished') {
if (this.queueItem.failed == 0) {
text = 'done'
} else {
if (this.queueItem.failed >= this.queueItem.size) {
text = 'error'
} else {
text = 'warning'
}
}
}
return text
}
},
methods: {
onResultIconClick() {
if (this.finishedWithFails) {
this.$emit('show-errors', this.queueItem)
}
if (this.queueItem.status === 'downloading') {
this.isLoading = true
this.$emit('remove-item', this.queueItem.uuid)
}
}
}
}
</script>

View File

@ -0,0 +1,51 @@
<template>
<div id="album_search" class="search_tabcontent">
<BaseLoadingPlaceholder v-if="!results.albumTab.loaded" />
<div v-else-if="results.albumTab.data.length == 0">
<h1>{{ $t('search.noResultsAlbum') }}</h1>
</div>
<div class="release_grid" v-if="results.albumTab.data.length > 0">
<div
v-for="release in results.albumTab.data"
class="release clickable"
@click.stop="$emit('album-view', $event)"
:data-id="release.id"
>
<div class="cover_container">
<img aria-hidden="true" class="rounded coverart" :src="release.cover_medium" />
<div
role="button"
aria-label="download"
@click.stop="$emit('add-to-queue', $event)"
:data-link="release.link"
class="download_overlay"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</div>
</div>
<p class="primary-text inline-flex">
<i v-if="release.explicit_lyrics" class="material-icons explicit_icon">explicit</i>
{{ release.title }}
</p>
<p class="secondary-text">
{{
$t('globals.by', { artist: release.artist.name }) +
' - ' +
$tc('globals.listTabs.trackN', release.nb_tracks)
}}
</p>
</div>
</div>
</div>
</template>
<script>
import BaseLoadingPlaceholder from '@components/BaseLoadingPlaceholder.vue'
export default {
props: ['results'],
components: {
BaseLoadingPlaceholder
}
}
</script>

View File

@ -0,0 +1,234 @@
<template>
<div id="main_search" class="search_tabcontent">
<template v-for="section in results.allTab.ORDER">
<section
v-if="
(section != 'TOP_RESULT' && results.allTab[section].data.length > 0) || results.allTab[section].length > 0
"
class="search_section"
>
<h2
@click="$emit('change-search-tab', section)"
class="search_header"
:class="{ top_result_header: section === 'TOP_RESULT' }"
>
{{ $tc(`globals.listTabs.${section.toLowerCase()}`, 2) }}
</h2>
<!-- Top result -->
<div
v-if="section == 'TOP_RESULT'"
class="top_result clickable"
@click.stop="$emit(`${topResultType}-view`, $event)"
:data-id="results.allTab.TOP_RESULT[0].id"
>
<div class="cover_container">
<img
aria-hidden="true"
:src="results.allTab.TOP_RESULT[0].picture"
:class="(results.allTab.TOP_RESULT[0].type == 'artist' ? 'circle' : 'rounded') + ' coverart'"
/>
<div
role="button"
aria-label="download"
@click.stop="$emit('add-to-queue', $event)"
:data-link="results.allTab.TOP_RESULT[0].link"
class="download_overlay"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</div>
</div>
<div class="info_box">
<p class="primary-text">{{ results.allTab.TOP_RESULT[0].title }}</p>
<p class="secondary-text">
{{
results.allTab.TOP_RESULT[0].type == 'artist'
? $t('search.fans', { n: $n(results.allTab.TOP_RESULT[0].nb_fan) })
: $t('globals.by', { artist: results.allTab.TOP_RESULT[0].artist }) +
' - ' +
$tc('globals.listTabs.trackN', results.allTab.TOP_RESULT[0].nb_song)
}}
</p>
<span class="tag">{{ $tc(`globals.listTabs.${results.allTab.TOP_RESULT[0].type}`, 1) }}</span>
</div>
</div>
<div v-else-if="section == 'TRACK'">
<table class="table table--tracks">
<tbody>
<tr v-for="track in results.allTab.TRACK.data.slice(0, 6)">
<td class="table__icon" aria-hidden="true">
<img
class="rounded coverart"
:src="
'https://e-cdns-images.dzcdn.net/images/cover/' + track.ALB_PICTURE + '/32x32-000000-80-0-0.jpg'
"
/>
</td>
<td class="table__cell table__cell--large breakline">
<div class="table__cell-content table__cell-content--vertical-center">
<i v-if="track.EXPLICIT_LYRICS == 1" class="material-icons explicit_icon"> explicit </i>
{{ track.SNG_TITLE + (track.VERSION ? ' ' + track.VERSION : '') }}
</div>
</td>
<td class="table__cell table__cell--medium table__cell--center breakline">
<span
class="clickable"
@click.stop="$emit('artist-view', $event)"
:data-id="artist.ART_ID"
v-for="artist in track.ARTISTS"
:key="artist.ART_ID"
>
{{ artist.ART_NAME }}
</span>
</td>
<td
class="table__cell--medium table__cell--center breakline clickable"
@click.stop="$emit('album-view', $event)"
:data-id="track.ALB_ID"
>
{{ track.ALB_TITLE }}
</td>
<td class="table__cell table__cell--center">
{{ convertDuration(track.DURATION) }}
</td>
<td
class="table__cell--download table__cell--center clickable"
@click.stop="$emit('add-to-queue', $event)"
:data-link="'https://www.deezer.com/track/' + track.SNG_ID"
role="button"
aria-label="download"
>
<i class="material-icons" :title="$t('globals.download_hint')"> get_app </i>
</td>
</tr>
</tbody>
</table>
</div>
<div v-else-if="section == 'ARTIST'" class="release_grid firstrow_only">
<div
v-for="release in results.allTab.ARTIST.data.slice(0, 10)"
class="release clickable"
@click.stop="$emit('artist-view', $event)"
:data-id="release.ART_ID"
>
<div class="cover_container">
<img
aria-hidden="true"
class="circle coverart"
:src="
'https://e-cdns-images.dzcdn.net/images/artist/' + release.ART_PICTURE + '/156x156-000000-80-0-0.jpg'
"
/>
<div
role="button"
aria-label="download"
@click.stop="$emit('add-to-queue', $event)"
:data-link="'https://deezer.com/artist/' + release.ART_ID"
class="download_overlay"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</div>
</div>
<p class="primary-text">{{ release.ART_NAME }}</p>
<p class="secondary-text">{{ $t('search.fans', { n: $n(release.NB_FAN) }) }}</p>
</div>
</div>
<div v-else-if="section == 'ALBUM'" class="release_grid firstrow_only">
<div
v-for="release in results.allTab.ALBUM.data.slice(0, 10)"
class="release clickable"
@click.stop="$emit('album-view', $event)"
:data-id="release.ALB_ID"
>
<div class="cover_container">
<img
aria-hidden="true"
class="rounded coverart"
:src="
'https://e-cdns-images.dzcdn.net/images/cover/' + release.ALB_PICTURE + '/156x156-000000-80-0-0.jpg'
"
/>
<div
role="button"
aria-label="download"
@click.stop="$emit('add-to-queue', $event)"
:data-link="'https://deezer.com/album/' + release.ALB_ID"
class="download_overlay"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</div>
</div>
<p class="primary-text inline-flex">
<i
v-if="[1, 4].indexOf(release.EXPLICIT_ALBUM_CONTENT.EXPLICIT_LYRICS_STATUS) != -1"
class="material-icons explicit_icon"
>explicit</i
>
{{ release.ALB_TITLE }}
</p>
<p class="secondary-text">
{{ release.ART_NAME + ' - ' + $tc('globals.listTabs.trackN', release.NUMBER_TRACK) }}
</p>
</div>
</div>
<div v-else-if="section == 'PLAYLIST'" class="release_grid firstrow_only">
<div
v-for="release in results.allTab.PLAYLIST.data.slice(0, 10)"
class="release clickable"
@click.stop="$emit('playlist-view', $event)"
:data-id="release.PLAYLIST_ID"
>
<div class="cover_container">
<img
aria-hidden="true"
class="rounded coverart"
:src="
'https://e-cdns-images.dzcdn.net/images/' +
release.PICTURE_TYPE +
'/' +
release.PLAYLIST_PICTURE +
'/156x156-000000-80-0-0.jpg'
"
/>
<div
role="button"
aria-label="download"
@click.stop="$emit('add-to-queue', $event)"
:data-link="'https://deezer.com/playlist/' + release.PLAYLIST_ID"
class="download_overlay"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</div>
</div>
<p class="primary-text">{{ release.TITLE }}</p>
<p class="secondary-text">{{ $tc('globals.listTabs.trackN', release.NB_SONG) }}</p>
</div>
</div>
</section>
</template>
<div v-if="noResults">
<h1>{{ $t('search.noResults') }}</h1>
</div>
</div>
</template>
<script>
import { convertDuration } from '@/utils/utils'
export default {
props: ['results'],
computed: {
topResultType() {
return this.results.allTab.TOP_RESULT[0].type
},
noResults() {
return this.results.allTab.ORDER.every(section =>
section == 'TOP_RESULT'
? this.results.allTab[section].length == 0
: this.results.allTab[section].data.length == 0
)
}
},
methods: {
convertDuration
}
}
</script>

View File

@ -0,0 +1,42 @@
<template>
<div id="artist_search" class="search_tabcontent">
<base-loading-placeholder v-if="!results.artistTab.loaded"></base-loading-placeholder>
<div v-else-if="results.artistTab.data.length == 0">
<h1>{{ $t('search.noResultsArtist') }}</h1>
</div>
<div class="release_grid" v-if="results.artistTab.data.length > 0">
<div
v-for="release in results.artistTab.data"
class="release clickable"
@click.stop="$emit('artist-view', $event)"
:data-id="release.id"
>
<div class="cover_container">
<img aria-hidden="true" class="circle coverart" :src="release.picture_medium" />
<div
role="button"
aria-label="download"
@click.stop="$emit('add-to-queue', $event)"
:data-link="release.link"
class="download_overlay"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</div>
</div>
<p class="primary-text">{{ release.name }}</p>
<p class="secondary-text">{{ $tc('globals.listTabs.releaseN', release.nb_album) }}</p>
</div>
</div>
</div>
</template>
<script>
import BaseLoadingPlaceholder from '@components/BaseLoadingPlaceholder.vue'
export default {
props: ['results'],
components: {
BaseLoadingPlaceholder
}
}
</script>

View File

@ -0,0 +1,46 @@
<template>
<div id="playlist_search" class="search_tabcontent">
<BaseLoadingPlaceholder v-if="!results.playlistTab.loaded" />
<div v-else-if="results.playlistTab.data.length == 0">
<h1>{{ $t('search.noResultsPlaylist') }}</h1>
</div>
<div class="release_grid" v-if="results.playlistTab.data.length > 0">
<div
v-for="release in results.playlistTab.data"
class="release clickable"
@click.stop="$emit('playlist-view', $event)"
:data-id="release.id"
>
<div class="cover_container">
<img aria-hidden="true" class="rounded coverart" :src="release.picture_medium" />
<div
role="button"
aria-label="download"
@click.stop="$emit('add-to-queue', $event)"
:data-link="release.link"
class="download_overlay"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</div>
</div>
<p class="primary-text">{{ release.title }}</p>
<p class="secondary-text">
{{
`${$t('globals.by', { artist: release.user.name })} - ${$tc('globals.listTabs.trackN', release.nb_tracks)}`
}}
</p>
</div>
</div>
</div>
</template>
<script>
import BaseLoadingPlaceholder from '@components/BaseLoadingPlaceholder.vue'
export default {
props: ['results'],
components: {
BaseLoadingPlaceholder
}
}
</script>

View File

@ -0,0 +1,114 @@
<template>
<div id="track_search" class="search_tabcontent">
<BaseLoadingPlaceholder v-if="!results.trackTab.loaded" />
<div v-else-if="results.trackTab.data.length == 0">
<h1>{{ $t('search.noResultsTrack') }}</h1>
</div>
<table class="table table--tracks" v-if="results.trackTab.data.length > 0">
<thead>
<tr>
<th colspan="2">{{ $tc('globals.listTabs.title', 1) }}</th>
<th>{{ $tc('globals.listTabs.artist', 1) }}</th>
<th>{{ $tc('globals.listTabs.album', 1) }}</th>
<th>
<i class="material-icons"> timer </i>
</th>
<th style="width: 56px"></th>
</tr>
</thead>
<tbody>
<tr v-for="track in results.trackTab.data">
<td class="table__icon table__icon--big">
<a
href="#"
@click="playPausePreview"
:class="'rounded' + (track.preview ? ' single-cover' : '')"
:data-preview="track.preview"
>
<i
@mouseenter="previewMouseEnter"
@mouseleave="previewMouseLeave"
v-if="track.preview"
class="material-icons preview_controls"
:title="$t('globals.play_hint')"
>
play_arrow
</i>
<img class="rounded coverart" :src="track.album.cover_small" />
</a>
</td>
<td class="table__cell table__cell--large breakline">
<div class="table__cell-content table__cell-content--vertical-center">
<i v-if="track.explicit_lyrics" class="material-icons explicit_icon"> explicit </i>
{{
track.title +
(track.title_version && track.title.indexOf(track.title_version) == -1 ? ' ' + track.title_version : '')
}}
</div>
</td>
<td
class="table__cell table__cell--medium table__cell--center breakline clickable"
@click.stop="artistView"
:data-id="track.artist.id"
>
{{ track.artist.name }}
</td>
<td
class="table__cell table__cell--medium table__cell--center breakline clickable"
@click.stop="albumView"
:data-id="track.album.id"
>
{{ track.album.title }}
</td>
<td class="table__cell table__cell--small table__cell--center">
{{ convertDuration(track.duration) }}
</td>
<td
class="table__cell--download table__cell--center clickable"
@click.stop="$emit('add-to-queue', $event)"
:data-link="track.link"
role="button"
aria-label="download"
>
<i class="material-icons" :title="$t('globals.download_hint')"> get_app </i>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import BaseLoadingPlaceholder from '@components/BaseLoadingPlaceholder.vue'
import EventBus from '@/utils/EventBus.js'
import { convertDuration } from '@/utils/utils'
export default {
props: ['results'],
components: {
BaseLoadingPlaceholder
},
methods: {
convertDuration,
artistView(event) {
this.$emit('artist-view', event)
},
albumView(event) {
this.$emit('album-view', event)
},
playlistView(event) {
this.$emit('playlist-view', event)
},
playPausePreview(e) {
EventBus.$emit('trackPreview:playPausePreview', e)
},
previewMouseEnter(e) {
EventBus.$emit('trackPreview:previewMouseEnter', e)
},
previewMouseLeave(e) {
EventBus.$emit('trackPreview:previewMouseLeave', e)
}
}
}
</script>

22
src/data/charts.js Normal file
View File

@ -0,0 +1,22 @@
import { socket } from '@/utils/socket'
let chartsData = {}
let cached = false
export function getChartsData() {
if (cached) {
return chartsData
} else {
socket.emit('get_charts_data')
return new Promise((resolve, reject) => {
socket.on('init_charts', data => {
chartsData = data
cached = true
socket.off('init_charts')
resolve(data)
})
})
}
}

22
src/data/favorites.js Normal file
View File

@ -0,0 +1,22 @@
import { socket } from '@/utils/socket'
let favoritesData = {}
let cached = false
export function getFavoritesData() {
if (cached) {
return favoritesData
} else {
socket.emit('get_favorites_data')
return new Promise((resolve, reject) => {
socket.on('init_favorites', data => {
favoritesData = data
cached = true
socket.off('init_favorites')
resolve(data)
})
})
}
}

22
src/data/home.js Normal file
View File

@ -0,0 +1,22 @@
import { socket } from '@/utils/socket'
let homeData = {}
let cached = false
export function getHomeData() {
if (cached) {
return homeData
} else {
socket.emit('get_home_data')
return new Promise((resolve, reject) => {
socket.on('init_home', data => {
homeData = data
cached = true
socket.off('init_home')
resolve(data)
})
})
}
}

27
src/data/settings.js Normal file
View File

@ -0,0 +1,27 @@
import { socket } from '@/utils/socket'
let settingsData = {}
let defaultSettingsData = {}
let spotifyCredentials = {}
let cached = false
export function getSettingsData() {
if (cached) {
return { settingsData, defaultSettingsData, spotifyCredentials }
} else {
socket.emit('get_settings_data')
return new Promise((resolve, reject) => {
socket.on('init_settings', (settings, credentials, defaults) => {
settingsData = settings
defaultSettingsData = defaults
spotifyCredentials = credentials
// cached = true
socket.off('init_settings')
resolve({ settingsData, defaultSettingsData, spotifyCredentials })
})
})
}
}

View File

@ -1,5 +1,5 @@
import { socket } from '@/utils/socket'
import EventBus from '@/utils/EventBus'
import router from '@/router'
/* ===== Globals ====== */
window.search_selected = ''
@ -7,78 +7,35 @@ window.main_selected = ''
window.windows_stack = []
window.currentStack = {}
// Exporting this function out of the default export
// because it's used in components that are needed
// in this file too
export function showView(viewType, event) {
// console.error('SHOW VIEW')
const {
currentTarget: {
dataset: { id }
}
} = event
switch (viewType) {
case 'artist':
EventBus.$emit('artistTab:reset')
break
case 'album':
case 'playlist':
case 'spotifyplaylist':
EventBus.$emit('tracklistTab:reset')
break
default:
break
}
socket.emit('getTracklist', { type: viewType, id })
showTab(viewType, id)
}
/**
* Changes the tab to the wanted one
* Need to understand the difference from showTab
*
* Needs EventBus
*/
// Used only in errors tab
export function changeTab(sidebarEl, section, tabName) {
// console.error('CHANGE TAB')
window.windows_stack = []
window.currentStack = {}
// * The visualized content of the tab
// ! Can be more than one per tab, happens in MainSearch and Favorites tab
// ! because they have more tablinks (see below)
const tabContent = document.getElementsByClassName(`${section}_tabcontent`)
// * Only in section search
updateTabLink(section)
for (let i = 0; i < tabContent.length; i++) {
tabContent[i].style.display = 'none'
}
// * Only when clicking the settings icon in the sidebar
// resetSettings(tabName)
// * Tabs inside the actual tab (like albums, tracks, playlists...)
const tabLinks = document.getElementsByClassName(`${section}_tablinks`)
// * Only in section search
setSelectedTab(section, tabName)
for (let i = 0; i < tabLinks.length; i++) {
tabLinks[i].classList.remove('active')
}
if (tabName === 'settings_tab' && window.main_selected !== 'settings_tab') {
EventBus.$emit('settingsTab:revertSettings')
EventBus.$emit('settingsTab:revertCredentials')
}
document.getElementById(tabName).style.display = 'block'
// * Only if window.main_selected === 'search_tab'
checkNeedToLoadMoreContent()
}
function setSelectedTab(section, tabName) {
if (section === 'main') {
window.main_selected = tabName
} else if ('search' === section) {
} else if (section === 'search') {
window.search_selected = tabName
}
}
sidebarEl.classList.add('active')
// Check if you need to load more content in the search tab
function checkNeedToLoadMoreContent() {
// * Check if you need to load more content in the search tab
// * Happens when the user changes the tab in the main search
if (
window.main_selected === 'search_tab' &&
['track_search', 'album_search', 'artist_search', 'playlist_search'].indexOf(window.search_selected) !== -1
@ -87,77 +44,50 @@ export function changeTab(sidebarEl, section, tabName) {
}
}
/**
* Shows the passed tab, keeping track of the one that the user is coming from.
*
* Needs EventBus
*/
function showTab(type, id, back = false) {
if (window.windows_stack.length === 0) {
window.windows_stack.push({ tab: window.main_selected })
} else if (!back) {
if (window.currentStack.type === 'artist') {
EventBus.$emit('artistTab:updateSelected')
function resetSettings(tabName) {
if (tabName === 'settings_tab' && window.main_selected !== 'settings_tab') {
EventBus.$emit('settingsTab:revertSettings')
EventBus.$emit('settingsTab:revertCredentials')
}
}
function updateTabLink(section) {
// * Tabs inside the actual tab (like albums, tracks, playlists...)
// * or sidebar links
if (section == 'main') return
const tabLinks = document.getElementsByClassName(`${section}_tablinks`)
for (let i = 0; i < tabLinks.length; i++) {
tabLinks[i].classList.remove('active')
}
}
export function showView(viewType, event) {
const {
currentTarget: {
dataset: { id }
}
} = event
const isArtist = viewType === 'artist'
const name = isArtist ? 'Artist' : 'Tracklist'
const params = isArtist ? { id } : { type: viewType, id }
window.windows_stack.push(window.currentStack)
}
window.tab = type === 'artist' ? 'artist_tab' : 'tracklist_tab'
window.currentStack = { type, id }
let tabcontent = document.getElementsByClassName('main_tabcontent')
for (let i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = 'none'
}
document.getElementById(window.tab).style.display = 'block'
EventBus.$emit('trackPreview:stopStackedTabsPreview')
router.push({
name,
params
})
}
/**
* Goes back to the previous tab according to the global window stack.
*
* Needs EventBus and socket
*/
function backTab() {
if (window.windows_stack.length == 1) {
document.getElementById(`main_${window.main_selected}link`).click()
} else {
// Retrieving tab type and tab id
let data = window.windows_stack.pop()
let { type, id, selected } = data
if (type === 'artist') {
EventBus.$emit('artistTab:reset')
if (selected) {
EventBus.$emit('artistTab:changeTab', selected)
}
} else {
EventBus.$emit('tracklistTab:reset')
}
socket.emit('getTracklist', { type, id })
showTab(type, id, true)
}
EventBus.$emit('trackPreview:stopStackedTabsPreview')
}
function _linkListeners() {
const backButtons = Array.prototype.slice.call(document.getElementsByClassName('back-button'))
backButtons.forEach(button => {
button.addEventListener('click', backTab)
})
export function backTab() {
// ! Need to implement the memory of the opened artist tab
router.back()
}
export function init() {
// Open default tab
changeTab(document.getElementById('main_home_tablink'), 'main', 'home_tab')
_linkListeners()
}

View File

@ -8,5 +8,8 @@
"@components/*": ["./components/*"]
}
},
"typeAcquisition": {
"include": ["socket.io-client"]
},
"exclude": ["assets/**/*", "styles/**/*"]
}

View File

@ -41,6 +41,12 @@ const en = {
}
},
about: {
updates: {
currentVersion: 'Current Version',
versionNotAvailable: 'N/A',
updateAvailable: `You're not running the latest version available: {version}`,
deemixVersion: 'deemix lib version'
},
titles: {
usefulLinks: 'Useful Links',
bugReports: 'Bug Reports',
@ -98,7 +104,8 @@ const en = {
wrongBitrateNoAlternative: 'Track not found at desired bitrate and no alternative found!',
no360RA: 'Track is not available in Reality Audio 360.',
notAvailable: "Track not available on Deezer's servers!",
notAvailableNoAlternative: "Track not available on Deezer's servers and no alternative found!"
notAvailableNoAlternative: "Track not available on Deezer's servers and no alternative found!",
noSpaceLeft: "No space left on the device!"
}
},
favorites: {
@ -168,7 +175,8 @@ const en = {
finishAddingArtist: 'Added {artist} albums to queue',
startConvertingSpotifyPlaylist: 'Converting spotify tracks to Deezer tracks',
finishConvertingSpotifyPlaylist: 'Spotify playlist converted',
loginNeededToDownload: 'You need to log in to download tracks!'
loginNeededToDownload: 'You need to log in to download tracks!',
deezerNotAvailable: 'Deezer is not available in your country. You should use a VPN.'
},
settings: {
title: 'Settings',
@ -180,7 +188,8 @@ const en = {
question: 'How do I get my own ARL?',
update: 'Update ARL'
},
logout: 'Logout'
logout: 'Logout',
login: 'Login via deezer.com'
},
appearance: {
title: 'Appearance',
@ -256,7 +265,8 @@ const en = {
jpegImageQuality: 'JPEG image quality',
embeddedArtworkPNG: 'Save embedded artwork as PNG',
embeddedPNGWarning: 'PNGs are not officialy supported by Deezer and can be buggy',
imageSizeWarning: 'Anything above x1200 is not officialy used by Deezer, you may encounter issues'
imageSizeWarning: 'Anything above x1200 is not officialy used by Deezer, you may encounter issues',
coverDescriptionUTF8: 'Save cover description using UTF8 (iTunes Cover Fix)'
},
tags: {
head: 'Which tags to save',
@ -280,6 +290,7 @@ const en = {
replayGain: 'Replay Gain',
label: 'Album Label',
lyrics: 'Unsynchronized Lyrics',
syncedLyrics: 'Synchronized Lyrics',
copyright: 'Copyright',
composer: 'Composer',
involvedPeople: 'Involved People'
@ -332,7 +343,8 @@ const en = {
title: 'Spotify Features',
clientID: 'Spotify ClientID',
clientSecret: 'Spotify Client Secret',
username: 'Spotify Username'
username: 'Spotify Username',
question: 'How do I enable Spotify Features?'
},
reset: 'Reset to Default',
save: 'Save',

View File

@ -40,6 +40,12 @@ const es = {
}
},
about: {
updates: {
currentVersion: 'Versión actual',
versionNotAvailable: 'N/D',
updateAvailable: `No estás ejecutando la última versión disponible: {versión}`,
deemixVersion: 'versión de la biblioteca deemix'
},
titles: {
usefulLinks: 'Enlaces útiles',
bugReports: 'Reportar fallos',
@ -96,7 +102,8 @@ const es = {
wrongBitrateNoAlternative: '¡Pista no encontrada a la tasa de bits deseada y no se ha encontrado ninguna alternativa!',
no360RA: 'La pista no está disponible en Reality Audio 360.',
notAvailable: '¡La pista no está disponible en los servidores de Deezer!',
notAvailableNoAlternative: '¡La pista no está disponible en los servidores de Deezer y no se ha encontrado ninguna alternativa!'
notAvailableNoAlternative: '¡La pista no está disponible en los servidores de Deezer y no se ha encontrado ninguna alternativa!',
noSpaceLeft: '¡No queda espacio en el dispositivo!'
}
},
favorites: {
@ -166,7 +173,8 @@ const es = {
finishAddingArtist: 'Añadido {artist} álbumes a la cola',
startConvertingSpotifyPlaylist: 'Convertir las pistas de Spotify en pistas de Deezer',
finishConvertingSpotifyPlaylist: 'Lista de reproducción de Spotify convertida',
loginNeededToDownload: '¡Necesitas iniciar sesión para descargar títulos!'
loginNeededToDownload: '¡Necesitas iniciar sesión para descargar títulos!',
deezerNotAvailable: 'Deezer no está disponible en su país. Deberías usar una VPN.'
},
settings: {
title: 'Configuración',
@ -178,7 +186,8 @@ const es = {
question: '¿Cómo consigo mi propio ARL?',
update: 'Actualiza la ARL'
},
logout: 'Cerrar sesión'
logout: 'Cerrar sesión',
login: 'Ingresa a través de deezer.com'
},
appearance: {
title: 'Apariencia',
@ -225,7 +234,8 @@ const es = {
y: 'Sí, sobrescribir el archivo',
n: 'No, no sobrescribir el archivo',
t: 'Sobrescribir sólo las etiquetas',
b: 'No, mantener los dos archivos y agrega un número al archivo duplicado'
b: 'No, mantener los dos archivos y agrega un número al archivo duplicado',
e: 'No, y no mirar las extensiones'
},
fallbackBitrate: 'La solución alternativa de bitrate',
fallbackSearch: 'Búsqueda de la segunda opción',
@ -253,7 +263,8 @@ const es = {
jpegImageQuality: 'Calidad de la imagen JPEG',
embeddedArtworkPNG: 'Guardar las imágenes incrustadas como PNG',
embeddedPNGWarning: 'Las PNG no están oficialmente soportadas por Deezer y puede encontrar errores.',
imageSizeWarning: 'Nada por encima de x1200 no es usado oficialmente por Deezer, tú podrías encontrar inconvenientes'
imageSizeWarning: 'Nada por encima de x1200 no es usado oficialmente por Deezer, tú podrías encontrar inconvenientes',
coverDescriptionUTF8: 'Guardar la descripción de la portada usando UTF8 (arregla la portada de iTunes)'
},
tags: {
head: '¿Qué etiquetas guardar?',
@ -277,6 +288,7 @@ const es = {
replayGain: 'Ganancia de la reproducción',
label: 'Etiqueta del álbum',
lyrics: 'Letras no sincronizadas',
syncedLyrics: 'Letra sincronizada',
copyright: 'Derechos de autor',
composer: 'Compositor',
involvedPeople: 'Personas involucradas'

View File

@ -32,14 +32,21 @@ const fr = {
playlist: 'playlist | playlists',
compile: 'compilation | compilations',
ep: 'ep | eps',
bundle: 'bundle | bundles',
more: "Plus d'albums",
featured: 'En vedette dans',
featured: 'Apparaît dans',
spotifyPlaylist: 'playlist spotify | playlists spotify',
releaseDate: 'date de sortie',
error: 'erreur'
}
},
about: {
updates: {
currentVersion: 'Version Actuelle',
versionNotAvailable: 'N/A',
updateAvailable: "Vous n'utilisez pas la dernière version disponible : {version}",
deemixVersion: 'Version de la bibliothèque deemix'
},
titles: {
usefulLinks: 'Liens Utiles',
bugReports: 'Rapports De Bug',
@ -61,7 +68,7 @@ const fr = {
officialSubreddit: 'Subreddit Officiel',
newsChannel: "Canal d'Informations",
questions: `Si vous avez des questions ou des problèmes avec l'application, cherchez d'abord une solution dans le <a href="https://www.reddit.com/r/deemix" target="_blank">subreddit</a>. Ensuite, si la solution ne s'y trouve pas, vous pouvez publier un message dans le subreddit en décrivant votre problème.`,
beforeReporting: "Avant de signaler un bug, assurez-vous que vous utilisez la version la plus récente de l'application. Vérifiez que vous souhaitez nous rapporter un bug et non quelque chose qui ne fonctionne pas de votre côté.",
beforeReporting: "Avant de signaler un bug, assurez-vous que vous utilisez la version la plus récente de l'application. Vérifiez que vous souhaitez nous rapporter un bug et non quelque chose qui ne fonctionne pas uniquement de votre côté.",
beSure: "Assurez-vous que le bug soit reproductible sur d'autres appareils mais aussi de <strong>NE PAS</strong> signaler un bug si celui-ci a déjà été recensé.",
duplicateReports: "Les rapports de bug répétitifs seront supprimés, merci d'en prendre bonne note.",
dontOpenIssues: "<strong>NE PAS</strong> rapporter de problème s'il ne s'agit que de simples questions. Un subreddit existe pour ces questions.",
@ -97,7 +104,8 @@ const fr = {
wrongBitrateNoAlternative: "La piste est introuvable au débit souhaité et aucune alternative n'a été trouvée !",
no360RA: 'La piste est indisponible au format Reality Audio 360.',
notAvailable: 'La piste est indisponible sur les serveurs de Deezer !',
notAvailableNoAlternative: "La piste est indisponible sur les serveurs de Deezer et aucune alternative n'a été trouvée !"
notAvailableNoAlternative: "La piste est indisponible sur les serveurs de Deezer et aucune alternative n'a été trouvée !",
noSpaceLeft: "L'espace disponible sur cet appareil est insuffisant !"
}
},
favorites: {
@ -111,7 +119,7 @@ const fr = {
needTologin: 'Vous devez vous connecter à votre compte Deezer avant de pouvoir démarrer un téléchargement.',
openSettings: 'Ouvrir Les Paramètres',
sections: {
popularPlaylists: 'Playlists populaires',
popularPlaylists: 'Playlists les plus écoutées',
popularAlbums: 'Albums les plus écoutés'
}
},
@ -136,8 +144,8 @@ const fr = {
}
},
search: {
startSearching: 'Démarrer la recherche !',
description: 'Vous pouvez rechercher une piste, un album entier, un artiste, une playlist... tout ce que vous voulez ! Vous pouvez également copier-coller un lien Deezer',
startSearching: 'Démarrer une recherche !',
description: 'Vous pouvez rechercher une piste, un album entier, un artiste, une playlist... tout ce que vous voulez ! Vous pouvez également coller un lien Deezer.',
fans: '{n} fans',
noResults: 'Aucun résultat',
noResultsTrack: "Aucune piste n'a été trouvée",
@ -163,11 +171,12 @@ const fr = {
loggedOut: 'Déconnecté',
cancellingCurrentItem: "Annulation de l'élément actuel.",
currentItemCancelled: 'Élément actuel annulé.',
startAddingArtist: "Ajout de {artist} albums à la file d'attente",
finishAddingArtist: "{artist} albums ajoutés à la file d'attente",
startAddingArtist: "Ajout des albums de {artist} à la file d'attente",
finishAddingArtist: "Les albums de {artist} ont été ajoutés à la file d'attente",
startConvertingSpotifyPlaylist: 'Conversion de pistes Spotify en équivalents Deezer',
finishConvertingSpotifyPlaylist: 'Playlist Spotify convertie',
loginNeededToDownload: 'Vous devez vous connecter pour pouvoir télécharger des pistes !'
loginNeededToDownload: 'Vous devez vous connecter pour pouvoir télécharger des pistes !',
deezerNotAvailable: "Deezer est indisponible dans votre pays. Vous devez utiliser un VPN."
},
settings: {
title: 'Paramètres',
@ -179,7 +188,8 @@ const fr = {
question: 'Comment obtenir mon ARL personnel ?',
update: "Mettre à jour l'ARL"
},
logout: 'Déconnexion'
logout: 'Déconnexion',
login: 'Connexion via deezer.com'
},
appearance: {
title: 'Apparence',
@ -210,7 +220,7 @@ const fr = {
title: 'Titres des pistes',
padTracks: "Longueur uniforme des numéros de piste (ajoute automatiquement des zéros devant le numéro initial de la piste)",
paddingSize: 'Nombre de zéros à ajouter en permanence devant le numéro initial de la piste',
illegalCharacterReplacer: 'Remplacement de caractère inapproprié'
illegalCharacterReplacer: "Substitut aux caractères non autorisés (dans les noms de fichiers et de dossiers)"
},
downloads: {
title: 'Téléchargements',
@ -222,7 +232,7 @@ const fr = {
1: 'MP3 128kbps'
},
overwriteFile: {
title: 'Faut-il écraser les fichiers ?',
title: 'Les fichiers doivent-ils être écrasés ?',
y: 'Oui, écraser le fichier',
n: 'Non, ne pas écraser le fichier',
t: 'Écraser uniquement les métadonnées',
@ -254,8 +264,9 @@ const fr = {
},
jpegImageQuality: "Qualité de l'image JPEG",
embeddedArtworkPNG: "Enregistrer l'illustration incorporée aux fichiers audio en tant que PNG",
embeddedPNGWarning: 'Les images PNG ne sont pas officiellement utilisées par Deezer et pourraient causer des problèmes',
imageSizeWarning: "Toute valeur supérieure à x1200 n'est pas officiellement supportée par Deezer, vous pourriez donc rencontrer des problèmes"
embeddedPNGWarning: 'Les images PNG ne sont pas officiellement utilisées par Deezer et pourraient causer des problèmes.',
imageSizeWarning: "Toute valeur supérieure à x1200 n'est pas officiellement supportée par Deezer, vous pourriez donc rencontrer des problèmes.",
coverDescriptionUTF8: 'Enregistrer la description de la pochette au format UTF8 (iTunes Cover Fix)'
},
tags: {
head: 'Métadonnées à sauvegarder',
@ -279,6 +290,7 @@ const fr = {
replayGain: 'Gain En Relecture (Replay Gain)',
label: "Label De l'Album",
lyrics: 'Paroles Non-Synchronisées',
syncedLyrics: 'Paroles Synchronisées',
copyright: "Droits d'Auteur (Copyright)",
composer: 'Compositeur',
involvedPeople: 'Personnes Impliquées'
@ -306,7 +318,7 @@ const fr = {
day: 'JJ'
},
featuredToTitle: {
title: 'Que faut-il faire avec les artistes participants (featuring) ?',
title: 'Que faire des artistes participants (featuring) ?',
0: 'Ne rien faire',
1: 'Les retirer du titre de la piste',
3: "Les supprimer du titre de la piste et du titre de l'album",
@ -331,7 +343,8 @@ const fr = {
title: 'Fonctionnalités Spotify',
clientID: 'clientID Spotify',
clientSecret: 'Client Secret Spotify',
username: "Nom d'utilisateur Spotify"
username: "Nom d'utilisateur Spotify",
question: 'Comment activer les Fonctionnalités Spotify ?'
},
reset: 'Rétablir les valeurs par défaut',
save: 'Sauvegarder',
@ -346,7 +359,7 @@ const fr = {
search: 'recherche',
charts: 'classements',
favorites: 'favoris',
linkAnalyzer: 'analyseur de liens',
linkAnalyzer: 'analyseur de lien',
settings: 'paramètres',
about: 'à propos'
},

View File

@ -41,6 +41,12 @@ const it = {
}
},
about: {
updates: {
currentVersion: 'Versione corrente',
versionNotAvailable: 'N/A',
updateAvailable: `Non stai usando l'ultima versione disponibile: {version}`,
deemixVersion: 'Versione libreria deemix'
},
titles: {
usefulLinks: 'Link Utili',
bugReports: 'Segnalazione di bug',
@ -101,7 +107,8 @@ const it = {
wrongBitrateNoAlternative: 'Brano non trovato con il bitrate specificato e nessuna alternativa trovata!',
no360RA: 'Brano non disponibile in Reality Audio 360.',
notAvailable: 'Brano non presente sui server di Deezer!',
notAvailableNoAlternative: 'Brano non presente sui server di Deezer e nessuna alternativa trovata!'
notAvailableNoAlternative: 'Brano non presente sui server di Deezer e nessuna alternativa trovata!',
noSpaceLeft: "Spazio su disco esaurito!"
}
},
favorites: {
@ -174,7 +181,8 @@ const it = {
finishAddingArtist: 'Aggiunto gli album di {artist} alla coda',
startConvertingSpotifyPlaylist: 'Convertendo i brani da spotify a deezer',
finishConvertingSpotifyPlaylist: 'Playlist di spotify convertita',
loginNeededToDownload: 'Devi accedere prima di poter scaricare brani!'
loginNeededToDownload: 'Devi accedere prima di poter scaricare brani!',
deezerNotAvailable: 'Deezer non è disponibile nel tuo paese. Dovresti usare una VPN.'
},
settings: {
title: 'Impostazioni',
@ -186,7 +194,8 @@ const it = {
question: 'Come ottengo il mio ARL?',
update: 'Aggiorna ARL'
},
logout: 'Disconnettiti'
logout: 'Disconnettiti',
login: 'Accedi tramite deezer.com'
},
appearance: {
title: 'Aspetto',
@ -262,7 +271,8 @@ const it = {
jpegImageQuality: 'Qualità immagine JPEG',
embeddedArtworkPNG: 'Salva copertina incorporata come PNG',
embeddedPNGWarning: 'Le immagini PNG non sono usate ufficialmente da Deezer e potrebbero dare problemi',
imageSizeWarning: 'Dimensioni maggiori di x1200 non sono usate ufficialmente da Deezer, potresti incontrare problemi'
imageSizeWarning: 'Dimensioni maggiori di x1200 non sono usate ufficialmente da Deezer, potresti incontrare problemi',
coverDescriptionUTF8: 'Salva la descrizione della copertina in UTF8 (iTunes Cover Fix)'
},
tags: {
head: 'Quali tag salvare',
@ -286,6 +296,7 @@ const it = {
replayGain: 'Replay gain',
label: 'Casa Discografica',
lyrics: 'Testo non Sincronizzato',
syncedLyrics: 'Testo Sincronizzato',
copyright: 'Copyright',
composer: 'Compositori',
involvedPeople: 'Persone Coinvolte'
@ -338,7 +349,8 @@ const it = {
title: 'Spotify Features',
clientID: 'Spotify clientID',
clientSecret: 'Spotify Client Secret',
username: 'Spotify username'
username: 'Spotify username',
question: 'Come attivo le Spotify Features?'
},
reset: 'Reimposta Default',
save: 'Salva',

View File

@ -32,6 +32,7 @@ const ru = {
playlist: 'плейлист | плейлисты | плейлисты',
compile: 'сплит | сплиты | сплиты',
ep: 'ep',
bundle: 'бандл | бандлы | бандлы',
more: 'Больше альбомов',
featured: 'Представлено в',
spotifyPlaylist: 'плейлист spotify | плейлисты spotify | плейлисты spotify',
@ -40,6 +41,12 @@ const ru = {
}
},
about: {
updates: {
currentVersion: 'Текущая версия',
versionNotAvailable: 'Н/Д',
updateAvailable: `Вы используете не последнюю доступную версию: {version}`,
deemixVersion: 'Версия библиотеки deemix'
},
titles: {
usefulLinks: 'Полезные ссылки',
bugReports: 'Отчёты об ошибках',
@ -97,7 +104,8 @@ const ru = {
wrongBitrateNoAlternative: 'Данного трека нет в нужном битрейте. Альтернатив не найдено!',
no360RA: 'Трек недоступен в формате Reality Audio 360.',
notAvailable: "Трек недоступен на серверах Deezer!",
notAvailableNoAlternative: "Трек недоступен на серверах Deezer. Альтернатив не найдено!"
notAvailableNoAlternative: "Трек недоступен на серверах Deezer. Альтернатив не найдено!",
noSpaceLeft: "На устройстве не осталось свободного места!"
}
},
favorites: {
@ -179,7 +187,8 @@ const ru = {
question: 'Как узнать свой ARL?',
update: 'Обновить ARL'
},
logout: 'Выйти'
logout: 'Выйти',
login: 'Войти через deezer.com'
},
appearance: {
title: 'Внешний вид',
@ -279,6 +288,7 @@ const ru = {
replayGain: 'Replay Gain',
label: 'Издатель',
lyrics: 'Текст песни',
syncedLyrics: 'Синхрон. текст песни',
copyright: 'Права (копирайт)',
composer: 'Композитор',
involvedPeople: 'Вовлечённые люди'
@ -331,7 +341,8 @@ const ru = {
title: 'Настройки Spotify',
clientID: 'Spotify clientID',
clientSecret: 'Spotify Client Secret',
username: 'Spotify username'
username: 'Spotify username',
question: 'Как включить функции Spotify?'
},
reset: 'По умолчанию',
save: 'Сохранить',

View File

@ -1,33 +0,0 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import TracklistTab from '@components/TracklistTab.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/tracklist/:id',
component: TracklistTab
},
// 404
{
path: '*',
component: TracklistTab
}
]
const router = new VueRouter({
mode: 'history',
// linkActiveClass: 'open',
routes,
scrollBehavior(to, from, savedPosition) {
return { x: 0, y: 0 }
}
})
router.beforeEach((to, from, next) => {
next()
})
export default router

126
src/router.js Normal file
View File

@ -0,0 +1,126 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import { socket } from '@/utils/socket'
import EventBus from '@/utils/EventBus'
import ArtistTab from '@components/ArtistTab.vue'
import TracklistTab from '@components/TracklistTab.vue'
import TheHomeTab from '@components/TheHomeTab.vue'
import TheChartsTab from '@components/TheChartsTab.vue'
import TheFavoritesTab from '@components/TheFavoritesTab.vue'
import TheErrorsTab from '@components/TheErrorsTab.vue'
import TheLinkAnalyzerTab from '@components/TheLinkAnalyzerTab.vue'
import TheAboutTab from '@components/TheAboutTab.vue'
import TheSettingsTab from '@components/TheSettingsTab.vue'
import TheMainSearch from '@components/TheMainSearch.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: TheHomeTab,
meta: {
notKeepAlive: true
}
},
{
path: '/tracklist/:type/:id',
name: 'Tracklist',
component: TracklistTab
},
{
path: '/artist/:id',
name: 'Artist',
component: ArtistTab
},
{
path: '/charts',
name: 'Charts',
component: TheChartsTab,
meta: {
notKeepAlive: true
}
},
{
path: '/favorites',
name: 'Favorites',
component: TheFavoritesTab,
meta: {
notKeepAlive: true
}
},
{
path: '/errors',
name: 'Errors',
component: TheErrorsTab
},
{
path: '/link-analyzer',
name: 'Link Analyzer',
component: TheLinkAnalyzerTab
},
{
path: '/about',
name: 'About',
component: TheAboutTab
},
{
path: '/settings',
name: 'Settings',
component: TheSettingsTab
},
{
path: '/search',
name: 'Search',
component: TheMainSearch
},
// 404 client side
{
path: '*',
component: TheHomeTab
}
]
const router = new VueRouter({
mode: 'history',
// linkActiveClass: 'open',
routes,
scrollBehavior(to, from, savedPosition) {
return { x: 0, y: 0 }
}
})
router.beforeEach((to, from, next) => {
let getTracklistParams = null
switch (to.name) {
case 'Artist':
getTracklistParams = {
type: 'artist',
id: to.params.id
}
break
case 'Tracklist':
getTracklistParams = {
type: to.params.type,
id: to.params.id
}
break
default:
break
}
if (getTracklistParams) {
socket.emit('getTracklist', getTracklistParams)
}
EventBus.$emit('trackPreview:stopStackedTabsPreview')
next()
})
export default router

19
src/store/index.js Normal file
View File

@ -0,0 +1,19 @@
import Vuex from 'vuex'
import Vue from 'vue'
import about from '@/store/modules/about'
import login from '@/store/modules/login'
import errors from '@/store/modules/errors'
// Load Vuex
Vue.use(Vuex)
// Create store
export default new Vuex.Store({
modules: {
about,
login,
errors
},
strict: process.env.NODE_ENV !== 'production'
})

View File

@ -0,0 +1,41 @@
const state = {
currentCommit: null,
latestCommit: null,
updateAvailable: false,
deemixVersion: null
}
const actions = {
setAboutInfo({ commit }, payload) {
commit('SET_CURRENT_COMMIT', payload.currentCommit)
commit('SET_LATEST_COMMIT', payload.latestCommit)
commit('SET_UPDATE_AVAILABLE', payload.updateAvailable)
commit('SET_DEEMIX_VERSION', payload.deemixVersion)
}
}
const getters = {
getAboutInfo: state => state
}
const mutations = {
SET_CURRENT_COMMIT: (state, payload) => {
state.currentCommit = payload
},
SET_LATEST_COMMIT: (state, payload) => {
state.latestCommit = payload
},
SET_UPDATE_AVAILABLE: (state, payload) => {
state.updateAvailable = payload
},
SET_DEEMIX_VERSION: (state, payload) => {
state.deemixVersion = payload
}
}
export default {
state,
getters,
actions,
mutations
}

View File

@ -0,0 +1,31 @@
import Vue from 'vue'
const state = {}
const actions = {
setDefaultSettings({ commit }, payload) {
for (const settingName in payload) {
if (!payload.hasOwnProperty(settingName)) return
const settingValue = payload[settingName]
commit('SET_UNKNOWN_DEFAULT_SETTING', { settingName, settingValue })
}
}
}
const getters = {
getDefaultSettings: state => state
}
const mutations = {
SET_UNKNOWN_DEFAULT_SETTING(state, payload) {
Vue.set(state, payload.settingName, payload.settingValue)
}
}
export default {
state,
actions,
getters,
mutations
}

View File

@ -0,0 +1,45 @@
const state = {
artist: '',
bitrate: '',
cover: '',
downloaded: 0,
errors: [],
failed: 0,
id: '',
progress: 0,
silent: true,
size: 0,
title: '',
type: '',
uuid: ''
}
const actions = {
setErrors({ commit }, payload) {
commit('SET_ERRORS', payload)
}
}
const getters = {
getErrors: state => state
}
const mutations = {
SET_ERRORS(state, payload) {
// The payload has useless data for the GUI, so only the needed data is saved in the store
for (const errorName in state) {
if (state.hasOwnProperty(errorName)) {
const error = payload[errorName]
state[errorName] = error
}
}
}
}
export default {
state,
getters,
actions,
mutations
}

View File

@ -0,0 +1,84 @@
const getDefaultState = () => {
return {
arl: localStorage.getItem('arl') || '',
status: null,
user: {
id: null,
name: '',
picture: ''
},
clientMode: false
}
}
const state = getDefaultState()
const actions = {
login({ commit, dispatch }, payload) {
const { arl, user, status } = payload
dispatch('setARL', { arl })
commit('SET_USER', user)
commit('SET_STATUS', status)
},
logout({ commit }) {
localStorage.removeItem('arl')
commit('RESET_LOGIN')
},
setARL({ commit }, payload) {
let { arl, saveOnLocalStorage } = payload
saveOnLocalStorage = typeof saveOnLocalStorage === 'undefined' ? true : saveOnLocalStorage
commit('SET_ARL', arl)
if (saveOnLocalStorage) {
localStorage.setItem('arl', arl)
}
},
removeARL({ commit }) {
commit('SET_ARL', '')
localStorage.removeItem('arl')
},
setUser({ commit }, payload) {
commit('SET_USER', payload)
},
setClientMode({ commit }, payload) {
commit('SET_CLIENT_MODE', payload)
}
}
const getters = {
getARL: state => state.arl,
getUser: state => state.user,
getClientMode: state => state.clientMode,
isLoggedIn: state => !!state.arl
}
const mutations = {
SET_ARL(state, payload) {
state.arl = payload
},
SET_STATUS(state, payload) {
state.status = payload
},
SET_USER(state, payload) {
state.user = payload
},
SET_CLIENT_MODE(state, payload) {
state.clientMode = payload
},
RESET_LOGIN(state) {
// Needed for reactivity
Object.assign(state, getDefaultState())
}
}
export default {
state,
getters,
actions,
mutations
}

View File

@ -0,0 +1,31 @@
const state = {
clientId: '',
clientSecret: ''
}
const actions = {
setCredentials({ commit }, payload) {
commit('SET_CLIENT_ID', payload.clientId)
commit('SET_CLIENT_SECRET', payload.clientSecret)
}
}
const getters = {
getCredentials: state => state
}
const mutations = {
SET_CLIENT_ID(state, payload) {
state.clientId = payload
},
SET_CLIENT_SECRET(state, payload) {
state.clientSecret = payload
}
}
export default {
state,
getters,
actions,
mutations
}

View File

@ -1,11 +1,3 @@
/* Middle section */
#middle_section {
background-color: var(--main-background);
width: 100%;
height: 100%;
min-width: 10px;
}
/* Center section */
$icon-dimension: 2rem;
$searchbar-height: calc(2rem + 1em);
@ -63,31 +55,6 @@ $searchbar-height: calc(2rem + 1em);
}
}
#content {
background-color: var(--main-background);
// width: calc(100% - 10px);
width: 100%;
height: calc(100% - 93px);
overflow-y: scroll;
overflow-x: hidden;
// padding-left: 10px;
&::-webkit-scrollbar {
width: 10px;
}
&::-webkit-scrollbar-track {
background: var(--main-background);
}
&::-webkit-scrollbar-thumb {
background: var(--main-scroll);
border-radius: 4px;
width: 6px;
padding: 0px 2px;
}
}
#container {
--container-width: 95%;
@ -100,12 +67,6 @@ $searchbar-height: calc(2rem + 1em);
}
}
#container {
margin: 0 auto;
max-width: 1280px;
width: var(--container-width);
}
/* Modal Content */
.smallmodal-content {
--modal-content-width: 95%;

View File

@ -316,13 +316,6 @@ a {
}
}
#main_content {
margin-left: $sidebar-width;
width: calc(100% - #{$sidebar-width});
height: 100%;
display: flex;
}
// TODO Remove
.inline-flex {
display: flex;
@ -341,3 +334,11 @@ a {
.hide {
display: none !important;
}
.changing-theme {
transition: all 200ms ease-in-out;
}
[v-cloak] {
display: none;
}

View File

@ -22,6 +22,7 @@
cursor: pointer;
font-size: 1.75rem;
margin-bottom: 25px;
text-transform: capitalize;
&:not(.top_result_header) {
transition: color 200ms ease-in-out;

View File

@ -3,14 +3,6 @@
height: 125px;
}
#logged_in_info {
height: 250px;
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
}
#log_info {
display: flex;
flex-direction: column;

View File

@ -1,8 +1,9 @@
.search_tabcontent,
.main_tabcontent,
.favorites_tabcontent {
display: none;
}
// .search_tabcontent
// .main_tabcontent,
// .favorites_tabcontent
// {
// display: none;
// }
.main_tabcontent {
h1 {

View File

@ -0,0 +1,28 @@
// https://stackoverflow.com/questions/7451508/html5-audio-playback-with-fade-in-and-fade-out#answer-13149848
export async function adjustVolume(element, newVolume, { duration = 1000, easing = swing, interval = 13 } = {}) {
const originalVolume = element.volume
const delta = newVolume - originalVolume
if (!delta || !duration || !easing || !interval) {
element.volume = newVolume
return Promise.resolve()
}
const ticks = Math.floor(duration / interval)
let tick = 1
return new Promise(resolve => {
const timer = setInterval(() => {
element.volume = originalVolume + easing(tick / ticks) * delta
// console.log(element.volume)
if (++tick === ticks) {
clearInterval(timer)
resolve()
}
}, interval)
})
}
export function swing(p) {
return 0.5 - Math.cos(p * Math.PI) / 2
}

251
src/utils/countries.js Normal file
View File

@ -0,0 +1,251 @@
export const COUNTRIES = {
AF: 'Afghanistan',
AX: '\u00c5land Islands',
AL: 'Albania',
DZ: 'Algeria',
AS: 'American Samoa',
AD: 'Andorra',
AO: 'Angola',
AI: 'Anguilla',
AQ: 'Antarctica',
AG: 'Antigua and Barbuda',
AR: 'Argentina',
AM: 'Armenia',
AW: 'Aruba',
AU: 'Australia',
AT: 'Austria',
AZ: 'Azerbaijan',
BS: 'Bahamas',
BH: 'Bahrain',
BD: 'Bangladesh',
BB: 'Barbados',
BY: 'Belarus',
BE: 'Belgium',
BZ: 'Belize',
BJ: 'Benin',
BM: 'Bermuda',
BT: 'Bhutan',
BO: 'Bolivia, Plurinational State of',
BQ: 'Bonaire, Sint Eustatius and Saba',
BA: 'Bosnia and Herzegovina',
BW: 'Botswana',
BV: 'Bouvet Island',
BR: 'Brazil',
IO: 'British Indian Ocean Territory',
BN: 'Brunei Darussalam',
BG: 'Bulgaria',
BF: 'Burkina Faso',
BI: 'Burundi',
KH: 'Cambodia',
CM: 'Cameroon',
CA: 'Canada',
CV: 'Cape Verde',
KY: 'Cayman Islands',
CF: 'Central African Republic',
TD: 'Chad',
CL: 'Chile',
CN: 'China',
CX: 'Christmas Island',
CC: 'Cocos (Keeling) Islands',
CO: 'Colombia',
KM: 'Comoros',
CG: 'Congo',
CD: 'Congo, the Democratic Republic of the',
CK: 'Cook Islands',
CR: 'Costa Rica',
CI: "C\u00f4te d'Ivoire",
HR: 'Croatia',
CU: 'Cuba',
CW: 'Cura\u00e7ao',
CY: 'Cyprus',
CZ: 'Czech Republic',
DK: 'Denmark',
DJ: 'Djibouti',
DM: 'Dominica',
DO: 'Dominican Republic',
EC: 'Ecuador',
EG: 'Egypt',
SV: 'El Salvador',
GQ: 'Equatorial Guinea',
ER: 'Eritrea',
EE: 'Estonia',
ET: 'Ethiopia',
FK: 'Falkland Islands (Malvinas)',
FO: 'Faroe Islands',
FJ: 'Fiji',
FI: 'Finland',
FR: 'France',
GF: 'French Guiana',
PF: 'French Polynesia',
TF: 'French Southern Territories',
GA: 'Gabon',
GM: 'Gambia',
GE: 'Georgia',
DE: 'Germany',
GH: 'Ghana',
GI: 'Gibraltar',
GR: 'Greece',
GL: 'Greenland',
GD: 'Grenada',
GP: 'Guadeloupe',
GU: 'Guam',
GT: 'Guatemala',
GG: 'Guernsey',
GN: 'Guinea',
GW: 'Guinea-Bissau',
GY: 'Guyana',
HT: 'Haiti',
HM: 'Heard Island and McDonald Islands',
VA: 'Holy See (Vatican City State)',
HN: 'Honduras',
HK: 'Hong Kong',
HU: 'Hungary',
IS: 'Iceland',
IN: 'India',
ID: 'Indonesia',
IR: 'Iran, Islamic Republic of',
IQ: 'Iraq',
IE: 'Ireland',
IM: 'Isle of Man',
IL: 'Israel',
IT: 'Italy',
JM: 'Jamaica',
JP: 'Japan',
JE: 'Jersey',
JO: 'Jordan',
KZ: 'Kazakhstan',
KE: 'Kenya',
KI: 'Kiribati',
KP: "Korea, Democratic People's Republic of",
KR: 'Korea, Republic of',
KW: 'Kuwait',
KG: 'Kyrgyzstan',
LA: "Lao People's Democratic Republic",
LV: 'Latvia',
LB: 'Lebanon',
LS: 'Lesotho',
LR: 'Liberia',
LY: 'Libya',
LI: 'Liechtenstein',
LT: 'Lithuania',
LU: 'Luxembourg',
MO: 'Macao',
MK: 'Macedonia, the Former Yugoslav Republic of',
MG: 'Madagascar',
MW: 'Malawi',
MY: 'Malaysia',
MV: 'Maldives',
ML: 'Mali',
MT: 'Malta',
MH: 'Marshall Islands',
MQ: 'Martinique',
MR: 'Mauritania',
MU: 'Mauritius',
YT: 'Mayotte',
MX: 'Mexico',
FM: 'Micronesia, Federated States of',
MD: 'Moldova, Republic of',
MC: 'Monaco',
MN: 'Mongolia',
ME: 'Montenegro',
MS: 'Montserrat',
MA: 'Morocco',
MZ: 'Mozambique',
MM: 'Myanmar',
NA: 'Namibia',
NR: 'Nauru',
NP: 'Nepal',
NL: 'Netherlands',
NC: 'New Caledonia',
NZ: 'New Zealand',
NI: 'Nicaragua',
NE: 'Niger',
NG: 'Nigeria',
NU: 'Niue',
NF: 'Norfolk Island',
MP: 'Northern Mariana Islands',
NO: 'Norway',
OM: 'Oman',
PK: 'Pakistan',
PW: 'Palau',
PS: 'Palestine, State of',
PA: 'Panama',
PG: 'Papua New Guinea',
PY: 'Paraguay',
PE: 'Peru',
PH: 'Philippines',
PN: 'Pitcairn',
PL: 'Poland',
PT: 'Portugal',
PR: 'Puerto Rico',
QA: 'Qatar',
RE: 'R\u00e9union',
RO: 'Romania',
RU: 'Russian Federation',
RW: 'Rwanda',
BL: 'Saint Barth\u00e9lemy',
SH: 'Saint Helena, Ascension and Tristan da Cunha',
KN: 'Saint Kitts and Nevis',
LC: 'Saint Lucia',
MF: 'Saint Martin (French part)',
PM: 'Saint Pierre and Miquelon',
VC: 'Saint Vincent and the Grenadines',
WS: 'Samoa',
SM: 'San Marino',
ST: 'Sao Tome and Principe',
SA: 'Saudi Arabia',
SN: 'Senegal',
RS: 'Serbia',
SC: 'Seychelles',
SL: 'Sierra Leone',
SG: 'Singapore',
SX: 'Sint Maarten (Dutch part)',
SK: 'Slovakia',
SI: 'Slovenia',
SB: 'Solomon Islands',
SO: 'Somalia',
ZA: 'South Africa',
GS: 'South Georgia and the South Sandwich Islands',
SS: 'South Sudan',
ES: 'Spain',
LK: 'Sri Lanka',
SD: 'Sudan',
SR: 'Suriname',
SJ: 'Svalbard and Jan Mayen',
SZ: 'Swaziland',
SE: 'Sweden',
CH: 'Switzerland',
SY: 'Syrian Arab Republic',
TW: 'Taiwan, Province of China',
TJ: 'Tajikistan',
TZ: 'Tanzania, United Republic of',
TH: 'Thailand',
TL: 'Timor-Leste',
TG: 'Togo',
TK: 'Tokelau',
TO: 'Tonga',
TT: 'Trinidad and Tobago',
TN: 'Tunisia',
TR: 'Turkey',
TM: 'Turkmenistan',
TC: 'Turks and Caicos Islands',
TV: 'Tuvalu',
UG: 'Uganda',
UA: 'Ukraine',
AE: 'United Arab Emirates',
GB: 'United Kingdom',
US: 'United States',
UM: 'United States Minor Outlying Islands',
UY: 'Uruguay',
UZ: 'Uzbekistan',
VU: 'Vanuatu',
VE: 'Venezuela, Bolivarian Republic of',
VN: 'Viet Nam',
VG: 'Virgin Islands, British',
VI: 'Virgin Islands, U.S.',
WF: 'Wallis and Futuna',
EH: 'Western Sahara',
YE: 'Yemen',
ZM: 'Zambia',
ZW: 'Zimbabwe'
}

View File

@ -1,6 +1,6 @@
import { socket } from '@/utils/socket'
function sendAddToQueue(url, bitrate = null) {
export function sendAddToQueue(url, bitrate = null) {
if (!url) return
socket.emit('addToQueue', { url, bitrate }, () => {})

View File

@ -1,5 +1,11 @@
import store from '@/store'
export const socket = io.connect(window.location.href)
socket.on('connect', () => {
document.getElementById('start_app_placeholder').classList.add('loading_placeholder--hidden')
})
socket.on('init_update', data => {
store.dispatch('setAboutInfo', data)
})

View File

@ -1,5 +1,4 @@
import Toastify from 'toastify-js'
import $ from 'jquery'
import { socket } from '@/utils/socket'
@ -8,49 +7,83 @@ let toastsWithId = {}
export const toast = function(msg, icon = null, dismiss = true, id = null) {
if (toastsWithId[id]) {
let toastObj = toastsWithId[id]
let toastDOM = $(`div.toastify[toast_id=${id}]`)
let toastElement = document.querySelectorAll(`div.toastify[toast_id=${id}]`)
if (msg) {
toastDOM.find('.toast-message').html(msg)
toastElement.forEach(toast => {
const messages = toast.querySelectorAll('.toast-message')
messages.forEach(message => {
message.innerHTML = msg
})
})
}
if (icon) {
if (icon == 'loading') icon = `<div class="circle-loader"></div>`
else icon = `<i class="material-icons">${icon}</i>`
toastDOM.find('.toast-icon').html(icon)
if (icon == 'loading') {
icon = `<div class="circle-loader"></div>`
} else {
icon = `<i class="material-icons">${icon}</i>`
}
toastElement.forEach(toast => {
const icons = toast.querySelectorAll('.toast-icon')
icons.forEach(toastIcon => {
toastIcon.innerHTML = icon
})
})
}
if (dismiss !== null && dismiss) {
toastDOM.addClass('dismissable')
setTimeout(function() {
toastElement.forEach(toast => {
toast.classList.add('dismissable')
})
setTimeout(() => {
toastObj.hideToast()
delete toastsWithId[id]
}, 3000)
}
} else {
if (icon == null) icon = ''
else if (icon == 'loading') icon = `<div class="circle-loader"></div>`
else icon = `<i class="material-icons">${icon}</i>`
if (icon == null) {
icon = ''
} else if (icon == 'loading') {
icon = `<div class="circle-loader"></div>`
} else {
icon = `<i class="material-icons">${icon}</i>`
}
let toastObj = Toastify({
text: `<span class="toast-icon">${icon}</span><span class="toast-message">${msg}</toast>`,
duration: dismiss ? 3000 : 0,
gravity: 'bottom',
position: 'left',
className: dismiss ? 'dismissable' : '',
onClick: function(){
onClick: function() {
let dismissable = true
if (id){
if (id) {
let toastClasses = document.querySelector(`div.toastify[toast_id=${id}]`).classList
if (toastClasses){
dismissable = Array.from(toastClasses).indexOf('dismissable') != -1
if (toastClasses) {
dismissable = Array.prototype.slice.call(toastClasses).indexOf('dismissable') != -1
}
}
if (toastObj && dismissable) {
toastObj.hideToast()
if (id) delete toastsWithId[id]
if (id) {
delete toastsWithId[id]
}
}
}
}).showToast()
if (id) {
toastsWithId[id] = toastObj
$(toastObj.toastElement).attr('toast_id', id)
toastObj.toastElement.setAttribute('toast_id', id)
}
}
}

View File

@ -21,7 +21,11 @@ export function isValidURL(text) {
let lowerCaseText = text.toLowerCase()
if (lowerCaseText.startsWith('http')) {
if (lowerCaseText.indexOf('deezer.com') >= 0 || lowerCaseText.indexOf('open.spotify.com') >= 0) {
if (
lowerCaseText.indexOf('deezer.com') >= 0 ||
lowerCaseText.indexOf('deezer.page.link') >= 0 ||
lowerCaseText.indexOf('open.spotify.com') >= 0
) {
return true
}
} else if (lowerCaseText.startsWith('spotify:')) {
@ -91,263 +95,10 @@ export function copyToClipboard(text) {
ghostInput.remove()
}
export const COUNTRIES = {
AF: 'Afghanistan',
AX: '\u00c5land Islands',
AL: 'Albania',
DZ: 'Algeria',
AS: 'American Samoa',
AD: 'Andorra',
AO: 'Angola',
AI: 'Anguilla',
AQ: 'Antarctica',
AG: 'Antigua and Barbuda',
AR: 'Argentina',
AM: 'Armenia',
AW: 'Aruba',
AU: 'Australia',
AT: 'Austria',
AZ: 'Azerbaijan',
BS: 'Bahamas',
BH: 'Bahrain',
BD: 'Bangladesh',
BB: 'Barbados',
BY: 'Belarus',
BE: 'Belgium',
BZ: 'Belize',
BJ: 'Benin',
BM: 'Bermuda',
BT: 'Bhutan',
BO: 'Bolivia, Plurinational State of',
BQ: 'Bonaire, Sint Eustatius and Saba',
BA: 'Bosnia and Herzegovina',
BW: 'Botswana',
BV: 'Bouvet Island',
BR: 'Brazil',
IO: 'British Indian Ocean Territory',
BN: 'Brunei Darussalam',
BG: 'Bulgaria',
BF: 'Burkina Faso',
BI: 'Burundi',
KH: 'Cambodia',
CM: 'Cameroon',
CA: 'Canada',
CV: 'Cape Verde',
KY: 'Cayman Islands',
CF: 'Central African Republic',
TD: 'Chad',
CL: 'Chile',
CN: 'China',
CX: 'Christmas Island',
CC: 'Cocos (Keeling) Islands',
CO: 'Colombia',
KM: 'Comoros',
CG: 'Congo',
CD: 'Congo, the Democratic Republic of the',
CK: 'Cook Islands',
CR: 'Costa Rica',
CI: "C\u00f4te d'Ivoire",
HR: 'Croatia',
CU: 'Cuba',
CW: 'Cura\u00e7ao',
CY: 'Cyprus',
CZ: 'Czech Republic',
DK: 'Denmark',
DJ: 'Djibouti',
DM: 'Dominica',
DO: 'Dominican Republic',
EC: 'Ecuador',
EG: 'Egypt',
SV: 'El Salvador',
GQ: 'Equatorial Guinea',
ER: 'Eritrea',
EE: 'Estonia',
ET: 'Ethiopia',
FK: 'Falkland Islands (Malvinas)',
FO: 'Faroe Islands',
FJ: 'Fiji',
FI: 'Finland',
FR: 'France',
GF: 'French Guiana',
PF: 'French Polynesia',
TF: 'French Southern Territories',
GA: 'Gabon',
GM: 'Gambia',
GE: 'Georgia',
DE: 'Germany',
GH: 'Ghana',
GI: 'Gibraltar',
GR: 'Greece',
GL: 'Greenland',
GD: 'Grenada',
GP: 'Guadeloupe',
GU: 'Guam',
GT: 'Guatemala',
GG: 'Guernsey',
GN: 'Guinea',
GW: 'Guinea-Bissau',
GY: 'Guyana',
HT: 'Haiti',
HM: 'Heard Island and McDonald Islands',
VA: 'Holy See (Vatican City State)',
HN: 'Honduras',
HK: 'Hong Kong',
HU: 'Hungary',
IS: 'Iceland',
IN: 'India',
ID: 'Indonesia',
IR: 'Iran, Islamic Republic of',
IQ: 'Iraq',
IE: 'Ireland',
IM: 'Isle of Man',
IL: 'Israel',
IT: 'Italy',
JM: 'Jamaica',
JP: 'Japan',
JE: 'Jersey',
JO: 'Jordan',
KZ: 'Kazakhstan',
KE: 'Kenya',
KI: 'Kiribati',
KP: "Korea, Democratic People's Republic of",
KR: 'Korea, Republic of',
KW: 'Kuwait',
KG: 'Kyrgyzstan',
LA: "Lao People's Democratic Republic",
LV: 'Latvia',
LB: 'Lebanon',
LS: 'Lesotho',
LR: 'Liberia',
LY: 'Libya',
LI: 'Liechtenstein',
LT: 'Lithuania',
LU: 'Luxembourg',
MO: 'Macao',
MK: 'Macedonia, the Former Yugoslav Republic of',
MG: 'Madagascar',
MW: 'Malawi',
MY: 'Malaysia',
MV: 'Maldives',
ML: 'Mali',
MT: 'Malta',
MH: 'Marshall Islands',
MQ: 'Martinique',
MR: 'Mauritania',
MU: 'Mauritius',
YT: 'Mayotte',
MX: 'Mexico',
FM: 'Micronesia, Federated States of',
MD: 'Moldova, Republic of',
MC: 'Monaco',
MN: 'Mongolia',
ME: 'Montenegro',
MS: 'Montserrat',
MA: 'Morocco',
MZ: 'Mozambique',
MM: 'Myanmar',
NA: 'Namibia',
NR: 'Nauru',
NP: 'Nepal',
NL: 'Netherlands',
NC: 'New Caledonia',
NZ: 'New Zealand',
NI: 'Nicaragua',
NE: 'Niger',
NG: 'Nigeria',
NU: 'Niue',
NF: 'Norfolk Island',
MP: 'Northern Mariana Islands',
NO: 'Norway',
OM: 'Oman',
PK: 'Pakistan',
PW: 'Palau',
PS: 'Palestine, State of',
PA: 'Panama',
PG: 'Papua New Guinea',
PY: 'Paraguay',
PE: 'Peru',
PH: 'Philippines',
PN: 'Pitcairn',
PL: 'Poland',
PT: 'Portugal',
PR: 'Puerto Rico',
QA: 'Qatar',
RE: 'R\u00e9union',
RO: 'Romania',
RU: 'Russian Federation',
RW: 'Rwanda',
BL: 'Saint Barth\u00e9lemy',
SH: 'Saint Helena, Ascension and Tristan da Cunha',
KN: 'Saint Kitts and Nevis',
LC: 'Saint Lucia',
MF: 'Saint Martin (French part)',
PM: 'Saint Pierre and Miquelon',
VC: 'Saint Vincent and the Grenadines',
WS: 'Samoa',
SM: 'San Marino',
ST: 'Sao Tome and Principe',
SA: 'Saudi Arabia',
SN: 'Senegal',
RS: 'Serbia',
SC: 'Seychelles',
SL: 'Sierra Leone',
SG: 'Singapore',
SX: 'Sint Maarten (Dutch part)',
SK: 'Slovakia',
SI: 'Slovenia',
SB: 'Solomon Islands',
SO: 'Somalia',
ZA: 'South Africa',
GS: 'South Georgia and the South Sandwich Islands',
SS: 'South Sudan',
ES: 'Spain',
LK: 'Sri Lanka',
SD: 'Sudan',
SR: 'Suriname',
SJ: 'Svalbard and Jan Mayen',
SZ: 'Swaziland',
SE: 'Sweden',
CH: 'Switzerland',
SY: 'Syrian Arab Republic',
TW: 'Taiwan, Province of China',
TJ: 'Tajikistan',
TZ: 'Tanzania, United Republic of',
TH: 'Thailand',
TL: 'Timor-Leste',
TG: 'Togo',
TK: 'Tokelau',
TO: 'Tonga',
TT: 'Trinidad and Tobago',
TN: 'Tunisia',
TR: 'Turkey',
TM: 'Turkmenistan',
TC: 'Turks and Caicos Islands',
TV: 'Tuvalu',
UG: 'Uganda',
UA: 'Ukraine',
AE: 'United Arab Emirates',
GB: 'United Kingdom',
US: 'United States',
UM: 'United States Minor Outlying Islands',
UY: 'Uruguay',
UZ: 'Uzbekistan',
VU: 'Vanuatu',
VE: 'Venezuela, Bolivarian Republic of',
VN: 'Viet Nam',
VG: 'Virgin Islands, British',
VI: 'Virgin Islands, U.S.',
WF: 'Wallis and Futuna',
EH: 'Western Sahara',
YE: 'Yemen',
ZM: 'Zambia',
ZW: 'Zimbabwe'
}
export default {
isValidURL,
convertDuration,
convertDurationSeparated,
numberWithDots,
debounce,
COUNTRIES
debounce
}