diff --git a/.env b/.env index b30e8d2050..a65a393d6a 100644 --- a/.env +++ b/.env @@ -6,4 +6,6 @@ INVIDIOUS_URL=https://invidious.snopyta.org JAMENDO_CLIENT_ID=836523a7 LAST_FM_API_KEY=2b75dcb291e2b0c9a2c994aca522ac14 LAST_FM_API_SECRET=2ee49e35f08b837d43b2824198171fc8 -SOUNDCLOUD_API_KEY=22e8f71d7ca75e156d6b2f0e0a5172b3 \ No newline at end of file +SOUNDCLOUD_API_KEY=22e8f71d7ca75e156d6b2f0e0a5172b3 +NUCLEAR_SERVICES_URL=https://qjrujsisccsvyvjnqtnq.supabase.co +NUCLEAR_SERVICES_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFqcnVqc2lzY2Nzdnl2am5xdG5xIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTY1OTY0NjEsImV4cCI6MTk3MjE3MjQ2MX0.WQWcwBAQFNE259f2o8ruFln_UMLTFEn5aUD7KHrs9Aw \ No newline at end of file diff --git a/lerna.json b/lerna.json index d3be7af157..14986222ab 100644 --- a/lerna.json +++ b/lerna.json @@ -3,4 +3,4 @@ "packages/*" ], "version": "0.6.17" -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0ea6222454..dee2f6f5fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,9 +25,10 @@ "@storybook/cli": "^6.3.8", "@storybook/preset-typescript": "^3.0.0", "@storybook/react": "^6.3.8", - "@testing-library/dom": "^8.14.0", + "@supabase/supabase-js": "^1.35.4", + "@testing-library/dom": "^8.17.1", "@testing-library/jest-dom": "^5.16.4", - "@testing-library/react": "^11.2.5", + "@testing-library/react": "^11.2.7", "@testing-library/user-event": "^13.5.0", "@types/body-parser": "^1.17.1", "@types/concat-stream": "^1.6.0", @@ -44,7 +45,6 @@ "@types/node": "^12.12.17", "@types/node-fetch": "^2.5.4", "@types/numeral": "0.0.28", - "@types/react": "^16.9.43", "@types/react-beautiful-dnd": "^13.0.0", "@types/react-measure": "^2.0.6", "@types/react-redux": "^7.1.16", @@ -12127,6 +12127,59 @@ "node": ">=8" } }, + "node_modules/@supabase/functions-js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-1.3.4.tgz", + "integrity": "sha512-yYVgkECjv7IZEBKBI3EB5Q7R1p0FJ10g8Q9N7SWKIHUU6i6DnbEGHIMFLyQRm1hmiNWD8fL7bRVEYacmTRJhHw==", + "dependencies": { + "cross-fetch": "^3.1.5" + } + }, + "node_modules/@supabase/gotrue-js": { + "version": "1.22.22", + "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-1.22.22.tgz", + "integrity": "sha512-exbCopLo4tLawKZR25wEdipm+IRVujuqFRR4h2wiXd11YA+N8rfgg/4S4L6BTFHmzV+KMjy0kDF3yMbsjIR/xA==", + "dependencies": { + "cross-fetch": "^3.0.6" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "0.37.4", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-0.37.4.tgz", + "integrity": "sha512-x+c2rk1fz9s6f1PrGxCJ0QTUgXPDI0G3ngIqD5sSiXhhCyfl8Q5V92mXl2EYtlDhkiUkjFNrOZFhXVbXOHgvDw==", + "dependencies": { + "cross-fetch": "^3.1.5" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-1.7.4.tgz", + "integrity": "sha512-h/Jk3PCLkIVonsNavof/LvHbvF41UD+D+mWcA3m8yHzJ2TLbV3S4XDYId+A3AkvFOAork7Ns/9O8rK0uY4F4zw==", + "dependencies": { + "@types/phoenix": "^1.5.4", + "websocket": "^1.0.34" + } + }, + "node_modules/@supabase/storage-js": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-1.7.3.tgz", + "integrity": "sha512-jnIZWqOc9TGclOozgX9v/RWGFCgJAyW/yvmauexgRZhWknUXoA4b2i8tj7vfwE0WTvNRuA5JpXID98rfJeSG7Q==", + "dependencies": { + "cross-fetch": "^3.1.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "1.35.6", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-1.35.6.tgz", + "integrity": "sha512-KDRXRr+kdGwruIUizZPALbe5YccMYFVyZJf1sFWKLncaLYSFiM6iKFnqCrNeQ4JFoZZiICkiTl1FUuai62jVpg==", + "dependencies": { + "@supabase/functions-js": "^1.3.4", + "@supabase/gotrue-js": "^1.22.21", + "@supabase/postgrest-js": "^0.37.4", + "@supabase/realtime-js": "^1.7.4", + "@supabase/storage-js": "^1.7.2" + } + }, "node_modules/@szmarczak/http-timer": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", @@ -12882,6 +12935,11 @@ "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz", "integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==" }, + "node_modules/@types/phoenix": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.5.4.tgz", + "integrity": "sha512-L5eZmzw89eXBKkiqVBcJfU1QGx9y+wurRIEgt0cuLH0hwNtVUxtx+6cu0R2STwWj468sjXyBYPYDtGclUd1kjQ==" + }, "node_modules/@types/prettier": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.0.tgz", @@ -12916,9 +12974,9 @@ } }, "node_modules/@types/react": { - "version": "16.14.30", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.30.tgz", - "integrity": "sha512-tG+xGtDDSuIl1l63mN0LnaROAc99knkYyN4YTheE80iPzYvSy0U8LVie+OBZkrgjVrpkQV6bMCkSphPBnVNk6g==", + "version": "17.0.49", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.49.tgz", + "integrity": "sha512-CCBPMZaPhcKkYUTqFs/hOWqKjPxhTEmnZWjlHHgIMop67DsXywf9B5Os9Hz8KSacjNOgIdnZVJamwl232uxoPg==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -16179,6 +16237,18 @@ "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==" }, + "node_modules/bufferutil": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.6.tgz", + "integrity": "sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw==", + "hasInstallScript": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/builder-util": { "version": "21.2.0", "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-21.2.0.tgz", @@ -19327,6 +19397,14 @@ "yarn": ">=1" } }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dependencies": { + "node-fetch": "2.6.7" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -19630,6 +19708,15 @@ "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", "integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==" }, + "node_modules/d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dependencies": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, "node_modules/d3-dispatch": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", @@ -21453,6 +21540,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es5-ext": { + "version": "0.10.62", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", + "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "hasInstallScript": true, + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/es5-shim": { "version": "4.6.7", "resolved": "https://registry.npmjs.org/es5-shim/-/es5-shim-4.6.7.tgz", @@ -21466,6 +21567,16 @@ "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==" }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, "node_modules/es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -21486,6 +21597,15 @@ "resolved": "https://registry.npmjs.org/es6-shim/-/es6-shim-0.35.6.tgz", "integrity": "sha512-EmTr31wppcaIAgblChZiuN/l9Y7DPyw8Xtbg7fIVngn6zMW+IEBJDJngeKC3x6wr0V/vcA2wqeFnaw1bFJbDdA==" }, + "node_modules/es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "dependencies": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -22501,6 +22621,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ext": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", + "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==", + "dependencies": { + "type": "^2.5.0" + } + }, "node_modules/ext-list": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", @@ -22524,6 +22652,11 @@ "node": ">=4" } }, + "node_modules/ext/node_modules/type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -31809,6 +31942,11 @@ "resolved": "https://registry.npmjs.org/nested-object-assign/-/nested-object-assign-1.0.4.tgz", "integrity": "sha512-FlZ7oN9ICt+fbcJ4ag2IsALIcalfE/E16ttdSA8peBiHJI+oEKdOcafqDnUbeUe5NwWGn/m9zZGO9qrAGzfesg==" }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" + }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -40902,6 +41040,11 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, + "node_modules/type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -43044,6 +43187,22 @@ "node": ">=10.13.0" } }, + "node_modules/websocket": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz", + "integrity": "sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==", + "dependencies": { + "bufferutil": "^4.0.1", + "debug": "^2.2.0", + "es5-ext": "^0.10.50", + "typedarray-to-buffer": "^3.1.5", + "utf-8-validate": "^5.0.2", + "yaeti": "^0.0.6" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -43065,6 +43224,19 @@ "node": ">=0.8.0" } }, + "node_modules/websocket/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/websocket/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/well-known-symbols": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/well-known-symbols/-/well-known-symbols-2.0.0.tgz", @@ -43520,6 +43692,14 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" }, + "node_modules/yaeti": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", + "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", + "engines": { + "node": ">=0.10.32" + } + }, "node_modules/yaku": { "version": "0.16.7", "resolved": "https://registry.npmjs.org/yaku/-/yaku-0.16.7.tgz", @@ -52789,6 +52969,59 @@ } } }, + "@supabase/functions-js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-1.3.4.tgz", + "integrity": "sha512-yYVgkECjv7IZEBKBI3EB5Q7R1p0FJ10g8Q9N7SWKIHUU6i6DnbEGHIMFLyQRm1hmiNWD8fL7bRVEYacmTRJhHw==", + "requires": { + "cross-fetch": "^3.1.5" + } + }, + "@supabase/gotrue-js": { + "version": "1.22.22", + "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-1.22.22.tgz", + "integrity": "sha512-exbCopLo4tLawKZR25wEdipm+IRVujuqFRR4h2wiXd11YA+N8rfgg/4S4L6BTFHmzV+KMjy0kDF3yMbsjIR/xA==", + "requires": { + "cross-fetch": "^3.0.6" + } + }, + "@supabase/postgrest-js": { + "version": "0.37.4", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-0.37.4.tgz", + "integrity": "sha512-x+c2rk1fz9s6f1PrGxCJ0QTUgXPDI0G3ngIqD5sSiXhhCyfl8Q5V92mXl2EYtlDhkiUkjFNrOZFhXVbXOHgvDw==", + "requires": { + "cross-fetch": "^3.1.5" + } + }, + "@supabase/realtime-js": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-1.7.4.tgz", + "integrity": "sha512-h/Jk3PCLkIVonsNavof/LvHbvF41UD+D+mWcA3m8yHzJ2TLbV3S4XDYId+A3AkvFOAork7Ns/9O8rK0uY4F4zw==", + "requires": { + "@types/phoenix": "^1.5.4", + "websocket": "^1.0.34" + } + }, + "@supabase/storage-js": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-1.7.3.tgz", + "integrity": "sha512-jnIZWqOc9TGclOozgX9v/RWGFCgJAyW/yvmauexgRZhWknUXoA4b2i8tj7vfwE0WTvNRuA5JpXID98rfJeSG7Q==", + "requires": { + "cross-fetch": "^3.1.0" + } + }, + "@supabase/supabase-js": { + "version": "1.35.6", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-1.35.6.tgz", + "integrity": "sha512-KDRXRr+kdGwruIUizZPALbe5YccMYFVyZJf1sFWKLncaLYSFiM6iKFnqCrNeQ4JFoZZiICkiTl1FUuai62jVpg==", + "requires": { + "@supabase/functions-js": "^1.3.4", + "@supabase/gotrue-js": "^1.22.21", + "@supabase/postgrest-js": "^0.37.4", + "@supabase/realtime-js": "^1.7.4", + "@supabase/storage-js": "^1.7.2" + } + }, "@szmarczak/http-timer": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", @@ -53449,6 +53682,11 @@ "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz", "integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==" }, + "@types/phoenix": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.5.4.tgz", + "integrity": "sha512-L5eZmzw89eXBKkiqVBcJfU1QGx9y+wurRIEgt0cuLH0hwNtVUxtx+6cu0R2STwWj468sjXyBYPYDtGclUd1kjQ==" + }, "@types/prettier": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.0.tgz", @@ -53483,9 +53721,9 @@ } }, "@types/react": { - "version": "16.14.30", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.30.tgz", - "integrity": "sha512-tG+xGtDDSuIl1l63mN0LnaROAc99knkYyN4YTheE80iPzYvSy0U8LVie+OBZkrgjVrpkQV6bMCkSphPBnVNk6g==", + "version": "17.0.49", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.49.tgz", + "integrity": "sha512-CCBPMZaPhcKkYUTqFs/hOWqKjPxhTEmnZWjlHHgIMop67DsXywf9B5Os9Hz8KSacjNOgIdnZVJamwl232uxoPg==", "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -56119,6 +56357,14 @@ "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==" }, + "bufferutil": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.6.tgz", + "integrity": "sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw==", + "requires": { + "node-gyp-build": "^4.3.0" + } + }, "builder-util": { "version": "21.2.0", "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-21.2.0.tgz", @@ -58642,6 +58888,14 @@ "cross-spawn": "^7.0.1" } }, + "cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "requires": { + "node-fetch": "2.6.7" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -58857,6 +59111,15 @@ "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", "integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==" }, + "d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, "d3-dispatch": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", @@ -60301,6 +60564,16 @@ "is-symbol": "^1.0.2" } }, + "es5-ext": { + "version": "0.10.62", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", + "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "requires": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "next-tick": "^1.1.0" + } + }, "es5-shim": { "version": "4.6.7", "resolved": "https://registry.npmjs.org/es5-shim/-/es5-shim-4.6.7.tgz", @@ -60311,6 +60584,16 @@ "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==" }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, "es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -60331,6 +60614,15 @@ "resolved": "https://registry.npmjs.org/es6-shim/-/es6-shim-0.35.6.tgz", "integrity": "sha512-EmTr31wppcaIAgblChZiuN/l9Y7DPyw8Xtbg7fIVngn6zMW+IEBJDJngeKC3x6wr0V/vcA2wqeFnaw1bFJbDdA==" }, + "es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "requires": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -61103,6 +61395,21 @@ "ajv": "^6.6.2" } }, + "ext": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", + "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==", + "requires": { + "type": "^2.5.0" + }, + "dependencies": { + "type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" + } + } + }, "ext-list": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", @@ -68175,6 +68482,11 @@ "resolved": "https://registry.npmjs.org/nested-object-assign/-/nested-object-assign-1.0.4.tgz", "integrity": "sha512-FlZ7oN9ICt+fbcJ4ag2IsALIcalfE/E16ttdSA8peBiHJI+oEKdOcafqDnUbeUe5NwWGn/m9zZGO9qrAGzfesg==" }, + "next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -75269,6 +75581,11 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, + "type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -76803,6 +77120,34 @@ } } }, + "websocket": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz", + "integrity": "sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==", + "requires": { + "bufferutil": "^4.0.1", + "debug": "^2.2.0", + "es5-ext": "^0.10.50", + "typedarray-to-buffer": "^3.1.5", + "utf-8-validate": "^5.0.2", + "yaeti": "^0.0.6" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, "websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -77179,6 +77524,11 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" }, + "yaeti": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", + "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==" + }, "yaku": { "version": "0.16.7", "resolved": "https://registry.npmjs.org/yaku/-/yaku-0.16.7.tgz", diff --git a/packages/app/app/App.js b/packages/app/app/App.js index 9017f3571c..dffde58eb7 100644 --- a/packages/app/app/App.js +++ b/packages/app/app/App.js @@ -18,6 +18,7 @@ import * as ImportFavActions from './actions/importfavs'; import * as ConnectivityActions from './actions/connectivity'; import * as GithubContribActions from './actions/githubContrib'; import * as WindowActions from './actions/window'; +import * as NuclearConfigActions from './actions/nuclear/configuration'; import './app.global.scss'; import styles from './styles.scss'; @@ -60,6 +61,8 @@ class App extends React.PureComponent { this.props.actions.createPlugins(PluginConfig.plugins); this.props.actions.deserializePlugins(); this.props.actions.githubContribInfo(); + this.props.actions.fetchNuclearConfiguration(); + this.props.actions.fetchNuclearParams(); this.updateConnectivityStatus(navigator.onLine); window.addEventListener('online', () => this.updateConnectivityStatus(true)); @@ -187,7 +190,8 @@ function mapDispatchToProps(dispatch) { ConnectivityActions, SearchActions, GithubContribActions, - WindowActions + WindowActions, + NuclearConfigActions ), dispatch ) diff --git a/packages/app/app/actions/actionTypes.ts b/packages/app/app/actions/actionTypes.ts index 8fcd99e780..6cbf6be1c4 100644 --- a/packages/app/app/actions/actionTypes.ts +++ b/packages/app/app/actions/actionTypes.ts @@ -104,6 +104,18 @@ export enum Dashboard { LOAD_TOP_TRACKS_START = 'LOAD_TOP_TRACKS_START', LOAD_TOP_TRACKS_SUCCESS = 'LOAD_TOP_TRACKS_SUCCESS', LOAD_TOP_TRACKS_ERROR = 'LOAD_TOP_TRACKS_ERROR', + + LOAD_EDITORIAL_CHARTS_START = 'LOAD_EDITORIAL_CHARTS_START', + LOAD_EDITORIAL_CHARTS_SUCCESS = 'LOAD_EDITORIAL_CHARTS_SUCCESS', + LOAD_EDITORIAL_CHARTS_ERROR = 'LOAD_EDITORIAL_CHARTS_ERROR', + + LOAD_EDITORIAL_PLAYLIST_START = 'LOAD_EDITORIAL_PLAYLIST_START', + LOAD_EDITORIAL_PLAYLIST_SUCCESS = 'LOAD_EDITORIAL_PLAYLIST_SUCCESS', + LOAD_EDITORIAL_PLAYLIST_ERROR = 'LOAD_EDITORIAL_PLAYLIST_ERROR', + + LOAD_PROMOTED_ARTISTS_START = 'LOAD_PROMOTED_ARTISTS_START', + LOAD_PROMOTED_ARTISTS_SUCCESS = 'LOAD_PROMOTED_ARTISTS_SUCCESS', + LOAD_PROMOTED_ARTISTS_ERROR = 'LOAD_PROMOTED_ARTISTS_ERROR', } export enum NuclearIdentity { @@ -118,6 +130,16 @@ export enum NuclearIdentity { SIGN_OUT = 'SIGN_OUT' } +export enum NuclearConfiguration { + FETCH_NUCLEAR_CONFIG_START = 'FETCH_NUCLEAR_CONFIG_START', + FETCH_NUCLEAR_CONFIG_SUCCESS = 'FETCH_NUCLEAR_CONFIG_SUCCESS', + FETCH_NUCLEAR_CONFIG_ERROR = 'FETCH_NUCLEAR_CONFIG_ERROR', + + FETCH_NUCLEAR_PARAMS_START = 'FETCH_NUCLEAR_PARAMS_START', + FETCH_NUCLEAR_PARAMS_SUCCESS = 'FETCH_NUCLEAR_PARAMS_SUCCESS', + FETCH_NUCLEAR_PARAMS_ERROR = 'FETCH_NUCLEAR_PARAMS_ERROR', +} + export enum Equalizer { CHANGE_VALUE = 'CHANGE_VALUE', SELECT_PRESET = 'SELECT_PRESET', diff --git a/packages/app/app/actions/dashboard.ts b/packages/app/app/actions/dashboard.ts index ac0540ff02..3cd6d5b7aa 100644 --- a/packages/app/app/actions/dashboard.ts +++ b/packages/app/app/actions/dashboard.ts @@ -1,12 +1,14 @@ import logger from 'electron-timber'; -import { rest } from '@nuclear/core'; import { getBestNewAlbums, getBestNewTracks } from 'pitchfork-bnm'; +import { createAsyncAction } from 'typesafe-actions'; + +import { rest } from '@nuclear/core'; +import { LastfmTopTag } from '@nuclear/core/src/rest/Lastfm.types'; +import { DeezerEditorialCharts, mapDeezerTrackToInternal } from '@nuclear/core/src/rest/Deezer'; import globals from '../globals'; import { Dashboard } from './actionTypes'; -import { createAsyncAction } from 'typesafe-actions'; -import { LastfmTopTag } from '@nuclear/core/src/rest/Lastfm.types'; -import { DeezerTopTrack } from '@nuclear/core/src/rest/Deezer'; +import { PromotedArtist } from '@nuclear/core/src/rest/Nuclear/Promotion'; const lastfm = new rest.LastFmApi( globals.lastfmApiKey, @@ -98,16 +100,76 @@ export const loadTopTracksAction = createAsyncAction( Dashboard.LOAD_TOP_TRACKS_START, Dashboard.LOAD_TOP_TRACKS_SUCCESS, Dashboard.LOAD_TOP_TRACKS_ERROR -)(); +)[], undefined>(); export const loadTopTracks = () => async (dispatch) => { dispatch(loadTopTracksAction.request()); try { const tracks = await rest.Deezer.getTopTracks(); + dispatch(loadTopTracksAction.success(tracks.data.map(rest.Deezer.mapDeezerTrackToInternal))); } catch (error) { dispatch(loadTopTracksAction.failure()); logger.error(error); } }; + +export const loadEditorialChartsAction = createAsyncAction( + Dashboard.LOAD_EDITORIAL_CHARTS_START, + Dashboard.LOAD_EDITORIAL_CHARTS_SUCCESS, + Dashboard.LOAD_EDITORIAL_CHARTS_ERROR +)(); + +export const loadEditorialCharts = () => async (dispatch) => { + dispatch(loadEditorialChartsAction.request()); + + try { + const charts = await rest.Deezer.getEditorialCharts(); + dispatch(loadEditorialChartsAction.success(charts)); + } catch (error) { + dispatch(loadEditorialChartsAction.failure(error.message)); + logger.error(error); + } +}; + +export const loadEditorialPlaylistAction = createAsyncAction( + Dashboard.LOAD_EDITORIAL_PLAYLIST_START, + Dashboard.LOAD_EDITORIAL_PLAYLIST_SUCCESS, + Dashboard.LOAD_EDITORIAL_PLAYLIST_ERROR +)<{id: number}, {id: number; tracklist: ReturnType[]}, { id: number; error: string; }>(); + +export const loadEditorialPlaylist = (id: number) => async (dispatch) => { + dispatch(loadEditorialPlaylistAction.request({id})); + try { + const tracklist = await rest.Deezer.getPlaylistTracks(id); + dispatch(loadEditorialPlaylistAction.success({ + id, + tracklist: tracklist.data.map(mapDeezerTrackToInternal) + })); + } catch (error) { + dispatch(loadEditorialPlaylistAction.failure({ id, error: error.message })); + logger.error(error); + } +}; + +export const loadPromotedArtistsAction = createAsyncAction( + Dashboard.LOAD_PROMOTED_ARTISTS_START, + Dashboard.LOAD_PROMOTED_ARTISTS_SUCCESS, + Dashboard.LOAD_PROMOTED_ARTISTS_ERROR +)(); + +export const loadPromotedArtists = () => async (dispatch) => { + dispatch(loadPromotedArtistsAction.request()); + try { + const service = new rest.NuclearPromotionService( + process.env.NUCLEAR_SERVICES_URL, + process.env.NUCLEAR_SERVICES_ANON_KEY + ); + const artists = await service.getPromotedArtists(); + dispatch(loadPromotedArtistsAction.success(artists?.data)); + } catch (error) { + dispatch(loadPromotedArtistsAction.failure(error.message)); + logger.error(error); + } +}; diff --git a/packages/app/app/actions/nuclear/configuration.ts b/packages/app/app/actions/nuclear/configuration.ts new file mode 100644 index 0000000000..978843fe04 --- /dev/null +++ b/packages/app/app/actions/nuclear/configuration.ts @@ -0,0 +1,49 @@ +import logger from 'electron-timber'; +import { createAsyncAction } from 'typesafe-actions'; + +import { rest } from '@nuclear/core'; +import { Configuration, Parameters } from '@nuclear/core/src/rest/Nuclear/Configuration'; + +import { NuclearConfiguration } from '../actionTypes'; + +export const fetchNuclearConfigurationAction = createAsyncAction( + NuclearConfiguration.FETCH_NUCLEAR_CONFIG_START, + NuclearConfiguration.FETCH_NUCLEAR_CONFIG_SUCCESS, + NuclearConfiguration.FETCH_NUCLEAR_CONFIG_ERROR +)(); + +export const fetchNuclearParamsAction = createAsyncAction( + NuclearConfiguration.FETCH_NUCLEAR_PARAMS_START, + NuclearConfiguration.FETCH_NUCLEAR_PARAMS_SUCCESS, + NuclearConfiguration.FETCH_NUCLEAR_PARAMS_ERROR +)(); + +export const fetchNuclearConfiguration = () => async (dispatch) => { + dispatch(fetchNuclearConfigurationAction.request()); + try { + const service = new rest.NuclearConfigurationService( + process.env.NUCLEAR_SERVICES_URL, + process.env.NUCLEAR_SERVICES_ANON_KEY + ); + const config = await service.getConfiguration(); + dispatch(fetchNuclearConfigurationAction.success(config)); + } catch (error) { + dispatch(fetchNuclearConfigurationAction.failure(error.message)); + logger.error(error); + } +}; + +export const fetchNuclearParams = () => async (dispatch) => { + dispatch(fetchNuclearParamsAction.request()); + try { + const service = new rest.NuclearConfigurationService( + process.env.NUCLEAR_SERVICES_URL, + process.env.NUCLEAR_SERVICES_ANON_KEY + ); + const params = await service.getParams(); + dispatch(fetchNuclearParamsAction.success(params)); + } catch (error) { + dispatch(fetchNuclearParamsAction.failure(error.message)); + logger.error(error); + } +}; diff --git a/packages/app/app/actions/search.ts b/packages/app/app/actions/search.ts index 7c7485606a..982e1325a4 100644 --- a/packages/app/app/actions/search.ts +++ b/packages/app/app/actions/search.ts @@ -300,11 +300,11 @@ export const artistInfoSearchByName = (artistName: string, history: History) => } }; -export const albumInfoSearchByName = (albumName: string, history: History) => async (dispatch, getState: () => RootState) => { +export const albumInfoSearchByName = (albumName: string, artistName: string, history: History) => async (dispatch, getState: () => RootState) => { const selectedProvider = getSelectedMetaProvider(getState); const { settings } = getState(); try { - const albumDetails = await selectedProvider.fetchAlbumDetailsByName(albumName); + const albumDetails = await selectedProvider.fetchAlbumDetailsByName(albumName, 'master', artistName); dispatch(SearchActions.albumInfoSuccess(albumDetails.id, albumDetails)); _.invoke(history, 'push', `/album/${albumDetails.id}`); } catch (e) { diff --git a/packages/app/app/app.global.scss b/packages/app/app/app.global.scss index f9b36a9508..cd1dc0259a 100644 --- a/packages/app/app/app.global.scss +++ b/packages/app/app/app.global.scss @@ -1,4 +1,4 @@ -@import './vars.scss'; +@import "./vars.scss"; html { position: absolute; @@ -159,7 +159,6 @@ table { background-color: darken($blue, 10%); } - &:active { background-color: darken($blue, 20%); } @@ -278,3 +277,7 @@ hr { color: $white !important; } } + +i.red.icon { + color: $red !important; +} diff --git a/packages/app/app/components/AlbumView/styles.scss b/packages/app/app/components/AlbumView/styles.scss index c465160a40..4f02cf9533 100644 --- a/packages/app/app/components/AlbumView/styles.scss +++ b/packages/app/app/components/AlbumView/styles.scss @@ -5,7 +5,7 @@ .album_view_container { width: 100%; height: 100%; - margin: 1rem 0 0 0; + padding: 1rem 0 0 0; .album_buttons { display: flex; diff --git a/packages/app/app/components/ArtistView/index.tsx b/packages/app/app/components/ArtistView/index.tsx index a7ed2cc62e..20b28a535d 100644 --- a/packages/app/app/components/ArtistView/index.tsx +++ b/packages/app/app/components/ArtistView/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import cx from 'classnames'; -import _ from 'lodash'; +import _, { isEmpty } from 'lodash'; import { Dimmer, Loader, Icon } from 'semantic-ui-react'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router'; @@ -157,17 +157,21 @@ const ArtistView: React.FC = ({ )} -
- {renderPopularTracks()} - - {renderSimilarArtists()} -
-
+ { + (!isEmpty(artist?.topTracks) || !isEmpty(artist?.similar) || isLoading()) && + <> +
+ {renderPopularTracks()} + {renderSimilarArtists()} +
+
+ + } { return b.year - a.year; diff --git a/packages/app/app/components/Dashboard/BestNewMusicTab/index.js b/packages/app/app/components/Dashboard/BestNewMusicTab/index.js index dc51760fb1..a5e8b5e7df 100644 --- a/packages/app/app/components/Dashboard/BestNewMusicTab/index.js +++ b/packages/app/app/components/Dashboard/BestNewMusicTab/index.js @@ -15,30 +15,26 @@ class BestNewMusicTab extends React.Component { }; } - UNSAFE_componentWillReceiveProps(nextProps) { + componentDidMount() { if (this.state.activeItem === null) { - const firstAlbum = _.head( - _.get(nextProps, 'dashboardData.bestNewAlbums') - ); + const firstAlbum = _.head(this.props.dashboardData.bestNewAlbums); if (firstAlbum) { - this.setState({ - activeItem: firstAlbum - }); + this.setActiveItem(firstAlbum); } } } - isLoading () { - return (this.props.dashboardData.bestNewAlbums && this.props.dashboardData.bestNewTracks) ? this.props.dashboardData.bestNewAlbums.length < 1 || this.props.dashboardData.bestNewTracks.length < 1 : true; + isLoading() { + return (this.props.dashboardData.bestNewAlbums && this.props.dashboardData.bestNewTracks) ? this.props.dashboardData.bestNewAlbums.length < 1 || this.props.dashboardData.bestNewTracks.length < 1 : true; } setActiveItem(activeItem) { this.setState({ activeItem }); - document.getElementsByClassName('best_new_music_content')[0]?.scrollTo(0, 0); } - render () { + + render() { const { dashboardData, artistInfoSearchByName, @@ -74,7 +70,7 @@ class BestNewMusicTab extends React.Component { history={history} /> - ); + ); } } diff --git a/packages/app/app/components/Dashboard/ChartsTab/index.tsx b/packages/app/app/components/Dashboard/ChartsTab/index.tsx index f6d76e8557..d6f753393a 100644 --- a/packages/app/app/components/Dashboard/ChartsTab/index.tsx +++ b/packages/app/app/components/Dashboard/ChartsTab/index.tsx @@ -1,18 +1,18 @@ import React from 'react'; import { Tab } from 'semantic-ui-react'; -import { DeezerTopTrack } from '@nuclear/core/src/rest/Deezer'; import { Track } from '@nuclear/ui/lib/types'; import TrackTableContainer from '../../../containers/TrackTableContainer'; import styles from './styles.scss'; import { useTranslation } from 'react-i18next'; +import { InternalTopTrack } from '../../../reducers/dashboard'; type ChartsTabProps = { - topTracks: DeezerTopTrack[]; + topTracks: InternalTopTrack[]; } -const mapDeezerTopTrackToTrack = (topTrack: DeezerTopTrack): Track => ({ - artist: topTrack.artist?.name, +const mapDeezerTopTrackToTrack = (topTrack: InternalTopTrack): Track => ({ + artist: topTrack.artist, title: topTrack.title, album: topTrack.album?.title, duration: topTrack.duration, @@ -31,7 +31,7 @@ const ChartsTab: React.FC = ({ className={styles.popular_tracks_container} >
-

{t('popular-track-title')}

+

{t('popular-track-title')}

void; + albumInfoSearchByName: (albumName: string, artistName: string) => void; + onEditorialPlaylistClick: (playlistId: number) => void; +} + +const EditorialsTab: React.FC = ({ + isLoading, + playlists, + artists, + albums, + isPromotedArtistsEnabled, + artistInfoSearchByName, + albumInfoSearchByName, + onEditorialPlaylistClick +}) => { + const { t } = useTranslation('dashboard'); + + return + { + !isLoading && + <> +
+ ({ + id: playlist.id.toString(), + header: playlist.title, + image: playlist.picture_medium, + onClick: () => onEditorialPlaylistClick(playlist.id) + })) + } + header={t('trending-playlists')} + filterPlaceholder={t('filter')} + nothingFoundLabel={t('nothing-found')} + /> +
+ { + isPromotedArtistsEnabled && + + } +
+ ({ + id: artist.id.toString(), + header: artist.name, + image: artist.picture_medium, + onClick: () => artistInfoSearchByName(artist.name) + })) + } + header={t('trending-artists')} + filterPlaceholder={t('filter')} + nothingFoundLabel={t('nothing-found')} + /> +
+
+ ({ + id: album.id.toString(), + header: album.title, + image: album.cover_medium, + onClick: () => albumInfoSearchByName(album.title, album.artist.name) + })) + } + header={t('trending-albums')} + filterPlaceholder={t('filter')} + nothingFoundLabel={t('nothing-found')} + /> +
+ + } +
; +}; + +export default EditorialsTab; diff --git a/packages/app/app/components/Dashboard/EditorialsTab/styles.scss b/packages/app/app/components/Dashboard/EditorialsTab/styles.scss new file mode 100644 index 0000000000..690db3a94f --- /dev/null +++ b/packages/app/app/components/Dashboard/EditorialsTab/styles.scss @@ -0,0 +1,7 @@ +.editorials_tab { + .row { + display: flex; + flex-flow: row; + align-items: center; + } +} diff --git a/packages/app/app/components/Dashboard/index.js b/packages/app/app/components/Dashboard/index.js deleted file mode 100644 index 8877c14bda..0000000000 --- a/packages/app/app/components/Dashboard/index.js +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; -import { Tab } from 'semantic-ui-react'; -import { withTranslation } from 'react-i18next'; -import { Redirect } from 'react-router-dom'; - -import BestNewMusicTab from './BestNewMusicTab'; -import ChartsTab from './ChartsTab'; -import GenresTab from './GenresTab'; - -import styles from './styles.scss'; - -@withTranslation('dashboard') -class Dashboard extends React.Component { - - panes() { - const { - actions, - dashboardData, - history, - streamProviders, - t - } = this.props; - - const { - artistInfoSearchByName, - albumInfoSearchByName, - addToQueue, - selectSong, - clearQueue, - startPlayback - } = actions; - - return [ - { - menuItem: t('best'), - render: () => ( - - ) - }, - { - menuItem: t('top'), - render: () => ( - - ) - }, - { - menuItem: t('genres'), - render: () => ( - - ) - } - ]; - } - - componentDidMount() { - if (this.props.isConnected) { - this.props.actions.loadBestNewTracks(); - this.props.actions.loadBestNewAlbums(); - this.props.actions.loadTopTags(); - this.props.actions.loadTopTracks(); - } - } - - render() { - const { isConnected } = this.props; - - return ( -
- {isConnected && ( - - )} - {!isConnected && } -
- ); - } -} - -export default Dashboard; diff --git a/packages/app/app/components/Dashboard/index.tsx b/packages/app/app/components/Dashboard/index.tsx new file mode 100644 index 0000000000..a607b22991 --- /dev/null +++ b/packages/app/app/components/Dashboard/index.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { Tab } from 'semantic-ui-react'; +import { useTranslation } from 'react-i18next'; +import { Redirect, useHistory } from 'react-router-dom'; + +import BestNewMusicTab from './BestNewMusicTab'; +import ChartsTab from './ChartsTab'; +import GenresTab from './GenresTab'; + +import styles from './styles.scss'; +import { DashboardReducerState } from '../../reducers/dashboard'; +import StreamProviderPlugin from '@nuclear/core/src/plugins/streamProvider'; +import { QueueItem } from '../../reducers/queue'; +import EditorialsTab from './EditorialsTab'; + +type DashboardProps = { + dashboardData: DashboardReducerState; + streamProviders: StreamProviderPlugin[]; + isConnected: boolean; + isPromotedArtistsEnabled: boolean; + + artistInfoSearchByName: (artistName: string) => void; + albumInfoSearchByName: (albumName: string, artistName: string) => void; + addToQueue: (item: QueueItem) => void; + selectSong: (song: number) => void; + clearQueue: () => void; + startPlayback: () => void; + onEditorialPlaylistClick: (playlistId: number) => void; +}; + +export const Dashboard: React.FC = ({ + dashboardData, + streamProviders, + isConnected, + isPromotedArtistsEnabled, + + artistInfoSearchByName, + albumInfoSearchByName, + addToQueue, + selectSong, + clearQueue, + startPlayback, + onEditorialPlaylistClick +}) => { + const { t } = useTranslation('dashboard'); + const history = useHistory(); + + const {editorialCharts} = dashboardData; + + return ( +
+ {isConnected && ( + ( + + ) + }, + { + menuItem: t('best'), + render: () => ( + + ) + }, + { + menuItem: t('top'), + render: () => + }, + { + menuItem: t('genres'), + render: () => ( + + ) + } + ]} + className={styles.dashboard_tabs} + /> + )} + {!isConnected && } +
+ ); +}; + +export default Dashboard; diff --git a/packages/app/app/components/PlaylistView/index.tsx b/packages/app/app/components/PlaylistView/index.tsx index 35896f7b19..56750c70a6 100644 --- a/packages/app/app/components/PlaylistView/index.tsx +++ b/packages/app/app/components/PlaylistView/index.tsx @@ -25,6 +25,7 @@ export type PlaylistViewProps = { selectSong: (i: number) => void; addTracks: (tracks: Playlist['tracks']) => void; onReorderTracks: (isource: number, idest: number) => void; + isEditable?: boolean; } const PlaylistView: React.FC = ({ @@ -36,7 +37,8 @@ const PlaylistView: React.FC = ({ addTracks, onReorderTracks, selectSong, - startPlayback + startPlayback, + isEditable = true }) => { const { t, i18n } = useTranslation('playlists'); const history = useHistory(); @@ -59,13 +61,15 @@ const PlaylistView: React.FC = ({ startPlayback(false); }, [addTracks, clearQueue, playlist, selectSong, startPlayback]); - const onDeleteTrack = useCallback((trackToRemove: Track, trackIndex: number) => { - const newPlaylist = { - ...playlist, - tracks: playlist.tracks.filter((_, index) => index !== trackIndex) - }; - updatePlaylist(newPlaylist); - }, [playlist, updatePlaylist]); + const onDeleteTrack = isEditable + ? useCallback((trackToRemove: Track, trackIndex: number) => { + const newPlaylist = { + ...playlist, + tracks: playlist.tracks.filter((_, index) => index !== trackIndex) + }; + updatePlaylist(newPlaylist); + }, [playlist, updatePlaylist]) + : undefined; const onDeletePlaylist = useCallback(() => { deletePlaylist(playlist.id); @@ -77,7 +81,10 @@ const PlaylistView: React.FC = ({ }, [exportPlaylist, playlist, t]); return ( -
+
@@ -98,6 +105,7 @@ const PlaylistView: React.FC = ({ initialString={playlist.name} onAccept={onRenamePlaylist} trigger={ + isEditable &&
diff --git a/packages/app/app/containers/AlbumViewContainer/AlbumViewContainer.test.tsx b/packages/app/app/containers/AlbumViewContainer/AlbumViewContainer.test.tsx index 8be75a3bc0..a3ec056def 100644 --- a/packages/app/app/containers/AlbumViewContainer/AlbumViewContainer.test.tsx +++ b/packages/app/app/containers/AlbumViewContainer/AlbumViewContainer.test.tsx @@ -195,23 +195,26 @@ describe('Album view container', () => { expect(state?.queue?.queueItems[1].stream).toBeUndefined(); expect(state?.queue?.queueItems[2].stream).toBeUndefined(); - waitFor(() => expect(state?.queue?.queueItems).toEqual([ - expect.objectContaining({ - artist: 'test artist', - name: 'test track 1', - stream: { - 'data': 'test-stream-data' - } - }), - expect.objectContaining({ - artist: 'test artist', - name: 'test track 2' - }), - expect.objectContaining({ - artist: 'test artist', - name: 'test track 3' - }) - ])); + await waitFor(() => { + const currentState = store.getState(); + return expect(currentState?.queue?.queueItems).toEqual([ + expect.objectContaining({ + artist: 'test artist', + name: 'test track 1', + stream: { + 'data': 'test-stream-data' + } + }), + expect.objectContaining({ + artist: 'test artist', + name: 'test track 2' + }), + expect.objectContaining({ + artist: 'test artist', + name: 'test track 3' + }) + ]); + }); }); it('should add album to downloads after clicking the button', async () => { diff --git a/packages/app/app/containers/DashboardContainer/DashboardContainer.test.tsx b/packages/app/app/containers/DashboardContainer/DashboardContainer.test.tsx index 3f8801d24b..87d1e0c654 100644 --- a/packages/app/app/containers/DashboardContainer/DashboardContainer.test.tsx +++ b/packages/app/app/containers/DashboardContainer/DashboardContainer.test.tsx @@ -24,12 +24,7 @@ describe('Dashboard container', () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - Deezer.getTopTracks = jest.fn().mockResolvedValue({ data: mockState.dashboard.topTracks.map(track => ({ - ...track, - artist: { - ...track.artist - } - })) }); + Deezer.getTopTracks = jest.fn().mockResolvedValue({ data: mockState.dashboard.topTracks }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -40,24 +35,27 @@ describe('Dashboard container', () => { jest.clearAllMocks(); }); - it('should display the best new music page of the dashboard', () => { + it('should display the best new music page of the dashboard', async () => { const { component } = mountComponent(); + await waitFor(() => component.getByText(/Best new music/i).click()); expect(component.asFragment()).toMatchSnapshot(); }); - it('should go to best new album review after clicking it', () => { + it('should go to best new album review after clicking it', async () => { const { component } = mountComponent(); + await waitFor(() => component.getByText(/Best new music/i).click()); - waitFor(() => component.getByText(/test title 2/i).click()); + await waitFor(() => component.getByText(/test title 2/i).click()); expect(component.queryByText(/test review 2/i)).not.toBeNull(); expect(component.asFragment()).toMatchSnapshot(); }); - it('should go to best new track review after clicking it', () => { + it('should go to best new track review after clicking it', async () => { const { component } = mountComponent(); + await waitFor(() => component.getByText(/Best new music/i).click()); - waitFor(() => component.getByText(/test track title 2/i).click()); + await waitFor(() => component.getByText(/test track title 2/i).click()); expect(component.queryByText(/track review 2/i)).not.toBeNull(); expect(component.asFragment()).toMatchSnapshot(); @@ -65,6 +63,7 @@ describe('Dashboard container', () => { it('should add/remove a best new track to favorites after clicking its star', async () => { const { component, store } = mountComponent(); + await waitFor(() => component.getByText(/Best new music/i).click()); const addOrRemove = async () => waitFor( () => component @@ -88,6 +87,29 @@ describe('Dashboard container', () => { expect(state.favorites.tracks).toEqual([]); }); + it('should display the trending music', async () => { + const { component } = mountComponent(); + expect(component.asFragment()).toMatchSnapshot(); + }); + + it('should search for the promoted artist using his meta provider', async () => { + const { component, store, history } = mountComponent(); + await waitFor(() => component.getByText(/Check out/i).click()); + + const state = store.getState(); + expect(state.plugin.selected.metaProviders).toEqual('Bandcamp Meta Provider'); + expect(state.search.unifiedSearchStarted).toBe(true); + }); + + it('should open the promoted artist link in an external browser', async () => { + const { component } = mountComponent(); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const shell = require('electron').shell; + await waitFor(() => component.getByText(/External link/i).click()); + + expect(shell.openExternal).toHaveBeenCalledWith('https://promoted-artist-1.example'); + }); + it('should display top tracks after going to top tracks tab', async () => { const { component } = mountComponent(); @@ -150,6 +172,9 @@ describe('Dashboard container', () => { .withDashboard() .withPlugins() .withConnectivity() + .withSettings({ + promotedArtists: true + }) .build() ); }); diff --git a/packages/app/app/containers/DashboardContainer/__snapshots__/DashboardContainer.test.tsx.snap b/packages/app/app/containers/DashboardContainer/__snapshots__/DashboardContainer.test.tsx.snap index cafc60f781..7eb718567d 100644 --- a/packages/app/app/containers/DashboardContainer/__snapshots__/DashboardContainer.test.tsx.snap +++ b/packages/app/app/containers/DashboardContainer/__snapshots__/DashboardContainer.test.tsx.snap @@ -14,6 +14,11 @@ exports[`Dashboard container should display genres after going to genres tab 1`]
+
+
+
+ test title 1 +
+
+
+ test artist 1 +
+
+ test title 1 +
+
+
+
+ 5.0 +
+
+
+

+ test review 1 +

+
+
+
+ + + +`; + +exports[`Dashboard container should display the trending music 1`] = ` + +
+
+
+ +
+
+
+
+

+ Trending playlists +

+
+
+
+ +
+