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 $ 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: 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 [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 - 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.** **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 ### 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. 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 # 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 You can find more informations about deemix at https://deemix.app/
- [X] First step: rewrite the app in Single File Components way
- [ ] Second step: Implement routing for the whole app using Vue Router ⚒ - [deemix](https://codeberg.org/RemixDev/deemix)
- [ ] Third step: Remove jQuery - [deemix-pyweb](https://codeberg.org/RemixDev/deemix-pyweb)
- [ ] Implement custom contextmenu ⚒ - [deemix-tools](https://codeberg.org/RemixDev/deemix-tools)
- [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
- ?
# License # License

26
package-lock.json generated
View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "deemix", "name": "deemix-webui",
"version": "1.0.16", "version": "0.2.0",
"homepage": "https://codeberg.org/RemixDev/deemix-webui/src/master/README.md", "homepage": "https://codeberg.org/RemixDev/deemix-webui/src/master/README.md",
"repository": { "repository": {
"type": "git", "type": "git",
@ -13,17 +13,19 @@
"build:js": "rollup -c", "build:js": "rollup -c",
"watch:js": "rollup -c -w", "watch:js": "rollup -c -w",
"serve": "python ../server.py", "serve": "python ../server.py",
"serve:gui": "python ../deemix-pyweb.py --dev",
"dev": "npm-run-all --parallel serve watch:js watch:styles", "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" "build": "npm-run-all --sequential clean build:js build:styles"
}, },
"dependencies": { "dependencies": {
"jquery": "^3.5.1",
"lodash-es": "^4.17.15", "lodash-es": "^4.17.15",
"svg-country-flags": "^1.2.7", "svg-country-flags": "^1.2.7",
"toastify-js": "^1.8.0", "toastify-js": "^1.8.0",
"vue": "^2.6.11", "vue": "^2.6.12",
"vue-i18n": "^8.18.2", "vue-i18n": "^8.18.2",
"vue-router": "^3.3.4" "vue-router": "^3.3.4",
"vuex": "^3.5.1"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-alias": "^3.1.0", "@rollup/plugin-alias": "^3.1.0",
@ -39,6 +41,6 @@
"rollup-plugin-terser": "^6.1.0", "rollup-plugin-terser": "^6.1.0",
"rollup-plugin-vue": "^4.2.0", "rollup-plugin-vue": "^4.2.0",
"sass": "^1.26.8", "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-style: italic;
font-weight: 300; font-weight: 300;
font-display: swap; 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; unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
} }
/* cyrillic */ /* cyrillic */
@ -13,7 +13,7 @@
font-style: italic; font-style: italic;
font-weight: 300; font-weight: 300;
font-display: swap; 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; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
/* greek-ext */ /* greek-ext */
@ -22,7 +22,7 @@
font-style: italic; font-style: italic;
font-weight: 300; font-weight: 300;
font-display: swap; 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; unicode-range: U+1F00-1FFF;
} }
/* greek */ /* greek */
@ -31,7 +31,7 @@
font-style: italic; font-style: italic;
font-weight: 300; font-weight: 300;
font-display: swap; 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; unicode-range: U+0370-03FF;
} }
/* vietnamese */ /* vietnamese */
@ -40,7 +40,7 @@
font-style: italic; font-style: italic;
font-weight: 300; font-weight: 300;
font-display: swap; 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; 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 */ /* latin-ext */
@ -49,7 +49,7 @@
font-style: italic; font-style: italic;
font-weight: 300; font-weight: 300;
font-display: swap; 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; 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 */ /* latin */
@ -58,7 +58,7 @@
font-style: italic; font-style: italic;
font-weight: 300; font-weight: 300;
font-display: swap; 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; 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 */ /* cyrillic-ext */
@ -67,7 +67,7 @@
font-style: italic; font-style: italic;
font-weight: 400; font-weight: 400;
font-display: swap; 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; unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
} }
/* cyrillic */ /* cyrillic */
@ -76,7 +76,7 @@
font-style: italic; font-style: italic;
font-weight: 400; font-weight: 400;
font-display: swap; 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; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
/* greek-ext */ /* greek-ext */
@ -85,7 +85,7 @@
font-style: italic; font-style: italic;
font-weight: 400; font-weight: 400;
font-display: swap; 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; unicode-range: U+1F00-1FFF;
} }
/* greek */ /* greek */
@ -94,7 +94,7 @@
font-style: italic; font-style: italic;
font-weight: 400; font-weight: 400;
font-display: swap; 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; unicode-range: U+0370-03FF;
} }
/* vietnamese */ /* vietnamese */
@ -103,7 +103,7 @@
font-style: italic; font-style: italic;
font-weight: 400; font-weight: 400;
font-display: swap; 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; 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 */ /* latin-ext */
@ -112,7 +112,7 @@
font-style: italic; font-style: italic;
font-weight: 400; font-weight: 400;
font-display: swap; 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; 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 */ /* latin */
@ -121,7 +121,7 @@
font-style: italic; font-style: italic;
font-weight: 400; font-weight: 400;
font-display: swap; 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; 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 */ /* cyrillic-ext */
@ -130,7 +130,7 @@
font-style: italic; font-style: italic;
font-weight: 600; font-weight: 600;
font-display: swap; 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; unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
} }
/* cyrillic */ /* cyrillic */
@ -139,7 +139,7 @@
font-style: italic; font-style: italic;
font-weight: 600; font-weight: 600;
font-display: swap; 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; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
/* greek-ext */ /* greek-ext */
@ -148,7 +148,7 @@
font-style: italic; font-style: italic;
font-weight: 600; font-weight: 600;
font-display: swap; 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; unicode-range: U+1F00-1FFF;
} }
/* greek */ /* greek */
@ -157,7 +157,7 @@
font-style: italic; font-style: italic;
font-weight: 600; font-weight: 600;
font-display: swap; 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; unicode-range: U+0370-03FF;
} }
/* vietnamese */ /* vietnamese */
@ -166,7 +166,7 @@
font-style: italic; font-style: italic;
font-weight: 600; font-weight: 600;
font-display: swap; 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; 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 */ /* latin-ext */
@ -175,7 +175,7 @@
font-style: italic; font-style: italic;
font-weight: 600; font-weight: 600;
font-display: swap; 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; 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 */ /* latin */
@ -184,7 +184,7 @@
font-style: italic; font-style: italic;
font-weight: 600; font-weight: 600;
font-display: swap; 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; 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 */ /* cyrillic-ext */
@ -193,7 +193,7 @@
font-style: italic; font-style: italic;
font-weight: 700; font-weight: 700;
font-display: swap; 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; unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
} }
/* cyrillic */ /* cyrillic */
@ -202,7 +202,7 @@
font-style: italic; font-style: italic;
font-weight: 700; font-weight: 700;
font-display: swap; 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; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
/* greek-ext */ /* greek-ext */
@ -211,7 +211,7 @@
font-style: italic; font-style: italic;
font-weight: 700; font-weight: 700;
font-display: swap; 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; unicode-range: U+1F00-1FFF;
} }
/* greek */ /* greek */
@ -220,7 +220,7 @@
font-style: italic; font-style: italic;
font-weight: 700; font-weight: 700;
font-display: swap; 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; unicode-range: U+0370-03FF;
} }
/* vietnamese */ /* vietnamese */
@ -229,7 +229,7 @@
font-style: italic; font-style: italic;
font-weight: 700; font-weight: 700;
font-display: swap; 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; 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 */ /* latin-ext */
@ -238,7 +238,7 @@
font-style: italic; font-style: italic;
font-weight: 700; font-weight: 700;
font-display: swap; 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; 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 */ /* latin */
@ -247,7 +247,7 @@
font-style: italic; font-style: italic;
font-weight: 700; font-weight: 700;
font-display: swap; 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; 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 */ /* cyrillic-ext */
@ -256,7 +256,7 @@
font-style: italic; font-style: italic;
font-weight: 800; font-weight: 800;
font-display: swap; 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; unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
} }
/* cyrillic */ /* cyrillic */
@ -265,7 +265,7 @@
font-style: italic; font-style: italic;
font-weight: 800; font-weight: 800;
font-display: swap; 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; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
/* greek-ext */ /* greek-ext */
@ -274,7 +274,7 @@
font-style: italic; font-style: italic;
font-weight: 800; font-weight: 800;
font-display: swap; 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; unicode-range: U+1F00-1FFF;
} }
/* greek */ /* greek */
@ -283,7 +283,7 @@
font-style: italic; font-style: italic;
font-weight: 800; font-weight: 800;
font-display: swap; 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; unicode-range: U+0370-03FF;
} }
/* vietnamese */ /* vietnamese */
@ -292,7 +292,7 @@
font-style: italic; font-style: italic;
font-weight: 800; font-weight: 800;
font-display: swap; 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; 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 */ /* latin-ext */
@ -301,7 +301,7 @@
font-style: italic; font-style: italic;
font-weight: 800; font-weight: 800;
font-display: swap; 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; 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 */ /* latin */
@ -310,7 +310,7 @@
font-style: italic; font-style: italic;
font-weight: 800; font-weight: 800;
font-display: swap; 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; 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 */ /* cyrillic-ext */
@ -319,7 +319,7 @@
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
font-display: swap; 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; unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
} }
/* cyrillic */ /* cyrillic */
@ -328,7 +328,7 @@
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
font-display: swap; 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; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
/* greek-ext */ /* greek-ext */
@ -337,7 +337,7 @@
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
font-display: swap; 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; unicode-range: U+1F00-1FFF;
} }
/* greek */ /* greek */
@ -346,7 +346,7 @@
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
font-display: swap; 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; unicode-range: U+0370-03FF;
} }
/* vietnamese */ /* vietnamese */
@ -355,7 +355,7 @@
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
font-display: swap; 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; 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 */ /* latin-ext */
@ -364,7 +364,7 @@
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
font-display: swap; 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; 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 */ /* latin */
@ -373,7 +373,7 @@
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
font-display: swap; 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; 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 */ /* cyrillic-ext */
@ -382,7 +382,7 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; 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; unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
} }
/* cyrillic */ /* cyrillic */
@ -391,7 +391,7 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; 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; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
/* greek-ext */ /* greek-ext */
@ -400,7 +400,7 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; 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; unicode-range: U+1F00-1FFF;
} }
/* greek */ /* greek */
@ -409,7 +409,7 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; 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; unicode-range: U+0370-03FF;
} }
/* vietnamese */ /* vietnamese */
@ -418,7 +418,7 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; 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; 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 */ /* latin-ext */
@ -427,7 +427,7 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; 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; 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 */ /* latin */
@ -436,7 +436,7 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; 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; 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 */ /* cyrillic-ext */
@ -445,7 +445,7 @@
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; 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; unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
} }
/* cyrillic */ /* cyrillic */
@ -454,7 +454,7 @@
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; 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; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
/* greek-ext */ /* greek-ext */
@ -463,7 +463,7 @@
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; 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; unicode-range: U+1F00-1FFF;
} }
/* greek */ /* greek */
@ -472,7 +472,7 @@
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; 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; unicode-range: U+0370-03FF;
} }
/* vietnamese */ /* vietnamese */
@ -481,7 +481,7 @@
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; 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; 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 */ /* latin-ext */
@ -490,7 +490,7 @@
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; 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; 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 */ /* latin */
@ -499,7 +499,7 @@
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; 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; 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 */ /* cyrillic-ext */
@ -508,7 +508,7 @@
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: swap; 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; unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
} }
/* cyrillic */ /* cyrillic */
@ -517,7 +517,7 @@
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: swap; 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; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
/* greek-ext */ /* greek-ext */
@ -526,7 +526,7 @@
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: swap; 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; unicode-range: U+1F00-1FFF;
} }
/* greek */ /* greek */
@ -535,7 +535,7 @@
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: swap; 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; unicode-range: U+0370-03FF;
} }
/* vietnamese */ /* vietnamese */
@ -544,7 +544,7 @@
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: swap; 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; 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 */ /* latin-ext */
@ -553,7 +553,7 @@
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: swap; 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; 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 */ /* latin */
@ -562,7 +562,7 @@
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: swap; 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; 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 */ /* cyrillic-ext */
@ -571,7 +571,7 @@
font-style: normal; font-style: normal;
font-weight: 800; font-weight: 800;
font-display: swap; 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; unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
} }
/* cyrillic */ /* cyrillic */
@ -580,7 +580,7 @@
font-style: normal; font-style: normal;
font-weight: 800; font-weight: 800;
font-display: swap; 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; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
/* greek-ext */ /* greek-ext */
@ -589,7 +589,7 @@
font-style: normal; font-style: normal;
font-weight: 800; font-weight: 800;
font-display: swap; 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; unicode-range: U+1F00-1FFF;
} }
/* greek */ /* greek */
@ -598,7 +598,7 @@
font-style: normal; font-style: normal;
font-weight: 800; font-weight: 800;
font-display: swap; 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; unicode-range: U+0370-03FF;
} }
/* vietnamese */ /* vietnamese */
@ -607,7 +607,7 @@
font-style: normal; font-style: normal;
font-weight: 800; font-weight: 800;
font-display: swap; 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; 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 */ /* latin-ext */
@ -616,7 +616,7 @@
font-style: normal; font-style: normal;
font-weight: 800; font-weight: 800;
font-display: swap; 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; 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 */ /* latin */
@ -625,6 +625,6 @@
font-style: normal; font-style: normal;
font-weight: 800; font-weight: 800;
font-display: swap; 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; 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> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>deemix</title> <title>deemix</title>
<link rel="stylesheet" type="text/css" href="/public/css/vendor/animate.css"> <link rel="stylesheet" type="text/css" href="/css/vendor/animate.css">
<link rel="stylesheet" type="text/css" href="/public/css/vendor/material-icons.css"> <link rel="stylesheet" type="text/css" href="/css/vendor/material-icons.css">
<link rel="stylesheet" type="text/css" href="/public/css/vendor/OpenSans.css"> <link rel="stylesheet" type="text/css" href="/css/vendor/OpenSans.css">
<link rel="stylesheet" type="text/css" href="/public/css/vendor/toastify.css"> <link rel="stylesheet" type="text/css" href="/css/vendor/toastify.css">
<link rel="stylesheet" type="text/css" href="/public/css/style.css"> <link rel="stylesheet" type="text/css" href="/css/style.css">
<link rel="shortcut icon" href="/public/favicon.ico"> <link rel="shortcut icon" href="/favicon.ico">
<meta name="viewport" <meta name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=0"> content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=0">
<script> <script>
@ -23,8 +23,8 @@
<div id="app"></div> <div id="app"></div>
</body> </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> </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 App from '@components/App.vue'
import i18n from '@/plugins/i18n' 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 { socket } from '@/utils/socket'
import { toast } from '@/utils/toasts' import { toast } from '@/utils/toasts'
import { init as initTabs } from '@js/tabs.js' import { init as initTabs } from '@js/tabs.js'
@ -27,17 +27,15 @@ function startApp() {
function mountApp() { function mountApp() {
new Vue({ new Vue({
// router, store,
router,
i18n, i18n,
render: h => h(App) render: h => h(App)
}).$mount('#app') }).$mount('#app')
} }
function initClient() { function initClient() {
window.clientMode = true store.dispatch('setClientMode', 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')
} }
document.addEventListener('DOMContentLoaded', startApp) document.addEventListener('DOMContentLoaded', startApp)
@ -45,25 +43,18 @@ window.addEventListener('pywebviewready', initClient)
/* ===== Global shortcuts ===== */ /* ===== 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 => { document.addEventListener('paste', pasteEvent => {
pasteText = pasteEvent.clipboardData.getData('Text') let pasteText = pasteEvent.clipboardData.getData('Text')
if (pasteEvent.target.localName != "input"){
if (isValidURL(pasteText)){ if (pasteEvent.target.localName != 'input') {
if (main_selected === 'analyzer_tab') { if (isValidURL(pasteText)) {
if (window.main_selected === 'analyzer_tab') {
EventBus.$emit('linkAnalyzerTab:reset') EventBus.$emit('linkAnalyzerTab:reset')
socket.emit('analyzeLink', pasteText) socket.emit('analyzeLink', pasteText)
} else { } else {
Downloads.sendAddToQueue(pasteText) Downloads.sendAddToQueue(pasteText)
} }
}else{ } else {
let searchbar = document.querySelector('#searchbar') let searchbar = document.querySelector('#searchbar')
searchbar.select() searchbar.select()
searchbar.setSelectionRange(0, 99999) searchbar.setSelectionRange(0, 99999)
@ -85,8 +76,10 @@ socket.on('logging_in', function() {
socket.on('init_autologin', function() { socket.on('init_autologin', function() {
let arl = localStorage.getItem('arl') let arl = localStorage.getItem('arl')
let accountNum = localStorage.getItem('accountNum') let accountNum = localStorage.getItem('accountNum')
if (arl) { if (arl) {
arl = arl.trim() arl = arl.trim()
if (accountNum != 0) { if (accountNum != 0) {
socket.emit('login', arl, true, accountNum) socket.emit('login', arl, true, accountNum)
} else { } else {
@ -96,62 +89,42 @@ socket.on('init_autologin', function() {
}) })
socket.on('logged_in', function(data) { socket.on('logged_in', function(data) {
switch (data.status) { const { status, user } = data
switch (status) {
case 1: case 1:
case 3: case 3:
// Login ok
toast(i18n.t('toasts.loggedIn'), 'done', true, 'login-toast') toast(i18n.t('toasts.loggedIn'), 'done', true, 'login-toast')
if (data.arl) {
localStorage.setItem('arl', data.arl) store.dispatch('login', data)
$('#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')
break break
case 2: case 2:
// Already logged in
toast(i18n.t('toasts.alreadyLogged'), 'done', true, 'login-toast') toast(i18n.t('toasts.alreadyLogged'), 'done', true, 'login-toast')
if (data.user) {
$('#settings_username').text(data.user.name) store.dispatch('setUser', user)
$('#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')
break break
case 0: case 0:
// Login failed
toast(i18n.t('toasts.loginFailed'), 'close', true, 'login-toast') 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() $('#open_login_prompt').show()
document.getElementById('logged_in_info').classList.add('hide') document.getElementById('logged_in_info').classList.add('hide')
// $('#logged_in_info').hide()
$('#settings_username').text('Not Logged') $('#settings_username').text('Not Logged')
$('#settings_picture').attr('src', `https://e-cdns-images.dzcdn.net/images/user/125x125-000000-80-0-0.jpg`) $('#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') document.getElementById('home_not_logged_in').classList.remove('hide')
break
} }
}) })
socket.on('logged_out', function() { socket.on('logged_out', function() {
toast(i18n.t('toasts.loggedOut'), 'done', true, 'login-toast') toast(i18n.t('toasts.loggedOut'), 'done', true, 'login-toast')
localStorage.removeItem('arl')
$('#login_input_arl').val('') store.dispatch('logout')
$('#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')
}) })
socket.on('restoringQueue', function() { socket.on('restoringQueue', function() {
@ -167,11 +140,11 @@ socket.on('currentItemCancelled', function(uuid) {
}) })
socket.on('startAddingArtist', function(data) { 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) { 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) { socket.on('startConvertingSpotifyPlaylist', function(id) {
@ -187,12 +160,15 @@ socket.on('errorMessage', function(error) {
}) })
socket.on('queueError', function(queueItem) { socket.on('queueError', function(queueItem) {
if (queueItem.errid) toast(i18n.t(`errors.ids.${queueItem.errid}`), 'error') if (queueItem.errid) {
else toast(queueItem.error, 'error') toast(i18n.t(`errors.ids.${queueItem.errid}`), 'error')
} else {
toast(queueItem.error, 'error')
}
}) })
socket.on('alreadyInQueue', function(data) { 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) { socket.on('loginNeededToDownload', function(data) {

View File

@ -1,9 +1,16 @@
<template> <template>
<div style="height: inherit;"> <div>
<BaseLoadingPlaceholder id="start_app_placeholder" text="Connecting to the server..." />
<TheSidebar /> <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 /> <TheTrackPreview />
<TheQualityModal /> <TheQualityModal />
@ -12,18 +19,39 @@
</div> </div>
</template> </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> <script>
import TheSidebar from '@components/TheSidebar.vue' 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 TheTrackPreview from '@components/TheTrackPreview.vue'
import TheQualityModal from '@components/TheQualityModal.vue' import TheQualityModal from '@components/TheQualityModal.vue'
import BaseLoadingPlaceholder from '@components/BaseLoadingPlaceholder.vue' import BaseLoadingPlaceholder from '@components/BaseLoadingPlaceholder.vue'
import TheContextMenu from '@components/TheContextMenu.vue' import TheContextMenu from '@components/TheContextMenu.vue'
import TheDownloadTab from '@components/TheDownloadTab.vue'
import TheSearchBar from '@components/TheSearchBar.vue'
export default { export default {
components: { components: {
TheSidebar, TheSidebar,
TheMainContent, TheSearchBar,
TheMiddleSection,
TheDownloadTab,
TheTrackPreview, TheTrackPreview,
TheQualityModal, TheQualityModal,
BaseLoadingPlaceholder, BaseLoadingPlaceholder,

View File

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

View File

@ -1,6 +1,15 @@
<template> <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> <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> <ul>
<li v-html="$t('about.usesLibrary')"></li> <li v-html="$t('about.usesLibrary')"></li>
<li v-html="$t('about.thanks')"></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"> <a rel="license" href="https://www.gnu.org/licenses/gpl-3.0.en.html" target="_blank">
<img <img
alt="GNU General Public License" alt="GNU General Public License"
style="border-width: 0;" style="border-width: 0"
src="https://www.gnu.org/graphics/gplv3-127x51.png" src="https://www.gnu.org/graphics/gplv3-127x51.png"
/> />
</a> </a>
@ -101,6 +110,7 @@
<p v-html="$t('about.lincensedUnder')"></p> <p v-html="$t('about.lincensedUnder')"></p>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
li, li,
p, p,
@ -190,14 +200,36 @@ ul {
} }
} }
</style> </style>
<script> <script>
import { socket } from '@/utils/socket'
import paypal from '@/assets/paypal.svg' import paypal from '@/assets/paypal.svg'
import ethereum from '@/assets/ethereum.svg' import ethereum from '@/assets/ethereum.svg'
import { mapGetters } from 'vuex'
export default { export default {
data: () => ({ data: () => ({
paypal, 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> </script>

View File

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

View File

@ -1,72 +1,87 @@
<template> <template>
<section id="content" @scroll="handleContentScroll" ref="content"> <section id="content" @scroll="$route.name === 'Search' ? handleContentScroll($event) : null" ref="content">
<div id="container"> <div id="container">
<ArtistTab /> <BaseLoadingPlaceholder id="search_placeholder" text="Searching..." :hidden="!loading" />
<TheChartsTab />
<TheFavoritesTab /> <keep-alive>
<TheErrorsTab /> <router-view
<TheHomeTab /> v-if="!$route.meta.notKeepAlive"
<TheLinkAnalyzerTab /> v-show="!loading"
<TheAboutTab /> :key="$route.fullPath"
<TheSettingsTab /> :perform-scrolled-search="performScrolledSearch"
<TheMainSearch :scrolled-search-type="newScrolled" /> exclude=""
<TracklistTab /> ></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> </div>
</section> </section>
</template> </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> <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 { debounce } from '@/utils/utils'
import EventBus from '@/utils/EventBus.js' import EventBus from '@/utils/EventBus.js'
import BaseLoadingPlaceholder from '@components/BaseLoadingPlaceholder.vue'
export default { export default {
components: { components: {
ArtistTab, BaseLoadingPlaceholder
TheChartsTab,
TheFavoritesTab,
TheErrorsTab,
TheHomeTab,
TheLinkAnalyzerTab,
TheAboutTab,
TheSettingsTab,
TheMainSearch,
TracklistTab
}, },
data: () => ({ data: () => ({
newScrolled: null performScrolledSearch: false,
loading: false
}), }),
mounted() {
this.$root.$on('updateSearchLoadingState', loading => {
this.loading = loading
})
},
methods: { methods: {
handleContentScroll: debounce(async function() { handleContentScroll: debounce(async function () {
if (this.$refs.content.scrollTop + this.$refs.content.clientHeight < this.$refs.content.scrollHeight) return if (this.$refs.content.scrollTop + this.$refs.content.clientHeight < this.$refs.content.scrollHeight) return
this.performScrolledSearch = true
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]
await this.$nextTick() await this.$nextTick()
this.newScrolled = null this.performScrolledSearch = false
}, 100) }, 100)
} }
} }
</script> </script>
<style>
</style>

View File

@ -15,16 +15,54 @@
:title="$t('globals.toggle_download_tab_hint')" :title="$t('globals.toggle_download_tab_hint')"
></i> ></i>
<div id="queue_buttons"> <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
<i id="clean_queue" class="material-icons download_bar_icon" @click="cleanQueue" :title="$t('globals.clean_queue_hint')">clear_all</i> id="open_downloads_folder"
<i id="cancel_queue" class="material-icons download_bar_icon" @click="cancelQueue" :title="$t('globals.cancel_queue_hint')">delete_sweep</i> 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>
<div id="download_list" @click="handleListClick" ref="list"></div>
</div> </div>
</template> </template>
<style lang="scss" scoped>
#download_tab_container {
height: 100vh;
}
</style>
<script> <script>
import $ from 'jquery' import { mapActions, mapGetters } from 'vuex'
import QueueItem from '@components/downloads/QueueItem.vue'
import { socket } from '@/utils/socket' import { socket } from '@/utils/socket'
import { toast } from '@/utils/toasts' import { toast } from '@/utils/toasts'
@ -32,12 +70,23 @@ const tabMinWidth = 250
const tabMaxWidth = 500 const tabMaxWidth = 500
export default { export default {
data: () => ({ components: {
cachedTabWidth: parseInt(localStorage.getItem('downloadTabWidth')) || 300, QueueItem
queue: [], },
queueList: {}, data() {
queueComplete: [] return {
}), cachedTabWidth: parseInt(localStorage.getItem('downloadTabWidth')) || 300,
queue: [],
queueList: {},
queueComplete: []
// clientMode: window.clientMode
}
},
computed: {
...mapGetters({
clientMode: 'getClientMode'
})
},
mounted() { mounted() {
socket.on('startDownload', this.startDownload) socket.on('startDownload', this.startDownload)
socket.on('startConversion', this.startConversion) socket.on('startConversion', this.startConversion)
@ -69,6 +118,10 @@ export default {
}) })
}, },
methods: { methods: {
...mapActions(['setErrors']),
onRemoveItem(uuid) {
socket.emit('removeFromQueue', uuid)
},
setTabWidth(newWidth) { setTabWidth(newWidth) {
if (undefined === newWidth) { if (undefined === newWidth) {
this.$refs.container.style.width = '' this.$refs.container.style.width = ''
@ -78,25 +131,14 @@ export default {
this.$refs.list.style.width = newWidth + 'px' 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) { 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) { if (initQueueComplete.length) {
initQueueComplete.forEach(item => { initQueueComplete.forEach(item => {
@ -115,97 +157,57 @@ export default {
this.addToQueue(initQueueList[item]) this.addToQueue(initQueueList[item])
}) })
if (restored){ if (restored) {
toast(this.$t('toasts.queueRestored'), 'done', true, 'restoring_queue') toast(this.$t('toasts.queueRestored'), 'done', true, 'restoring_queue')
socket.emit('queueRestored') socket.emit('queueRestored')
} }
}, },
addToQueue(queueItem, current = false) { addToQueue(queueItem, current = false) {
if (Array.isArray(queueItem)){ if (Array.isArray(queueItem)) {
if (queueItem.length > 1){ if (queueItem.length > 1) {
queueItem.forEach((item, i) => { queueItem.forEach((item, i) => {
item.silent = true item.silent = true
this.addToQueue(item) 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 return
}else{ } else {
queueItem = queueItem[0] queueItem = queueItem[0]
} }
} }
this.queueList[queueItem.uuid] = queueItem
if (queueItem.downloaded + queueItem.failed == queueItem.size) { // * Here we have only queueItem objects
if (this.queueComplete.indexOf(queueItem.uuid) == -1) { 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) this.queueComplete.push(queueItem.uuid)
} }
} else { } else {
if (this.queue.indexOf(queueItem.uuid) == -1) { const itemIsNotInQueue = this.queue.indexOf(queueItem.uuid) == -1
if (itemIsNotInQueue) {
this.queue.push(queueItem.uuid) 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) { if (needToStartDownload) {
$(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) {
this.startDownload(queueItem.uuid) 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) { 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) { updateQueue(update) {
@ -215,34 +217,19 @@ export default {
if (uuid && this.queue.indexOf(uuid) > -1) { if (uuid && this.queue.indexOf(uuid) > -1) {
if (downloaded) { if (downloaded) {
this.queueList[uuid].downloaded++ this.queueList[uuid].downloaded++
$('#download_' + uuid + ' .queue_downloaded').text(
this.queueList[uuid].downloaded + this.queueList[uuid].failed
)
} }
if (failed) { if (failed) {
this.queueList[uuid].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 }) this.queueList[uuid].errors.push({ message: error, data: data, errid: errid })
} }
if (progress) { if (progress) {
this.queueList[uuid].progress = progress this.queueList[uuid].progress = progress
$('#bar_' + uuid).css('width', progress + '%')
} }
if (conversion) { 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) let index = this.queue.indexOf(uuid)
if (index > -1) { if (index > -1) {
this.queue.splice(index, 1) this.$delete(this.queue, index)
$(`#download_${this.queueList[uuid].uuid}`).remove() this.$delete(this.queueList, uuid)
delete this.queueList[uuid]
} }
}, },
removeAllDownloads(currentItem) { removeAllDownloads(currentItem) {
this.queueComplete = [] this.queueComplete = []
if (currentItem == '') { if (!currentItem) {
this.queue = [] this.queue = []
this.queueList = {} this.queueList = {}
$(listEl).html('')
} else { } else {
this.queue = [currentItem] this.queue = [currentItem]
let tempQueueItem = this.queueList[currentItem] let tempQueueItem = this.queueList[currentItem]
this.queueList = {} this.queueList = {}
this.queueList[currentItem] = tempQueueItem this.queueList[currentItem] = tempQueueItem
$('.download_object').each(function(index) {
if ($(this).attr('id') != 'download_' + currentItem) $(this).remove()
})
} }
}, },
removedFinishedDownloads() { removedFinishedDownloads() {
this.queueComplete.forEach(item => { this.queueComplete.forEach(uuid => {
$('#download_' + item).remove() this.$delete(this.queueList, uuid)
}) })
this.queueComplete = [] this.queueComplete = []
@ -300,47 +283,10 @@ export default {
cancelQueue() { cancelQueue() {
socket.emit('cancelAllDownloads') 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() { openDownloadsFolder() {
if (window.clientMode) { // if (this.clientMode) {
socket.emit('openDownloadsFolder') socket.emit('openDownloadsFolder')
} // }
}, },
handleDrag(event) { handleDrag(event) {
let newWidth = window.innerWidth - event.pageX + 2 let newWidth = window.innerWidth - event.pageX + 2
@ -358,24 +304,37 @@ export default {
document.addEventListener('mousemove', this.handleDrag) document.addEventListener('mousemove', this.handleDrag)
}, },
startDownload(uuid) { startDownload(uuid) {
$('#bar_' + uuid) this.$set(this.queueList[uuid], 'status', 'downloading')
.removeClass('converting') },
.removeClass('indeterminate') finishDownload(uuid) {
.addClass('determinate') 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) { startConversion(uuid) {
$('#bar_' + uuid) this.$set(this.queueList[uuid], 'status', 'converting')
.addClass('converting') this.$set(this.queueList[uuid], 'conversion', 0)
.removeClass('indeterminate')
.addClass('determinate')
.css('width', '100%')
}, },
showErrorsTab(clickEvent) { async showErrorsTab(item) {
this.$root.$emit('showTabErrors', clickEvent.data.item, clickEvent.target) await this.setErrors(item)
this.$router.push({ name: 'Errors' })
} }
} }
} }
</script> </script>
<style>
</style>

View File

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

View File

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

View File

@ -1,10 +1,15 @@
<template> <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> <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> <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>
<section v-if="playlists.length" class="home_section"> <section v-if="playlists.length" class="home_section">
<h3 class="section_heading">{{ $t('home.sections.popularPlaylists') }}</h3> <h3 class="section_heading">{{ $t('home.sections.popularPlaylists') }}</h3>
<div class="release_grid"> <div class="release_grid">
@ -29,11 +34,17 @@
</div> </div>
<p class="primary-text">{{ release.title }}</p> <p class="primary-text">{{ release.title }}</p>
<p class="secondary-text"> <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> </p>
</div> </div>
</div> </div>
</section> </section>
<section v-if="albums.length" class="home_section"> <section v-if="albums.length" class="home_section">
<h3 class="section_heading">{{ $t('home.sections.popularAlbums') }}</h3> <h3 class="section_heading">{{ $t('home.sections.popularAlbums') }}</h3>
<div class="release_grid"> <div class="release_grid">
@ -57,7 +68,7 @@
</div> </div>
</div> </div>
<p class="primary-text">{{ release.title }}</p> <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>
</section> </section>
@ -65,27 +76,36 @@
</template> </template>
<script> <script>
import { socket } from '@/utils/socket' import { mapGetters } from 'vuex'
import { showView } from '@js/tabs.js'
import Downloads from '@/utils/downloads' import { showView } from '@js/tabs'
import { sendAddToQueue } from '@/utils/downloads'
import { getHomeData } from '@/data/home'
export default { export default {
name: 'the-home-tab',
data() { data() {
return { return {
playlists: [], playlists: [],
albums: [] 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: { methods: {
artistView: showView.bind(null, 'artist'), artistView: showView.bind(null, 'artist'),
albumView: showView.bind(null, 'album'), albumView: showView.bind(null, 'album'),
playlistView: showView.bind(null, 'playlist'), playlistView: showView.bind(null, 'playlist'),
openSettings() {
document.getElementById('main_settings_tablink').click()
},
addToQueue(e) { addToQueue(e) {
Downloads.sendAddToQueue(e.currentTarget.dataset.link) sendAddToQueue(e.currentTarget.dataset.link)
}, },
initHome(data) { initHome(data) {
const { const {
@ -96,13 +116,6 @@ export default {
this.playlists = playlistData this.playlists = playlistData
this.albums = albumData this.albums = albumData
} }
},
mounted() {
if (localStorage.getItem('arl')) {
this.$refs.notLogged.classList.add('hide')
}
socket.on('init_home', this.initHome)
} }
} }
</script> </script>

View File

@ -1,7 +1,8 @@
<template> <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> <h2 class="page_heading page_heading--capitalize">{{ $t('sidebar.linkAnalyzer') }}</h2>
<div v-if="link == ''">
<div v-if="link === ''">
<p> <p>
{{ $t('linkAnalyzer.info') }} {{ $t('linkAnalyzer.info') }}
</p> </p>
@ -9,10 +10,11 @@
{{ $t('linkAnalyzer.useful') }} {{ $t('linkAnalyzer.useful') }}
</p> </p>
</div> </div>
<div v-else-if="link == 'error'"> <div v-else-if="link === 'error'">
<h2>{{ $t('linkAnalyzer.linkNotSupported') }}</h2> <h2>{{ $t('linkAnalyzer.linkNotSupported') }}</h2>
<p>{{ $t('linkAnalyzer.linkNotSupportedYet') }}</p> <p>{{ $t('linkAnalyzer.linkNotSupportedYet') }}</p>
</div> </div>
<div v-else> <div v-else>
<header <header
class="inline-flex" class="inline-flex"
@ -115,21 +117,21 @@
<script> <script>
import { socket } from '@/utils/socket' import { socket } from '@/utils/socket'
import { showView } from '@js/tabs.js' import { showView } from '@js/tabs'
import Utils from '@/utils/utils' import { convertDuration } from '@/utils/utils'
import { COUNTRIES } from '@/utils/countries'
import EventBus from '@/utils/EventBus' import EventBus from '@/utils/EventBus'
import Downloads from '@/utils/downloads' import { sendAddToQueue } from '@/utils/downloads'
export default { export default {
name: 'the-link-analyzer-tab',
data() { data() {
return { return {
link: '',
title: '', title: '',
subtitle: '', subtitle: '',
image: '', image: '',
data: {}, data: {},
type: '', type: '',
link: '',
id: '0', id: '0',
countries: [] countries: []
} }
@ -137,7 +139,7 @@ export default {
methods: { methods: {
artistView: showView.bind(null, 'artist'), artistView: showView.bind(null, 'artist'),
albumView: showView.bind(null, 'album'), albumView: showView.bind(null, 'album'),
convertDuration: Utils.convertDuration, convertDuration,
reset() { reset() {
this.title = 'Loading...' this.title = 'Loading...'
this.subtitle = '' this.subtitle = ''
@ -148,6 +150,7 @@ export default {
this.countries = [] this.countries = []
}, },
showTrack(data) { showTrack(data) {
this.reset()
const { const {
title, title,
title_version, title_version,
@ -167,13 +170,14 @@ export default {
let temp = [] let temp = []
let chars = [...cc].map(c => c.charCodeAt() + 127397) let chars = [...cc].map(c => c.charCodeAt() + 127397)
temp.push(String.fromCodePoint(...chars)) temp.push(String.fromCodePoint(...chars))
temp.push(Utils.COUNTRIES[cc]) temp.push(COUNTRIES[cc])
this.countries.push(temp) this.countries.push(temp)
}) })
this.data = data this.data = data
}, },
showAlbum(data) { showAlbum(data) {
this.reset()
const { title, cover_xl, link, id } = data const { title, cover_xl, link, id } = data
this.title = title this.title = title
@ -187,7 +191,7 @@ export default {
this.link = 'error' this.link = 'error'
}, },
addToQueue(e) { addToQueue(e) {
Downloads.sendAddToQueue(e.currentTarget.dataset.link) sendAddToQueue(e.currentTarget.dataset.link)
} }
}, },
mounted() { 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> <template>
<div id="search_tab" class="main_tabcontent" @click="handleSearchTabClick"> <div id="search_tab" class="main_tabcontent" ref="root">
<div :class="{ hide: results.query != '' }"> <div v-show="!showSearchTab">
<h2>{{ $t('search.startSearching') }}</h2> <h2>{{ $t('search.startSearching') }}</h2>
<p>{{ $t('search.description') }}</p> <p>{{ $t('search.description') }}</p>
</div> </div>
<div v-show="results.query !== ''">
<div v-show="showSearchTab">
<ul class="section-tabs"> <ul class="section-tabs">
<li class="section-tabs__tab search_tablinks" id="search_all_tab">{{ $t('globals.listTabs.all') }}</li> <li
<li class="section-tabs__tab search_tablinks" id="search_track_tab">{{ $tc('globals.listTabs.track', 2) }}</li> class="section-tabs__tab"
<li class="section-tabs__tab search_tablinks" id="search_album_tab">{{ $tc('globals.listTabs.album', 2) }}</li> v-for="tab in tabs"
<li class="section-tabs__tab search_tablinks" id="search_artist_tab"> :key="tab.name"
{{ $tc('globals.listTabs.artist', 2) }} @click="currentTab = tab"
</li> :class="{ active: currentTab.name === tab.name }"
<li class="section-tabs__tab search_tablinks" id="search_playlist_tab"> >
{{ $tc('globals.listTabs.playlist', 2) }} {{ tab.name }}
</li> </li>
</ul> </ul>
<div id="search_tab_content">
<!-- ### Main Search Tab ### --> <keep-alive>
<div id="main_search" class="search_tabcontent"> <component
<template v-for="section in results.allTab.ORDER"> :is="currentTab.component"
<section :results="results"
v-if=" @add-to-queue="addToQueue"
(section != 'TOP_RESULT' && results.allTab[section].data.length > 0) || @artist-view="artistView"
results.allTab[section].length > 0 @album-view="albumView"
" @playlist-view="playlistView"
class="search_section" @change-search-tab="changeSearchTab"
> ></component>
<h2 </keep-alive>
@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>
</div> </div>
</div> </div>
</template> </template>
<script> <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 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 { socket } from '@/utils/socket'
import EventBus from '@/utils/EventBus.js' import { showView } from '@js/tabs'
import { sendAddToQueue } from '@/utils/downloads'
import { numberWithDots, convertDuration } from '@/utils/utils'
import EventBus from '@/utils/EventBus'
export default { export default {
name: 'the-main-search-tab',
components: { components: {
BaseLoadingPlaceholder BaseLoadingPlaceholder
}, },
data() { data() {
const $t = this.$t.bind(this)
const $tc = this.$tc.bind(this)
return { 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: { results: {
query: '', query: '',
allTab: { 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: { props: {
scrolledSearchType: { performScrolledSearch: {
type: String, type: Boolean,
required: false required: false
} }
}, },
created() {
this.currentTab = this.tabs[0]
},
mounted() { mounted() {
EventBus.$on('mainSearch:checkLoadMoreContent', this.checkLoadMoreContent) 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('mainSearch', this.handleMainSearch)
socket.on('search', this.handleSearch) socket.on('search', this.handleSearch)
}, },
@ -504,138 +164,65 @@ export default {
artistView: showView.bind(null, 'artist'), artistView: showView.bind(null, 'artist'),
albumView: showView.bind(null, 'album'), albumView: showView.bind(null, 'album'),
playlistView: showView.bind(null, 'playlist'), playlistView: showView.bind(null, 'playlist'),
playPausePreview(e) { changeSearchTab(sectionName) {
EventBus.$emit('trackPreview:playPausePreview', e) sectionName = sectionName.toLowerCase()
},
previewMouseEnter(e) {
EventBus.$emit('trackPreview:previewMouseEnter', e)
},
previewMouseLeave(e) {
EventBus.$emit('trackPreview:previewMouseLeave', e)
},
handleSearchTabClick(event) {
const {
target,
target: { id }
} = event
let selectedTab = null
switch (id) { let newTab = this.tabs.find(tab => {
case 'search_all_tab': return tab.searchType === sectionName
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
default: if (!newTab) {
break console.error(`No tab ${sectionName} found`)
return
} }
if (!selectedTab) return window.scrollTo(0, 0)
this.currentTab = newTab
changeTab(target, 'search', selectedTab)
}, },
handleClickTopResult(event) { checkIfShowNewResults(term, mainSelected) {
let topResultType = this.results.allTab.TOP_RESULT[0].type let needToPerformNewSearch = term !== this.results.query || mainSelected == 'search_tab'
switch (topResultType) { if (needToPerformNewSearch) {
case 'artist': this.showNewResults(term)
this.artistView(event)
break
case 'album':
this.albumView(event)
break
case 'playlist':
this.playlistView(event)
break
default:
break
} }
}, },
showNewResults(term, mainSelected) { showNewResults(term) {
if (term !== this.results.query || mainSelected == 'search_tab') { socket.emit('mainSearch', { term })
document.getElementById('search_tab_content').style.display = 'none'
socket.emit('mainSearch', { term })
// Showing loading placeholder // Showing loading placeholder
document.getElementById('content').style.display = 'none' this.$root.$emit('updateSearchLoadingState', true)
document.getElementById('search_placeholder').classList.toggle('loading_placeholder--hidden') this.currentTab = this.tabs[0]
} else {
document.getElementById('search_tab_content').style.display = 'block'
document.getElementById('main_search_tablink').click()
}
}, },
checkLoadMoreContent(searchSelected) { checkLoadMoreContent(searchSelected) {
if (this.results[searchSelected.split('_')[0] + 'Tab'].data.length !== 0) return if (this.results[searchSelected.split('_')[0] + 'Tab'].data.length !== 0) return
this.search(searchSelected.split('_')[0]) 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) { addToQueue(e) {
Downloads.sendAddToQueue(e.currentTarget.dataset.link) sendAddToQueue(e.currentTarget.dataset.link)
}, },
numberWithDots: Utils.numberWithDots, numberWithDots,
convertDuration: Utils.convertDuration, convertDuration,
search(type) { search(type) {
socket.emit('search', { socket.emit('search', {
term: this.results.query, term: this.results.query,
type: type, type,
start: this.results[type + 'Tab'].next, start: this.results[`${type}Tab`].next,
nb: 30 nb: 30
}) })
}, },
scrolledSearch(type) { scrolledSearch() {
let currentTab = type + 'Tab' if (this.currentTab.searchType === 'all') return
let currentTab = `${this.currentTab.searchType}Tab`
if (this.results[currentTab].next < this.results[currentTab].total) { if (this.results[currentTab].next < this.results[currentTab].total) {
socket.emit('search', { this.search(this.currentTab.searchType)
term: this.results.query,
type: type,
start: this.results[currentTab].next,
nb: 30
})
} }
}, },
handleMainSearch(result) { handleMainSearch(result) {
// Hiding loading placeholder // Hiding loading placeholder
document.getElementById('content').style.display = '' this.$root.$emit('updateSearchLoadingState', false)
document.getElementById('search_placeholder').classList.toggle('loading_placeholder--hidden')
let resetObj = { data: [], next: 0, total: 0, loaded: false } let resetObj = { data: [], next: 0, total: 0, loaded: false }
@ -644,12 +231,7 @@ export default {
this.results.albumTab = { ...resetObj } this.results.albumTab = { ...resetObj }
this.results.artistTab = { ...resetObj } this.results.artistTab = { ...resetObj }
this.results.playlistTab = { ...resetObj } this.results.playlistTab = { ...resetObj }
if (this.results.query == '') document.getElementById('search_all_tab').click()
this.results.query = result.QUERY this.results.query = result.QUERY
document.getElementById('search_tab_content').style.display = 'block'
document.getElementById('main_search_tablink').click()
}, },
handleSearch(result) { handleSearch(result) {
const { next: nextResult, total, type, data } = result const { next: nextResult, total, type, data } = result
@ -673,13 +255,21 @@ export default {
} }
this.results[currentTab].loaded = true this.results[currentTab].loaded = true
},
isTabLoaded(tab) {
return this.loadedTabs.indexOf(tab.searchType) !== -1 || tab.searchType === 'all'
} }
}, },
watch: { watch: {
scrolledSearchType(newType) { performScrolledSearch(needToSearch) {
if (!newType) return 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> <template>
<div id="middle_section"> <main id="main_content">
<TheSearchBar />
<TheContent /> <TheContent />
<BaseLoadingPlaceholder id="search_placeholder" text="Searching..." :hidden="true" /> <!-- <BaseLoadingPlaceholder id="search_placeholder" text="Searching..." :hidden="true" /> -->
</div> </main>
</template> </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> <script>
import TheContent from '@components/TheContent.vue' import TheContent from '@components/TheContent.vue'
import TheSearchBar from '@components/TheSearchBar.vue'
import BaseLoadingPlaceholder from '@components/BaseLoadingPlaceholder.vue' import BaseLoadingPlaceholder from '@components/BaseLoadingPlaceholder.vue'
export default { export default {
components: { components: {
TheContent, TheContent,
TheSearchBar,
BaseLoadingPlaceholder BaseLoadingPlaceholder
} }
} }

View File

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

View File

@ -1,40 +1,28 @@
<template> <template>
<aside id="sidebar" role="navigation" @click="handleSidebarClick"> <aside id="sidebar" role="navigation">
<span id="main_home_tablink" class="main_tablinks" role="link" aria-label="home"> <router-link
<i class="material-icons side_icon">home</i> v-for="link in links"
<span class="main_tablinks_text">{{ $t('sidebar.home') }}</span> :key="link.id"
</span> tag="span"
<span id="main_search_tablink" class="main_tablinks" role="link" aria-label="search"> class="main_tablinks"
<i class="material-icons side_icon">search</i> role="link"
<span class="main_tablinks_text">{{ $t('sidebar.search') }}</span> :id="link.id"
</span> :class="{ active: activeTablink === link.name }"
<span id="main_charts_tablink" class="main_tablinks" role="link" aria-label="charts"> :aria-label="link.ariaLabel"
<i class="material-icons side_icon">show_chart</i> :to="{ name: link.routerName }"
<span class="main_tablinks_text">{{ $t('sidebar.charts') }}</span> @click.native="activeTablink = link.name"
</span> >
<span id="main_favorites_tablink" class="main_tablinks" role="link" aria-label="favorites"> <i class="material-icons side_icon">{{ link.icon }}</i>
<i class="material-icons side_icon">star</i> <span class="main_tablinks_text">{{ link.label }}</span>
<span class="main_tablinks_text">{{ $t('sidebar.favorites') }}</span> </router-link>
</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>
<span id="theme_selector" class="main_tablinks" role="link" aria-label="theme selector"> <span id="theme_selector" class="main_tablinks" role="link" aria-label="theme selector">
<i class="material-icons side_icon side_icon--theme">palette</i> <i class="material-icons side_icon side_icon--theme">palette</i>
<div id="theme_togglers"> <div id="theme_togglers">
<div <div
v-for="theme of themes" v-for="theme of themes"
:key="theme" :key="theme"
class="theme_toggler " class="theme_toggler"
:class="[{ 'theme_toggler--active': activeTheme === theme }, `theme_toggler--${theme}`]" :class="[{ 'theme_toggler--active': activeTheme === theme }, `theme_toggler--${theme}`]"
@click="changeTheme(theme)" @click="changeTheme(theme)"
></div> ></div>
@ -77,15 +65,76 @@
</style> </style>
<script> <script>
import { changeTab } from '@js/tabs.js'
export default { export default {
name: 'the-sidebar', data() {
data: () => ({ const $t = this.$t.bind(this)
appOnline: null, const $tc = this.$tc.bind(this)
activeTheme: 'light',
themes: ['purple', 'dark', 'light'] 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() { mounted() {
/* === Online status handling === */ /* === Online status handling === */
this.appOnline = navigator.onLine this.appOnline = navigator.onLine
@ -100,6 +149,14 @@ export default {
/* === Current theme handling === */ /* === Current theme handling === */
this.activeTheme = localStorage.getItem('selectedTheme') || 'light' 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: { methods: {
changeTheme(newTheme) { changeTheme(newTheme) {
@ -110,65 +167,19 @@ export default {
localStorage.setItem('selectedTheme', newTheme) localStorage.setItem('selectedTheme', newTheme)
// Animating everything to have a smoother theme switch // Animating everything to have a smoother theme switch
document.querySelectorAll('*').forEach(el => { const allElements = document.querySelectorAll('*')
el.style.transition = 'all 200ms ease-in-out'
allElements.forEach(el => {
el.classList.add('changing-theme')
}) })
document.documentElement.addEventListener('transitionend', function transitionHandler() { document.documentElement.addEventListener('transitionend', function transitionHandler() {
document.querySelectorAll('*').forEach(el => { allElements.forEach(el => {
el.style.transition = '' el.classList.remove('changing-theme')
}) })
document.documentElement.removeEventListener('transitionend', transitionHandler) 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> </template>
<script> <script>
import $ from 'jquery' // import $ from 'jquery'
import EventBus from '@/utils/EventBus' import EventBus from '@/utils/EventBus'
import { adjustVolume } from '@/utils/adjust-volume'
export default { export default {
data: () => ({ data: () => ({
previewStopped: false previewStopped: false
@ -25,98 +27,113 @@ export default {
await this.$refs.preview.play() await this.$refs.preview.play()
this.previewStopped = false 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 // Prevents first time entering in this function
if (isNaN(this.$refs.preview.duration)) return if (isNaN(this.$refs.preview.duration)) return
let duration = this.$refs.preview.duration 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 (duration - this.$refs.preview.currentTime >= 1) return
if (this.previewStopped) return if (this.previewStopped) return
$(this.$refs.preview).animate({ volume: 0 }, 800) await adjustVolume(this.$refs.preview, 0, { duration: 800 })
this.previewStopped = true this.previewStopped = true
$('a[playing] > .preview_controls').css({ opacity: 0 }) document.querySelectorAll('a[playing] > .preview_controls').forEach(control => {
$('*').removeAttr('playing') control.style.opacity = 0
$('.preview_controls').text('play_arrow') })
$('.preview_playlist_controls').text('play_arrow')
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.preventDefault()
e.stopPropagation() e.stopPropagation()
const { currentTarget: obj } = event 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) { if (this.$refs.preview.paused) {
this.$refs.preview.play() this.$refs.preview.play()
this.previewStopped = false 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 { } else {
this.previewStopped = true this.previewStopped = true
$icon.text('play_arrow') icon.innerText = 'play_arrow'
$(this.$refs.preview).animate({ volume: 0 }, 250, 'swing', () => { await adjustVolume(this.$refs.preview, 0, { duration: 250 })
this.$refs.preview.pause()
}) this.$refs.preview.pause()
} }
} else { } else {
$('*').removeAttr('playing') document.querySelectorAll('*').forEach(el => {
$(obj).attr('playing', true) el.removeAttribute('playing')
})
obj.setAttribute('playing', true)
$('.preview_controls').text('play_arrow') document.querySelectorAll('.preview_controls, .preview_playlist_controls').forEach(el => {
$('.preview_playlist_controls').text('play_arrow') el.textContent = 'play_arrow'
$('.preview_controls').css({ opacity: 0 }) })
$icon.text('pause') document.querySelectorAll('.preview_controls').forEach(el => {
$icon.css({ opacity: 1 }) el.style.opacity = 0
})
icon.innerText = 'pause'
icon.style.opacity = 1
this.previewStopped = false this.previewStopped = false
$(this.$refs.preview).animate({ volume: 0 }, 250, 'swing', () => { await adjustVolume(this.$refs.preview, 0, { duration: 250 })
this.$refs.preview.pause() this.$refs.preview.pause()
$('#preview-track_source').prop('src', $(obj).data('preview'))
this.$refs.preview.load() document.getElementById('preview-track_source').src = obj.getAttribute('data-preview')
})
this.$refs.preview.load()
} }
}, },
stopStackedTabsPreview() { async stopStackedTabsPreview() {
if ( let controls = Array.prototype.slice.call(document.querySelectorAll('.preview_playlist_controls[playing]'))
$('.preview_playlist_controls').filter(function() {
return $(this).attr('playing') if (controls.length === 0) return
}).length > 0
) { await adjustVolume(this.$refs.preview, 0, { duration: 800 })
$(this.$refs.preview).animate({ volume: 0 }, 800)
this.previewStopped = true this.previewStopped = true
$('.preview_playlist_controls').removeAttr('playing')
$('.preview_playlist_controls').text('play_arrow') controls.forEach(control => {
} control.removeAttribute('playing')
control.innerText = 'play_arrow'
})
}, },
previewMouseEnter(e) { previewMouseEnter(e) {
$(e.currentTarget).css({ opacity: 1 }) e.currentTarget.style.opacity = 1
}, },
previewMouseLeave(event) { previewMouseLeave(event) {
const { currentTarget: obj } = event const { currentTarget: obj } = event
const parentIsPlaying = obj.parentElement.hasAttribute('playing')
if ( if ((parentIsPlaying && this.previewStopped) || !parentIsPlaying) {
($(obj) obj.style.opacity = 0
.parent()
.attr('playing') &&
this.previewStopped) ||
!$(obj)
.parent()
.attr('playing')
) {
$(obj).css({ opacity: 0 }, 200)
} }
} }
} }

View File

@ -1,5 +1,5 @@
<template> <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 <header
:style="{ :style="{
'background-image': 'background-image':
@ -55,9 +55,7 @@
</td> </td>
<td class="table__cell--large table__cell--with-icon"> <td class="table__cell--large table__cell--with-icon">
<div class="table__cell-content table__cell-content--vertical-center"> <div class="table__cell-content table__cell-content--vertical-center">
<i v-if="track.explicit_lyrics" class="material-icons explicit_icon"> <i v-if="track.explicit_lyrics" class="material-icons explicit_icon"> explicit </i>
explicit
</i>
{{ {{
track.title + track.title +
(track.title_version && track.title.indexOf(track.title_version) == -1 (track.title_version && track.title.indexOf(track.title_version) == -1
@ -91,9 +89,9 @@
<input class="clickable" type="checkbox" v-model="track.selected" /> <input class="clickable" type="checkbox" v-model="track.selected" />
</td> </td>
</tr> </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> <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> <i class="material-icons">album</i>
</div> </div>
</td> </td>
@ -131,15 +129,15 @@
</template> </template>
</tbody> </tbody>
</table> </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> <footer>
<button @click.stop="addToQueue" :data-link="link"> <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>
<button class="with_icon" @click.stop="addToQueue" :data-link="selectedLinks()"> <button class="with_icon" @click.stop="addToQueue" :data-link="selectedLinks()">
{{ $t('tracklist.downloadSelection') }}<i class="material-icons">file_download</i> {{ $t('tracklist.downloadSelection') }}<i class="material-icons">file_download</i>
</button> </button>
<button class="back-button">{{ $t('globals.back') }}</button> <button class="back-button" @click="backTab">{{ $t('globals.back') }}</button>
</footer> </footer>
</div> </div>
</template> </template>
@ -147,7 +145,7 @@
<script> <script>
import { isEmpty } from 'lodash-es' import { isEmpty } from 'lodash-es'
import { socket } from '@/utils/socket' import { socket } from '@/utils/socket'
import { showView } from '@js/tabs.js' import { showView, backTab } from '@js/tabs.js'
import Downloads from '@/utils/downloads' import Downloads from '@/utils/downloads'
import Utils from '@/utils/utils' import Utils from '@/utils/utils'
import EventBus from '@/utils/EventBus' import EventBus from '@/utils/EventBus'
@ -166,6 +164,7 @@ export default {
body: [] body: []
}), }),
methods: { methods: {
backTab,
artistView: showView.bind(null, 'artist'), artistView: showView.bind(null, 'artist'),
albumView: showView.bind(null, 'album'), albumView: showView.bind(null, 'album'),
playPausePreview(e) { playPausePreview(e) {
@ -203,6 +202,8 @@ export default {
}, },
convertDuration: Utils.convertDuration, convertDuration: Utils.convertDuration,
showAlbum(data) { showAlbum(data) {
this.reset()
const { const {
id: albumID, id: albumID,
title: albumTitle, title: albumTitle,
@ -231,6 +232,8 @@ export default {
} }
}, },
showPlaylist(data) { showPlaylist(data) {
this.reset()
const { const {
id: playlistID, id: playlistID,
title: playlistTitle, title: playlistTitle,
@ -246,7 +249,10 @@ export default {
this.title = playlistTitle this.title = playlistTitle
this.image = playlistCover this.image = playlistCover
this.release_date = creation_date.substring(0, 10) 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)) { if (isEmpty(playlistTracks)) {
this.body = null this.body = null
@ -255,6 +261,8 @@ export default {
} }
}, },
showSpotifyPlaylist(data) { showSpotifyPlaylist(data) {
this.reset()
const { const {
uri: playlistURI, uri: playlistURI,
name: playlistName, name: playlistName,
@ -272,7 +280,10 @@ export default {
? images[0].url ? images[0].url
: 'https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/1000x1000-000000-80-0-0.jpg' : 'https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/1000x1000-000000-80-0-0.jpg'
this.release_date = '' 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)) { if (isEmpty(playlistTracks)) {
this.body = null this.body = null
@ -285,7 +296,6 @@ export default {
} }
}, },
mounted() { mounted() {
EventBus.$on('tracklistTab:reset', this.reset)
EventBus.$on('tracklistTab:selectRow', this.selectRow) EventBus.$on('tracklistTab:selectRow', this.selectRow)
socket.on('show_album', this.showAlbum) 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 EventBus from '@/utils/EventBus'
import router from '@/router'
/* ===== Globals ====== */ /* ===== Globals ====== */
window.search_selected = '' window.search_selected = ''
@ -7,78 +7,35 @@ window.main_selected = ''
window.windows_stack = [] window.windows_stack = []
window.currentStack = {} window.currentStack = {}
// Exporting this function out of the default export // Used only in errors tab
// 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
*/
export function changeTab(sidebarEl, section, tabName) { export function changeTab(sidebarEl, section, tabName) {
// console.error('CHANGE TAB')
window.windows_stack = [] window.windows_stack = []
window.currentStack = {} window.currentStack = {}
// * The visualized content of the tab // * Only in section search
// ! Can be more than one per tab, happens in MainSearch and Favorites tab updateTabLink(section)
// ! because they have more tablinks (see below)
const tabContent = document.getElementsByClassName(`${section}_tabcontent`)
for (let i = 0; i < tabContent.length; i++) { // * Only when clicking the settings icon in the sidebar
tabContent[i].style.display = 'none' // resetSettings(tabName)
}
// * Tabs inside the actual tab (like albums, tracks, playlists...) // * Only in section search
const tabLinks = document.getElementsByClassName(`${section}_tablinks`) setSelectedTab(section, tabName)
for (let i = 0; i < tabLinks.length; i++) { // * Only if window.main_selected === 'search_tab'
tabLinks[i].classList.remove('active') checkNeedToLoadMoreContent()
} }
if (tabName === 'settings_tab' && window.main_selected !== 'settings_tab') {
EventBus.$emit('settingsTab:revertSettings')
EventBus.$emit('settingsTab:revertCredentials')
}
document.getElementById(tabName).style.display = 'block'
function setSelectedTab(section, tabName) {
if (section === 'main') { if (section === 'main') {
window.main_selected = tabName window.main_selected = tabName
} else if ('search' === section) { } else if (section === 'search') {
window.search_selected = tabName window.search_selected = tabName
} }
}
sidebarEl.classList.add('active') function checkNeedToLoadMoreContent() {
// * Check if you need to load more content in the search tab
// Check if you need to load more content in the search tab // * Happens when the user changes the tab in the main search
if ( if (
window.main_selected === 'search_tab' && window.main_selected === 'search_tab' &&
['track_search', 'album_search', 'artist_search', 'playlist_search'].indexOf(window.search_selected) !== -1 ['track_search', 'album_search', 'artist_search', 'playlist_search'].indexOf(window.search_selected) !== -1
@ -87,77 +44,50 @@ export function changeTab(sidebarEl, section, tabName) {
} }
} }
/** function resetSettings(tabName) {
* Shows the passed tab, keeping track of the one that the user is coming from. if (tabName === 'settings_tab' && window.main_selected !== 'settings_tab') {
* EventBus.$emit('settingsTab:revertSettings')
* Needs EventBus EventBus.$emit('settingsTab:revertCredentials')
*/ }
function showTab(type, id, back = false) { }
if (window.windows_stack.length === 0) {
window.windows_stack.push({ tab: window.main_selected }) function updateTabLink(section) {
} else if (!back) { // * Tabs inside the actual tab (like albums, tracks, playlists...)
if (window.currentStack.type === 'artist') { // * or sidebar links
EventBus.$emit('artistTab:updateSelected') 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) router.push({
} name,
params
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')
} }
/** /**
* Goes back to the previous tab according to the global window stack. * Goes back to the previous tab according to the global window stack.
*
* Needs EventBus and socket
*/ */
function backTab() { export function backTab() {
if (window.windows_stack.length == 1) { // ! Need to implement the memory of the opened artist tab
document.getElementById(`main_${window.main_selected}link`).click() router.back()
} 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 init() { export function init() {
// Open default tab // Open default tab
changeTab(document.getElementById('main_home_tablink'), 'main', 'home_tab') changeTab(document.getElementById('main_home_tablink'), 'main', 'home_tab')
_linkListeners()
} }

View File

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

View File

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

View File

@ -40,6 +40,12 @@ const es = {
} }
}, },
about: { 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: { titles: {
usefulLinks: 'Enlaces útiles', usefulLinks: 'Enlaces útiles',
bugReports: 'Reportar fallos', 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!', 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.', no360RA: 'La pista no está disponible en Reality Audio 360.',
notAvailable: '¡La pista no está disponible en los servidores de Deezer!', 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: { favorites: {
@ -166,7 +173,8 @@ const es = {
finishAddingArtist: 'Añadido {artist} álbumes a la cola', finishAddingArtist: 'Añadido {artist} álbumes a la cola',
startConvertingSpotifyPlaylist: 'Convertir las pistas de Spotify en pistas de Deezer', startConvertingSpotifyPlaylist: 'Convertir las pistas de Spotify en pistas de Deezer',
finishConvertingSpotifyPlaylist: 'Lista de reproducción de Spotify convertida', 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: { settings: {
title: 'Configuración', title: 'Configuración',
@ -178,7 +186,8 @@ const es = {
question: '¿Cómo consigo mi propio ARL?', question: '¿Cómo consigo mi propio ARL?',
update: 'Actualiza la ARL' update: 'Actualiza la ARL'
}, },
logout: 'Cerrar sesión' logout: 'Cerrar sesión',
login: 'Ingresa a través de deezer.com'
}, },
appearance: { appearance: {
title: 'Apariencia', title: 'Apariencia',
@ -225,7 +234,8 @@ const es = {
y: 'Sí, sobrescribir el archivo', y: 'Sí, sobrescribir el archivo',
n: 'No, no sobrescribir el archivo', n: 'No, no sobrescribir el archivo',
t: 'Sobrescribir sólo las etiquetas', 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', fallbackBitrate: 'La solución alternativa de bitrate',
fallbackSearch: 'Búsqueda de la segunda opción', fallbackSearch: 'Búsqueda de la segunda opción',
@ -253,7 +263,8 @@ const es = {
jpegImageQuality: 'Calidad de la imagen JPEG', jpegImageQuality: 'Calidad de la imagen JPEG',
embeddedArtworkPNG: 'Guardar las imágenes incrustadas como PNG', embeddedArtworkPNG: 'Guardar las imágenes incrustadas como PNG',
embeddedPNGWarning: 'Las PNG no están oficialmente soportadas por Deezer y puede encontrar errores.', 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: { tags: {
head: '¿Qué etiquetas guardar?', head: '¿Qué etiquetas guardar?',
@ -277,6 +288,7 @@ const es = {
replayGain: 'Ganancia de la reproducción', replayGain: 'Ganancia de la reproducción',
label: 'Etiqueta del álbum', label: 'Etiqueta del álbum',
lyrics: 'Letras no sincronizadas', lyrics: 'Letras no sincronizadas',
syncedLyrics: 'Letra sincronizada',
copyright: 'Derechos de autor', copyright: 'Derechos de autor',
composer: 'Compositor', composer: 'Compositor',
involvedPeople: 'Personas involucradas' involvedPeople: 'Personas involucradas'

View File

@ -32,14 +32,21 @@ const fr = {
playlist: 'playlist | playlists', playlist: 'playlist | playlists',
compile: 'compilation | compilations', compile: 'compilation | compilations',
ep: 'ep | eps', ep: 'ep | eps',
bundle: 'bundle | bundles',
more: "Plus d'albums", more: "Plus d'albums",
featured: 'En vedette dans', featured: 'Apparaît dans',
spotifyPlaylist: 'playlist spotify | playlists spotify', spotifyPlaylist: 'playlist spotify | playlists spotify',
releaseDate: 'date de sortie', releaseDate: 'date de sortie',
error: 'erreur' error: 'erreur'
} }
}, },
about: { 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: { titles: {
usefulLinks: 'Liens Utiles', usefulLinks: 'Liens Utiles',
bugReports: 'Rapports De Bug', bugReports: 'Rapports De Bug',
@ -61,7 +68,7 @@ const fr = {
officialSubreddit: 'Subreddit Officiel', officialSubreddit: 'Subreddit Officiel',
newsChannel: "Canal d'Informations", 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.`, 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é.", 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.", 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.", 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 !", 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.', no360RA: 'La piste est indisponible au format Reality Audio 360.',
notAvailable: 'La piste est indisponible sur les serveurs de Deezer !', 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: { favorites: {
@ -111,7 +119,7 @@ const fr = {
needTologin: 'Vous devez vous connecter à votre compte Deezer avant de pouvoir démarrer un téléchargement.', needTologin: 'Vous devez vous connecter à votre compte Deezer avant de pouvoir démarrer un téléchargement.',
openSettings: 'Ouvrir Les Paramètres', openSettings: 'Ouvrir Les Paramètres',
sections: { sections: {
popularPlaylists: 'Playlists populaires', popularPlaylists: 'Playlists les plus écoutées',
popularAlbums: 'Albums les plus écoutés' popularAlbums: 'Albums les plus écoutés'
} }
}, },
@ -136,8 +144,8 @@ const fr = {
} }
}, },
search: { search: {
startSearching: 'Démarrer la recherche !', 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 copier-coller un lien Deezer', 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', fans: '{n} fans',
noResults: 'Aucun résultat', noResults: 'Aucun résultat',
noResultsTrack: "Aucune piste n'a été trouvée", noResultsTrack: "Aucune piste n'a été trouvée",
@ -163,11 +171,12 @@ const fr = {
loggedOut: 'Déconnecté', loggedOut: 'Déconnecté',
cancellingCurrentItem: "Annulation de l'élément actuel.", cancellingCurrentItem: "Annulation de l'élément actuel.",
currentItemCancelled: 'Élément actuel annulé.', currentItemCancelled: 'Élément actuel annulé.',
startAddingArtist: "Ajout de {artist} albums à la file d'attente", startAddingArtist: "Ajout des albums de {artist} à la file d'attente",
finishAddingArtist: "{artist} albums ajoutés à 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', startConvertingSpotifyPlaylist: 'Conversion de pistes Spotify en équivalents Deezer',
finishConvertingSpotifyPlaylist: 'Playlist Spotify convertie', 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: { settings: {
title: 'Paramètres', title: 'Paramètres',
@ -179,7 +188,8 @@ const fr = {
question: 'Comment obtenir mon ARL personnel ?', question: 'Comment obtenir mon ARL personnel ?',
update: "Mettre à jour l'ARL" update: "Mettre à jour l'ARL"
}, },
logout: 'Déconnexion' logout: 'Déconnexion',
login: 'Connexion via deezer.com'
}, },
appearance: { appearance: {
title: 'Apparence', title: 'Apparence',
@ -210,7 +220,7 @@ const fr = {
title: 'Titres des pistes', 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)", 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', 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: { downloads: {
title: 'Téléchargements', title: 'Téléchargements',
@ -222,7 +232,7 @@ const fr = {
1: 'MP3 128kbps' 1: 'MP3 128kbps'
}, },
overwriteFile: { overwriteFile: {
title: 'Faut-il écraser les fichiers ?', title: 'Les fichiers doivent-ils être écrasés ?',
y: 'Oui, écraser le fichier', y: 'Oui, écraser le fichier',
n: 'Non, ne pas écraser le fichier', n: 'Non, ne pas écraser le fichier',
t: 'Écraser uniquement les métadonnées', t: 'Écraser uniquement les métadonnées',
@ -254,8 +264,9 @@ const fr = {
}, },
jpegImageQuality: "Qualité de l'image JPEG", jpegImageQuality: "Qualité de l'image JPEG",
embeddedArtworkPNG: "Enregistrer l'illustration incorporée aux fichiers audio en tant que PNG", 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', 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" 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: { tags: {
head: 'Métadonnées à sauvegarder', head: 'Métadonnées à sauvegarder',
@ -279,6 +290,7 @@ const fr = {
replayGain: 'Gain En Relecture (Replay Gain)', replayGain: 'Gain En Relecture (Replay Gain)',
label: "Label De l'Album", label: "Label De l'Album",
lyrics: 'Paroles Non-Synchronisées', lyrics: 'Paroles Non-Synchronisées',
syncedLyrics: 'Paroles Synchronisées',
copyright: "Droits d'Auteur (Copyright)", copyright: "Droits d'Auteur (Copyright)",
composer: 'Compositeur', composer: 'Compositeur',
involvedPeople: 'Personnes Impliquées' involvedPeople: 'Personnes Impliquées'
@ -306,7 +318,7 @@ const fr = {
day: 'JJ' day: 'JJ'
}, },
featuredToTitle: { featuredToTitle: {
title: 'Que faut-il faire avec les artistes participants (featuring) ?', title: 'Que faire des artistes participants (featuring) ?',
0: 'Ne rien faire', 0: 'Ne rien faire',
1: 'Les retirer du titre de la piste', 1: 'Les retirer du titre de la piste',
3: "Les supprimer du titre de la piste et du titre de l'album", 3: "Les supprimer du titre de la piste et du titre de l'album",
@ -331,7 +343,8 @@ const fr = {
title: 'Fonctionnalités Spotify', title: 'Fonctionnalités Spotify',
clientID: 'clientID Spotify', clientID: 'clientID Spotify',
clientSecret: 'Client Secret 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', reset: 'Rétablir les valeurs par défaut',
save: 'Sauvegarder', save: 'Sauvegarder',
@ -346,7 +359,7 @@ const fr = {
search: 'recherche', search: 'recherche',
charts: 'classements', charts: 'classements',
favorites: 'favoris', favorites: 'favoris',
linkAnalyzer: 'analyseur de liens', linkAnalyzer: 'analyseur de lien',
settings: 'paramètres', settings: 'paramètres',
about: 'à propos' about: 'à propos'
}, },

View File

@ -41,6 +41,12 @@ const it = {
} }
}, },
about: { about: {
updates: {
currentVersion: 'Versione corrente',
versionNotAvailable: 'N/A',
updateAvailable: `Non stai usando l'ultima versione disponibile: {version}`,
deemixVersion: 'Versione libreria deemix'
},
titles: { titles: {
usefulLinks: 'Link Utili', usefulLinks: 'Link Utili',
bugReports: 'Segnalazione di bug', bugReports: 'Segnalazione di bug',
@ -101,7 +107,8 @@ const it = {
wrongBitrateNoAlternative: 'Brano non trovato con il bitrate specificato e nessuna alternativa trovata!', wrongBitrateNoAlternative: 'Brano non trovato con il bitrate specificato e nessuna alternativa trovata!',
no360RA: 'Brano non disponibile in Reality Audio 360.', no360RA: 'Brano non disponibile in Reality Audio 360.',
notAvailable: 'Brano non presente sui server di Deezer!', 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: { favorites: {
@ -174,7 +181,8 @@ const it = {
finishAddingArtist: 'Aggiunto gli album di {artist} alla coda', finishAddingArtist: 'Aggiunto gli album di {artist} alla coda',
startConvertingSpotifyPlaylist: 'Convertendo i brani da spotify a deezer', startConvertingSpotifyPlaylist: 'Convertendo i brani da spotify a deezer',
finishConvertingSpotifyPlaylist: 'Playlist di spotify convertita', 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: { settings: {
title: 'Impostazioni', title: 'Impostazioni',
@ -186,7 +194,8 @@ const it = {
question: 'Come ottengo il mio ARL?', question: 'Come ottengo il mio ARL?',
update: 'Aggiorna ARL' update: 'Aggiorna ARL'
}, },
logout: 'Disconnettiti' logout: 'Disconnettiti',
login: 'Accedi tramite deezer.com'
}, },
appearance: { appearance: {
title: 'Aspetto', title: 'Aspetto',
@ -262,7 +271,8 @@ const it = {
jpegImageQuality: 'Qualità immagine JPEG', jpegImageQuality: 'Qualità immagine JPEG',
embeddedArtworkPNG: 'Salva copertina incorporata come PNG', embeddedArtworkPNG: 'Salva copertina incorporata come PNG',
embeddedPNGWarning: 'Le immagini PNG non sono usate ufficialmente da Deezer e potrebbero dare problemi', 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: { tags: {
head: 'Quali tag salvare', head: 'Quali tag salvare',
@ -286,6 +296,7 @@ const it = {
replayGain: 'Replay gain', replayGain: 'Replay gain',
label: 'Casa Discografica', label: 'Casa Discografica',
lyrics: 'Testo non Sincronizzato', lyrics: 'Testo non Sincronizzato',
syncedLyrics: 'Testo Sincronizzato',
copyright: 'Copyright', copyright: 'Copyright',
composer: 'Compositori', composer: 'Compositori',
involvedPeople: 'Persone Coinvolte' involvedPeople: 'Persone Coinvolte'
@ -338,7 +349,8 @@ const it = {
title: 'Spotify Features', title: 'Spotify Features',
clientID: 'Spotify clientID', clientID: 'Spotify clientID',
clientSecret: 'Spotify Client Secret', clientSecret: 'Spotify Client Secret',
username: 'Spotify username' username: 'Spotify username',
question: 'Come attivo le Spotify Features?'
}, },
reset: 'Reimposta Default', reset: 'Reimposta Default',
save: 'Salva', save: 'Salva',

View File

@ -32,6 +32,7 @@ const ru = {
playlist: 'плейлист | плейлисты | плейлисты', playlist: 'плейлист | плейлисты | плейлисты',
compile: 'сплит | сплиты | сплиты', compile: 'сплит | сплиты | сплиты',
ep: 'ep', ep: 'ep',
bundle: 'бандл | бандлы | бандлы',
more: 'Больше альбомов', more: 'Больше альбомов',
featured: 'Представлено в', featured: 'Представлено в',
spotifyPlaylist: 'плейлист spotify | плейлисты spotify | плейлисты spotify', spotifyPlaylist: 'плейлист spotify | плейлисты spotify | плейлисты spotify',
@ -40,6 +41,12 @@ const ru = {
} }
}, },
about: { about: {
updates: {
currentVersion: 'Текущая версия',
versionNotAvailable: 'Н/Д',
updateAvailable: `Вы используете не последнюю доступную версию: {version}`,
deemixVersion: 'Версия библиотеки deemix'
},
titles: { titles: {
usefulLinks: 'Полезные ссылки', usefulLinks: 'Полезные ссылки',
bugReports: 'Отчёты об ошибках', bugReports: 'Отчёты об ошибках',
@ -97,7 +104,8 @@ const ru = {
wrongBitrateNoAlternative: 'Данного трека нет в нужном битрейте. Альтернатив не найдено!', wrongBitrateNoAlternative: 'Данного трека нет в нужном битрейте. Альтернатив не найдено!',
no360RA: 'Трек недоступен в формате Reality Audio 360.', no360RA: 'Трек недоступен в формате Reality Audio 360.',
notAvailable: "Трек недоступен на серверах Deezer!", notAvailable: "Трек недоступен на серверах Deezer!",
notAvailableNoAlternative: "Трек недоступен на серверах Deezer. Альтернатив не найдено!" notAvailableNoAlternative: "Трек недоступен на серверах Deezer. Альтернатив не найдено!",
noSpaceLeft: "На устройстве не осталось свободного места!"
} }
}, },
favorites: { favorites: {
@ -179,7 +187,8 @@ const ru = {
question: 'Как узнать свой ARL?', question: 'Как узнать свой ARL?',
update: 'Обновить ARL' update: 'Обновить ARL'
}, },
logout: 'Выйти' logout: 'Выйти',
login: 'Войти через deezer.com'
}, },
appearance: { appearance: {
title: 'Внешний вид', title: 'Внешний вид',
@ -279,6 +288,7 @@ const ru = {
replayGain: 'Replay Gain', replayGain: 'Replay Gain',
label: 'Издатель', label: 'Издатель',
lyrics: 'Текст песни', lyrics: 'Текст песни',
syncedLyrics: 'Синхрон. текст песни',
copyright: 'Права (копирайт)', copyright: 'Права (копирайт)',
composer: 'Композитор', composer: 'Композитор',
involvedPeople: 'Вовлечённые люди' involvedPeople: 'Вовлечённые люди'
@ -331,7 +341,8 @@ const ru = {
title: 'Настройки Spotify', title: 'Настройки Spotify',
clientID: 'Spotify clientID', clientID: 'Spotify clientID',
clientSecret: 'Spotify Client Secret', clientSecret: 'Spotify Client Secret',
username: 'Spotify username' username: 'Spotify username',
question: 'Как включить функции Spotify?'
}, },
reset: 'По умолчанию', reset: 'По умолчанию',
save: 'Сохранить', 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 */ /* Center section */
$icon-dimension: 2rem; $icon-dimension: 2rem;
$searchbar-height: calc(2rem + 1em); $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 {
--container-width: 95%; --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 */ /* Modal Content */
.smallmodal-content { .smallmodal-content {
--modal-content-width: 95%; --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 // TODO Remove
.inline-flex { .inline-flex {
display: flex; display: flex;
@ -341,3 +334,11 @@ a {
.hide { .hide {
display: none !important; display: none !important;
} }
.changing-theme {
transition: all 200ms ease-in-out;
}
[v-cloak] {
display: none;
}

View File

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

View File

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

View File

@ -1,8 +1,9 @@
.search_tabcontent, // .search_tabcontent
.main_tabcontent, // .main_tabcontent,
.favorites_tabcontent { // .favorites_tabcontent
display: none; // {
} // display: none;
// }
.main_tabcontent { .main_tabcontent {
h1 { 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' import { socket } from '@/utils/socket'
function sendAddToQueue(url, bitrate = null) { export function sendAddToQueue(url, bitrate = null) {
if (!url) return if (!url) return
socket.emit('addToQueue', { url, bitrate }, () => {}) socket.emit('addToQueue', { url, bitrate }, () => {})

View File

@ -1,5 +1,11 @@
import store from '@/store'
export const socket = io.connect(window.location.href) export const socket = io.connect(window.location.href)
socket.on('connect', () => { socket.on('connect', () => {
document.getElementById('start_app_placeholder').classList.add('loading_placeholder--hidden') 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 Toastify from 'toastify-js'
import $ from 'jquery'
import { socket } from '@/utils/socket' import { socket } from '@/utils/socket'
@ -8,49 +7,83 @@ let toastsWithId = {}
export const toast = function(msg, icon = null, dismiss = true, id = null) { export const toast = function(msg, icon = null, dismiss = true, id = null) {
if (toastsWithId[id]) { if (toastsWithId[id]) {
let toastObj = toastsWithId[id] let toastObj = toastsWithId[id]
let toastDOM = $(`div.toastify[toast_id=${id}]`)
let toastElement = document.querySelectorAll(`div.toastify[toast_id=${id}]`)
if (msg) { 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) {
if (icon == 'loading') icon = `<div class="circle-loader"></div>` if (icon == 'loading') {
else icon = `<i class="material-icons">${icon}</i>` icon = `<div class="circle-loader"></div>`
toastDOM.find('.toast-icon').html(icon) } 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) { if (dismiss !== null && dismiss) {
toastDOM.addClass('dismissable') toastElement.forEach(toast => {
setTimeout(function() { toast.classList.add('dismissable')
})
setTimeout(() => {
toastObj.hideToast() toastObj.hideToast()
delete toastsWithId[id] delete toastsWithId[id]
}, 3000) }, 3000)
} }
} else { } else {
if (icon == null) icon = '' if (icon == null) {
else if (icon == 'loading') icon = `<div class="circle-loader"></div>` icon = ''
else icon = `<i class="material-icons">${icon}</i>` } else if (icon == 'loading') {
icon = `<div class="circle-loader"></div>`
} else {
icon = `<i class="material-icons">${icon}</i>`
}
let toastObj = Toastify({ let toastObj = Toastify({
text: `<span class="toast-icon">${icon}</span><span class="toast-message">${msg}</toast>`, text: `<span class="toast-icon">${icon}</span><span class="toast-message">${msg}</toast>`,
duration: dismiss ? 3000 : 0, duration: dismiss ? 3000 : 0,
gravity: 'bottom', gravity: 'bottom',
position: 'left', position: 'left',
className: dismiss ? 'dismissable' : '', className: dismiss ? 'dismissable' : '',
onClick: function(){ onClick: function() {
let dismissable = true let dismissable = true
if (id){
if (id) {
let toastClasses = document.querySelector(`div.toastify[toast_id=${id}]`).classList 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) { if (toastObj && dismissable) {
toastObj.hideToast() toastObj.hideToast()
if (id) delete toastsWithId[id]
if (id) {
delete toastsWithId[id]
}
} }
} }
}).showToast() }).showToast()
if (id) { if (id) {
toastsWithId[id] = toastObj 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() let lowerCaseText = text.toLowerCase()
if (lowerCaseText.startsWith('http')) { 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 return true
} }
} else if (lowerCaseText.startsWith('spotify:')) { } else if (lowerCaseText.startsWith('spotify:')) {
@ -91,263 +95,10 @@ export function copyToClipboard(text) {
ghostInput.remove() 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 { export default {
isValidURL, isValidURL,
convertDuration, convertDuration,
convertDurationSeparated, convertDurationSeparated,
numberWithDots, numberWithDots,
debounce, debounce
COUNTRIES
} }