From ede72d62b0894a38fbf981f46c35feb686619633 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 8 Jan 2018 22:47:14 +0100 Subject: [PATCH 01/32] Initial setup for phtantomjs --- dev.yml | 4 +--- front/Dockerfile | 16 +++++++--------- front/docker/Dockerfile.dev | 13 ------------- front/test/unit/karma.conf.js | 2 ++ 4 files changed, 10 insertions(+), 25 deletions(-) delete mode 100644 front/docker/Dockerfile.dev diff --git a/dev.yml b/dev.yml index befc4b243..971e38b62 100644 --- a/dev.yml +++ b/dev.yml @@ -3,9 +3,7 @@ version: '2' services: front: - build: - dockerfile: docker/Dockerfile.dev - context: ./front + build: front env_file: .env.dev environment: - "HOST=0.0.0.0" diff --git a/front/Dockerfile b/front/Dockerfile index ad05f72eb..b22359b46 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -1,13 +1,11 @@ -FROM node:6-alpine +FROM node:6 EXPOSE 8080 - -RUN mkdir /app -WORKDIR /app +WORKDIR /app/ ADD package.json . +RUN npm install --only=production +RUN npm install --only=dev +VOLUME ["/app/node_modules"] +COPY . . -RUN npm install - -ADD . . - -RUN npm run build +CMD ["npm", "run", "dev"] diff --git a/front/docker/Dockerfile.dev b/front/docker/Dockerfile.dev deleted file mode 100644 index 1a0c90c9e..000000000 --- a/front/docker/Dockerfile.dev +++ /dev/null @@ -1,13 +0,0 @@ -FROM node:6-alpine - -EXPOSE 8080 - -RUN mkdir /app -WORKDIR /app -ADD package.json . - -RUN npm install - -VOLUME ["/app/node_modules"] - -CMD ["npm", "run", "dev"] diff --git a/front/test/unit/karma.conf.js b/front/test/unit/karma.conf.js index 8e4951c9e..3fcc5f02a 100644 --- a/front/test/unit/karma.conf.js +++ b/front/test/unit/karma.conf.js @@ -18,6 +18,8 @@ module.exports = function (config) { preprocessors: { './index.js': ['webpack', 'sourcemap'] }, + captureTimeout: 5000, + retryLimit: 1, webpack: webpackConfig, webpackMiddleware: { noInfo: true From 91d6a71a21d655f6f103de6a66a21ee4bdd352e3 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 8 Jan 2018 23:12:31 +0100 Subject: [PATCH 02/32] Can now launch tests --- front/package.json | 1 + front/src/assets/logo/{favicon.ico => favicon.png} | Bin front/src/config.js | 2 +- front/test/unit/karma.conf.js | 5 ++++- front/test/unit/specs/Hello.spec.js | 11 ----------- 5 files changed, 6 insertions(+), 13 deletions(-) rename front/src/assets/logo/{favicon.ico => favicon.png} (100%) delete mode 100644 front/test/unit/specs/Hello.spec.js diff --git a/front/package.json b/front/package.json index 58c22a408..e79ffcbeb 100644 --- a/front/package.json +++ b/front/package.json @@ -46,6 +46,7 @@ "cross-env": "^4.0.0", "cross-spawn": "^5.0.1", "css-loader": "^0.28.0", + "es6-promise": "^4.2.2", "eslint": "^3.19.0", "eslint-config-standard": "^6.2.1", "eslint-friendly-formatter": "^2.0.7", diff --git a/front/src/assets/logo/favicon.ico b/front/src/assets/logo/favicon.png similarity index 100% rename from front/src/assets/logo/favicon.ico rename to front/src/assets/logo/favicon.png diff --git a/front/src/config.js b/front/src/config.js index 3ba1247ac..76a6f5e5f 100644 --- a/front/src/config.js +++ b/front/src/config.js @@ -4,7 +4,7 @@ class Config { if (this.BACKEND_URL === '/') { this.BACKEND_URL = window.location.protocol + '//' + window.location.hostname + ':' + window.location.port } - if (!this.BACKEND_URL.endsWith('/')) { + if (!this.BACKEND_URL.slice(-1) === '/') { this.BACKEND_URL += '/' } this.API_URL = this.BACKEND_URL + 'api/v1/' diff --git a/front/test/unit/karma.conf.js b/front/test/unit/karma.conf.js index 3fcc5f02a..da5a21876 100644 --- a/front/test/unit/karma.conf.js +++ b/front/test/unit/karma.conf.js @@ -14,7 +14,10 @@ module.exports = function (config) { browsers: ['PhantomJS'], frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'], reporters: ['spec', 'coverage'], - files: ['./index.js'], + files: [ + '../../node_modules/es6-promise/dist/es6-promise.auto.js', + './index.js' + ], preprocessors: { './index.js': ['webpack', 'sourcemap'] }, diff --git a/front/test/unit/specs/Hello.spec.js b/front/test/unit/specs/Hello.spec.js deleted file mode 100644 index 80140baa9..000000000 --- a/front/test/unit/specs/Hello.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -import Vue from 'vue' -import Hello from '@/components/Hello' - -describe('Hello.vue', () => { - it('should render correct contents', () => { - const Constructor = Vue.extend(Hello) - const vm = new Constructor().$mount() - expect(vm.$el.querySelector('.hello h1').textContent) - .to.equal('Welcome to Your Vue.js App') - }) -}) From f7d876aec664b0884ecb1c157b373de3cdb83471 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 8 Jan 2018 23:12:45 +0100 Subject: [PATCH 03/32] First store tests \o/ :party: --- front/test/unit/specs/store/auth.spec.js | 52 ++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 front/test/unit/specs/store/auth.spec.js diff --git a/front/test/unit/specs/store/auth.spec.js b/front/test/unit/specs/store/auth.spec.js new file mode 100644 index 000000000..6ef558f6e --- /dev/null +++ b/front/test/unit/specs/store/auth.spec.js @@ -0,0 +1,52 @@ +import store from '@/store/player' + +describe('mutations', () => { + it('set volume', () => { + // mock state + const state = { volume: 0 } + // apply mutation + store.mutations.volume(state, 0.9) + // assert result + expect(state.volume).to.equal(0.9) + }) + it('set volume max 1', () => { + // mock state + const state = { volume: 0 } + // apply mutation + store.mutations.volume(state, 2) + // assert result + expect(state.volume).to.equal(1) + }) + it('set volume min to 0', () => { + // mock state + const state = { volume: 0.5 } + // apply mutation + store.mutations.volume(state, -2) + // assert result + expect(state.volume).to.equal(0) + }) + it('increment volume', () => { + // mock state + const state = { volume: 0 } + // apply mutation + store.mutations.incrementVolume(state, 0.1) + // assert result + expect(state.volume).to.equal(0.1) + }) + it('increment volume max 1', () => { + // mock state + const state = { volume: 0 } + // apply mutation + store.mutations.incrementVolume(state, 2) + // assert result + expect(state.volume).to.equal(1) + }) + it('increment volume min to 0', () => { + // mock state + const state = { volume: 0.5 } + // apply mutation + store.mutations.incrementVolume(state, -2) + // assert result + expect(state.volume).to.equal(0) + }) +}) From cb2238c5761f87228aa619c6f86c4a0b74cd99cb Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 9 Jan 2018 20:47:03 +0100 Subject: [PATCH 04/32] Should now run frontend tests on gitlab --- .gitlab-ci.yml | 51 ++++++++++++++++++++++++++++++++++-------------- front/Dockerfile | 6 +++--- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 91b11e8bd..cde12894a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,7 +2,7 @@ variables: IMAGE_NAME: funkwhale/funkwhale IMAGE: $IMAGE_NAME:$CI_COMMIT_REF_NAME IMAGE_LATEST: $IMAGE_NAME:latest - + PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache" stages: @@ -14,40 +14,61 @@ test_api: services: - postgres:9.4 stage: test - image: funkwhale/funkwhale:base + image: funkwhale/funkwhale:latest + cache: + key: "$CI_PROJECT_ID/pip_cache" + paths: + - "$PIP_CACHE_DIR" variables: - PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache" DATABASE_URL: "postgresql://postgres@postgres/postgres" before_script: - - python3 -m venv --copies virtualenv - - source virtualenv/bin/activate - cd api - pip install -r requirements/base.txt - pip install -r requirements/local.txt - pip install -r requirements/test.txt script: - pytest - cache: - key: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" - paths: - - "$CI_PROJECT_DIR/pip-cache" tags: - docker -build_front: - stage: build - image: node:6-alpine + +test_front: + stage: test + image: node:9 before_script: - cd front script: - - npm install - - npm run build + - yarn install + - yarn run unit cache: - key: "$CI_COMMIT_REF_NAME" + key: "$CI_PROJECT_ID/front_dependencies" paths: - front/node_modules + - front/yarn.lock + artifacts: + name: "front_${CI_COMMIT_REF_NAME}" + paths: + - front/dist/ + tags: + - docker + + +build_front: + stage: build + image: node:9 + before_script: + - cd front + + script: + - yarn install + - yarn run build + cache: + key: "$CI_PROJECT_ID/front_dependencies" + paths: + - front/node_modules + - front/yarn.lock artifacts: name: "front_${CI_COMMIT_REF_NAME}" paths: diff --git a/front/Dockerfile b/front/Dockerfile index b22359b46..cdf92446b 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -1,10 +1,10 @@ -FROM node:6 +FROM node:9 EXPOSE 8080 WORKDIR /app/ ADD package.json . -RUN npm install --only=production -RUN npm install --only=dev +RUN yarn install --only=production +RUN yarn install --only=dev VOLUME ["/app/node_modules"] COPY . . From 03364dd0d08e66efd79d31622b28a36b7c31ab42 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 9 Jan 2018 21:22:05 +0100 Subject: [PATCH 05/32] Now use taggit from taggit --- api/requirements/base.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/requirements/base.txt b/api/requirements/base.txt index cff16d3f1..ce0eb9b85 100644 --- a/api/requirements/base.txt +++ b/api/requirements/base.txt @@ -47,8 +47,7 @@ mutagen>=1.39,<1.40 # Until this is merged -#django-taggit>=0.22,<0.23 -git+https://github.com/alex/django-taggit.git@95776ac66948ed7ba7c12e35c1170551e3be66a5 +django-taggit>=0.22,<0.23 # Until this is merged git+https://github.com/EliotBerriot/PyMemoize.git@django # Until this is merged From bc76048b4a179cfe3b871c1683ffbb3e1bea4954 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 9 Jan 2018 22:54:06 +0100 Subject: [PATCH 06/32] Added tests for auth, favorites and player in store --- front/package.json | 1 + front/src/store/auth.js | 1 + front/src/store/player.js | 4 +- front/test/unit/karma.conf.js | 2 +- front/test/unit/specs/store/auth.spec.js | 150 +++++++++++------ front/test/unit/specs/store/favorites.spec.js | 52 ++++++ front/test/unit/specs/store/player.spec.js | 153 ++++++++++++++++++ front/test/unit/utils.js | 68 ++++++++ 8 files changed, 381 insertions(+), 50 deletions(-) create mode 100644 front/test/unit/specs/store/favorites.spec.js create mode 100644 front/test/unit/specs/store/player.spec.js create mode 100644 front/test/unit/utils.js diff --git a/front/package.json b/front/package.json index e79ffcbeb..2818757a3 100644 --- a/front/package.json +++ b/front/package.json @@ -9,6 +9,7 @@ "start": "node build/dev-server.js", "build": "node build/build.js", "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", + "unit-watch": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js", "e2e": "node test/e2e/runner.js", "test": "npm run unit && npm run e2e", "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs" diff --git a/front/src/store/auth.js b/front/src/store/auth.js index d8bd197f3..6021f30fa 100644 --- a/front/src/store/auth.js +++ b/front/src/store/auth.js @@ -44,6 +44,7 @@ export default { state.token = value if (value) { state.tokenData = jwtDecode(value) + console.log(state.tokenData) } else { state.tokenData = {} } diff --git a/front/src/store/player.js b/front/src/store/player.js index 74b0b9f9e..9e50d6c19 100644 --- a/front/src/store/player.js +++ b/front/src/store/player.js @@ -61,8 +61,8 @@ export default { } }, actions: { - incrementVolume (context, value) { - context.commit('volume', context.state.volume + value) + incrementVolume ({commit, state}, value) { + commit('volume', state.volume + value) }, stop (context) { }, diff --git a/front/test/unit/karma.conf.js b/front/test/unit/karma.conf.js index da5a21876..47b468807 100644 --- a/front/test/unit/karma.conf.js +++ b/front/test/unit/karma.conf.js @@ -21,7 +21,7 @@ module.exports = function (config) { preprocessors: { './index.js': ['webpack', 'sourcemap'] }, - captureTimeout: 5000, + captureTimeout: 15000, retryLimit: 1, webpack: webpackConfig, webpackMiddleware: { diff --git a/front/test/unit/specs/store/auth.spec.js b/front/test/unit/specs/store/auth.spec.js index 6ef558f6e..8be2708f3 100644 --- a/front/test/unit/specs/store/auth.spec.js +++ b/front/test/unit/specs/store/auth.spec.js @@ -1,52 +1,108 @@ -import store from '@/store/player' +import store from '@/store/auth' -describe('mutations', () => { - it('set volume', () => { - // mock state - const state = { volume: 0 } - // apply mutation - store.mutations.volume(state, 0.9) - // assert result - expect(state.volume).to.equal(0.9) +import { testAction } from '../../utils' + +describe('store/auth', () => { + describe('mutations', () => { + it('profile', () => { + const state = {} + store.mutations.profile(state, {}) + expect(state.profile).to.deep.equal({}) + }) + it('username', () => { + const state = {} + store.mutations.username(state, 'world') + expect(state.username).to.equal('world') + }) + it('authenticated true', () => { + const state = {} + store.mutations.authenticated(state, true) + expect(state.authenticated).to.equal(true) + }) + it('authenticated false', () => { + const state = { + username: 'dummy', + token: 'dummy', + tokenData: 'dummy', + profile: 'dummy', + availablePermissions: 'dummy' + } + store.mutations.authenticated(state, false) + expect(state.authenticated).to.equal(false) + expect(state.username).to.equal(null) + expect(state.token).to.equal(null) + expect(state.tokenData).to.equal(null) + expect(state.profile).to.equal(null) + expect(state.availablePermissions).to.deep.equal({}) + }) + it('token null', () => { + const state = {} + store.mutations.token(state, null) + expect(state.token).to.equal(null) + expect(state.tokenData).to.deep.equal({}) + }) + it('token real', () => { + // generated on http://kjur.github.io/jsjws/tool_jwt.html + const state = {} + let token = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczovL2p3dC1pZHAuZXhhbXBsZS5jb20iLCJzdWIiOiJtYWlsdG86bWlrZUBleGFtcGxlLmNvbSIsIm5iZiI6MTUxNTUzMzQyOSwiZXhwIjoxNTE1NTM3MDI5LCJpYXQiOjE1MTU1MzM0MjksImp0aSI6ImlkMTIzNDU2IiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9.' + let tokenData = { + iss: 'https://jwt-idp.example.com', + sub: 'mailto:mike@example.com', + nbf: 1515533429, + exp: 1515537029, + iat: 1515533429, + jti: 'id123456', + typ: 'https://example.com/register' + } + store.mutations.token(state, token) + expect(state.token).to.equal(token) + expect(state.tokenData).to.deep.equal(tokenData) + }) + it('permissions', () => { + const state = { availablePermissions: {} } + store.mutations.permission(state, {key: 'admin', status: true}) + expect(state.availablePermissions).to.deep.equal({admin: true}) + }) }) - it('set volume max 1', () => { - // mock state - const state = { volume: 0 } - // apply mutation - store.mutations.volume(state, 2) - // assert result - expect(state.volume).to.equal(1) + describe('getters', () => { + it('header', () => { + const state = { token: 'helloworld' } + expect(store.getters['header'](state)).to.equal('JWT helloworld') + }) }) - it('set volume min to 0', () => { - // mock state - const state = { volume: 0.5 } - // apply mutation - store.mutations.volume(state, -2) - // assert result - expect(state.volume).to.equal(0) - }) - it('increment volume', () => { - // mock state - const state = { volume: 0 } - // apply mutation - store.mutations.incrementVolume(state, 0.1) - // assert result - expect(state.volume).to.equal(0.1) - }) - it('increment volume max 1', () => { - // mock state - const state = { volume: 0 } - // apply mutation - store.mutations.incrementVolume(state, 2) - // assert result - expect(state.volume).to.equal(1) - }) - it('increment volume min to 0', () => { - // mock state - const state = { volume: 0.5 } - // apply mutation - store.mutations.incrementVolume(state, -2) - // assert result - expect(state.volume).to.equal(0) + describe('actions', () => { + it('logout', (done) => { + testAction({ + action: store.actions.logout, + params: {state: {}}, + expectedMutations: [ + { type: 'authenticated', payload: false } + ] + }, done) + }) + it('check jwt null', (done) => { + testAction({ + action: store.actions.check, + params: {state: {}}, + expectedMutations: [ + { type: 'authenticated', payload: false } + ] + }, done) + }) + it('check jwt set', (done) => { + testAction({ + action: store.actions.check, + params: {state: {token: 'test', username: 'user'}}, + expectedMutations: [ + { type: 'authenticated', payload: true }, + { type: 'username', payload: 'user' }, + { type: 'token', payload: 'test' } + ], + expectedActions: [ + { type: 'fetchProfile' }, + { type: 'refreshToken' } + ] + }, done) + }) }) }) diff --git a/front/test/unit/specs/store/favorites.spec.js b/front/test/unit/specs/store/favorites.spec.js new file mode 100644 index 000000000..6d4314ca6 --- /dev/null +++ b/front/test/unit/specs/store/favorites.spec.js @@ -0,0 +1,52 @@ +import store from '@/store/favorites' + +import { testAction } from '../../utils' + +describe('store/favorites', () => { + describe('mutations', () => { + it('track true', () => { + const state = { tracks: [] } + store.mutations.track(state, {id: 1, value: true}) + expect(state.tracks).to.deep.equal([1]) + expect(state.count).to.deep.equal(1) + }) + it('track false', () => { + const state = { tracks: [1] } + store.mutations.track(state, {id: 1, value: false}) + expect(state.tracks).to.deep.equal([]) + expect(state.count).to.deep.equal(0) + }) + }) + describe('getters', () => { + it('isFavorite true', () => { + const state = { tracks: [1] } + expect(store.getters['isFavorite'](state)(1)).to.equal(true) + }) + it('isFavorite false', () => { + const state = { tracks: [] } + expect(store.getters['isFavorite'](state)(1)).to.equal(false) + }) + }) + describe('actions', () => { + it('toggle true', (done) => { + testAction({ + action: store.actions.toggle, + payload: 1, + params: {getters: {isFavorite: () => false}}, + expectedActions: [ + { type: 'set', payload: {id: 1, value: true} } + ] + }, done) + }) + it('toggle true', (done) => { + testAction({ + action: store.actions.toggle, + payload: 1, + params: {getters: {isFavorite: () => true}}, + expectedActions: [ + { type: 'set', payload: {id: 1, value: false} } + ] + }, done) + }) + }) +}) diff --git a/front/test/unit/specs/store/player.spec.js b/front/test/unit/specs/store/player.spec.js new file mode 100644 index 000000000..af0b6b435 --- /dev/null +++ b/front/test/unit/specs/store/player.spec.js @@ -0,0 +1,153 @@ +import store from '@/store/player' + +import { testAction } from '../../utils' + +describe('store/player', () => { + describe('mutations', () => { + it('set volume', () => { + const state = { volume: 0 } + store.mutations.volume(state, 0.9) + expect(state.volume).to.equal(0.9) + }) + it('set volume max 1', () => { + const state = { volume: 0 } + store.mutations.volume(state, 2) + expect(state.volume).to.equal(1) + }) + it('set volume min to 0', () => { + const state = { volume: 0.5 } + store.mutations.volume(state, -2) + expect(state.volume).to.equal(0) + }) + it('increment volume', () => { + const state = { volume: 0 } + store.mutations.incrementVolume(state, 0.1) + expect(state.volume).to.equal(0.1) + }) + it('increment volume max 1', () => { + const state = { volume: 0 } + store.mutations.incrementVolume(state, 2) + expect(state.volume).to.equal(1) + }) + it('increment volume min to 0', () => { + const state = { volume: 0.5 } + store.mutations.incrementVolume(state, -2) + expect(state.volume).to.equal(0) + }) + it('set duration', () => { + const state = { duration: 42 } + store.mutations.duration(state, 14) + expect(state.duration).to.equal(14) + }) + it('set errored', () => { + const state = { errored: false } + store.mutations.errored(state, true) + expect(state.errored).to.equal(true) + }) + it('set looping', () => { + const state = { looping: 1 } + store.mutations.looping(state, 2) + expect(state.looping).to.equal(2) + }) + it('set playing', () => { + const state = { playing: false } + store.mutations.playing(state, true) + expect(state.playing).to.equal(true) + }) + it('set current time', () => { + const state = { currentTime: 1 } + store.mutations.currentTime(state, 2) + expect(state.currentTime).to.equal(2) + }) + it('toggle looping from 0', () => { + const state = { looping: 0 } + store.mutations.toggleLooping(state) + expect(state.looping).to.equal(1) + }) + it('toggle looping from 1', () => { + const state = { looping: 1 } + store.mutations.toggleLooping(state) + expect(state.looping).to.equal(2) + }) + it('toggle looping from 2', () => { + const state = { looping: 2 } + store.mutations.toggleLooping(state) + expect(state.looping).to.equal(0) + }) + }) + describe('getters', () => { + it('durationFormatted', () => { + const state = { duration: 12.51 } + expect(store.getters['durationFormatted'](state)).to.equal('00:13') + }) + it('currentTimeFormatted', () => { + const state = { currentTime: 12.51 } + expect(store.getters['currentTimeFormatted'](state)).to.equal('00:13') + }) + it('progress', () => { + const state = { currentTime: 4, duration: 10 } + expect(store.getters['progress'](state)).to.equal(40) + }) + }) + describe('actions', () => { + it('incrementVolume', (done) => { + testAction({ + action: store.actions.incrementVolume, + payload: 0.2, + params: {state: {volume: 0.7}}, + expectedMutations: [ + { type: 'volume', payload: 0.7 + 0.2 } + ] + }, done) + }) + it('toggle play false', (done) => { + testAction({ + action: store.actions.togglePlay, + params: {state: {playing: false}}, + expectedMutations: [ + { type: 'playing', payload: true } + ] + }, done) + }) + it('toggle play true', (done) => { + testAction({ + action: store.actions.togglePlay, + params: {state: {playing: true}}, + expectedMutations: [ + { type: 'playing', payload: false } + ] + }, done) + }) + it('trackEnded', (done) => { + testAction({ + action: store.actions.trackEnded, + payload: {test: 'track'}, + expectedActions: [ + { type: 'trackListened', payload: {test: 'track'} }, + { type: 'queue/next', payload: null, options: {root: true} } + ] + }, done) + }) + it('trackErrored', (done) => { + testAction({ + action: store.actions.trackErrored, + payload: {test: 'track'}, + expectedMutations: [ + { type: 'errored', payload: true } + ], + expectedActions: [ + { type: 'queue/next', payload: null, options: {root: true} } + ] + }, done) + }) + it('updateProgress', (done) => { + testAction({ + action: store.actions.updateProgress, + payload: 1, + expectedMutations: [ + { type: 'currentTime', payload: 1 } + ] + }, done) + }) + }) +}) diff --git a/front/test/unit/utils.js b/front/test/unit/utils.js new file mode 100644 index 000000000..e67c7687f --- /dev/null +++ b/front/test/unit/utils.js @@ -0,0 +1,68 @@ +// helper for testing action with expected mutations +export const testAction = ({action, payload, params, expectedMutations, expectedActions}, done) => { + let mutationsCount = 0 + let actionsCount = 0 + + if (!expectedMutations) { + expectedMutations = [] + } + if (!expectedActions) { + expectedActions = [] + } + const isOver = () => { + return mutationsCount >= expectedMutations.length && actionsCount >= expectedActions.length + } + // mock commit + const commit = (type, payload) => { + const mutation = expectedMutations[mutationsCount] + + try { + expect(mutation.type).to.equal(type) + if (payload) { + expect(mutation.payload).to.deep.equal(payload) + } + } catch (error) { + done(error) + } + + mutationsCount++ + if (isOver()) { + done() + } + } + // mock dispatch + const dispatch = (type, payload, options) => { + const a = expectedActions[actionsCount] + + try { + expect(a.type).to.equal(type) + if (payload) { + expect(a.payload).to.deep.equal(payload) + } + if (a.options) { + expect(options).to.deep.equal(a.options) + } + } catch (error) { + done(error) + } + + actionsCount++ + if (isOver()) { + done() + } + } + + // call the action with mocked store and arguments + action({ commit, dispatch, ...params }, payload) + + // check if no mutations should have been dispatched + if (expectedMutations.length === 0) { + expect(mutationsCount).to.equal(0) + } + if (expectedActions.length === 0) { + expect(actionsCount).to.equal(0) + } + if (isOver()) { + done() + } +} From c7070042fb3312640fa35da1b1f8a573b0b1bf98 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 11 Jan 2018 18:46:28 +0100 Subject: [PATCH 07/32] Unit tests for queue store --- front/src/store/queue.js | 35 ++- front/test/unit/specs/store/queue.spec.js | 321 ++++++++++++++++++++++ 2 files changed, 338 insertions(+), 18 deletions(-) create mode 100644 front/test/unit/specs/store/queue.spec.js diff --git a/front/src/store/queue.js b/front/src/store/queue.js index 5dde19bd8..ac28a1e0e 100644 --- a/front/src/store/queue.js +++ b/front/src/store/queue.js @@ -55,33 +55,32 @@ export default { } }, actions: { - append (context, {track, index, skipPlay}) { - index = index || context.state.tracks.length - if (index > context.state.tracks.length - 1) { + append ({commit, state, dispatch}, {track, index, skipPlay}) { + index = index || state.tracks.length + if (index > state.tracks.length - 1) { // we simply push to the end - context.commit('insert', {track, index: context.state.tracks.length}) + commit('insert', {track, index: state.tracks.length}) } else { // we insert the track at given position - context.commit('insert', {track, index}) + commit('insert', {track, index}) } if (!skipPlay) { - context.dispatch('resume') + dispatch('resume') } - // this.cache() }, - appendMany (context, {tracks, index}) { + appendMany ({state, dispatch}, {tracks, index}) { logger.default.info('Appending many tracks to the queue', tracks.map(e => { return e.title })) - if (context.state.tracks.length === 0) { + if (state.tracks.length === 0) { index = 0 } else { - index = index || context.state.tracks.length + index = index || state.tracks.length } tracks.forEach((t) => { - context.dispatch('append', {track: t, index: index, skipPlay: true}) + dispatch('append', {track: t, index: index, skipPlay: true}) index += 1 }) - context.dispatch('resume') + dispatch('resume') }, cleanTrack ({state, dispatch, commit}, index) { @@ -100,14 +99,14 @@ export default { } }, - resume (context) { - if (context.state.ended | context.rootState.player.errored) { - context.dispatch('next') + resume ({state, dispatch, rootState}) { + if (state.ended | rootState.player.errored) { + dispatch('next') } }, - previous (context) { - if (context.state.currentIndex > 0) { - context.dispatch('currentIndex', context.state.currentIndex - 1) + previous ({state, dispatch}) { + if (state.currentIndex > 0) { + dispatch('currentIndex', state.currentIndex - 1) } }, next ({state, dispatch, commit, rootState}) { diff --git a/front/test/unit/specs/store/queue.spec.js b/front/test/unit/specs/store/queue.spec.js new file mode 100644 index 000000000..2e12a254b --- /dev/null +++ b/front/test/unit/specs/store/queue.spec.js @@ -0,0 +1,321 @@ +import _ from 'lodash' + +import store from '@/store/queue' +import { testAction } from '../../utils' + +describe('store/queue', () => { + describe('mutations', () => { + it('currentIndex', () => { + const state = {} + store.mutations.currentIndex(state, 2) + expect(state.currentIndex).to.equal(2) + }) + it('ended', () => { + const state = {} + store.mutations.ended(state, false) + expect(state.ended).to.equal(false) + }) + it('tracks', () => { + const state = {} + store.mutations.tracks(state, [1, 2]) + expect(state.tracks).to.deep.equal([1, 2]) + }) + it('splice', () => { + const state = {tracks: [1, 2, 3]} + store.mutations.splice(state, {start: 1, size: 2}) + expect(state.tracks).to.deep.equal([1]) + }) + it('insert', () => { + const state = {tracks: [1, 3]} + store.mutations.insert(state, {track: 2, index: 1}) + expect(state.tracks).to.deep.equal([1, 2, 3]) + }) + it('reorder before', () => { + const state = {currentIndex: 3} + store.mutations.reorder(state, {oldIndex: 2, newIndex: 1}) + expect(state.currentIndex).to.equal(3) + }) + it('reorder from after to before', () => { + const state = {currentIndex: 3} + store.mutations.reorder(state, {oldIndex: 4, newIndex: 1}) + expect(state.currentIndex).to.equal(4) + }) + it('reorder after', () => { + const state = {currentIndex: 3} + store.mutations.reorder(state, {oldIndex: 4, newIndex: 5}) + expect(state.currentIndex).to.equal(3) + }) + it('reorder before to after', () => { + const state = {currentIndex: 3} + store.mutations.reorder(state, {oldIndex: 1, newIndex: 5}) + expect(state.currentIndex).to.equal(2) + }) + it('reorder current', () => { + const state = {currentIndex: 3} + store.mutations.reorder(state, {oldIndex: 3, newIndex: 1}) + expect(state.currentIndex).to.equal(1) + }) + }) + describe('getters', () => { + it('currentTrack', () => { + const state = { tracks: [1, 2, 3], currentIndex: 2 } + expect(store.getters['currentTrack'](state)).to.equal(3) + }) + it('hasNext true', () => { + const state = { tracks: [1, 2, 3], currentIndex: 1 } + expect(store.getters['hasNext'](state)).to.equal(true) + }) + it('hasNext false', () => { + const state = { tracks: [1, 2, 3], currentIndex: 2 } + expect(store.getters['hasNext'](state)).to.equal(false) + }) + it('hasPrevious true', () => { + const state = { currentIndex: 1 } + expect(store.getters['hasPrevious'](state)).to.equal(true) + }) + it('hasPrevious false', () => { + const state = { currentIndex: 0 } + expect(store.getters['hasPrevious'](state)).to.equal(false) + }) + }) + describe('actions', () => { + it('append at end', (done) => { + testAction({ + action: store.actions.append, + payload: {track: 4, skipPlay: true}, + params: {state: {tracks: [1, 2, 3]}}, + expectedMutations: [ + { type: 'insert', payload: {track: 4, index: 3} } + ] + }, done) + }) + it('append at index', (done) => { + testAction({ + action: store.actions.append, + payload: {track: 2, index: 1, skipPlay: true}, + params: {state: {tracks: [1, 3]}}, + expectedMutations: [ + { type: 'insert', payload: {track: 2, index: 1} } + ] + }, done) + }) + it('append and play', (done) => { + testAction({ + action: store.actions.append, + payload: {track: 3}, + params: {state: {tracks: [1, 2]}}, + expectedMutations: [ + { type: 'insert', payload: {track: 3, index: 2} } + ], + expectedActions: [ + { type: 'resume' } + ] + }, done) + }) + it('appendMany', (done) => { + const tracks = [{title: 1}, {title: 2}] + testAction({ + action: store.actions.appendMany, + payload: {tracks: tracks}, + params: {state: {tracks: []}}, + expectedActions: [ + { type: 'append', payload: {track: tracks[0], index: 0, skipPlay: true} }, + { type: 'append', payload: {track: tracks[1], index: 1, skipPlay: true} }, + { type: 'resume' } + ] + }, done) + }) + it('appendMany at index', (done) => { + const tracks = [{title: 1}, {title: 2}] + testAction({ + action: store.actions.appendMany, + payload: {tracks: tracks, index: 1}, + params: {state: {tracks: [1, 2]}}, + expectedActions: [ + { type: 'append', payload: {track: tracks[0], index: 1, skipPlay: true} }, + { type: 'append', payload: {track: tracks[1], index: 2, skipPlay: true} }, + { type: 'resume' } + ] + }, done) + }) + it('cleanTrack after current', (done) => { + testAction({ + action: store.actions.cleanTrack, + payload: 3, + params: {state: {currentIndex: 2}}, + expectedMutations: [ + { type: 'splice', payload: {start: 3, size: 1} } + ] + }, done) + }) + it('cleanTrack before current', (done) => { + testAction({ + action: store.actions.cleanTrack, + payload: 1, + params: {state: {currentIndex: 2}}, + expectedMutations: [ + { type: 'splice', payload: {start: 1, size: 1} } + ], + expectedActions: [ + { type: 'currentIndex', payload: 1 } + ] + }, done) + }) + it('cleanTrack current', (done) => { + testAction({ + action: store.actions.cleanTrack, + payload: 2, + params: {state: {currentIndex: 2}}, + expectedMutations: [ + { type: 'splice', payload: {start: 2, size: 1} } + ], + expectedActions: [ + { type: 'player/stop', payload: null, options: {root: true} }, + { type: 'currentIndex', payload: 2 } + ] + }, done) + }) + it('resume when ended', (done) => { + testAction({ + action: store.actions.resume, + params: {state: {ended: true}, rootState: {player: {errored: false}}}, + expectedActions: [ + { type: 'next' } + ] + }, done) + }) + it('resume when errored', (done) => { + testAction({ + action: store.actions.resume, + params: {state: {ended: false}, rootState: {player: {errored: true}}}, + expectedActions: [ + { type: 'next' } + ] + }, done) + }) + it('skip resume when not ended or not error', (done) => { + testAction({ + action: store.actions.resume, + params: {state: {ended: false}, rootState: {player: {errored: false}}}, + expectedActions: [] + }, done) + }) + it('previous when at beginning does nothing', (done) => { + testAction({ + action: store.actions.previous, + params: {state: {currentIndex: 0}}, + expectedActions: [] + }, done) + }) + it('previous', (done) => { + testAction({ + action: store.actions.previous, + params: {state: {currentIndex: 1}}, + expectedActions: [ + { type: 'currentIndex', payload: 0 } + ] + }, done) + }) + it('next on last track when looping on queue', (done) => { + testAction({ + action: store.actions.next, + params: {state: {tracks: [1, 2], currentIndex: 1}, rootState: {player: {looping: 2}}}, + expectedActions: [ + { type: 'currentIndex', payload: 0 } + ] + }, done) + }) + it('next track when last track', (done) => { + testAction({ + action: store.actions.next, + params: {state: {tracks: [1, 2], currentIndex: 1}, rootState: {player: {looping: 0}}}, + expectedMutations: [ + { type: 'ended', payload: true } + ] + }, done) + }) + it('next track when not last track', (done) => { + testAction({ + action: store.actions.next, + params: {state: {tracks: [1, 2], currentIndex: 0}, rootState: {player: {looping: 0}}}, + expectedActions: [ + { type: 'currentIndex', payload: 1 } + ] + }, done) + }) + it('currentIndex', (done) => { + testAction({ + action: store.actions.currentIndex, + payload: 1, + params: {state: {tracks: [1, 2], currentIndex: 0}, rootState: {radios: {running: false}}}, + expectedMutations: [ + { type: 'ended', payload: false }, + { type: 'player/currentTime', payload: 0, options: {root: true} }, + { type: 'player/playing', payload: true, options: {root: true} }, + { type: 'player/errored', payload: false, options: {root: true} }, + { type: 'currentIndex', payload: 1 } + ] + }, done) + }) + it('currentIndex with radio and many tracks remaining', (done) => { + testAction({ + action: store.actions.currentIndex, + payload: 1, + params: {state: {tracks: [1, 2, 3, 4], currentIndex: 0}, rootState: {radios: {running: true}}}, + expectedMutations: [ + { type: 'ended', payload: false }, + { type: 'player/currentTime', payload: 0, options: {root: true} }, + { type: 'player/playing', payload: true, options: {root: true} }, + { type: 'player/errored', payload: false, options: {root: true} }, + { type: 'currentIndex', payload: 1 } + ] + }, done) + }) + it('currentIndex with radio and less than two tracks remaining', (done) => { + testAction({ + action: store.actions.currentIndex, + payload: 1, + params: {state: {tracks: [1, 2, 3], currentIndex: 0}, rootState: {radios: {running: true}}}, + expectedMutations: [ + { type: 'ended', payload: false }, + { type: 'player/currentTime', payload: 0, options: {root: true} }, + { type: 'player/playing', payload: true, options: {root: true} }, + { type: 'player/errored', payload: false, options: {root: true} }, + { type: 'currentIndex', payload: 1 } + ], + expectedActions: [ + { type: 'radios/populateQueue', payload: null, options: {root: true} } + ] + }, done) + }) + it('clean', (done) => { + testAction({ + action: store.actions.clean, + expectedMutations: [ + { type: 'tracks', payload: [] }, + { type: 'ended', payload: true } + ], + expectedActions: [ + { type: 'player/stop', payload: null, options: {root: true} }, + { type: 'currentIndex', payload: -1 } + ] + }, done) + }) + it('shuffle', (done) => { + let _shuffle = sinon.stub(_, 'shuffle') + let tracks = [1, 2, 3] + let shuffledTracks = [2, 3, 1] + _shuffle.returns(shuffledTracks) + testAction({ + action: store.actions.shuffle, + params: {state: {tracks: tracks}}, + expectedMutations: [ + { type: 'tracks', payload: [] } + ], + expectedActions: [ + { type: 'appendMany', payload: {tracks: shuffledTracks} } + ] + }, done) + }) + }) +}) From e4edf55c4753aaac807316c029354532c112fce9 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 11 Jan 2018 21:35:51 +0100 Subject: [PATCH 08/32] Replaced vue-resource by axios --- api/funkwhale_api/favorites/views.py | 2 +- api/funkwhale_api/music/fake_data.py | 4 +-- api/tests/test_favorites.py | 7 ++-- front/package.json | 4 ++- front/src/components/audio/Search.vue | 17 ++------- front/src/components/auth/Settings.vue | 6 ++-- front/src/components/favorites/List.vue | 6 ++-- front/src/components/library/Album.vue | 7 ++-- front/src/components/library/Artist.vue | 7 ++-- front/src/components/library/Artists.vue | 6 ++-- front/src/components/library/Home.vue | 6 ++-- front/src/components/library/Radios.vue | 6 ++-- front/src/components/library/Track.vue | 9 +++-- .../library/import/ArtistImport.vue | 7 ++-- .../components/library/import/BatchDetail.vue | 7 ++-- .../components/library/import/BatchList.vue | 6 ++-- .../components/library/import/FileUpload.vue | 9 ++--- .../components/library/import/ImportMixin.vue | 8 ++--- .../components/library/import/TrackImport.vue | 8 ++--- .../src/components/library/radios/Builder.vue | 22 ++++++------ .../src/components/library/radios/Filter.vue | 5 +-- front/src/components/metadata/CardMixin.vue | 9 ++--- front/src/config.js | 2 +- front/src/main.js | 35 ++++++++++++------- front/src/store/auth.js | 16 +++------ front/src/store/favorites.js | 17 +++------ front/src/store/player.js | 7 ++-- front/src/store/radios.js | 13 ++----- 28 files changed, 111 insertions(+), 147 deletions(-) diff --git a/api/funkwhale_api/favorites/views.py b/api/funkwhale_api/favorites/views.py index 98c0cfc08..08ae00b68 100644 --- a/api/funkwhale_api/favorites/views.py +++ b/api/funkwhale_api/favorites/views.py @@ -43,7 +43,7 @@ class TrackFavoriteViewSet(mixins.CreateModelMixin, favorite = models.TrackFavorite.add(track=track, user=self.request.user) return favorite - @list_route(methods=['delete']) + @list_route(methods=['delete', 'post']) def remove(self, request, *args, **kwargs): try: pk = int(request.data['track']) diff --git a/api/funkwhale_api/music/fake_data.py b/api/funkwhale_api/music/fake_data.py index ef5eaf4be..892b784ca 100644 --- a/api/funkwhale_api/music/fake_data.py +++ b/api/funkwhale_api/music/fake_data.py @@ -4,7 +4,7 @@ Populates the database with fake data import random from funkwhale_api.music import models -from funkwhale_api.music.tests import factories +from funkwhale_api.music import factories def create_data(count=25): @@ -19,4 +19,4 @@ def create_data(count=25): if __name__ == '__main__': - main() + create_data() diff --git a/api/tests/test_favorites.py b/api/tests/test_favorites.py index 418166d8e..8165722ea 100644 --- a/api/tests/test_favorites.py +++ b/api/tests/test_favorites.py @@ -58,11 +58,14 @@ def test_user_can_remove_favorite_via_api(logged_in_client, factories, client): assert response.status_code == 204 assert TrackFavorite.objects.count() == 0 -def test_user_can_remove_favorite_via_api_using_track_id(factories, logged_in_client): + +@pytest.mark.parametrize('method', ['delete', 'post']) +def test_user_can_remove_favorite_via_api_using_track_id( + method, factories, logged_in_client): favorite = factories['favorites.TrackFavorite'](user=logged_in_client.user) url = reverse('api:v1:favorites:tracks-remove') - response = logged_in_client.delete( + response = getattr(logged_in_client, method)( url, json.dumps({'track': favorite.track.pk}), content_type='application/json' ) diff --git a/front/package.json b/front/package.json index 2818757a3..aa5024d24 100644 --- a/front/package.json +++ b/front/package.json @@ -15,6 +15,7 @@ "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs" }, "dependencies": { + "axios": "^0.17.1", "dateformat": "^2.0.0", "js-logger": "^1.3.0", "jwt-decode": "^2.2.0", @@ -22,7 +23,6 @@ "semantic-ui-css": "^2.2.10", "vue": "^2.3.3", "vue-lazyload": "^1.1.4", - "vue-resource": "^1.3.4", "vue-router": "^2.3.1", "vue-upload-component": "^2.7.4", "vuedraggable": "^2.14.1", @@ -69,6 +69,7 @@ "karma-phantomjs-launcher": "^1.0.2", "karma-phantomjs-shim": "^1.4.0", "karma-sinon-chai": "^1.3.1", + "karma-sinon-stub-promise": "^1.0.0", "karma-sourcemap-loader": "^0.3.7", "karma-spec-reporter": "0.0.30", "karma-webpack": "^2.0.2", @@ -87,6 +88,7 @@ "shelljs": "^0.7.6", "sinon": "^2.1.0", "sinon-chai": "^2.8.0", + "sinon-stub-promise": "^4.0.0", "url-loader": "^0.5.8", "vue-loader": "^12.1.0", "vue-style-loader": "^3.0.1", diff --git a/front/src/components/audio/Search.vue b/front/src/components/audio/Search.vue index 2811c2b5c..bb0881862 100644 --- a/front/src/components/audio/Search.vue +++ b/front/src/components/audio/Search.vue @@ -29,13 +29,11 @@ diff --git a/front/src/components/audio/SearchBar.vue b/front/src/components/audio/SearchBar.vue index 9d8b39f87..56a8e77be 100644 --- a/front/src/components/audio/SearchBar.vue +++ b/front/src/components/audio/SearchBar.vue @@ -6,6 +6,7 @@
+ @@ -25,6 +26,9 @@ export default { onSelect (result, response) { router.push(result.routerUrl) }, + onSearchQuery (query) { + self.$emit('search') + }, apiSettings: { beforeXHR: function (xhrObject) { xhrObject.setRequestHeader('Authorization', self.$store.getters['auth/header']) From 28c0121c89909bc0c805dee413401f2e1ce12cac Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 15 Feb 2018 22:34:14 +0100 Subject: [PATCH 13/32] Smaller padding in menu and sections on small resolutions --- front/src/App.vue | 11 ++++++----- front/src/components/library/Library.vue | 11 +++++++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/front/src/App.vue b/front/src/App.vue index fb1c27a89..d310c3ca9 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -40,6 +40,7 @@ export default { // and we end up with CSS rules not applied, // see https://github.com/webpack/webpack/issues/215 @import 'semantic/semantic.css'; +@import 'style/vendor/media'; #app { @@ -56,11 +57,11 @@ export default { .main-pusher { padding: 1.5rem 0; } -#footer { - padding: 4em; -} -.ui.stripe.segment { - padding: 4em; +.ui.stripe.segment, #footer { + padding: 2em; + @include media(">tablet") { + padding: 4em; + } } .ui.small.text.container { diff --git a/front/src/components/library/Library.vue b/front/src/components/library/Library.vue index c27313dc3..5fe192022 100644 --- a/front/src/components/library/Library.vue +++ b/front/src/components/library/Library.vue @@ -22,8 +22,12 @@ export default { From d60804c9db579b055f396468f85eff4531521711 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 17 Feb 2018 21:25:21 +0100 Subject: [PATCH 23/32] Added raven on server side --- .env.dev | 2 ++ api/config/settings/common.py | 19 +++++++++++++++++++ api/requirements/base.txt | 1 + deploy/env.prod.sample | 7 +++++++ 4 files changed, 29 insertions(+) diff --git a/.env.dev b/.env.dev index bc2d667b1..e27084a69 100644 --- a/.env.dev +++ b/.env.dev @@ -1,3 +1,5 @@ BACKEND_URL=http://localhost:6001 API_AUTHENTICATION_REQUIRED=True CACHALOT_ENABLED=False +RAVEN_ENABLED=false +RAVEN_DSN=https://44332e9fdd3d42879c7d35bf8562c6a4:0062dc16a22b41679cd5765e5342f716@sentry.eliotberriot.com/5 diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 2761f9152..9e17267bb 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -12,6 +12,7 @@ from __future__ import absolute_import, unicode_literals import os import environ +from funkwhale_api import __version__ ROOT_DIR = environ.Path(__file__) - 3 # (/a/b/myfile.py - 3 = /) APPS_DIR = ROOT_DIR.path('funkwhale_api') @@ -56,6 +57,23 @@ THIRD_PARTY_APPS = ( 'django_filters', ) + +# Sentry +RAVEN_ENABLED = env.bool("RAVEN_ENABLED", default=False) +RAVEN_DSN = env("RAVEN_DSN", default='') + +if RAVEN_ENABLED: + RAVEN_CONFIG = { + 'dsn': RAVEN_DSN, + # If you are using git, you can also automatically configure the + # release based on the git info. + 'release': __version__, + } + THIRD_PARTY_APPS += ( + 'raven.contrib.django.raven_compat', + ) + + # Apps specific for this project go here. LOCAL_APPS = ( 'funkwhale_api.users', # custom users app @@ -72,6 +90,7 @@ LOCAL_APPS = ( ) # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps + INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS # MIDDLEWARE CONFIGURATION diff --git a/api/requirements/base.txt b/api/requirements/base.txt index ce0eb9b85..f38da9629 100644 --- a/api/requirements/base.txt +++ b/api/requirements/base.txt @@ -56,3 +56,4 @@ git+https://github.com/EliotBerriot/django-cachalot.git@django-2 django-dynamic-preferences>=1.5,<1.6 pyacoustid>=1.1.5,<1.2 +raven>=6.5,<7 diff --git a/deploy/env.prod.sample b/deploy/env.prod.sample index 9cbe278e8..5bdfeb9c6 100644 --- a/deploy/env.prod.sample +++ b/deploy/env.prod.sample @@ -78,3 +78,10 @@ API_AUTHENTICATION_REQUIRED=True # public: anybody can register an account # disabled: nobody can register an account REGISTRATION_MODE=disabled + +# Sentry/Raven error reporting (server side) +# Enable Raven if you want to help improve funkwhale by +# automatically sending error reports our Sentry instance. +# This will help us detect and correct bugs +RAVEN_ENABLED=false +RAVEN_DSN=https://44332e9fdd3d42879c7d35bf8562c6a4:0062dc16a22b41679cd5765e5342f716@sentry.eliotberriot.com/5 From 358b3eb0604b98ea868194cc393f65d92721b429 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 17 Feb 2018 21:30:04 +0100 Subject: [PATCH 24/32] Changelog: can now report front and backend errors to sentry (fix #8) --- CHANGELOG | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 97362e827..c3aac8eac 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,11 @@ Changelog - Front: added some unittests for the store (#55) - Front: fixed broken login redirection when 401 - Front: Removed autoplay on page reload +- API: Added a /instance/settings endpoint +- Front: load /instance/settings on page load +- Added settings to report JS and Python error to a Sentry instance + This is disabled by default, but feel free to enable it if you want + to help us by sending your error reports :) (#8) 0.3.5 (2018-01-07) From 2f8a026afa15a4a1d92cdad89852f00fe0fbca74 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 18 Feb 2018 14:28:44 +0100 Subject: [PATCH 25/32] Can now configure allowed host in all settings --- .gitlab-ci.yml | 1 + api/config/settings/common.py | 4 ++++ api/config/settings/production.py | 1 - api/test.yml | 1 + 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cde12894a..0fa450c46 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,6 +20,7 @@ test_api: paths: - "$PIP_CACHE_DIR" variables: + DJANGO_ALLOWED_HOSTS: "localhost" DATABASE_URL: "postgresql://postgres@postgres/postgres" before_script: diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 9e17267bb..6d02cbbc1 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -23,6 +23,10 @@ try: env.read_env(ROOT_DIR.file('.env')) except FileNotFoundError: pass + +ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS') + + # APP CONFIGURATION # ------------------------------------------------------------------------------ DJANGO_APPS = ( diff --git a/api/config/settings/production.py b/api/config/settings/production.py index e00983305..df15d325f 100644 --- a/api/config/settings/production.py +++ b/api/config/settings/production.py @@ -54,7 +54,6 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') # ------------------------------------------------------------------------------ # Hosts/domain names that are valid for this site # See https://docs.djangoproject.com/en/1.6/ref/settings/#allowed-hosts -ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS') CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS # END SITE CONFIGURATION diff --git a/api/test.yml b/api/test.yml index c59ce45bb..e892dfb17 100644 --- a/api/test.yml +++ b/api/test.yml @@ -10,6 +10,7 @@ services: volumes: - .:/app environment: + - "DJANGO_ALLOWED_HOSTS=localhost" - "DATABASE_URL=postgresql://postgres@postgres/postgres" postgres: image: postgres From 50af04345bedc3f3d74b282e3207e37855e1bbd2 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 18 Feb 2018 14:29:35 +0100 Subject: [PATCH 26/32] Use webpack dev proxy to serve api/media --- api/funkwhale_api/music/serializers.py | 3 --- dev.yml | 4 +--- docker/nginx/conf.dev | 4 ++-- front/config/index.js | 15 ++++++++++++++- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index 9f96dad0e..506893a4d 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -31,10 +31,7 @@ class TrackFileSerializer(serializers.ModelSerializer): fields = ('id', 'path', 'duration', 'source', 'filename', 'track') def get_path(self, o): - request = self.context.get('request') url = o.path - if request: - url = request.build_absolute_uri(url) return url diff --git a/dev.yml b/dev.yml index 971e38b62..e3cd50da7 100644 --- a/dev.yml +++ b/dev.yml @@ -49,13 +49,11 @@ services: - ./api:/app - ./data/music:/music environment: - - "DJANGO_ALLOWED_HOSTS=localhost" + - "DJANGO_ALLOWED_HOSTS=localhost,nginx" - "DJANGO_SETTINGS_MODULE=config.settings.local" - "DJANGO_SECRET_KEY=dev" - "DATABASE_URL=postgresql://postgres@postgres/postgres" - "CACHE_URL=redis://redis:6379/0" - ports: - - "12081:12081" links: - postgres - redis diff --git a/docker/nginx/conf.dev b/docker/nginx/conf.dev index 48436173b..1b749c30a 100644 --- a/docker/nginx/conf.dev +++ b/docker/nginx/conf.dev @@ -40,8 +40,8 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Host $host:$server_port; - proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-Host localhost:8080; + proxy_set_header X-Forwarded-Port 8080; proxy_redirect off; proxy_pass http://api:12081/; } diff --git a/front/config/index.js b/front/config/index.js index a312c7b26..7ce6e26e1 100644 --- a/front/config/index.js +++ b/front/config/index.js @@ -28,7 +28,20 @@ module.exports = { autoOpenBrowser: true, assetsSubDirectory: 'static', assetsPublicPath: '/', - proxyTable: {}, + proxyTable: { + '/api': { + target: 'http://nginx:6001', + changeOrigin: true, + }, + '/media': { + target: 'http://nginx:6001', + changeOrigin: true, + }, + '/staticfiles': { + target: 'http://nginx:6001', + changeOrigin: true, + } + }, // CSS Sourcemaps off by default because relative paths are "buggy" // with this option, according to the CSS-Loader README // (https://github.com/webpack/css-loader#sourcemaps) From 827e4ff982a66340c18f09117e1daa1dfe9e0f99 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 18 Feb 2018 14:29:59 +0100 Subject: [PATCH 27/32] Removed now useless BACKEND_URL setting --- .env.dev | 1 - front/config/prod.env.js | 2 +- front/src/config.js | 6 ------ 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/.env.dev b/.env.dev index e27084a69..d42cdad02 100644 --- a/.env.dev +++ b/.env.dev @@ -1,4 +1,3 @@ -BACKEND_URL=http://localhost:6001 API_AUTHENTICATION_REQUIRED=True CACHALOT_ENABLED=False RAVEN_ENABLED=false diff --git a/front/config/prod.env.js b/front/config/prod.env.js index fe0e80b8f..decfe3615 100644 --- a/front/config/prod.env.js +++ b/front/config/prod.env.js @@ -1,4 +1,4 @@ module.exports = { NODE_ENV: '"production"', - BACKEND_URL: '"' + (process.env.BACKEND_URL || '/') + '"' + BACKEND_URL: '"/"' } diff --git a/front/src/config.js b/front/src/config.js index b0ceb7892..47d9d7b8b 100644 --- a/front/src/config.js +++ b/front/src/config.js @@ -1,12 +1,6 @@ class Config { constructor () { this.BACKEND_URL = process.env.BACKEND_URL - if (this.BACKEND_URL === '/') { - this.BACKEND_URL = window.location.protocol + '//' + window.location.hostname + ':' + window.location.port - } - if (this.BACKEND_URL.slice(-1) !== '/') { - this.BACKEND_URL += '/' - } this.API_URL = this.BACKEND_URL + 'api/v1/' } } From 0678d6ab72b964b1debea1bb7ba16cecc289dad8 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 18 Feb 2018 14:30:11 +0100 Subject: [PATCH 28/32] Added color-thief --- front/src/vendor/color-thief.js | 660 ++++++++++++++++++++++++++++++++ 1 file changed, 660 insertions(+) create mode 100644 front/src/vendor/color-thief.js diff --git a/front/src/vendor/color-thief.js b/front/src/vendor/color-thief.js new file mode 100644 index 000000000..0acb7c13a --- /dev/null +++ b/front/src/vendor/color-thief.js @@ -0,0 +1,660 @@ +/* eslint-disable */ +/* + * Color Thief v2.0 + * by Lokesh Dhakar - http://www.lokeshdhakar.com + * + * Thanks + * ------ + * Nick Rabinowitz - For creating quantize.js. + * John Schulz - For clean up and optimization. @JFSIII + * Nathan Spady - For adding drag and drop support to the demo page. + * + * License + * ------- + * Copyright 2011, 2015 Lokesh Dhakar + * Released under the MIT license + * https://raw.githubusercontent.com/lokesh/color-thief/master/LICENSE + * + * @license + */ + + +/* + CanvasImage Class + Class that wraps the html image element and canvas. + It also simplifies some of the canvas context manipulation + with a set of helper functions. +*/ +var CanvasImage = function (image) { + this.canvas = document.createElement('canvas'); + this.context = this.canvas.getContext('2d'); + + document.body.appendChild(this.canvas); + + this.width = this.canvas.width = image.width; + this.height = this.canvas.height = image.height; + + this.context.drawImage(image, 0, 0, this.width, this.height); +}; + +CanvasImage.prototype.clear = function () { + this.context.clearRect(0, 0, this.width, this.height); +}; + +CanvasImage.prototype.update = function (imageData) { + this.context.putImageData(imageData, 0, 0); +}; + +CanvasImage.prototype.getPixelCount = function () { + return this.width * this.height; +}; + +CanvasImage.prototype.getImageData = function () { + return this.context.getImageData(0, 0, this.width, this.height); +}; + +CanvasImage.prototype.removeCanvas = function () { + this.canvas.parentNode.removeChild(this.canvas); +}; + + +var ColorThief = function () {}; + +/* + * getColor(sourceImage[, quality]) + * returns {r: num, g: num, b: num} + * + * Use the median cut algorithm provided by quantize.js to cluster similar + * colors and return the base color from the largest cluster. + * + * Quality is an optional argument. It needs to be an integer. 1 is the highest quality settings. + * 10 is the default. There is a trade-off between quality and speed. The bigger the number, the + * faster a color will be returned but the greater the likelihood that it will not be the visually + * most dominant color. + * + * */ +ColorThief.prototype.getColor = function(sourceImage, quality) { + var palette = this.getPalette(sourceImage, 5, quality); + var dominantColor = palette[0]; + return dominantColor; +}; + + +/* + * getPalette(sourceImage[, colorCount, quality]) + * returns array[ {r: num, g: num, b: num}, {r: num, g: num, b: num}, ...] + * + * Use the median cut algorithm provided by quantize.js to cluster similar colors. + * + * colorCount determines the size of the palette; the number of colors returned. If not set, it + * defaults to 10. + * + * BUGGY: Function does not always return the requested amount of colors. It can be +/- 2. + * + * quality is an optional argument. It needs to be an integer. 1 is the highest quality settings. + * 10 is the default. There is a trade-off between quality and speed. The bigger the number, the + * faster the palette generation but the greater the likelihood that colors will be missed. + * + * + */ +ColorThief.prototype.getPalette = function(sourceImage, colorCount, quality) { + + if (typeof colorCount === 'undefined' || colorCount < 2 || colorCount > 256) { + colorCount = 10; + } + if (typeof quality === 'undefined' || quality < 1) { + quality = 10; + } + + // Create custom CanvasImage object + var image = new CanvasImage(sourceImage); + var imageData = image.getImageData(); + var pixels = imageData.data; + var pixelCount = image.getPixelCount(); + + // Store the RGB values in an array format suitable for quantize function + var pixelArray = []; + for (var i = 0, offset, r, g, b, a; i < pixelCount; i = i + quality) { + offset = i * 4; + r = pixels[offset + 0]; + g = pixels[offset + 1]; + b = pixels[offset + 2]; + a = pixels[offset + 3]; + // If pixel is mostly opaque and not white + if (a >= 125) { + if (!(r > 250 && g > 250 && b > 250)) { + pixelArray.push([r, g, b]); + } + } + } + + // Send array to quantize function which clusters values + // using median cut algorithm + var cmap = MMCQ.quantize(pixelArray, colorCount); + var palette = cmap? cmap.palette() : null; + + // Clean up + image.removeCanvas(); + + return palette; +}; + +ColorThief.prototype.getColorFromUrl = function(imageUrl, callback, quality) { + sourceImage = document.createElement("img"); + var thief = this; + sourceImage.addEventListener('load' , function(){ + var palette = thief.getPalette(sourceImage, 5, quality); + var dominantColor = palette[0]; + callback(dominantColor, imageUrl); + }); + sourceImage.src = imageUrl +}; + + +ColorThief.prototype.getImageData = function(imageUrl, callback) { + xhr = new XMLHttpRequest(); + xhr.open('GET', imageUrl, true); + xhr.responseType = 'arraybuffer' + xhr.onload = function(e) { + if (this.status == 200) { + uInt8Array = new Uint8Array(this.response) + i = uInt8Array.length + binaryString = new Array(i); + for (var i = 0; i < uInt8Array.length; i++){ + binaryString[i] = String.fromCharCode(uInt8Array[i]) + } + data = binaryString.join('') + base64 = window.btoa(data) + callback ("data:image/png;base64,"+base64) + } + } + xhr.send(); +}; + +ColorThief.prototype.getColorAsync = function(imageUrl, callback, quality) { + var thief = this; + this.getImageData(imageUrl, function(imageData){ + sourceImage = document.createElement("img"); + sourceImage.addEventListener('load' , function(){ + var palette = thief.getPalette(sourceImage, 5, quality); + var dominantColor = palette[0]; + callback(dominantColor, this); + }); + sourceImage.src = imageData; + }); +}; + + + +/*! + * quantize.js Copyright 2008 Nick Rabinowitz. + * Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + * @license + */ + +// fill out a couple protovis dependencies +/*! + * Block below copied from Protovis: http://mbostock.github.com/protovis/ + * Copyright 2010 Stanford Visualization Group + * Licensed under the BSD License: http://www.opensource.org/licenses/bsd-license.php + * @license + */ +if (!pv) { + var pv = { + map: function(array, f) { + var o = {}; + return f ? array.map(function(d, i) { o.index = i; return f.call(o, d); }) : array.slice(); + }, + naturalOrder: function(a, b) { + return (a < b) ? -1 : ((a > b) ? 1 : 0); + }, + sum: function(array, f) { + var o = {}; + return array.reduce(f ? function(p, d, i) { o.index = i; return p + f.call(o, d); } : function(p, d) { return p + d; }, 0); + }, + max: function(array, f) { + return Math.max.apply(null, f ? pv.map(array, f) : array); + } + }; +} + + + +/** + * Basic Javascript port of the MMCQ (modified median cut quantization) + * algorithm from the Leptonica library (http://www.leptonica.com/). + * Returns a color map you can use to map original pixels to the reduced + * palette. Still a work in progress. + * + * @author Nick Rabinowitz + * @example + +// array of pixels as [R,G,B] arrays +var myPixels = [[190,197,190], [202,204,200], [207,214,210], [211,214,211], [205,207,207] + // etc + ]; +var maxColors = 4; + +var cmap = MMCQ.quantize(myPixels, maxColors); +var newPalette = cmap.palette(); +var newPixels = myPixels.map(function(p) { + return cmap.map(p); +}); + + */ +var MMCQ = (function() { + // private constants + var sigbits = 5, + rshift = 8 - sigbits, + maxIterations = 1000, + fractByPopulations = 0.75; + + // get reduced-space color index for a pixel + function getColorIndex(r, g, b) { + return (r << (2 * sigbits)) + (g << sigbits) + b; + } + + // Simple priority queue + function PQueue(comparator) { + var contents = [], + sorted = false; + + function sort() { + contents.sort(comparator); + sorted = true; + } + + return { + push: function(o) { + contents.push(o); + sorted = false; + }, + peek: function(index) { + if (!sorted) sort(); + if (index===undefined) index = contents.length - 1; + return contents[index]; + }, + pop: function() { + if (!sorted) sort(); + return contents.pop(); + }, + size: function() { + return contents.length; + }, + map: function(f) { + return contents.map(f); + }, + debug: function() { + if (!sorted) sort(); + return contents; + } + }; + } + + // 3d color space box + function VBox(r1, r2, g1, g2, b1, b2, histo) { + var vbox = this; + vbox.r1 = r1; + vbox.r2 = r2; + vbox.g1 = g1; + vbox.g2 = g2; + vbox.b1 = b1; + vbox.b2 = b2; + vbox.histo = histo; + } + VBox.prototype = { + volume: function(force) { + var vbox = this; + if (!vbox._volume || force) { + vbox._volume = ((vbox.r2 - vbox.r1 + 1) * (vbox.g2 - vbox.g1 + 1) * (vbox.b2 - vbox.b1 + 1)); + } + return vbox._volume; + }, + count: function(force) { + var vbox = this, + histo = vbox.histo; + if (!vbox._count_set || force) { + var npix = 0, + index, i, j, k; + for (i = vbox.r1; i <= vbox.r2; i++) { + for (j = vbox.g1; j <= vbox.g2; j++) { + for (k = vbox.b1; k <= vbox.b2; k++) { + index = getColorIndex(i,j,k); + npix += (histo[index] || 0); + } + } + } + vbox._count = npix; + vbox._count_set = true; + } + return vbox._count; + }, + copy: function() { + var vbox = this; + return new VBox(vbox.r1, vbox.r2, vbox.g1, vbox.g2, vbox.b1, vbox.b2, vbox.histo); + }, + avg: function(force) { + var vbox = this, + histo = vbox.histo; + if (!vbox._avg || force) { + var ntot = 0, + mult = 1 << (8 - sigbits), + rsum = 0, + gsum = 0, + bsum = 0, + hval, + i, j, k, histoindex; + for (i = vbox.r1; i <= vbox.r2; i++) { + for (j = vbox.g1; j <= vbox.g2; j++) { + for (k = vbox.b1; k <= vbox.b2; k++) { + histoindex = getColorIndex(i,j,k); + hval = histo[histoindex] || 0; + ntot += hval; + rsum += (hval * (i + 0.5) * mult); + gsum += (hval * (j + 0.5) * mult); + bsum += (hval * (k + 0.5) * mult); + } + } + } + if (ntot) { + vbox._avg = [~~(rsum/ntot), ~~(gsum/ntot), ~~(bsum/ntot)]; + } else { +// console.log('empty box'); + vbox._avg = [ + ~~(mult * (vbox.r1 + vbox.r2 + 1) / 2), + ~~(mult * (vbox.g1 + vbox.g2 + 1) / 2), + ~~(mult * (vbox.b1 + vbox.b2 + 1) / 2) + ]; + } + } + return vbox._avg; + }, + contains: function(pixel) { + var vbox = this, + rval = pixel[0] >> rshift; + gval = pixel[1] >> rshift; + bval = pixel[2] >> rshift; + return (rval >= vbox.r1 && rval <= vbox.r2 && + gval >= vbox.g1 && gval <= vbox.g2 && + bval >= vbox.b1 && bval <= vbox.b2); + } + }; + + // Color map + function CMap() { + this.vboxes = new PQueue(function(a,b) { + return pv.naturalOrder( + a.vbox.count()*a.vbox.volume(), + b.vbox.count()*b.vbox.volume() + ); + }); + } + CMap.prototype = { + push: function(vbox) { + this.vboxes.push({ + vbox: vbox, + color: vbox.avg() + }); + }, + palette: function() { + return this.vboxes.map(function(vb) { return vb.color; }); + }, + size: function() { + return this.vboxes.size(); + }, + map: function(color) { + var vboxes = this.vboxes; + for (var i=0; i 251 + var idx = vboxes.length-1, + highest = vboxes[idx].color; + if (highest[0] > 251 && highest[1] > 251 && highest[2] > 251) + vboxes[idx].color = [255,255,255]; + } + }; + + // histo (1-d array, giving the number of pixels in + // each quantized region of color space), or null on error + function getHisto(pixels) { + var histosize = 1 << (3 * sigbits), + histo = new Array(histosize), + index, rval, gval, bval; + pixels.forEach(function(pixel) { + rval = pixel[0] >> rshift; + gval = pixel[1] >> rshift; + bval = pixel[2] >> rshift; + index = getColorIndex(rval, gval, bval); + histo[index] = (histo[index] || 0) + 1; + }); + return histo; + } + + function vboxFromPixels(pixels, histo) { + var rmin=1000000, rmax=0, + gmin=1000000, gmax=0, + bmin=1000000, bmax=0, + rval, gval, bval; + // find min/max + pixels.forEach(function(pixel) { + rval = pixel[0] >> rshift; + gval = pixel[1] >> rshift; + bval = pixel[2] >> rshift; + if (rval < rmin) rmin = rval; + else if (rval > rmax) rmax = rval; + if (gval < gmin) gmin = gval; + else if (gval > gmax) gmax = gval; + if (bval < bmin) bmin = bval; + else if (bval > bmax) bmax = bval; + }); + return new VBox(rmin, rmax, gmin, gmax, bmin, bmax, histo); + } + + function medianCutApply(histo, vbox) { + if (!vbox.count()) return; + + var rw = vbox.r2 - vbox.r1 + 1, + gw = vbox.g2 - vbox.g1 + 1, + bw = vbox.b2 - vbox.b1 + 1, + maxw = pv.max([rw, gw, bw]); + // only one pixel, no split + if (vbox.count() == 1) { + return [vbox.copy()]; + } + /* Find the partial sum arrays along the selected axis. */ + var total = 0, + partialsum = [], + lookaheadsum = [], + i, j, k, sum, index; + if (maxw == rw) { + for (i = vbox.r1; i <= vbox.r2; i++) { + sum = 0; + for (j = vbox.g1; j <= vbox.g2; j++) { + for (k = vbox.b1; k <= vbox.b2; k++) { + index = getColorIndex(i,j,k); + sum += (histo[index] || 0); + } + } + total += sum; + partialsum[i] = total; + } + } + else if (maxw == gw) { + for (i = vbox.g1; i <= vbox.g2; i++) { + sum = 0; + for (j = vbox.r1; j <= vbox.r2; j++) { + for (k = vbox.b1; k <= vbox.b2; k++) { + index = getColorIndex(j,i,k); + sum += (histo[index] || 0); + } + } + total += sum; + partialsum[i] = total; + } + } + else { /* maxw == bw */ + for (i = vbox.b1; i <= vbox.b2; i++) { + sum = 0; + for (j = vbox.r1; j <= vbox.r2; j++) { + for (k = vbox.g1; k <= vbox.g2; k++) { + index = getColorIndex(j,k,i); + sum += (histo[index] || 0); + } + } + total += sum; + partialsum[i] = total; + } + } + partialsum.forEach(function(d,i) { + lookaheadsum[i] = total-d; + }); + function doCut(color) { + var dim1 = color + '1', + dim2 = color + '2', + left, right, vbox1, vbox2, d2, count2=0; + for (i = vbox[dim1]; i <= vbox[dim2]; i++) { + if (partialsum[i] > total / 2) { + vbox1 = vbox.copy(); + vbox2 = vbox.copy(); + left = i - vbox[dim1]; + right = vbox[dim2] - i; + if (left <= right) + d2 = Math.min(vbox[dim2] - 1, ~~(i + right / 2)); + else d2 = Math.max(vbox[dim1], ~~(i - 1 - left / 2)); + // avoid 0-count boxes + while (!partialsum[d2]) d2++; + count2 = lookaheadsum[d2]; + while (!count2 && partialsum[d2-1]) count2 = lookaheadsum[--d2]; + // set dimensions + vbox1[dim2] = d2; + vbox2[dim1] = vbox1[dim2] + 1; +// console.log('vbox counts:', vbox.count(), vbox1.count(), vbox2.count()); + return [vbox1, vbox2]; + } + } + + } + // determine the cut planes + return maxw == rw ? doCut('r') : + maxw == gw ? doCut('g') : + doCut('b'); + } + + function quantize(pixels, maxcolors) { + // short-circuit + if (!pixels.length || maxcolors < 2 || maxcolors > 256) { +// console.log('wrong number of maxcolors'); + return false; + } + + // XXX: check color content and convert to grayscale if insufficient + + var histo = getHisto(pixels), + histosize = 1 << (3 * sigbits); + + // check that we aren't below maxcolors already + var nColors = 0; + histo.forEach(function() { nColors++; }); + if (nColors <= maxcolors) { + // XXX: generate the new colors from the histo and return + } + + // get the beginning vbox from the colors + var vbox = vboxFromPixels(pixels, histo), + pq = new PQueue(function(a,b) { return pv.naturalOrder(a.count(), b.count()); }); + pq.push(vbox); + + // inner function to do the iteration + function iter(lh, target) { + var ncolors = 1, + niters = 0, + vbox; + while (niters < maxIterations) { + vbox = lh.pop(); + if (!vbox.count()) { /* just put it back */ + lh.push(vbox); + niters++; + continue; + } + // do the cut + var vboxes = medianCutApply(histo, vbox), + vbox1 = vboxes[0], + vbox2 = vboxes[1]; + + if (!vbox1) { +// console.log("vbox1 not defined; shouldn't happen!"); + return; + } + lh.push(vbox1); + if (vbox2) { /* vbox2 can be null */ + lh.push(vbox2); + ncolors++; + } + if (ncolors >= target) return; + if (niters++ > maxIterations) { +// console.log("infinite loop; perhaps too few pixels!"); + return; + } + } + } + + // first set of colors, sorted by population + iter(pq, fractByPopulations * maxcolors); + + // Re-sort by the product of pixel occupancy times the size in color space. + var pq2 = new PQueue(function(a,b) { + return pv.naturalOrder(a.count()*a.volume(), b.count()*b.volume()); + }); + while (pq.size()) { + pq2.push(pq.pop()); + } + + // next set - generate the median cuts using the (npix * vol) sorting. + iter(pq2, maxcolors - pq2.size()); + + // calculate the actual colors + var cmap = new CMap(); + while (pq2.size()) { + cmap.push(pq2.pop()); + } + + return cmap; + } + + return { + quantize: quantize + }; +})(); + +export default ColorThief From e67e2903252a9e54fb24ba25f3728c33abecdac9 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 18 Feb 2018 14:30:45 +0100 Subject: [PATCH 29/32] Romoved unwanted space and margin in sidebar header --- front/src/components/Sidebar.vue | 6 +----- front/src/components/audio/SearchBar.vue | 3 +-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index 6aaf04c42..df9eb00ec 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -6,8 +6,7 @@ - - @@ -224,9 +223,6 @@ $sidebar-color: #1B1C1D; .ui.search { display: block; - > a { - margin-right: 1.5rem; - } .collapse.button { margin-right: 0.5rem; margin-top: 0.5rem; diff --git a/front/src/components/audio/SearchBar.vue b/front/src/components/audio/SearchBar.vue index 56a8e77be..988ff0a7d 100644 --- a/front/src/components/audio/SearchBar.vue +++ b/front/src/components/audio/SearchBar.vue @@ -1,7 +1,6 @@ @@ -150,6 +148,7 @@ export default { $sidebar-color: #1B1C1D; .sidebar { + background: $sidebar-color; @include media(">tablet") { display:flex; flex-direction:column; @@ -211,11 +210,6 @@ $sidebar-color: #1B1C1D; flex: 1; } -.player-wrapper { - border-top: 1px solid rgba(255, 255, 255, 0.1) !important; - background-color: rgb(46, 46, 46) !important; -} - .logo { cursor: pointer; display: inline-block; diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue index 9388c2682..e44a92d4f 100644 --- a/front/src/components/audio/Player.vue +++ b/front/src/components/audio/Player.vue @@ -1,145 +1,147 @@