Merge branch 'release/0.4'
This commit is contained in:
commit
bb3ed76023
3
.env.dev
3
.env.dev
|
@ -1,3 +1,4 @@
|
||||||
BACKEND_URL=http://localhost:6001
|
|
||||||
API_AUTHENTICATION_REQUIRED=True
|
API_AUTHENTICATION_REQUIRED=True
|
||||||
CACHALOT_ENABLED=False
|
CACHALOT_ENABLED=False
|
||||||
|
RAVEN_ENABLED=false
|
||||||
|
RAVEN_DSN=https://44332e9fdd3d42879c7d35bf8562c6a4:0062dc16a22b41679cd5765e5342f716@sentry.eliotberriot.com/5
|
||||||
|
|
|
@ -2,7 +2,7 @@ variables:
|
||||||
IMAGE_NAME: funkwhale/funkwhale
|
IMAGE_NAME: funkwhale/funkwhale
|
||||||
IMAGE: $IMAGE_NAME:$CI_COMMIT_REF_NAME
|
IMAGE: $IMAGE_NAME:$CI_COMMIT_REF_NAME
|
||||||
IMAGE_LATEST: $IMAGE_NAME:latest
|
IMAGE_LATEST: $IMAGE_NAME:latest
|
||||||
|
PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache"
|
||||||
|
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
|
@ -14,40 +14,62 @@ test_api:
|
||||||
services:
|
services:
|
||||||
- postgres:9.4
|
- postgres:9.4
|
||||||
stage: test
|
stage: test
|
||||||
image: funkwhale/funkwhale:base
|
image: funkwhale/funkwhale:latest
|
||||||
|
cache:
|
||||||
|
key: "$CI_PROJECT_ID/pip_cache"
|
||||||
|
paths:
|
||||||
|
- "$PIP_CACHE_DIR"
|
||||||
variables:
|
variables:
|
||||||
PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache"
|
DJANGO_ALLOWED_HOSTS: "localhost"
|
||||||
DATABASE_URL: "postgresql://postgres@postgres/postgres"
|
DATABASE_URL: "postgresql://postgres@postgres/postgres"
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- python3 -m venv --copies virtualenv
|
|
||||||
- source virtualenv/bin/activate
|
|
||||||
- cd api
|
- cd api
|
||||||
- pip install -r requirements/base.txt
|
- pip install -r requirements/base.txt
|
||||||
- pip install -r requirements/local.txt
|
- pip install -r requirements/local.txt
|
||||||
- pip install -r requirements/test.txt
|
- pip install -r requirements/test.txt
|
||||||
script:
|
script:
|
||||||
- pytest
|
- pytest
|
||||||
cache:
|
|
||||||
key: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME"
|
|
||||||
paths:
|
|
||||||
- "$CI_PROJECT_DIR/pip-cache"
|
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- docker
|
||||||
|
|
||||||
build_front:
|
|
||||||
stage: build
|
test_front:
|
||||||
image: node:6-alpine
|
stage: test
|
||||||
|
image: node:9
|
||||||
before_script:
|
before_script:
|
||||||
- cd front
|
- cd front
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- npm install
|
- yarn install
|
||||||
- npm run build
|
- yarn run unit
|
||||||
cache:
|
cache:
|
||||||
key: "$CI_COMMIT_REF_NAME"
|
key: "$CI_PROJECT_ID/front_dependencies"
|
||||||
paths:
|
paths:
|
||||||
- front/node_modules
|
- 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:
|
artifacts:
|
||||||
name: "front_${CI_COMMIT_REF_NAME}"
|
name: "front_${CI_COMMIT_REF_NAME}"
|
||||||
paths:
|
paths:
|
||||||
|
|
19
CHANGELOG
19
CHANGELOG
|
@ -2,8 +2,23 @@ Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
|
||||||
0.3.5 (Unreleased)
|
0.5 (Unreleased)
|
||||||
------------------
|
----------------
|
||||||
|
|
||||||
|
|
||||||
|
0.4 (2018-02-18)
|
||||||
|
----------------
|
||||||
|
|
||||||
|
- Front: ambiant colors in player based on current track cover (#59)
|
||||||
|
- Front: simplified front dev setup thanks to webpack proxy (#59)
|
||||||
|
- 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)
|
0.3.5 (2018-01-07)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
|
from funkwhale_api.instance import views as instance_views
|
||||||
from funkwhale_api.music import views
|
from funkwhale_api.music import views
|
||||||
from funkwhale_api.playlists import views as playlists_views
|
from funkwhale_api.playlists import views as playlists_views
|
||||||
from rest_framework_jwt import views as jwt_views
|
from rest_framework_jwt import views as jwt_views
|
||||||
|
@ -25,6 +26,10 @@ router.register(
|
||||||
v1_patterns = router.urls
|
v1_patterns = router.urls
|
||||||
|
|
||||||
v1_patterns += [
|
v1_patterns += [
|
||||||
|
url(r'^instance/',
|
||||||
|
include(
|
||||||
|
('funkwhale_api.instance.urls', 'instance'),
|
||||||
|
namespace='instance')),
|
||||||
url(r'^providers/',
|
url(r'^providers/',
|
||||||
include(
|
include(
|
||||||
('funkwhale_api.providers.urls', 'providers'),
|
('funkwhale_api.providers.urls', 'providers'),
|
||||||
|
|
|
@ -12,6 +12,7 @@ from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import environ
|
import environ
|
||||||
|
from funkwhale_api import __version__
|
||||||
|
|
||||||
ROOT_DIR = environ.Path(__file__) - 3 # (/a/b/myfile.py - 3 = /)
|
ROOT_DIR = environ.Path(__file__) - 3 # (/a/b/myfile.py - 3 = /)
|
||||||
APPS_DIR = ROOT_DIR.path('funkwhale_api')
|
APPS_DIR = ROOT_DIR.path('funkwhale_api')
|
||||||
|
@ -22,6 +23,10 @@ try:
|
||||||
env.read_env(ROOT_DIR.file('.env'))
|
env.read_env(ROOT_DIR.file('.env'))
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS')
|
||||||
|
|
||||||
|
|
||||||
# APP CONFIGURATION
|
# APP CONFIGURATION
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
DJANGO_APPS = (
|
DJANGO_APPS = (
|
||||||
|
@ -56,10 +61,28 @@ THIRD_PARTY_APPS = (
|
||||||
'django_filters',
|
'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.
|
# Apps specific for this project go here.
|
||||||
LOCAL_APPS = (
|
LOCAL_APPS = (
|
||||||
'funkwhale_api.users', # custom users app
|
'funkwhale_api.users', # custom users app
|
||||||
# Your stuff: custom apps go here
|
# Your stuff: custom apps go here
|
||||||
|
'funkwhale_api.instance',
|
||||||
'funkwhale_api.music',
|
'funkwhale_api.music',
|
||||||
'funkwhale_api.favorites',
|
'funkwhale_api.favorites',
|
||||||
'funkwhale_api.radios',
|
'funkwhale_api.radios',
|
||||||
|
@ -71,6 +94,7 @@ LOCAL_APPS = (
|
||||||
)
|
)
|
||||||
|
|
||||||
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
||||||
|
|
||||||
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
||||||
|
|
||||||
# MIDDLEWARE CONFIGURATION
|
# MIDDLEWARE CONFIGURATION
|
||||||
|
|
|
@ -54,7 +54,6 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Hosts/domain names that are valid for this site
|
# Hosts/domain names that are valid for this site
|
||||||
# See https://docs.djangoproject.com/en/1.6/ref/settings/#allowed-hosts
|
# See https://docs.djangoproject.com/en/1.6/ref/settings/#allowed-hosts
|
||||||
ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS')
|
|
||||||
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
|
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
|
||||||
|
|
||||||
# END SITE CONFIGURATION
|
# END SITE CONFIGURATION
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
__version__ = '0.3.5'
|
__version__ = '0.4'
|
||||||
__version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')])
|
__version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')])
|
||||||
|
|
|
@ -43,7 +43,7 @@ class TrackFavoriteViewSet(mixins.CreateModelMixin,
|
||||||
favorite = models.TrackFavorite.add(track=track, user=self.request.user)
|
favorite = models.TrackFavorite.add(track=track, user=self.request.user)
|
||||||
return favorite
|
return favorite
|
||||||
|
|
||||||
@list_route(methods=['delete'])
|
@list_route(methods=['delete', 'post'])
|
||||||
def remove(self, request, *args, **kwargs):
|
def remove(self, request, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
pk = int(request.data['track'])
|
pk = int(request.data['track'])
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
from dynamic_preferences import types
|
||||||
|
from dynamic_preferences.registries import global_preferences_registry
|
||||||
|
|
||||||
|
raven = types.Section('raven')
|
||||||
|
|
||||||
|
|
||||||
|
@global_preferences_registry.register
|
||||||
|
class RavenDSN(types.StringPreference):
|
||||||
|
show_in_api = True
|
||||||
|
section = raven
|
||||||
|
name = 'front_dsn'
|
||||||
|
default = 'https://9e0562d46b09442bb8f6844e50cbca2b@sentry.eliotberriot.com/4'
|
||||||
|
verbose_name = (
|
||||||
|
'A raven DSN key used to report front-ent errors to '
|
||||||
|
'a sentry instance'
|
||||||
|
)
|
||||||
|
help_text = (
|
||||||
|
'Keeping the default one will report errors to funkwhale developers'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
SENTRY_HELP_TEXT = (
|
||||||
|
'Error reporting is disabled by default but you can enable it if'
|
||||||
|
' you want to help us improve funkwhale'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@global_preferences_registry.register
|
||||||
|
class RavenEnabled(types.BooleanPreference):
|
||||||
|
show_in_api = True
|
||||||
|
section = raven
|
||||||
|
name = 'front_enabled'
|
||||||
|
default = False
|
||||||
|
verbose_name = (
|
||||||
|
'Wether error reporting to a Sentry instance using raven is enabled'
|
||||||
|
' for front-end errors'
|
||||||
|
)
|
|
@ -0,0 +1,7 @@
|
||||||
|
from django.conf.urls import url
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^settings/$', views.InstanceSettings.as_view(), name='settings'),
|
||||||
|
]
|
|
@ -0,0 +1,25 @@
|
||||||
|
from rest_framework import views
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from dynamic_preferences.api import serializers
|
||||||
|
from dynamic_preferences.registries import global_preferences_registry
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceSettings(views.APIView):
|
||||||
|
permission_classes = []
|
||||||
|
authentication_classes = []
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
manager = global_preferences_registry.manager()
|
||||||
|
manager.all()
|
||||||
|
all_preferences = manager.model.objects.all().order_by(
|
||||||
|
'section', 'name'
|
||||||
|
)
|
||||||
|
api_preferences = [
|
||||||
|
p
|
||||||
|
for p in all_preferences
|
||||||
|
if getattr(p.preference, 'show_in_api', False)
|
||||||
|
]
|
||||||
|
data = serializers.GlobalPreferenceSerializer(
|
||||||
|
api_preferences, many=True).data
|
||||||
|
return Response(data, status=200)
|
|
@ -4,7 +4,7 @@ Populates the database with fake data
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from funkwhale_api.music import models
|
from funkwhale_api.music import models
|
||||||
from funkwhale_api.music.tests import factories
|
from funkwhale_api.music import factories
|
||||||
|
|
||||||
|
|
||||||
def create_data(count=25):
|
def create_data(count=25):
|
||||||
|
@ -19,4 +19,4 @@ def create_data(count=25):
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
create_data()
|
||||||
|
|
|
@ -31,10 +31,7 @@ class TrackFileSerializer(serializers.ModelSerializer):
|
||||||
fields = ('id', 'path', 'duration', 'source', 'filename', 'track')
|
fields = ('id', 'path', 'duration', 'source', 'filename', 'track')
|
||||||
|
|
||||||
def get_path(self, o):
|
def get_path(self, o):
|
||||||
request = self.context.get('request')
|
|
||||||
url = o.path
|
url = o.path
|
||||||
if request:
|
|
||||||
url = request.build_absolute_uri(url)
|
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -47,8 +47,7 @@ mutagen>=1.39,<1.40
|
||||||
|
|
||||||
|
|
||||||
# Until this is merged
|
# Until this is merged
|
||||||
#django-taggit>=0.22,<0.23
|
django-taggit>=0.22,<0.23
|
||||||
git+https://github.com/alex/django-taggit.git@95776ac66948ed7ba7c12e35c1170551e3be66a5
|
|
||||||
# Until this is merged
|
# Until this is merged
|
||||||
git+https://github.com/EliotBerriot/PyMemoize.git@django
|
git+https://github.com/EliotBerriot/PyMemoize.git@django
|
||||||
# Until this is merged
|
# Until this is merged
|
||||||
|
@ -57,3 +56,4 @@ git+https://github.com/EliotBerriot/django-cachalot.git@django-2
|
||||||
|
|
||||||
django-dynamic-preferences>=1.5,<1.6
|
django-dynamic-preferences>=1.5,<1.6
|
||||||
pyacoustid>=1.1.5,<1.2
|
pyacoustid>=1.1.5,<1.2
|
||||||
|
raven>=6.5,<7
|
||||||
|
|
|
@ -10,6 +10,7 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
environment:
|
environment:
|
||||||
|
- "DJANGO_ALLOWED_HOSTS=localhost"
|
||||||
- "DATABASE_URL=postgresql://postgres@postgres/postgres"
|
- "DATABASE_URL=postgresql://postgres@postgres/postgres"
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres
|
image: postgres
|
||||||
|
|
|
@ -3,6 +3,7 @@ import shutil
|
||||||
import pytest
|
import pytest
|
||||||
from django.core.cache import cache as django_cache
|
from django.core.cache import cache as django_cache
|
||||||
from dynamic_preferences.registries import global_preferences_registry
|
from dynamic_preferences.registries import global_preferences_registry
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from funkwhale_api.taskapp import celery
|
from funkwhale_api.taskapp import celery
|
||||||
|
|
||||||
|
@ -29,7 +30,9 @@ def factories(db):
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def preferences(db):
|
def preferences(db):
|
||||||
yield global_preferences_registry.manager()
|
manager = global_preferences_registry.manager()
|
||||||
|
manager.all()
|
||||||
|
yield manager
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -48,6 +51,11 @@ def logged_in_client(db, factories, client):
|
||||||
delattr(client, 'user')
|
delattr(client, 'user')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def api_client(client):
|
||||||
|
return APIClient()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def superuser_client(db, factories, client):
|
def superuser_client(db, factories, client):
|
||||||
user = factories['users.SuperUser']()
|
user = factories['users.SuperUser']()
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from dynamic_preferences.api import serializers
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_list_settings_via_api(preferences, api_client):
|
||||||
|
url = reverse('api:v1:instance:settings')
|
||||||
|
all_preferences = preferences.model.objects.all()
|
||||||
|
expected_preferences = {
|
||||||
|
p.preference.identifier(): p
|
||||||
|
for p in all_preferences
|
||||||
|
if getattr(p.preference, 'show_in_api', False)}
|
||||||
|
|
||||||
|
assert len(expected_preferences) > 0
|
||||||
|
|
||||||
|
response = api_client.get(url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.data) == len(expected_preferences)
|
||||||
|
|
||||||
|
for p in response.data:
|
||||||
|
i = '__'.join([p['section'], p['name']])
|
||||||
|
assert i in expected_preferences
|
|
@ -58,11 +58,14 @@ def test_user_can_remove_favorite_via_api(logged_in_client, factories, client):
|
||||||
assert response.status_code == 204
|
assert response.status_code == 204
|
||||||
assert TrackFavorite.objects.count() == 0
|
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)
|
favorite = factories['favorites.TrackFavorite'](user=logged_in_client.user)
|
||||||
|
|
||||||
url = reverse('api:v1:favorites:tracks-remove')
|
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}),
|
url, json.dumps({'track': favorite.track.pk}),
|
||||||
content_type='application/json'
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
|
|
|
@ -78,3 +78,10 @@ API_AUTHENTICATION_REQUIRED=True
|
||||||
# public: anybody can register an account
|
# public: anybody can register an account
|
||||||
# disabled: nobody can register an account
|
# disabled: nobody can register an account
|
||||||
REGISTRATION_MODE=disabled
|
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
|
||||||
|
|
8
dev.yml
8
dev.yml
|
@ -3,9 +3,7 @@ version: '2'
|
||||||
services:
|
services:
|
||||||
|
|
||||||
front:
|
front:
|
||||||
build:
|
build: front
|
||||||
dockerfile: docker/Dockerfile.dev
|
|
||||||
context: ./front
|
|
||||||
env_file: .env.dev
|
env_file: .env.dev
|
||||||
environment:
|
environment:
|
||||||
- "HOST=0.0.0.0"
|
- "HOST=0.0.0.0"
|
||||||
|
@ -51,13 +49,11 @@ services:
|
||||||
- ./api:/app
|
- ./api:/app
|
||||||
- ./data/music:/music
|
- ./data/music:/music
|
||||||
environment:
|
environment:
|
||||||
- "DJANGO_ALLOWED_HOSTS=localhost"
|
- "DJANGO_ALLOWED_HOSTS=localhost,nginx"
|
||||||
- "DJANGO_SETTINGS_MODULE=config.settings.local"
|
- "DJANGO_SETTINGS_MODULE=config.settings.local"
|
||||||
- "DJANGO_SECRET_KEY=dev"
|
- "DJANGO_SECRET_KEY=dev"
|
||||||
- "DATABASE_URL=postgresql://postgres@postgres/postgres"
|
- "DATABASE_URL=postgresql://postgres@postgres/postgres"
|
||||||
- "CACHE_URL=redis://redis:6379/0"
|
- "CACHE_URL=redis://redis:6379/0"
|
||||||
ports:
|
|
||||||
- "12081:12081"
|
|
||||||
links:
|
links:
|
||||||
- postgres
|
- postgres
|
||||||
- redis
|
- redis
|
||||||
|
|
|
@ -40,8 +40,8 @@ http {
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header X-Forwarded-Host $host:$server_port;
|
proxy_set_header X-Forwarded-Host localhost:8080;
|
||||||
proxy_set_header X-Forwarded-Port $server_port;
|
proxy_set_header X-Forwarded-Port 8080;
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
proxy_pass http://api:12081/;
|
proxy_pass http://api:12081/;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
FROM node:6-alpine
|
FROM node:9
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
WORKDIR /app/
|
||||||
RUN mkdir /app
|
|
||||||
WORKDIR /app
|
|
||||||
ADD package.json .
|
ADD package.json .
|
||||||
|
RUN yarn install --only=production
|
||||||
|
RUN yarn install --only=dev
|
||||||
|
VOLUME ["/app/node_modules"]
|
||||||
|
COPY . .
|
||||||
|
|
||||||
RUN npm install
|
CMD ["npm", "run", "dev"]
|
||||||
|
|
||||||
ADD . .
|
|
||||||
|
|
||||||
RUN npm run build
|
|
||||||
|
|
|
@ -28,7 +28,20 @@ module.exports = {
|
||||||
autoOpenBrowser: true,
|
autoOpenBrowser: true,
|
||||||
assetsSubDirectory: 'static',
|
assetsSubDirectory: 'static',
|
||||||
assetsPublicPath: '/',
|
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"
|
// CSS Sourcemaps off by default because relative paths are "buggy"
|
||||||
// with this option, according to the CSS-Loader README
|
// with this option, according to the CSS-Loader README
|
||||||
// (https://github.com/webpack/css-loader#sourcemaps)
|
// (https://github.com/webpack/css-loader#sourcemaps)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
NODE_ENV: '"production"',
|
NODE_ENV: '"production"',
|
||||||
BACKEND_URL: '"' + (process.env.BACKEND_URL || '/') + '"'
|
BACKEND_URL: '"/"'
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"]
|
|
|
@ -9,24 +9,28 @@
|
||||||
"start": "node build/dev-server.js",
|
"start": "node build/dev-server.js",
|
||||||
"build": "node build/build.js",
|
"build": "node build/build.js",
|
||||||
"unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
|
"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",
|
"e2e": "node test/e2e/runner.js",
|
||||||
"test": "npm run unit && npm run e2e",
|
"test": "npm run unit && npm run e2e",
|
||||||
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs"
|
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^0.17.1",
|
||||||
"dateformat": "^2.0.0",
|
"dateformat": "^2.0.0",
|
||||||
"js-logger": "^1.3.0",
|
"js-logger": "^1.3.0",
|
||||||
"jwt-decode": "^2.2.0",
|
"jwt-decode": "^2.2.0",
|
||||||
"lodash": "^4.17.4",
|
"lodash": "^4.17.4",
|
||||||
|
"moxios": "^0.4.0",
|
||||||
|
"raven-js": "^3.22.3",
|
||||||
"semantic-ui-css": "^2.2.10",
|
"semantic-ui-css": "^2.2.10",
|
||||||
"vue": "^2.3.3",
|
"vue": "^2.3.3",
|
||||||
"vue-lazyload": "^1.1.4",
|
"vue-lazyload": "^1.1.4",
|
||||||
"vue-resource": "^1.3.4",
|
|
||||||
"vue-router": "^2.3.1",
|
"vue-router": "^2.3.1",
|
||||||
"vue-upload-component": "^2.7.4",
|
"vue-upload-component": "^2.7.4",
|
||||||
"vuedraggable": "^2.14.1",
|
"vuedraggable": "^2.14.1",
|
||||||
"vuex": "^3.0.1",
|
"vuex": "^3.0.1",
|
||||||
"vuex-persistedstate": "^2.4.2"
|
"vuex-persistedstate": "^2.4.2",
|
||||||
|
"vuex-router-sync": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"autoprefixer": "^6.7.2",
|
"autoprefixer": "^6.7.2",
|
||||||
|
@ -46,6 +50,7 @@
|
||||||
"cross-env": "^4.0.0",
|
"cross-env": "^4.0.0",
|
||||||
"cross-spawn": "^5.0.1",
|
"cross-spawn": "^5.0.1",
|
||||||
"css-loader": "^0.28.0",
|
"css-loader": "^0.28.0",
|
||||||
|
"es6-promise": "^4.2.2",
|
||||||
"eslint": "^3.19.0",
|
"eslint": "^3.19.0",
|
||||||
"eslint-config-standard": "^6.2.1",
|
"eslint-config-standard": "^6.2.1",
|
||||||
"eslint-friendly-formatter": "^2.0.7",
|
"eslint-friendly-formatter": "^2.0.7",
|
||||||
|
@ -67,6 +72,7 @@
|
||||||
"karma-phantomjs-launcher": "^1.0.2",
|
"karma-phantomjs-launcher": "^1.0.2",
|
||||||
"karma-phantomjs-shim": "^1.4.0",
|
"karma-phantomjs-shim": "^1.4.0",
|
||||||
"karma-sinon-chai": "^1.3.1",
|
"karma-sinon-chai": "^1.3.1",
|
||||||
|
"karma-sinon-stub-promise": "^1.0.0",
|
||||||
"karma-sourcemap-loader": "^0.3.7",
|
"karma-sourcemap-loader": "^0.3.7",
|
||||||
"karma-spec-reporter": "0.0.30",
|
"karma-spec-reporter": "0.0.30",
|
||||||
"karma-webpack": "^2.0.2",
|
"karma-webpack": "^2.0.2",
|
||||||
|
@ -85,6 +91,7 @@
|
||||||
"shelljs": "^0.7.6",
|
"shelljs": "^0.7.6",
|
||||||
"sinon": "^2.1.0",
|
"sinon": "^2.1.0",
|
||||||
"sinon-chai": "^2.8.0",
|
"sinon-chai": "^2.8.0",
|
||||||
|
"sinon-stub-promise": "^4.0.0",
|
||||||
"url-loader": "^0.5.8",
|
"url-loader": "^0.5.8",
|
||||||
"vue-loader": "^12.1.0",
|
"vue-loader": "^12.1.0",
|
||||||
"vue-style-loader": "^3.0.1",
|
"vue-style-loader": "^3.0.1",
|
||||||
|
|
|
@ -22,15 +22,26 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<raven
|
||||||
|
v-if="$store.state.instance.settings.raven.front_enabled.value"
|
||||||
|
:dsn="$store.state.instance.settings.raven.front_dsn.value">
|
||||||
|
</raven>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Sidebar from '@/components/Sidebar'
|
import Sidebar from '@/components/Sidebar'
|
||||||
|
import Raven from '@/components/Raven'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'app',
|
name: 'app',
|
||||||
components: { Sidebar }
|
components: {
|
||||||
|
Sidebar,
|
||||||
|
Raven
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.$store.dispatch('instance/fetchSettings')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -40,25 +51,33 @@ export default {
|
||||||
// and we end up with CSS rules not applied,
|
// and we end up with CSS rules not applied,
|
||||||
// see https://github.com/webpack/webpack/issues/215
|
// see https://github.com/webpack/webpack/issues/215
|
||||||
@import 'semantic/semantic.css';
|
@import 'semantic/semantic.css';
|
||||||
|
@import 'style/vendor/media';
|
||||||
|
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
@include media("<desktop") {
|
||||||
|
font-size: 200%;
|
||||||
|
}
|
||||||
|
}
|
||||||
#app {
|
#app {
|
||||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
.main.pusher, .footer {
|
.main.pusher, .footer {
|
||||||
margin-left: 350px !important;
|
@include media(">desktop") {
|
||||||
|
margin-left: 350px !important;
|
||||||
|
}
|
||||||
transform: none !important;
|
transform: none !important;
|
||||||
}
|
}
|
||||||
.main-pusher {
|
.main-pusher {
|
||||||
padding: 1.5rem 0;
|
padding: 1.5rem 0;
|
||||||
}
|
}
|
||||||
#footer {
|
.ui.stripe.segment, #footer {
|
||||||
padding: 4em;
|
padding: 2em;
|
||||||
}
|
@include media(">tablet") {
|
||||||
.ui.stripe.segment {
|
padding: 4em;
|
||||||
padding: 4em;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.small.text.container {
|
.ui.small.text.container {
|
||||||
|
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.0 KiB |
|
@ -0,0 +1,41 @@
|
||||||
|
<template>
|
||||||
|
<div class="raven"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Raven from 'raven-js'
|
||||||
|
import RavenVue from 'raven-js/plugins/vue'
|
||||||
|
import Vue from 'vue'
|
||||||
|
import logger from '@/logging'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['dsn'],
|
||||||
|
created () {
|
||||||
|
Raven.uninstall()
|
||||||
|
this.setUp()
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
Raven.uninstall()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setUp () {
|
||||||
|
Raven.uninstall()
|
||||||
|
logger.default.info('Installing raven...')
|
||||||
|
Raven.config(this.dsn).addPlugin(RavenVue, Vue).install()
|
||||||
|
console.log({}.test.test)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
dsn: function () {
|
||||||
|
this.setUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||||
|
<style scoped >
|
||||||
|
.raven {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,13 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="ui vertical left visible wide sidebar">
|
<div :class="['ui', 'vertical', 'left', 'visible', 'wide', {'collapsed': isCollapsed}, 'sidebar',]">
|
||||||
<div class="ui inverted segment header-wrapper">
|
<div class="ui inverted segment header-wrapper">
|
||||||
<search-bar>
|
<search-bar @search="isCollapsed = false">
|
||||||
<router-link :title="'Funkwhale'" :to="{name: 'index'}">
|
<router-link :title="'Funkwhale'" :to="{name: 'index'}">
|
||||||
<i class="logo bordered inverted orange big icon">
|
<i class="logo bordered inverted orange big icon">
|
||||||
<logo class="logo"></logo>
|
<logo class="logo"></logo>
|
||||||
</i>
|
</i>
|
||||||
</router-link>
|
</router-link><span
|
||||||
|
slot="after"
|
||||||
|
@click="isCollapsed = !isCollapsed"
|
||||||
|
:class="['ui', 'basic', 'big', {'inverted': isCollapsed}, 'orange', 'icon', 'collapse', 'button']">
|
||||||
|
<i class="sidebar icon"></i></span>
|
||||||
</search-bar>
|
</search-bar>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -49,7 +52,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui bottom attached tab" data-tab="queue">
|
<div class="ui bottom attached tab" data-tab="queue">
|
||||||
<table class="ui compact inverted very basic fixed single line table">
|
<table class="ui compact inverted very basic fixed single line unstackable table">
|
||||||
<draggable v-model="queue.tracks" element="tbody" @update="reorder">
|
<draggable v-model="queue.tracks" element="tbody" @update="reorder">
|
||||||
<tr @click="$store.dispatch('queue/currentIndex', index)" v-for="(track, index) in queue.tracks" :key="index" :class="[{'active': index === queue.currentIndex}]">
|
<tr @click="$store.dispatch('queue/currentIndex', index)" v-for="(track, index) in queue.tracks" :key="index" :class="[{'active': index === queue.currentIndex}]">
|
||||||
<td class="right aligned">{{ index + 1}}</td>
|
<td class="right aligned">{{ index + 1}}</td>
|
||||||
|
@ -84,9 +87,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui inverted segment player-wrapper">
|
<player></player>
|
||||||
<player></player>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -111,7 +112,8 @@ export default {
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
backend: backend
|
backend: backend,
|
||||||
|
isCollapsed: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
@ -119,7 +121,8 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
queue: state => state.queue
|
queue: state => state.queue,
|
||||||
|
url: state => state.route.path
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -129,19 +132,42 @@ export default {
|
||||||
reorder: function (oldValue, newValue) {
|
reorder: function (oldValue, newValue) {
|
||||||
this.$store.commit('queue/reorder', {oldValue, newValue})
|
this.$store.commit('queue/reorder', {oldValue, newValue})
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
url: function () {
|
||||||
|
this.isCollapsed = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
@import '../style/vendor/media';
|
||||||
|
|
||||||
$sidebar-color: #1B1C1D;
|
$sidebar-color: #1B1C1D;
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
display:flex;
|
background: $sidebar-color;
|
||||||
flex-direction:column;
|
@include media(">tablet") {
|
||||||
justify-content: space-between;
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
@include media(">desktop") {
|
||||||
|
.collapse.button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@include media("<desktop") {
|
||||||
|
position: static !important;
|
||||||
|
width: 100% !important;
|
||||||
|
&.collapsed {
|
||||||
|
.menu-area, .player-wrapper, .tabs {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -160,7 +186,12 @@ $sidebar-color: #1B1C1D;
|
||||||
}
|
}
|
||||||
.tabs {
|
.tabs {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
height: 0px;
|
@include media(">tablet") {
|
||||||
|
height: 0px;
|
||||||
|
}
|
||||||
|
@include media("<desktop") {
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.tab[data-tab="queue"] {
|
.tab[data-tab="queue"] {
|
||||||
tr {
|
tr {
|
||||||
|
@ -174,26 +205,22 @@ $sidebar-color: #1B1C1D;
|
||||||
|
|
||||||
.ui.inverted.segment.header-wrapper {
|
.ui.inverted.segment.header-wrapper {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
padding-bottom: 1rem;
|
|
||||||
}
|
}
|
||||||
.tabs {
|
.tabs {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-wrapper {
|
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.1) !important;
|
|
||||||
background-color: rgb(46, 46, 46) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.search {
|
.ui.search {
|
||||||
display: inline-block;
|
display: block;
|
||||||
> a {
|
.collapse.button {
|
||||||
margin-right: 1.5rem;
|
margin-right: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
float: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,145 +1,147 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="player">
|
<div class="ui inverted segment player-wrapper" :style="style">
|
||||||
<audio-track
|
<div class="player">
|
||||||
ref="currentAudio"
|
<audio-track
|
||||||
v-if="currentTrack"
|
ref="currentAudio"
|
||||||
:key="(currentIndex, currentTrack.id)"
|
v-if="currentTrack"
|
||||||
:is-current="true"
|
:key="(currentIndex, currentTrack.id)"
|
||||||
:start-time="$store.state.player.currentTime"
|
:is-current="true"
|
||||||
:autoplay="$store.state.player.playing"
|
:start-time="$store.state.player.currentTime"
|
||||||
:track="currentTrack">
|
:autoplay="$store.state.player.playing"
|
||||||
</audio-track>
|
:track="currentTrack">
|
||||||
|
</audio-track>
|
||||||
|
|
||||||
<div v-if="currentTrack" class="track-area ui items">
|
<div v-if="currentTrack" class="track-area ui unstackable items">
|
||||||
<div class="ui inverted item">
|
<div class="ui inverted item">
|
||||||
<div class="ui tiny image">
|
<div class="ui tiny image">
|
||||||
<img v-if="currentTrack.album.cover" :src="Track.getCover(currentTrack)">
|
<img ref="cover" @load="updateBackground" v-if="currentTrack.album.cover" :src="Track.getCover(currentTrack)">
|
||||||
<img v-else src="../../assets/audio/default-cover.png">
|
<img v-else src="../../assets/audio/default-cover.png">
|
||||||
</div>
|
</div>
|
||||||
<div class="middle aligned content">
|
<div class="middle aligned content">
|
||||||
<router-link class="small header discrete link track" :to="{name: 'library.tracks.detail', params: {id: currentTrack.id }}">
|
<router-link class="small header discrete link track" :to="{name: 'library.tracks.detail', params: {id: currentTrack.id }}">
|
||||||
{{ currentTrack.title }}
|
{{ currentTrack.title }}
|
||||||
</router-link>
|
|
||||||
<div class="meta">
|
|
||||||
<router-link class="artist" :to="{name: 'library.artists.detail', params: {id: currentTrack.artist.id }}">
|
|
||||||
{{ currentTrack.artist.name }}
|
|
||||||
</router-link> /
|
|
||||||
<router-link class="album" :to="{name: 'library.albums.detail', params: {id: currentTrack.album.id }}">
|
|
||||||
{{ currentTrack.album.title }}
|
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
<div class="meta">
|
||||||
<div class="description">
|
<router-link class="artist" :to="{name: 'library.artists.detail', params: {id: currentTrack.artist.id }}">
|
||||||
<track-favorite-icon :track="currentTrack"></track-favorite-icon>
|
{{ currentTrack.artist.name }}
|
||||||
|
</router-link> /
|
||||||
|
<router-link class="album" :to="{name: 'library.albums.detail', params: {id: currentTrack.album.id }}">
|
||||||
|
{{ currentTrack.album.title }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="description">
|
||||||
|
<track-favorite-icon :track="currentTrack"></track-favorite-icon>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="progress-area" v-if="currentTrack">
|
||||||
<div class="progress-area" v-if="currentTrack">
|
<div class="ui grid">
|
||||||
<div class="ui grid">
|
<div class="left floated four wide column">
|
||||||
<div class="left floated four wide column">
|
<p class="timer start" @click="updateProgress(0)">{{currentTimeFormatted}}</p>
|
||||||
<p class="timer start" @click="updateProgress(0)">{{currentTimeFormatted}}</p>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="right floated four wide column">
|
<div class="right floated four wide column">
|
||||||
<p class="timer total">{{durationFormatted}}</p>
|
<p class="timer total">{{durationFormatted}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ref="progress" class="ui small orange inverted progress" @click="touchProgress">
|
||||||
|
<div class="bar" :data-percent="progress" :style="{ 'width': progress + '%' }"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="progress" class="ui small orange inverted progress" @click="touchProgress">
|
|
||||||
<div class="bar" :data-percent="progress" :style="{ 'width': progress + '%' }"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="two wide column controls ui grid">
|
<div class="two wide column controls ui grid">
|
||||||
<div
|
<div
|
||||||
@click="previous"
|
@click="previous"
|
||||||
title="Previous track"
|
title="Previous track"
|
||||||
class="two wide column control"
|
class="two wide column control"
|
||||||
:disabled="!hasPrevious">
|
:disabled="!hasPrevious">
|
||||||
<i :class="['ui', {'disabled': !hasPrevious}, 'step', 'backward', 'big', 'icon']" ></i>
|
<i :class="['ui', {'disabled': !hasPrevious}, 'step', 'backward', 'big', 'icon']" ></i>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!playing"
|
v-if="!playing"
|
||||||
@click="togglePlay"
|
@click="togglePlay"
|
||||||
title="Play track"
|
title="Play track"
|
||||||
class="two wide column control">
|
class="two wide column control">
|
||||||
<i :class="['ui', 'play', {'disabled': !currentTrack}, 'big', 'icon']"></i>
|
<i :class="['ui', 'play', {'disabled': !currentTrack}, 'big', 'icon']"></i>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
@click="togglePlay"
|
@click="togglePlay"
|
||||||
title="Pause track"
|
title="Pause track"
|
||||||
class="two wide column control">
|
class="two wide column control">
|
||||||
<i :class="['ui', 'pause', {'disabled': !currentTrack}, 'big', 'icon']"></i>
|
<i :class="['ui', 'pause', {'disabled': !currentTrack}, 'big', 'icon']"></i>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@click="next"
|
@click="next"
|
||||||
title="Next track"
|
title="Next track"
|
||||||
class="two wide column control"
|
class="two wide column control"
|
||||||
:disabled="!hasNext">
|
:disabled="!hasNext">
|
||||||
<i :class="['ui', {'disabled': !hasNext}, 'step', 'forward', 'big', 'icon']" ></i>
|
<i :class="['ui', {'disabled': !hasNext}, 'step', 'forward', 'big', 'icon']" ></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="two wide column control volume-control">
|
<div class="two wide column control volume-control">
|
||||||
<i title="Unmute" @click="$store.commit('player/volume', 1)" v-if="volume === 0" class="volume off secondary icon"></i>
|
<i title="Unmute" @click="$store.commit('player/volume', 1)" v-if="volume === 0" class="volume off secondary icon"></i>
|
||||||
<i title="Mute" @click="$store.commit('player/volume', 0)" v-else-if="volume < 0.5" class="volume down secondary icon"></i>
|
<i title="Mute" @click="$store.commit('player/volume', 0)" v-else-if="volume < 0.5" class="volume down secondary icon"></i>
|
||||||
<i title="Mute" @click="$store.commit('player/volume', 0)" v-else class="volume up secondary icon"></i>
|
<i title="Mute" @click="$store.commit('player/volume', 0)" v-else class="volume up secondary icon"></i>
|
||||||
<input type="range" step="0.05" min="0" max="1" v-model="sliderVolume" />
|
<input type="range" step="0.05" min="0" max="1" v-model="sliderVolume" />
|
||||||
</div>
|
</div>
|
||||||
<div class="two wide column control looping">
|
<div class="two wide column control looping">
|
||||||
<i
|
<i
|
||||||
title="Looping disabled. Click to switch to single-track looping."
|
title="Looping disabled. Click to switch to single-track looping."
|
||||||
v-if="looping === 0"
|
v-if="looping === 0"
|
||||||
@click="$store.commit('player/looping', 1)"
|
@click="$store.commit('player/looping', 1)"
|
||||||
:disabled="!currentTrack"
|
:disabled="!currentTrack"
|
||||||
:class="['ui', {'disabled': !currentTrack}, 'step', 'repeat', 'secondary', 'icon']"></i>
|
:class="['ui', {'disabled': !currentTrack}, 'step', 'repeat', 'secondary', 'icon']"></i>
|
||||||
<i
|
<i
|
||||||
title="Looping on a single track. Click to switch to whole queue looping."
|
title="Looping on a single track. Click to switch to whole queue looping."
|
||||||
v-if="looping === 1"
|
v-if="looping === 1"
|
||||||
@click="$store.commit('player/looping', 2)"
|
@click="$store.commit('player/looping', 2)"
|
||||||
:disabled="!currentTrack"
|
:disabled="!currentTrack"
|
||||||
class="repeat secondary icon">
|
class="repeat secondary icon">
|
||||||
<span class="ui circular tiny orange label">1</span>
|
<span class="ui circular tiny orange label">1</span>
|
||||||
</i>
|
</i>
|
||||||
<i
|
<i
|
||||||
title="Looping on whole queue. Click to disable looping."
|
title="Looping on whole queue. Click to disable looping."
|
||||||
v-if="looping === 2"
|
v-if="looping === 2"
|
||||||
@click="$store.commit('player/looping', 0)"
|
@click="$store.commit('player/looping', 0)"
|
||||||
:disabled="!currentTrack"
|
:disabled="!currentTrack"
|
||||||
class="repeat orange secondary icon">
|
class="repeat orange secondary icon">
|
||||||
</i>
|
</i>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@click="shuffle()"
|
@click="shuffle()"
|
||||||
:disabled="queue.tracks.length === 0"
|
:disabled="queue.tracks.length === 0"
|
||||||
title="Shuffle your queue"
|
title="Shuffle your queue"
|
||||||
class="two wide column control">
|
class="two wide column control">
|
||||||
<i :class="['ui', 'random', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
|
<i :class="['ui', 'random', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="one wide column"></div>
|
<div class="one wide column"></div>
|
||||||
<div
|
<div
|
||||||
@click="clean()"
|
@click="clean()"
|
||||||
:disabled="queue.tracks.length === 0"
|
:disabled="queue.tracks.length === 0"
|
||||||
title="Clear your queue"
|
title="Clear your queue"
|
||||||
class="two wide column control">
|
class="two wide column control">
|
||||||
<i :class="['ui', 'trash', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
|
<i :class="['ui', 'trash', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<GlobalEvents
|
||||||
|
@keydown.space.prevent.exact="togglePlay"
|
||||||
|
@keydown.ctrl.left.prevent.exact="previous"
|
||||||
|
@keydown.ctrl.right.prevent.exact="next"
|
||||||
|
@keydown.ctrl.down.prevent.exact="$store.commit('player/incrementVolume', -0.1)"
|
||||||
|
@keydown.ctrl.up.prevent.exact="$store.commit('player/incrementVolume', 0.1)"
|
||||||
|
@keydown.f.prevent.exact="$store.dispatch('favorites/toggle', currentTrack.id)"
|
||||||
|
@keydown.l.prevent.exact="$store.commit('player/toggleLooping')"
|
||||||
|
@keydown.s.prevent.exact="shuffle"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<GlobalEvents
|
|
||||||
@keydown.space.prevent.exact="togglePlay"
|
|
||||||
@keydown.ctrl.left.prevent.exact="previous"
|
|
||||||
@keydown.ctrl.right.prevent.exact="next"
|
|
||||||
@keydown.ctrl.down.prevent.exact="$store.commit('player/incrementVolume', -0.1)"
|
|
||||||
@keydown.ctrl.up.prevent.exact="$store.commit('player/incrementVolume', 0.1)"
|
|
||||||
@keydown.f.prevent.exact="$store.dispatch('favorites/toggle', currentTrack.id)"
|
|
||||||
@keydown.l.prevent.exact="$store.commit('player/toggleLooping')"
|
|
||||||
@keydown.s.prevent.exact="shuffle"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {mapState, mapGetters, mapActions} from 'vuex'
|
import {mapState, mapGetters, mapActions} from 'vuex'
|
||||||
import GlobalEvents from '@/components/utils/global-events'
|
import GlobalEvents from '@/components/utils/global-events'
|
||||||
|
import ColorThief from '@/vendor/color-thief'
|
||||||
|
|
||||||
import Track from '@/audio/track'
|
import Track from '@/audio/track'
|
||||||
import AudioTrack from '@/components/audio/Track'
|
import AudioTrack from '@/components/audio/Track'
|
||||||
|
@ -153,9 +155,12 @@ export default {
|
||||||
AudioTrack
|
AudioTrack
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
|
let defaultAmbiantColors = [[46, 46, 46], [46, 46, 46], [46, 46, 46], [46, 46, 46]]
|
||||||
return {
|
return {
|
||||||
sliderVolume: this.volume,
|
sliderVolume: this.volume,
|
||||||
Track: Track
|
Track: Track,
|
||||||
|
defaultAmbiantColors: defaultAmbiantColors,
|
||||||
|
ambiantColors: defaultAmbiantColors
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
@ -177,6 +182,14 @@ export default {
|
||||||
let target = this.$refs.progress
|
let target = this.$refs.progress
|
||||||
time = e.layerX / target.offsetWidth * this.duration
|
time = e.layerX / target.offsetWidth * this.duration
|
||||||
this.$refs.currentAudio.setCurrentTime(time)
|
this.$refs.currentAudio.setCurrentTime(time)
|
||||||
|
},
|
||||||
|
updateBackground () {
|
||||||
|
if (!this.currentTrack.album.cover) {
|
||||||
|
this.ambiantColors = this.defaultAmbiantColors
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let image = this.$refs.cover
|
||||||
|
this.ambiantColors = ColorThief.prototype.getPalette(image, 4).slice(0, 4)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -195,9 +208,34 @@ export default {
|
||||||
durationFormatted: 'player/durationFormatted',
|
durationFormatted: 'player/durationFormatted',
|
||||||
currentTimeFormatted: 'player/currentTimeFormatted',
|
currentTimeFormatted: 'player/currentTimeFormatted',
|
||||||
progress: 'player/progress'
|
progress: 'player/progress'
|
||||||
})
|
}),
|
||||||
|
style: function () {
|
||||||
|
let style = {
|
||||||
|
'background': this.ambiantGradiant
|
||||||
|
}
|
||||||
|
return style
|
||||||
|
},
|
||||||
|
ambiantGradiant: function () {
|
||||||
|
let indexConf = [
|
||||||
|
{orientation: 330, percent: 100, opacity: 0.7},
|
||||||
|
{orientation: 240, percent: 90, opacity: 0.7},
|
||||||
|
{orientation: 150, percent: 80, opacity: 0.7},
|
||||||
|
{orientation: 60, percent: 70, opacity: 0.7}
|
||||||
|
]
|
||||||
|
let gradients = this.ambiantColors.map((e, i) => {
|
||||||
|
let [r, g, b] = e
|
||||||
|
let conf = indexConf[i]
|
||||||
|
return `linear-gradient(${conf.orientation}deg, rgba(${r}, ${g}, ${b}, ${conf.opacity}) 10%, rgba(255, 255, 255, 0) ${conf.percent}%)`
|
||||||
|
}).join(', ')
|
||||||
|
return gradients
|
||||||
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
currentTrack (newValue) {
|
||||||
|
if (!newValue) {
|
||||||
|
this.ambiantColors = this.defaultAmbiantColors
|
||||||
|
}
|
||||||
|
},
|
||||||
volume (newValue) {
|
volume (newValue) {
|
||||||
this.sliderVolume = newValue
|
this.sliderVolume = newValue
|
||||||
},
|
},
|
||||||
|
|
|
@ -29,13 +29,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import backend from '@/audio/backend'
|
import backend from '@/audio/backend'
|
||||||
import AlbumCard from '@/components/audio/album/Card'
|
import AlbumCard from '@/components/audio/album/Card'
|
||||||
import ArtistCard from '@/components/audio/artist/Card'
|
import ArtistCard from '@/components/audio/artist/Card'
|
||||||
import config from '@/config'
|
|
||||||
|
|
||||||
const SEARCH_URL = config.API_URL + 'search'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -73,17 +71,8 @@ export default {
|
||||||
let params = {
|
let params = {
|
||||||
query: this.query
|
query: this.query
|
||||||
}
|
}
|
||||||
this.$http.get(SEARCH_URL, {
|
axios.get('search', {
|
||||||
params: params,
|
params: params
|
||||||
before (request) {
|
|
||||||
// abort previous request, if exists
|
|
||||||
if (this.previousRequest) {
|
|
||||||
this.previousRequest.abort()
|
|
||||||
}
|
|
||||||
|
|
||||||
// set previous request on Vue instance
|
|
||||||
this.previousRequest = request
|
|
||||||
}
|
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
self.results = self.castResults(response.data)
|
self.results = self.castResults(response.data)
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="ui fluid category search">
|
<div class="ui fluid category search">
|
||||||
<slot></slot>
|
<slot></slot><div class="ui icon input">
|
||||||
<div class="ui icon input">
|
|
||||||
<input class="prompt" placeholder="Search for artists, albums, tracks..." type="text">
|
<input class="prompt" placeholder="Search for artists, albums, tracks..." type="text">
|
||||||
<i class="search icon"></i>
|
<i class="search icon"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="results"></div>
|
<div class="results"></div>
|
||||||
|
<slot name="after"></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -25,6 +25,9 @@ export default {
|
||||||
onSelect (result, response) {
|
onSelect (result, response) {
|
||||||
router.push(result.routerUrl)
|
router.push(result.routerUrl)
|
||||||
},
|
},
|
||||||
|
onSearchQuery (query) {
|
||||||
|
self.$emit('search')
|
||||||
|
},
|
||||||
apiSettings: {
|
apiSettings: {
|
||||||
beforeXHR: function (xhrObject) {
|
beforeXHR: function (xhrObject) {
|
||||||
xhrObject.setRequestHeader('Authorization', self.$store.getters['auth/header'])
|
xhrObject.setRequestHeader('Authorization', self.$store.getters['auth/header'])
|
||||||
|
|
|
@ -59,13 +59,15 @@ export default {
|
||||||
|
|
||||||
},
|
},
|
||||||
loaded: function () {
|
loaded: function () {
|
||||||
if (this.isCurrent && this.autoplay) {
|
if (this.isCurrent) {
|
||||||
this.$store.commit('player/duration', this.$refs.audio.duration)
|
this.$store.commit('player/duration', this.$refs.audio.duration)
|
||||||
if (this.startTime) {
|
if (this.startTime) {
|
||||||
this.setCurrentTime(this.startTime)
|
this.setCurrentTime(this.startTime)
|
||||||
}
|
}
|
||||||
this.$store.commit('player/playing', true)
|
if (this.autoplay) {
|
||||||
this.$refs.audio.play()
|
this.$store.commit('player/playing', true)
|
||||||
|
this.$refs.audio.play()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateProgress: function () {
|
updateProgress: function () {
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="description" v-if="mode === 'rich'">
|
<div class="description" v-if="mode === 'rich'">
|
||||||
<table class="ui very basic fixed single line compact table">
|
<table class="ui very basic fixed single line compact unstackable table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="track in tracks">
|
<tr v-for="track in tracks">
|
||||||
<td>
|
<td>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<table class="ui compact very basic fixed single line table">
|
<table class="ui compact very basic fixed single line unstackable table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="album in albums">
|
<tr v-for="album in albums">
|
||||||
<td>
|
<td>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<table class="ui compact very basic fixed single line table">
|
<table class="ui compact very basic fixed single line unstackable table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Vue from 'vue'
|
import axios from 'axios'
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
|
|
||||||
|
@ -61,8 +61,8 @@ export default {
|
||||||
new_password1: this.new_password,
|
new_password1: this.new_password,
|
||||||
new_password2: this.new_password
|
new_password2: this.new_password
|
||||||
}
|
}
|
||||||
let resource = Vue.resource(config.BACKEND_URL + 'api/auth/registration/change-password/')
|
let url = config.BACKEND_URL + 'api/auth/registration/change-password/'
|
||||||
return resource.save({}, credentials).then(response => {
|
return axios.post(url, credentials).then(response => {
|
||||||
logger.default.info('Password successfully changed')
|
logger.default.info('Password successfully changed')
|
||||||
self.$router.push('/profile/me')
|
self.$router.push('/profile/me')
|
||||||
}, response => {
|
}, response => {
|
||||||
|
|
|
@ -54,15 +54,15 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import config from '@/config'
|
|
||||||
import TrackTable from '@/components/audio/track/Table'
|
import TrackTable from '@/components/audio/track/Table'
|
||||||
import RadioButton from '@/components/radios/Button'
|
import RadioButton from '@/components/radios/Button'
|
||||||
import Pagination from '@/components/Pagination'
|
import Pagination from '@/components/Pagination'
|
||||||
import OrderingMixin from '@/components/mixins/Ordering'
|
import OrderingMixin from '@/components/mixins/Ordering'
|
||||||
import PaginationMixin from '@/components/mixins/Pagination'
|
import PaginationMixin from '@/components/mixins/Pagination'
|
||||||
const FAVORITES_URL = config.API_URL + 'tracks/'
|
const FAVORITES_URL = 'tracks/'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [OrderingMixin, PaginationMixin],
|
mixins: [OrderingMixin, PaginationMixin],
|
||||||
|
@ -115,7 +115,7 @@ export default {
|
||||||
ordering: this.getOrderingAsString()
|
ordering: this.getOrderingAsString()
|
||||||
}
|
}
|
||||||
logger.default.time('Loading user favorites')
|
logger.default.time('Loading user favorites')
|
||||||
this.$http.get(url, {params: params}).then((response) => {
|
axios.get(url, {params: params}).then((response) => {
|
||||||
self.results = response.data
|
self.results = response.data
|
||||||
self.nextLink = response.data.next
|
self.nextLink = response.data.next
|
||||||
self.previousLink = response.data.previous
|
self.previousLink = response.data.previous
|
||||||
|
|
|
@ -41,14 +41,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import backend from '@/audio/backend'
|
import backend from '@/audio/backend'
|
||||||
import PlayButton from '@/components/audio/PlayButton'
|
import PlayButton from '@/components/audio/PlayButton'
|
||||||
import TrackTable from '@/components/audio/track/Table'
|
import TrackTable from '@/components/audio/track/Table'
|
||||||
import config from '@/config'
|
|
||||||
|
|
||||||
const FETCH_URL = config.API_URL + 'albums/'
|
const FETCH_URL = 'albums/'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['id'],
|
props: ['id'],
|
||||||
|
@ -71,7 +70,7 @@ export default {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let url = FETCH_URL + this.id + '/'
|
let url = FETCH_URL + this.id + '/'
|
||||||
logger.default.debug('Fetching album "' + this.id + '"')
|
logger.default.debug('Fetching album "' + this.id + '"')
|
||||||
this.$http.get(url).then((response) => {
|
axios.get(url).then((response) => {
|
||||||
self.album = backend.Album.clean(response.data)
|
self.album = backend.Album.clean(response.data)
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
})
|
})
|
||||||
|
|
|
@ -41,15 +41,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import backend from '@/audio/backend'
|
import backend from '@/audio/backend'
|
||||||
import AlbumCard from '@/components/audio/album/Card'
|
import AlbumCard from '@/components/audio/album/Card'
|
||||||
import RadioButton from '@/components/radios/Button'
|
import RadioButton from '@/components/radios/Button'
|
||||||
import PlayButton from '@/components/audio/PlayButton'
|
import PlayButton from '@/components/audio/PlayButton'
|
||||||
import config from '@/config'
|
|
||||||
|
|
||||||
const FETCH_URL = config.API_URL + 'artists/'
|
const FETCH_URL = 'artists/'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['id'],
|
props: ['id'],
|
||||||
|
@ -74,7 +73,7 @@ export default {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let url = FETCH_URL + this.id + '/'
|
let url = FETCH_URL + this.id + '/'
|
||||||
logger.default.debug('Fetching artist "' + this.id + '"')
|
logger.default.debug('Fetching artist "' + this.id + '"')
|
||||||
this.$http.get(url).then((response) => {
|
axios.get(url).then((response) => {
|
||||||
self.artist = response.data
|
self.artist = response.data
|
||||||
self.albums = JSON.parse(JSON.stringify(self.artist.albums)).map((album) => {
|
self.albums = JSON.parse(JSON.stringify(self.artist.albums)).map((album) => {
|
||||||
return backend.Album.clean(album)
|
return backend.Album.clean(album)
|
||||||
|
|
|
@ -57,10 +57,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
|
|
||||||
import config from '@/config'
|
|
||||||
import backend from '@/audio/backend'
|
import backend from '@/audio/backend'
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ import PaginationMixin from '@/components/mixins/Pagination'
|
||||||
import ArtistCard from '@/components/audio/artist/Card'
|
import ArtistCard from '@/components/audio/artist/Card'
|
||||||
import Pagination from '@/components/Pagination'
|
import Pagination from '@/components/Pagination'
|
||||||
|
|
||||||
const FETCH_URL = config.API_URL + 'artists/'
|
const FETCH_URL = 'artists/'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [OrderingMixin, PaginationMixin],
|
mixins: [OrderingMixin, PaginationMixin],
|
||||||
|
@ -124,7 +124,7 @@ export default {
|
||||||
ordering: this.getOrderingAsString()
|
ordering: this.getOrderingAsString()
|
||||||
}
|
}
|
||||||
logger.default.debug('Fetching artists')
|
logger.default.debug('Fetching artists')
|
||||||
this.$http.get(url, {params: params}).then((response) => {
|
axios.get(url, {params: params}).then((response) => {
|
||||||
self.result = response.data
|
self.result = response.data
|
||||||
self.result.results.map((artist) => {
|
self.result.results.map((artist) => {
|
||||||
var albums = JSON.parse(JSON.stringify(artist.albums)).map((album) => {
|
var albums = JSON.parse(JSON.stringify(artist.albums)).map((album) => {
|
||||||
|
|
|
@ -24,14 +24,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
import Search from '@/components/audio/Search'
|
import Search from '@/components/audio/Search'
|
||||||
import backend from '@/audio/backend'
|
import backend from '@/audio/backend'
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import ArtistCard from '@/components/audio/artist/Card'
|
import ArtistCard from '@/components/audio/artist/Card'
|
||||||
import config from '@/config'
|
|
||||||
import RadioCard from '@/components/radios/Card'
|
import RadioCard from '@/components/radios/Card'
|
||||||
|
|
||||||
const ARTISTS_URL = config.API_URL + 'artists/'
|
const ARTISTS_URL = 'artists/'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'library',
|
name: 'library',
|
||||||
|
@ -58,7 +58,7 @@ export default {
|
||||||
}
|
}
|
||||||
let url = ARTISTS_URL
|
let url = ARTISTS_URL
|
||||||
logger.default.time('Loading latest artists')
|
logger.default.time('Loading latest artists')
|
||||||
this.$http.get(url, {params: params}).then((response) => {
|
axios.get(url, {params: params}).then((response) => {
|
||||||
self.artists = response.data.results
|
self.artists = response.data.results
|
||||||
self.artists.map((artist) => {
|
self.artists.map((artist) => {
|
||||||
var albums = JSON.parse(JSON.stringify(artist.albums)).map((album) => {
|
var albums = JSON.parse(JSON.stringify(artist.albums)).map((album) => {
|
||||||
|
|
|
@ -22,8 +22,12 @@ export default {
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@import '../../style/vendor/media';
|
||||||
|
|
||||||
.library.pusher > .ui.secondary.menu {
|
.library.pusher > .ui.secondary.menu {
|
||||||
margin: 0 2.5rem;
|
@include media(">tablet") {
|
||||||
|
margin: 0 2.5rem;
|
||||||
|
}
|
||||||
.item {
|
.item {
|
||||||
padding-top: 1.5em;
|
padding-top: 1.5em;
|
||||||
padding-bottom: 1.5em;
|
padding-bottom: 1.5em;
|
||||||
|
@ -37,7 +41,10 @@ export default {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
.segment-content {
|
.segment-content {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 4em;
|
padding: 2em;
|
||||||
|
@include media(">tablet") {
|
||||||
|
padding: 4em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&.with-background {
|
&.with-background {
|
||||||
.header {
|
.header {
|
||||||
|
|
|
@ -59,10 +59,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
|
|
||||||
import config from '@/config'
|
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
|
|
||||||
import OrderingMixin from '@/components/mixins/Ordering'
|
import OrderingMixin from '@/components/mixins/Ordering'
|
||||||
|
@ -70,7 +70,7 @@ import PaginationMixin from '@/components/mixins/Pagination'
|
||||||
import RadioCard from '@/components/radios/Card'
|
import RadioCard from '@/components/radios/Card'
|
||||||
import Pagination from '@/components/Pagination'
|
import Pagination from '@/components/Pagination'
|
||||||
|
|
||||||
const FETCH_URL = config.API_URL + 'radios/radios/'
|
const FETCH_URL = 'radios/radios/'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [OrderingMixin, PaginationMixin],
|
mixins: [OrderingMixin, PaginationMixin],
|
||||||
|
@ -125,7 +125,7 @@ export default {
|
||||||
ordering: this.getOrderingAsString()
|
ordering: this.getOrderingAsString()
|
||||||
}
|
}
|
||||||
logger.default.debug('Fetching radios')
|
logger.default.debug('Fetching radios')
|
||||||
this.$http.get(url, {params: params}).then((response) => {
|
axios.get(url, {params: params}).then((response) => {
|
||||||
self.result = response.data
|
self.result = response.data
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
})
|
})
|
||||||
|
|
|
@ -60,15 +60,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
import url from '@/utils/url'
|
import url from '@/utils/url'
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import backend from '@/audio/backend'
|
import backend from '@/audio/backend'
|
||||||
import PlayButton from '@/components/audio/PlayButton'
|
import PlayButton from '@/components/audio/PlayButton'
|
||||||
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
||||||
import config from '@/config'
|
|
||||||
|
|
||||||
const FETCH_URL = config.API_URL + 'tracks/'
|
const FETCH_URL = 'tracks/'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['id'],
|
props: ['id'],
|
||||||
|
@ -94,7 +93,7 @@ export default {
|
||||||
this.isLoadingTrack = true
|
this.isLoadingTrack = true
|
||||||
let url = FETCH_URL + this.id + '/'
|
let url = FETCH_URL + this.id + '/'
|
||||||
logger.default.debug('Fetching track "' + this.id + '"')
|
logger.default.debug('Fetching track "' + this.id + '"')
|
||||||
this.$http.get(url).then((response) => {
|
axios.get(url).then((response) => {
|
||||||
self.track = response.data
|
self.track = response.data
|
||||||
self.isLoadingTrack = false
|
self.isLoadingTrack = false
|
||||||
})
|
})
|
||||||
|
@ -104,7 +103,7 @@ export default {
|
||||||
this.isLoadingLyrics = true
|
this.isLoadingLyrics = true
|
||||||
let url = FETCH_URL + this.id + '/lyrics/'
|
let url = FETCH_URL + this.id + '/lyrics/'
|
||||||
logger.default.debug('Fetching lyrics for track "' + this.id + '"')
|
logger.default.debug('Fetching lyrics for track "' + this.id + '"')
|
||||||
this.$http.get(url).then((response) => {
|
axios.get(url).then((response) => {
|
||||||
self.lyrics = response.data
|
self.lyrics = response.data
|
||||||
self.isLoadingLyrics = false
|
self.isLoadingLyrics = false
|
||||||
}, (response) => {
|
}, (response) => {
|
||||||
|
|
|
@ -37,8 +37,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import config from '@/config'
|
|
||||||
|
|
||||||
import ImportMixin from './ImportMixin'
|
import ImportMixin from './ImportMixin'
|
||||||
import ReleaseImport from './ReleaseImport'
|
import ReleaseImport from './ReleaseImport'
|
||||||
|
@ -92,9 +92,8 @@ export default Vue.extend({
|
||||||
fetchReleaseGroupsData () {
|
fetchReleaseGroupsData () {
|
||||||
let self = this
|
let self = this
|
||||||
this.releaseGroups.forEach(group => {
|
this.releaseGroups.forEach(group => {
|
||||||
let url = config.API_URL + 'providers/musicbrainz/releases/browse/' + group.id + '/'
|
let url = 'providers/musicbrainz/releases/browse/' + group.id + '/'
|
||||||
let resource = Vue.resource(url)
|
return axios.get(url).then((response) => {
|
||||||
resource.get({}).then((response) => {
|
|
||||||
logger.default.info('successfully fetched release group', group.id)
|
logger.default.info('successfully fetched release group', group.id)
|
||||||
let release = response.data['release-list'].filter(r => {
|
let release = response.data['release-list'].filter(r => {
|
||||||
return r.status === 'Official'
|
return r.status === 'Official'
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
<div v-if="batch.status === 'pending'" class="label">Importing {{ batch.jobs.length }} tracks...</div>
|
<div v-if="batch.status === 'pending'" class="label">Importing {{ batch.jobs.length }} tracks...</div>
|
||||||
<div v-if="batch.status === 'finished'" class="label">Imported {{ batch.jobs.length }} tracks!</div>
|
<div v-if="batch.status === 'finished'" class="label">Imported {{ batch.jobs.length }} tracks!</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="ui table">
|
<table class="ui unstackable table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Job ID</th>
|
<th>Job ID</th>
|
||||||
|
@ -52,11 +52,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import config from '@/config'
|
|
||||||
|
|
||||||
const FETCH_URL = config.API_URL + 'import-batches/'
|
const FETCH_URL = 'import-batches/'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['id'],
|
props: ['id'],
|
||||||
|
@ -75,7 +74,7 @@ export default {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let url = FETCH_URL + this.id + '/'
|
let url = FETCH_URL + this.id + '/'
|
||||||
logger.default.debug('Fetching batch "' + this.id + '"')
|
logger.default.debug('Fetching batch "' + this.id + '"')
|
||||||
this.$http.get(url).then((response) => {
|
axios.get(url).then((response) => {
|
||||||
self.batch = response.data
|
self.batch = response.data
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
if (self.batch.status === 'pending') {
|
if (self.batch.status === 'pending') {
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
:disabled="!nextLink">Next <i class="right arrow icon"></i></button>
|
:disabled="!nextLink">Next <i class="right arrow icon"></i></button>
|
||||||
<div class="ui hidden clearing divider"></div>
|
<div class="ui hidden clearing divider"></div>
|
||||||
<div class="ui hidden clearing divider"></div>
|
<div class="ui hidden clearing divider"></div>
|
||||||
<table v-if="results.length > 0" class="ui table">
|
<table v-if="results.length > 0" class="ui unstackable table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
|
@ -42,10 +42,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import config from '@/config'
|
|
||||||
|
|
||||||
const BATCHES_URL = config.API_URL + 'import-batches/'
|
const BATCHES_URL = 'import-batches/'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {},
|
components: {},
|
||||||
|
@ -65,7 +65,7 @@ export default {
|
||||||
var self = this
|
var self = this
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
logger.default.time('Loading import batches')
|
logger.default.time('Loading import batches')
|
||||||
this.$http.get(url, {}).then((response) => {
|
axios.get(url, {}).then((response) => {
|
||||||
self.results = response.data.results
|
self.results = response.data.results
|
||||||
self.nextLink = response.data.next
|
self.nextLink = response.data.next
|
||||||
self.previousLink = response.data.previous
|
self.previousLink = response.data.previous
|
||||||
|
|
|
@ -62,10 +62,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Vue from 'vue'
|
import axios from 'axios'
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import FileUploadWidget from './FileUploadWidget'
|
import FileUploadWidget from './FileUploadWidget'
|
||||||
import config from '@/config'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -74,7 +73,7 @@ export default {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
files: [],
|
files: [],
|
||||||
uploadUrl: config.API_URL + 'import-jobs/',
|
uploadUrl: 'import-jobs/',
|
||||||
batch: null
|
batch: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -106,9 +105,7 @@ export default {
|
||||||
},
|
},
|
||||||
createBatch () {
|
createBatch () {
|
||||||
let self = this
|
let self = this
|
||||||
let url = config.API_URL + 'import-batches/'
|
return axios.post('import-batches/', {}).then((response) => {
|
||||||
let resource = Vue.resource(url)
|
|
||||||
resource.save({}, {}).then((response) => {
|
|
||||||
self.batch = response.data
|
self.batch = response.data
|
||||||
}, (response) => {
|
}, (response) => {
|
||||||
logger.default.error('error while launching creating batch')
|
logger.default.error('error while launching creating batch')
|
||||||
|
|
|
@ -3,9 +3,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import config from '@/config'
|
|
||||||
import Vue from 'vue'
|
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -31,10 +30,9 @@ export default {
|
||||||
launchImport () {
|
launchImport () {
|
||||||
let self = this
|
let self = this
|
||||||
this.isImporting = true
|
this.isImporting = true
|
||||||
let url = config.API_URL + 'submit/' + self.importType + '/'
|
let url = 'submit/' + self.importType + '/'
|
||||||
let payload = self.importData
|
let payload = self.importData
|
||||||
let resource = Vue.resource(url)
|
axios.post(url, payload).then((response) => {
|
||||||
resource.save({}, payload).then((response) => {
|
|
||||||
logger.default.info('launched import for', self.type, self.metadata.id)
|
logger.default.info('launched import for', self.type, self.metadata.id)
|
||||||
self.isImporting = false
|
self.isImporting = false
|
||||||
router.push({
|
router.push({
|
||||||
|
|
|
@ -70,9 +70,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import time from '@/utils/time'
|
import time from '@/utils/time'
|
||||||
import config from '@/config'
|
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import ImportMixin from './ImportMixin'
|
import ImportMixin from './ImportMixin'
|
||||||
|
|
||||||
|
@ -117,10 +117,8 @@ export default Vue.extend({
|
||||||
search () {
|
search () {
|
||||||
let self = this
|
let self = this
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let url = config.API_URL + 'providers/' + this.currentBackendId + '/search/'
|
let url = 'providers/' + this.currentBackendId + '/search/'
|
||||||
let resource = Vue.resource(url)
|
axios.get(url, {params: {query: this.query}}).then((response) => {
|
||||||
|
|
||||||
resource.get({query: this.query}).then((response) => {
|
|
||||||
logger.default.debug('searching', self.query, 'on', self.currentBackendId)
|
logger.default.debug('searching', self.query, 'on', self.currentBackendId)
|
||||||
self.results = response.data
|
self.results = response.data
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
|
|
|
@ -67,7 +67,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import config from '@/config'
|
import axios from 'axios'
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import BuilderFilter from './Filter'
|
import BuilderFilter from './Filter'
|
||||||
|
@ -107,8 +107,8 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
fetchFilters: function () {
|
fetchFilters: function () {
|
||||||
let self = this
|
let self = this
|
||||||
let url = config.API_URL + 'radios/radios/filters/'
|
let url = 'radios/radios/filters/'
|
||||||
return this.$http.get(url).then((response) => {
|
return axios.get(url).then((response) => {
|
||||||
self.availableFilters = response.data
|
self.availableFilters = response.data
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -130,8 +130,8 @@ export default {
|
||||||
},
|
},
|
||||||
fetch: function () {
|
fetch: function () {
|
||||||
let self = this
|
let self = this
|
||||||
let url = config.API_URL + 'radios/radios/' + this.id + '/'
|
let url = 'radios/radios/' + this.id + '/'
|
||||||
this.$http.get(url).then((response) => {
|
axios.get(url).then((response) => {
|
||||||
self.filters = response.data.config.map(f => {
|
self.filters = response.data.config.map(f => {
|
||||||
return {
|
return {
|
||||||
config: f,
|
config: f,
|
||||||
|
@ -145,7 +145,7 @@ export default {
|
||||||
},
|
},
|
||||||
fetchCandidates: function () {
|
fetchCandidates: function () {
|
||||||
let self = this
|
let self = this
|
||||||
let url = config.API_URL + 'radios/radios/validate/'
|
let url = 'radios/radios/validate/'
|
||||||
let final = this.filters.map(f => {
|
let final = this.filters.map(f => {
|
||||||
let c = _.clone(f.config)
|
let c = _.clone(f.config)
|
||||||
c.type = f.filter.type
|
c.type = f.filter.type
|
||||||
|
@ -156,7 +156,7 @@ export default {
|
||||||
{'type': 'group', filters: final}
|
{'type': 'group', filters: final}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
this.$http.post(url, final).then((response) => {
|
axios.post(url, final).then((response) => {
|
||||||
self.checkResult = response.data.filters[0]
|
self.checkResult = response.data.filters[0]
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -173,12 +173,12 @@ export default {
|
||||||
'config': final
|
'config': final
|
||||||
}
|
}
|
||||||
if (this.id) {
|
if (this.id) {
|
||||||
let url = config.API_URL + 'radios/radios/' + this.id + '/'
|
let url = 'radios/radios/' + this.id + '/'
|
||||||
this.$http.put(url, final).then((response) => {
|
axios.put(url, final).then((response) => {
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let url = config.API_URL + 'radios/radios/'
|
let url = 'radios/radios/'
|
||||||
this.$http.post(url, final).then((response) => {
|
axios.post(url, final).then((response) => {
|
||||||
self.$router.push({
|
self.$router.push({
|
||||||
name: 'library.radios.edit',
|
name: 'library.radios.edit',
|
||||||
params: {
|
params: {
|
||||||
|
|
|
@ -62,6 +62,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
@ -132,11 +133,11 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
fetchCandidates: function () {
|
fetchCandidates: function () {
|
||||||
let self = this
|
let self = this
|
||||||
let url = config.API_URL + 'radios/radios/validate/'
|
let url = 'radios/radios/validate/'
|
||||||
let final = _.clone(this.config)
|
let final = _.clone(this.config)
|
||||||
final.type = this.filter.type
|
final.type = this.filter.type
|
||||||
final = {'filters': [final]}
|
final = {'filters': [final]}
|
||||||
this.$http.post(url, final).then((response) => {
|
axios.post(url, final).then((response) => {
|
||||||
self.checkResult = response.data.filters[0]
|
self.checkResult = response.data.filters[0]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
|
|
||||||
import config from '@/config'
|
|
||||||
import Vue from 'vue'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
mbId: {type: String, required: true}
|
mbId: {type: String, required: true}
|
||||||
|
@ -25,9 +23,8 @@ export default {
|
||||||
fetchData () {
|
fetchData () {
|
||||||
let self = this
|
let self = this
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let url = config.API_URL + 'providers/musicbrainz/' + this.type + 's/' + this.mbId + '/'
|
let url = 'providers/musicbrainz/' + this.type + 's/' + this.mbId + '/'
|
||||||
let resource = Vue.resource(url)
|
axios.get(url).then((response) => {
|
||||||
resource.get({}).then((response) => {
|
|
||||||
logger.default.info('successfully fetched', self.type, self.mbId)
|
logger.default.info('successfully fetched', self.type, self.mbId)
|
||||||
self.data = response.data[self.type]
|
self.data = response.data[self.type]
|
||||||
this.$emit('metadata-changed', self.data)
|
this.$emit('metadata-changed', self.data)
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
class Config {
|
class Config {
|
||||||
constructor () {
|
constructor () {
|
||||||
this.BACKEND_URL = process.env.BACKEND_URL
|
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.endsWith('/')) {
|
|
||||||
this.BACKEND_URL += '/'
|
|
||||||
}
|
|
||||||
this.API_URL = this.BACKEND_URL + 'api/v1/'
|
this.API_URL = this.BACKEND_URL + 'api/v1/'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,13 @@ logger.default.debug('Environment variables:', process.env)
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import VueResource from 'vue-resource'
|
import axios from 'axios'
|
||||||
import VueLazyload from 'vue-lazyload'
|
import VueLazyload from 'vue-lazyload'
|
||||||
import store from './store'
|
import store from './store'
|
||||||
|
import config from './config'
|
||||||
|
import { sync } from 'vuex-router-sync'
|
||||||
|
|
||||||
|
sync(store, router)
|
||||||
|
|
||||||
window.$ = window.jQuery = require('jquery')
|
window.$ = window.jQuery = require('jquery')
|
||||||
|
|
||||||
|
@ -19,25 +23,33 @@ window.$ = window.jQuery = require('jquery')
|
||||||
// require('./semantic/semantic.css')
|
// require('./semantic/semantic.css')
|
||||||
require('semantic-ui-css/semantic.js')
|
require('semantic-ui-css/semantic.js')
|
||||||
|
|
||||||
Vue.use(VueResource)
|
|
||||||
Vue.use(VueLazyload)
|
Vue.use(VueLazyload)
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
Vue.http.interceptors.push(function (request, next) {
|
axios.defaults.baseURL = config.API_URL
|
||||||
// modify headers
|
axios.interceptors.request.use(function (config) {
|
||||||
|
// Do something before request is sent
|
||||||
if (store.state.auth.authenticated) {
|
if (store.state.auth.authenticated) {
|
||||||
request.headers.set('Authorization', store.getters['auth/header'])
|
config.headers['Authorization'] = store.getters['auth/header']
|
||||||
}
|
}
|
||||||
next(function (response) {
|
return config
|
||||||
// redirect to login form when we get unauthorized response from server
|
}, function (error) {
|
||||||
if (response.status === 401) {
|
// Do something with request error
|
||||||
store.commit('auth/authenticated', false)
|
return Promise.reject(error)
|
||||||
logger.default.warn('Received 401 response from API, redirecting to login form')
|
|
||||||
router.push({name: 'login', query: {next: router.currentRoute.fullPath}})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Add a response interceptor
|
||||||
|
axios.interceptors.response.use(function (response) {
|
||||||
|
return response
|
||||||
|
}, function (error) {
|
||||||
|
if (error.response.status === 401) {
|
||||||
|
store.commit('auth/authenticated', false)
|
||||||
|
logger.default.warn('Received 401 response from API, redirecting to login form')
|
||||||
|
router.push({name: 'login', query: {next: router.currentRoute.fullPath}})
|
||||||
|
}
|
||||||
|
// Do something with response error
|
||||||
|
return Promise.reject(error)
|
||||||
|
})
|
||||||
store.dispatch('auth/check')
|
store.dispatch('auth/check')
|
||||||
/* eslint-disable no-new */
|
/* eslint-disable no-new */
|
||||||
new Vue({
|
new Vue({
|
||||||
|
|
|
@ -515,7 +515,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-size: 14px;
|
font-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
@ -525,7 +525,7 @@ body {
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
background: #FFFFFF;
|
background: #FFFFFF;
|
||||||
font-family: 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
font-family: 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||||
font-size: 14px;
|
font-size: 100%;
|
||||||
line-height: 1.4285em;
|
line-height: 1.4285em;
|
||||||
color: rgba(0, 0, 0, 0.87);
|
color: rgba(0, 0, 0, 0.87);
|
||||||
font-smoothing: antialiased;
|
font-smoothing: antialiased;
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
import Vue from 'vue'
|
import axios from 'axios'
|
||||||
import jwtDecode from 'jwt-decode'
|
import jwtDecode from 'jwt-decode'
|
||||||
import config from '@/config'
|
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
|
|
||||||
const LOGIN_URL = config.API_URL + 'token/'
|
|
||||||
const REFRESH_TOKEN_URL = config.API_URL + 'token/refresh/'
|
|
||||||
const USER_PROFILE_URL = config.API_URL + 'users/users/me/'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
|
@ -54,9 +49,8 @@ export default {
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
// Send a request to the login URL and save the returned JWT
|
// Send a request to the login URL and save the returned JWT
|
||||||
login ({commit, dispatch, state}, {next, credentials, onError}) {
|
login ({commit, dispatch}, {next, credentials, onError}) {
|
||||||
let resource = Vue.resource(LOGIN_URL)
|
return axios.post('token/', credentials).then(response => {
|
||||||
return resource.save({}, credentials).then(response => {
|
|
||||||
logger.default.info('Successfully logged in as', credentials.username)
|
logger.default.info('Successfully logged in as', credentials.username)
|
||||||
commit('token', response.data.token)
|
commit('token', response.data.token)
|
||||||
commit('username', credentials.username)
|
commit('username', credentials.username)
|
||||||
|
@ -91,8 +85,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetchProfile ({commit, dispatch, state}) {
|
fetchProfile ({commit, dispatch, state}) {
|
||||||
let resource = Vue.resource(USER_PROFILE_URL)
|
return axios.get('users/users/me/').then((response) => {
|
||||||
return resource.get({}).then((response) => {
|
|
||||||
logger.default.info('Successfully fetched user profile')
|
logger.default.info('Successfully fetched user profile')
|
||||||
let data = response.data
|
let data = response.data
|
||||||
commit('profile', data)
|
commit('profile', data)
|
||||||
|
@ -107,8 +100,7 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
refreshToken ({commit, dispatch, state}) {
|
refreshToken ({commit, dispatch, state}) {
|
||||||
let resource = Vue.resource(REFRESH_TOKEN_URL)
|
return axios.post('token/refresh/', {token: state.token}).then(response => {
|
||||||
return resource.save({}, {token: state.token}).then(response => {
|
|
||||||
logger.default.info('Refreshed auth token')
|
logger.default.info('Refreshed auth token')
|
||||||
commit('token', response.data.token)
|
commit('token', response.data.token)
|
||||||
}, response => {
|
}, response => {
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import Vue from 'vue'
|
import axios from 'axios'
|
||||||
import config from '@/config'
|
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
|
|
||||||
const REMOVE_URL = config.API_URL + 'favorites/tracks/remove/'
|
|
||||||
const FAVORITES_URL = config.API_URL + 'favorites/tracks/'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
|
@ -35,16 +31,14 @@ export default {
|
||||||
set ({commit, state}, {id, value}) {
|
set ({commit, state}, {id, value}) {
|
||||||
commit('track', {id, value})
|
commit('track', {id, value})
|
||||||
if (value) {
|
if (value) {
|
||||||
let resource = Vue.resource(FAVORITES_URL)
|
return axios.post('favorites/tracks/', {'track': id}).then((response) => {
|
||||||
resource.save({}, {'track': id}).then((response) => {
|
|
||||||
logger.default.info('Successfully added track to favorites')
|
logger.default.info('Successfully added track to favorites')
|
||||||
}, (response) => {
|
}, (response) => {
|
||||||
logger.default.info('Error while adding track to favorites')
|
logger.default.info('Error while adding track to favorites')
|
||||||
commit('track', {id, value: !value})
|
commit('track', {id, value: !value})
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let resource = Vue.resource(REMOVE_URL)
|
return axios.post('favorites/tracks/remove/', {'track': id}).then((response) => {
|
||||||
resource.delete({}, {'track': id}).then((response) => {
|
|
||||||
logger.default.info('Successfully removed track from favorites')
|
logger.default.info('Successfully removed track from favorites')
|
||||||
}, (response) => {
|
}, (response) => {
|
||||||
logger.default.info('Error while removing track from favorites')
|
logger.default.info('Error while removing track from favorites')
|
||||||
|
@ -57,9 +51,8 @@ export default {
|
||||||
},
|
},
|
||||||
fetch ({dispatch, state, commit}, url) {
|
fetch ({dispatch, state, commit}, url) {
|
||||||
// will fetch favorites by batches from API to have them locally
|
// will fetch favorites by batches from API to have them locally
|
||||||
url = url || FAVORITES_URL
|
url = url || 'favorites/tracks/'
|
||||||
let resource = Vue.resource(url)
|
return axios.get(url).then((response) => {
|
||||||
resource.get().then((response) => {
|
|
||||||
logger.default.info('Fetched a batch of ' + response.data.results.length + ' favorites')
|
logger.default.info('Fetched a batch of ' + response.data.results.length + ' favorites')
|
||||||
response.data.results.forEach(result => {
|
response.data.results.forEach(result => {
|
||||||
commit('track', {id: result.track, value: true})
|
commit('track', {id: result.track, value: true})
|
||||||
|
|
|
@ -4,6 +4,7 @@ import createPersistedState from 'vuex-persistedstate'
|
||||||
|
|
||||||
import favorites from './favorites'
|
import favorites from './favorites'
|
||||||
import auth from './auth'
|
import auth from './auth'
|
||||||
|
import instance from './instance'
|
||||||
import queue from './queue'
|
import queue from './queue'
|
||||||
import radios from './radios'
|
import radios from './radios'
|
||||||
import player from './player'
|
import player from './player'
|
||||||
|
@ -14,6 +15,7 @@ export default new Vuex.Store({
|
||||||
modules: {
|
modules: {
|
||||||
auth,
|
auth,
|
||||||
favorites,
|
favorites,
|
||||||
|
instance,
|
||||||
queue,
|
queue,
|
||||||
radios,
|
radios,
|
||||||
player
|
player
|
||||||
|
@ -37,7 +39,6 @@ export default new Vuex.Store({
|
||||||
key: 'player',
|
key: 'player',
|
||||||
paths: [
|
paths: [
|
||||||
'player.looping',
|
'player.looping',
|
||||||
'player.playing',
|
|
||||||
'player.volume',
|
'player.volume',
|
||||||
'player.duration',
|
'player.duration',
|
||||||
'player.errored'],
|
'player.errored'],
|
||||||
|
@ -45,21 +46,6 @@ export default new Vuex.Store({
|
||||||
return mutation.type.startsWith('player/') && mutation.type !== 'player/currentTime'
|
return mutation.type.startsWith('player/') && mutation.type !== 'player/currentTime'
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
createPersistedState({
|
|
||||||
key: 'progress',
|
|
||||||
paths: ['player.currentTime'],
|
|
||||||
filter: (mutation) => {
|
|
||||||
let delay = 10
|
|
||||||
return mutation.type === 'player/currentTime' && parseInt(mutation.payload) % delay === 0
|
|
||||||
},
|
|
||||||
reducer: (state) => {
|
|
||||||
return {
|
|
||||||
player: {
|
|
||||||
currentTime: state.player.currentTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
createPersistedState({
|
createPersistedState({
|
||||||
key: 'queue',
|
key: 'queue',
|
||||||
filter: (mutation) => {
|
filter: (mutation) => {
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import axios from 'axios'
|
||||||
|
import logger from '@/logging'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
settings: {
|
||||||
|
raven: {
|
||||||
|
front_enabled: {
|
||||||
|
value: false
|
||||||
|
},
|
||||||
|
front_dsn: {
|
||||||
|
value: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
settings: (state, value) => {
|
||||||
|
_.merge(state.settings, value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
// Send a request to the login URL and save the returned JWT
|
||||||
|
fetchSettings ({commit}) {
|
||||||
|
return axios.get('instance/settings/').then(response => {
|
||||||
|
logger.default.info('Successfully fetched instance settings')
|
||||||
|
let sections = {}
|
||||||
|
response.data.forEach(e => {
|
||||||
|
sections[e.section] = {}
|
||||||
|
})
|
||||||
|
response.data.forEach(e => {
|
||||||
|
sections[e.section][e.name] = e
|
||||||
|
})
|
||||||
|
commit('settings', sections)
|
||||||
|
}, response => {
|
||||||
|
logger.default.error('Error while fetching settings', response.data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
import Vue from 'vue'
|
import axios from 'axios'
|
||||||
import config from '@/config'
|
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import time from '@/utils/time'
|
import time from '@/utils/time'
|
||||||
|
|
||||||
|
@ -61,8 +60,8 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
incrementVolume (context, value) {
|
incrementVolume ({commit, state}, value) {
|
||||||
context.commit('volume', context.state.volume + value)
|
commit('volume', state.volume + value)
|
||||||
},
|
},
|
||||||
stop (context) {
|
stop (context) {
|
||||||
},
|
},
|
||||||
|
@ -70,9 +69,7 @@ export default {
|
||||||
commit('playing', !state.playing)
|
commit('playing', !state.playing)
|
||||||
},
|
},
|
||||||
trackListened ({commit}, track) {
|
trackListened ({commit}, track) {
|
||||||
let url = config.API_URL + 'history/listenings/'
|
return axios.post('history/listenings/', {'track': track.id}).then((response) => {}, (response) => {
|
||||||
let resource = Vue.resource(url)
|
|
||||||
resource.save({}, {'track': track.id}).then((response) => {}, (response) => {
|
|
||||||
logger.default.error('Could not record track in history')
|
logger.default.error('Could not record track in history')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -55,33 +55,32 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
append (context, {track, index, skipPlay}) {
|
append ({commit, state, dispatch}, {track, index, skipPlay}) {
|
||||||
index = index || context.state.tracks.length
|
index = index || state.tracks.length
|
||||||
if (index > context.state.tracks.length - 1) {
|
if (index > state.tracks.length - 1) {
|
||||||
// we simply push to the end
|
// we simply push to the end
|
||||||
context.commit('insert', {track, index: context.state.tracks.length})
|
commit('insert', {track, index: state.tracks.length})
|
||||||
} else {
|
} else {
|
||||||
// we insert the track at given position
|
// we insert the track at given position
|
||||||
context.commit('insert', {track, index})
|
commit('insert', {track, index})
|
||||||
}
|
}
|
||||||
if (!skipPlay) {
|
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 }))
|
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
|
index = 0
|
||||||
} else {
|
} else {
|
||||||
index = index || context.state.tracks.length
|
index = index || state.tracks.length
|
||||||
}
|
}
|
||||||
tracks.forEach((t) => {
|
tracks.forEach((t) => {
|
||||||
context.dispatch('append', {track: t, index: index, skipPlay: true})
|
dispatch('append', {track: t, index: index, skipPlay: true})
|
||||||
index += 1
|
index += 1
|
||||||
})
|
})
|
||||||
context.dispatch('resume')
|
dispatch('resume')
|
||||||
},
|
},
|
||||||
|
|
||||||
cleanTrack ({state, dispatch, commit}, index) {
|
cleanTrack ({state, dispatch, commit}, index) {
|
||||||
|
@ -100,14 +99,14 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
resume (context) {
|
resume ({state, dispatch, rootState}) {
|
||||||
if (context.state.ended | context.rootState.player.errored) {
|
if (state.ended | rootState.player.errored) {
|
||||||
context.dispatch('next')
|
dispatch('next')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
previous (context) {
|
previous ({state, dispatch}) {
|
||||||
if (context.state.currentIndex > 0) {
|
if (state.currentIndex > 0) {
|
||||||
context.dispatch('currentIndex', context.state.currentIndex - 1)
|
dispatch('currentIndex', state.currentIndex - 1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
next ({state, dispatch, commit, rootState}) {
|
next ({state, dispatch, commit, rootState}) {
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import Vue from 'vue'
|
import axios from 'axios'
|
||||||
import config from '@/config'
|
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
|
|
||||||
const CREATE_RADIO_URL = config.API_URL + 'radios/sessions/'
|
|
||||||
const GET_TRACK_URL = config.API_URL + 'radios/tracks/'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
|
@ -39,13 +35,12 @@ export default {
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
start ({commit, dispatch}, {type, objectId, customRadioId}) {
|
start ({commit, dispatch}, {type, objectId, customRadioId}) {
|
||||||
let resource = Vue.resource(CREATE_RADIO_URL)
|
|
||||||
var params = {
|
var params = {
|
||||||
radio_type: type,
|
radio_type: type,
|
||||||
related_object_id: objectId,
|
related_object_id: objectId,
|
||||||
custom_radio: customRadioId
|
custom_radio: customRadioId
|
||||||
}
|
}
|
||||||
resource.save({}, params).then((response) => {
|
return axios.post('radios/sessions/', params).then((response) => {
|
||||||
logger.default.info('Successfully started radio ', type)
|
logger.default.info('Successfully started radio ', type)
|
||||||
commit('current', {type, objectId, session: response.data.id, customRadioId})
|
commit('current', {type, objectId, session: response.data.id, customRadioId})
|
||||||
commit('running', true)
|
commit('running', true)
|
||||||
|
@ -62,12 +57,10 @@ export default {
|
||||||
if (!state.running) {
|
if (!state.running) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let resource = Vue.resource(GET_TRACK_URL)
|
|
||||||
var params = {
|
var params = {
|
||||||
session: state.current.session
|
session: state.current.session
|
||||||
}
|
}
|
||||||
let promise = resource.save({}, params)
|
return axios.post('radios/tracks/', params).then((response) => {
|
||||||
promise.then((response) => {
|
|
||||||
logger.default.info('Adding track to queue from radio')
|
logger.default.info('Adding track to queue from radio')
|
||||||
dispatch('queue/append', {track: response.data.track}, {root: true})
|
dispatch('queue/append', {track: response.data.track}, {root: true})
|
||||||
}, (response) => {
|
}, (response) => {
|
||||||
|
|
|
@ -0,0 +1,567 @@
|
||||||
|
@charset "UTF-8";
|
||||||
|
|
||||||
|
// _ _ _ _ _
|
||||||
|
// (_) | | | | | (_)
|
||||||
|
// _ _ __ ___| |_ _ __| | ___ _ __ ___ ___ __| |_ __ _
|
||||||
|
// | | '_ \ / __| | | | |/ _` |/ _ \ | '_ ` _ \ / _ \/ _` | |/ _` |
|
||||||
|
// | | | | | (__| | |_| | (_| | __/ | | | | | | __/ (_| | | (_| |
|
||||||
|
// |_|_| |_|\___|_|\__,_|\__,_|\___| |_| |_| |_|\___|\__,_|_|\__,_|
|
||||||
|
//
|
||||||
|
// Simple, elegant and maintainable media queries in Sass
|
||||||
|
// v1.4.9
|
||||||
|
//
|
||||||
|
// http://include-media.com
|
||||||
|
//
|
||||||
|
// Authors: Eduardo Boucas (@eduardoboucas)
|
||||||
|
// Hugo Giraudel (@hugogiraudel)
|
||||||
|
//
|
||||||
|
// This project is licensed under the terms of the MIT license
|
||||||
|
|
||||||
|
|
||||||
|
////
|
||||||
|
/// include-media library public configuration
|
||||||
|
/// @author Eduardo Boucas
|
||||||
|
/// @access public
|
||||||
|
////
|
||||||
|
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Creates a list of global breakpoints
|
||||||
|
///
|
||||||
|
/// @example scss - Creates a single breakpoint with the label `phone`
|
||||||
|
/// $breakpoints: ('phone': 320px);
|
||||||
|
///
|
||||||
|
$breakpoints: (
|
||||||
|
'phone': 320px,
|
||||||
|
'tablet': 768px,
|
||||||
|
'desktop': 1024px
|
||||||
|
) !default;
|
||||||
|
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Creates a list of static expressions or media types
|
||||||
|
///
|
||||||
|
/// @example scss - Creates a single media type (screen)
|
||||||
|
/// $media-expressions: ('screen': 'screen');
|
||||||
|
///
|
||||||
|
/// @example scss - Creates a static expression with logical disjunction (OR operator)
|
||||||
|
/// $media-expressions: (
|
||||||
|
/// 'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)'
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
$media-expressions: (
|
||||||
|
'screen': 'screen',
|
||||||
|
'print': 'print',
|
||||||
|
'handheld': 'handheld',
|
||||||
|
'landscape': '(orientation: landscape)',
|
||||||
|
'portrait': '(orientation: portrait)',
|
||||||
|
'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi), (min-resolution: 2dppx)',
|
||||||
|
'retina3x': '(-webkit-min-device-pixel-ratio: 3), (min-resolution: 350dpi), (min-resolution: 3dppx)'
|
||||||
|
) !default;
|
||||||
|
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Defines a number to be added or subtracted from each unit when declaring breakpoints with exclusive intervals
|
||||||
|
///
|
||||||
|
/// @example scss - Interval for pixels is defined as `1` by default
|
||||||
|
/// @include media('>128px') {}
|
||||||
|
///
|
||||||
|
/// /* Generates: */
|
||||||
|
/// @media (min-width: 129px) {}
|
||||||
|
///
|
||||||
|
/// @example scss - Interval for ems is defined as `0.01` by default
|
||||||
|
/// @include media('>20em') {}
|
||||||
|
///
|
||||||
|
/// /* Generates: */
|
||||||
|
/// @media (min-width: 20.01em) {}
|
||||||
|
///
|
||||||
|
/// @example scss - Interval for rems is defined as `0.1` by default, to be used with `font-size: 62.5%;`
|
||||||
|
/// @include media('>2.0rem') {}
|
||||||
|
///
|
||||||
|
/// /* Generates: */
|
||||||
|
/// @media (min-width: 2.1rem) {}
|
||||||
|
///
|
||||||
|
$unit-intervals: (
|
||||||
|
'px': 1,
|
||||||
|
'em': 0.01,
|
||||||
|
'rem': 0.1,
|
||||||
|
'': 0
|
||||||
|
) !default;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Defines whether support for media queries is available, useful for creating separate stylesheets
|
||||||
|
/// for browsers that don't support media queries.
|
||||||
|
///
|
||||||
|
/// @example scss - Disables support for media queries
|
||||||
|
/// $im-media-support: false;
|
||||||
|
/// @include media('>=tablet') {
|
||||||
|
/// .foo {
|
||||||
|
/// color: tomato;
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// /* Generates: */
|
||||||
|
/// .foo {
|
||||||
|
/// color: tomato;
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
$im-media-support: true !default;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Selects which breakpoint to emulate when support for media queries is disabled. Media queries that start at or
|
||||||
|
/// intercept the breakpoint will be displayed, any others will be ignored.
|
||||||
|
///
|
||||||
|
/// @example scss - This media query will show because it intercepts the static breakpoint
|
||||||
|
/// $im-media-support: false;
|
||||||
|
/// $im-no-media-breakpoint: 'desktop';
|
||||||
|
/// @include media('>=tablet') {
|
||||||
|
/// .foo {
|
||||||
|
/// color: tomato;
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// /* Generates: */
|
||||||
|
/// .foo {
|
||||||
|
/// color: tomato;
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// @example scss - This media query will NOT show because it does not intercept the desktop breakpoint
|
||||||
|
/// $im-media-support: false;
|
||||||
|
/// $im-no-media-breakpoint: 'tablet';
|
||||||
|
/// @include media('>=desktop') {
|
||||||
|
/// .foo {
|
||||||
|
/// color: tomato;
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// /* No output */
|
||||||
|
///
|
||||||
|
$im-no-media-breakpoint: 'desktop' !default;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Selects which media expressions are allowed in an expression for it to be used when media queries
|
||||||
|
/// are not supported.
|
||||||
|
///
|
||||||
|
/// @example scss - This media query will show because it intercepts the static breakpoint and contains only accepted media expressions
|
||||||
|
/// $im-media-support: false;
|
||||||
|
/// $im-no-media-breakpoint: 'desktop';
|
||||||
|
/// $im-no-media-expressions: ('screen');
|
||||||
|
/// @include media('>=tablet', 'screen') {
|
||||||
|
/// .foo {
|
||||||
|
/// color: tomato;
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// /* Generates: */
|
||||||
|
/// .foo {
|
||||||
|
/// color: tomato;
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// @example scss - This media query will NOT show because it intercepts the static breakpoint but contains a media expression that is not accepted
|
||||||
|
/// $im-media-support: false;
|
||||||
|
/// $im-no-media-breakpoint: 'desktop';
|
||||||
|
/// $im-no-media-expressions: ('screen');
|
||||||
|
/// @include media('>=tablet', 'retina2x') {
|
||||||
|
/// .foo {
|
||||||
|
/// color: tomato;
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// /* No output */
|
||||||
|
///
|
||||||
|
$im-no-media-expressions: ('screen', 'portrait', 'landscape') !default;
|
||||||
|
|
||||||
|
////
|
||||||
|
/// Cross-engine logging engine
|
||||||
|
/// @author Hugo Giraudel
|
||||||
|
/// @access private
|
||||||
|
////
|
||||||
|
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Log a message either with `@error` if supported
|
||||||
|
/// else with `@warn`, using `feature-exists('at-error')`
|
||||||
|
/// to detect support.
|
||||||
|
///
|
||||||
|
/// @param {String} $message - Message to log
|
||||||
|
///
|
||||||
|
@function im-log($message) {
|
||||||
|
@if feature-exists('at-error') {
|
||||||
|
@error $message;
|
||||||
|
} @else {
|
||||||
|
@warn $message;
|
||||||
|
$_: noop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@return $message;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Wrapper mixin for the log function so it can be used with a more friendly
|
||||||
|
/// API than `@if im-log('..') {}` or `$_: im-log('..')`. Basically, use the function
|
||||||
|
/// within functions because it is not possible to include a mixin in a function
|
||||||
|
/// and use the mixin everywhere else because it's much more elegant.
|
||||||
|
///
|
||||||
|
/// @param {String} $message - Message to log
|
||||||
|
///
|
||||||
|
@mixin log($message) {
|
||||||
|
@if im-log($message) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Function with no `@return` called next to `@warn` in Sass 3.3
|
||||||
|
/// to trigger a compiling error and stop the process.
|
||||||
|
///
|
||||||
|
@function noop() {}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Determines whether a list of conditions is intercepted by the static breakpoint.
|
||||||
|
///
|
||||||
|
/// @param {Arglist} $conditions - Media query conditions
|
||||||
|
///
|
||||||
|
/// @return {Boolean} - Returns true if the conditions are intercepted by the static breakpoint
|
||||||
|
///
|
||||||
|
@function im-intercepts-static-breakpoint($conditions...) {
|
||||||
|
$no-media-breakpoint-value: map-get($breakpoints, $im-no-media-breakpoint);
|
||||||
|
|
||||||
|
@if not $no-media-breakpoint-value {
|
||||||
|
@if im-log('`#{$im-no-media-breakpoint}` is not a valid breakpoint.') {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@each $condition in $conditions {
|
||||||
|
@if not map-has-key($media-expressions, $condition) {
|
||||||
|
$operator: get-expression-operator($condition);
|
||||||
|
$prefix: get-expression-prefix($operator);
|
||||||
|
$value: get-expression-value($condition, $operator);
|
||||||
|
|
||||||
|
@if ($prefix == 'max' and $value <= $no-media-breakpoint-value) or
|
||||||
|
($prefix == 'min' and $value > $no-media-breakpoint-value) {
|
||||||
|
@return false;
|
||||||
|
}
|
||||||
|
} @else if not index($im-no-media-expressions, $condition) {
|
||||||
|
@return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
////
|
||||||
|
/// Parsing engine
|
||||||
|
/// @author Hugo Giraudel
|
||||||
|
/// @access private
|
||||||
|
////
|
||||||
|
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Get operator of an expression
|
||||||
|
///
|
||||||
|
/// @param {String} $expression - Expression to extract operator from
|
||||||
|
///
|
||||||
|
/// @return {String} - Any of `>=`, `>`, `<=`, `<`, `≥`, `≤`
|
||||||
|
///
|
||||||
|
@function get-expression-operator($expression) {
|
||||||
|
@each $operator in ('>=', '>', '<=', '<', '≥', '≤') {
|
||||||
|
@if str-index($expression, $operator) {
|
||||||
|
@return $operator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// It is not possible to include a mixin inside a function, so we have to
|
||||||
|
// rely on the `im-log(..)` function rather than the `log(..)` mixin. Because
|
||||||
|
// functions cannot be called anywhere in Sass, we need to hack the call in
|
||||||
|
// a dummy variable, such as `$_`. If anybody ever raise a scoping issue with
|
||||||
|
// Sass 3.3, change this line in `@if im-log(..) {}` instead.
|
||||||
|
$_: im-log('No operator found in `#{$expression}`.');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Get dimension of an expression, based on a found operator
|
||||||
|
///
|
||||||
|
/// @param {String} $expression - Expression to extract dimension from
|
||||||
|
/// @param {String} $operator - Operator from `$expression`
|
||||||
|
///
|
||||||
|
/// @return {String} - `width` or `height` (or potentially anything else)
|
||||||
|
///
|
||||||
|
@function get-expression-dimension($expression, $operator) {
|
||||||
|
$operator-index: str-index($expression, $operator);
|
||||||
|
$parsed-dimension: str-slice($expression, 0, $operator-index - 1);
|
||||||
|
$dimension: 'width';
|
||||||
|
|
||||||
|
@if str-length($parsed-dimension) > 0 {
|
||||||
|
$dimension: $parsed-dimension;
|
||||||
|
}
|
||||||
|
|
||||||
|
@return $dimension;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Get dimension prefix based on an operator
|
||||||
|
///
|
||||||
|
/// @param {String} $operator - Operator
|
||||||
|
///
|
||||||
|
/// @return {String} - `min` or `max`
|
||||||
|
///
|
||||||
|
@function get-expression-prefix($operator) {
|
||||||
|
@return if(index(('<', '<=', '≤'), $operator), 'max', 'min');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Get value of an expression, based on a found operator
|
||||||
|
///
|
||||||
|
/// @param {String} $expression - Expression to extract value from
|
||||||
|
/// @param {String} $operator - Operator from `$expression`
|
||||||
|
///
|
||||||
|
/// @return {Number} - A numeric value
|
||||||
|
///
|
||||||
|
@function get-expression-value($expression, $operator) {
|
||||||
|
$operator-index: str-index($expression, $operator);
|
||||||
|
$value: str-slice($expression, $operator-index + str-length($operator));
|
||||||
|
|
||||||
|
@if map-has-key($breakpoints, $value) {
|
||||||
|
$value: map-get($breakpoints, $value);
|
||||||
|
} @else {
|
||||||
|
$value: to-number($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
$interval: map-get($unit-intervals, unit($value));
|
||||||
|
|
||||||
|
@if not $interval {
|
||||||
|
// It is not possible to include a mixin inside a function, so we have to
|
||||||
|
// rely on the `im-log(..)` function rather than the `log(..)` mixin. Because
|
||||||
|
// functions cannot be called anywhere in Sass, we need to hack the call in
|
||||||
|
// a dummy variable, such as `$_`. If anybody ever raise a scoping issue with
|
||||||
|
// Sass 3.3, change this line in `@if im-log(..) {}` instead.
|
||||||
|
$_: im-log('Unknown unit `#{unit($value)}`.');
|
||||||
|
}
|
||||||
|
|
||||||
|
@if $operator == '>' {
|
||||||
|
$value: $value + $interval;
|
||||||
|
} @else if $operator == '<' {
|
||||||
|
$value: $value - $interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
@return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Parse an expression to return a valid media-query expression
|
||||||
|
///
|
||||||
|
/// @param {String} $expression - Expression to parse
|
||||||
|
///
|
||||||
|
/// @return {String} - Valid media query
|
||||||
|
///
|
||||||
|
@function parse-expression($expression) {
|
||||||
|
// If it is part of $media-expressions, it has no operator
|
||||||
|
// then there is no need to go any further, just return the value
|
||||||
|
@if map-has-key($media-expressions, $expression) {
|
||||||
|
@return map-get($media-expressions, $expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
$operator: get-expression-operator($expression);
|
||||||
|
$dimension: get-expression-dimension($expression, $operator);
|
||||||
|
$prefix: get-expression-prefix($operator);
|
||||||
|
$value: get-expression-value($expression, $operator);
|
||||||
|
|
||||||
|
@return '(#{$prefix}-#{$dimension}: #{$value})';
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Slice `$list` between `$start` and `$end` indexes
|
||||||
|
///
|
||||||
|
/// @access private
|
||||||
|
///
|
||||||
|
/// @param {List} $list - List to slice
|
||||||
|
/// @param {Number} $start [1] - Start index
|
||||||
|
/// @param {Number} $end [length($list)] - End index
|
||||||
|
///
|
||||||
|
/// @return {List} Sliced list
|
||||||
|
///
|
||||||
|
@function slice($list, $start: 1, $end: length($list)) {
|
||||||
|
@if length($list) < 1 or $start > $end {
|
||||||
|
@return ();
|
||||||
|
}
|
||||||
|
|
||||||
|
$result: ();
|
||||||
|
|
||||||
|
@for $i from $start through $end {
|
||||||
|
$result: append($result, nth($list, $i));
|
||||||
|
}
|
||||||
|
|
||||||
|
@return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
////
|
||||||
|
/// String to number converter
|
||||||
|
/// @author Hugo Giraudel
|
||||||
|
/// @access private
|
||||||
|
////
|
||||||
|
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Casts a string into a number
|
||||||
|
///
|
||||||
|
/// @param {String | Number} $value - Value to be parsed
|
||||||
|
///
|
||||||
|
/// @return {Number}
|
||||||
|
///
|
||||||
|
@function to-number($value) {
|
||||||
|
@if type-of($value) == 'number' {
|
||||||
|
@return $value;
|
||||||
|
} @else if type-of($value) != 'string' {
|
||||||
|
$_: im-log('Value for `to-number` should be a number or a string.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$first-character: str-slice($value, 1, 1);
|
||||||
|
$result: 0;
|
||||||
|
$digits: 0;
|
||||||
|
$minus: ($first-character == '-');
|
||||||
|
$numbers: ('0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9);
|
||||||
|
|
||||||
|
// Remove +/- sign if present at first character
|
||||||
|
@if ($first-character == '+' or $first-character == '-') {
|
||||||
|
$value: str-slice($value, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@for $i from 1 through str-length($value) {
|
||||||
|
$character: str-slice($value, $i, $i);
|
||||||
|
|
||||||
|
@if not (index(map-keys($numbers), $character) or $character == '.') {
|
||||||
|
@return to-length(if($minus, -$result, $result), str-slice($value, $i))
|
||||||
|
}
|
||||||
|
|
||||||
|
@if $character == '.' {
|
||||||
|
$digits: 1;
|
||||||
|
} @else if $digits == 0 {
|
||||||
|
$result: $result * 10 + map-get($numbers, $character);
|
||||||
|
} @else {
|
||||||
|
$digits: $digits * 10;
|
||||||
|
$result: $result + map-get($numbers, $character) / $digits;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@return if($minus, -$result, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Add `$unit` to `$value`
|
||||||
|
///
|
||||||
|
/// @param {Number} $value - Value to add unit to
|
||||||
|
/// @param {String} $unit - String representation of the unit
|
||||||
|
///
|
||||||
|
/// @return {Number} - `$value` expressed in `$unit`
|
||||||
|
///
|
||||||
|
@function to-length($value, $unit) {
|
||||||
|
$units: ('px': 1px, 'cm': 1cm, 'mm': 1mm, '%': 1%, 'ch': 1ch, 'pc': 1pc, 'in': 1in, 'em': 1em, 'rem': 1rem, 'pt': 1pt, 'ex': 1ex, 'vw': 1vw, 'vh': 1vh, 'vmin': 1vmin, 'vmax': 1vmax);
|
||||||
|
|
||||||
|
@if not index(map-keys($units), $unit) {
|
||||||
|
$_: im-log('Invalid unit `#{$unit}`.');
|
||||||
|
}
|
||||||
|
|
||||||
|
@return $value * map-get($units, $unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// This mixin aims at redefining the configuration just for the scope of
|
||||||
|
/// the call. It is helpful when having a component needing an extended
|
||||||
|
/// configuration such as custom breakpoints (referred to as tweakpoints)
|
||||||
|
/// for instance.
|
||||||
|
///
|
||||||
|
/// @author Hugo Giraudel
|
||||||
|
///
|
||||||
|
/// @param {Map} $tweakpoints [()] - Map of tweakpoints to be merged with `$breakpoints`
|
||||||
|
/// @param {Map} $tweak-media-expressions [()] - Map of tweaked media expressions to be merged with `$media-expression`
|
||||||
|
///
|
||||||
|
/// @example scss - Extend the global breakpoints with a tweakpoint
|
||||||
|
/// @include media-context(('custom': 678px)) {
|
||||||
|
/// .foo {
|
||||||
|
/// @include media('>phone', '<=custom') {
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// @example scss - Extend the global media expressions with a custom one
|
||||||
|
/// @include media-context($tweak-media-expressions: ('all': 'all')) {
|
||||||
|
/// .foo {
|
||||||
|
/// @include media('all', '>phone') {
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// @example scss - Extend both configuration maps
|
||||||
|
/// @include media-context(('custom': 678px), ('all': 'all')) {
|
||||||
|
/// .foo {
|
||||||
|
/// @include media('all', '>phone', '<=custom') {
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
@mixin media-context($tweakpoints: (), $tweak-media-expressions: ()) {
|
||||||
|
// Save global configuration
|
||||||
|
$global-breakpoints: $breakpoints;
|
||||||
|
$global-media-expressions: $media-expressions;
|
||||||
|
|
||||||
|
// Update global configuration
|
||||||
|
$breakpoints: map-merge($breakpoints, $tweakpoints) !global;
|
||||||
|
$media-expressions: map-merge($media-expressions, $tweak-media-expressions) !global;
|
||||||
|
|
||||||
|
@content;
|
||||||
|
|
||||||
|
// Restore global configuration
|
||||||
|
$breakpoints: $global-breakpoints !global;
|
||||||
|
$media-expressions: $global-media-expressions !global;
|
||||||
|
}
|
||||||
|
|
||||||
|
////
|
||||||
|
/// include-media public exposed API
|
||||||
|
/// @author Eduardo Boucas
|
||||||
|
/// @access public
|
||||||
|
////
|
||||||
|
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Generates a media query based on a list of conditions
|
||||||
|
///
|
||||||
|
/// @param {Arglist} $conditions - Media query conditions
|
||||||
|
///
|
||||||
|
/// @example scss - With a single set breakpoint
|
||||||
|
/// @include media('>phone') { }
|
||||||
|
///
|
||||||
|
/// @example scss - With two set breakpoints
|
||||||
|
/// @include media('>phone', '<=tablet') { }
|
||||||
|
///
|
||||||
|
/// @example scss - With custom values
|
||||||
|
/// @include media('>=358px', '<850px') { }
|
||||||
|
///
|
||||||
|
/// @example scss - With set breakpoints with custom values
|
||||||
|
/// @include media('>desktop', '<=1350px') { }
|
||||||
|
///
|
||||||
|
/// @example scss - With a static expression
|
||||||
|
/// @include media('retina2x') { }
|
||||||
|
///
|
||||||
|
/// @example scss - Mixing everything
|
||||||
|
/// @include media('>=350px', '<tablet', 'retina3x') { }
|
||||||
|
///
|
||||||
|
@mixin media($conditions...) {
|
||||||
|
@if ($im-media-support and length($conditions) == 0) or
|
||||||
|
(not $im-media-support and im-intercepts-static-breakpoint($conditions...)) {
|
||||||
|
@content;
|
||||||
|
} @else if ($im-media-support and length($conditions) > 0) {
|
||||||
|
@media #{unquote(parse-expression(nth($conditions, 1)))} {
|
||||||
|
// Recursive call
|
||||||
|
@include media(slice($conditions, 2)...) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<vboxes.size(); i++) {
|
||||||
|
if (vboxes.peek(i).vbox.contains(color)) {
|
||||||
|
return vboxes.peek(i).color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.nearest(color);
|
||||||
|
},
|
||||||
|
nearest: function(color) {
|
||||||
|
var vboxes = this.vboxes,
|
||||||
|
d1, d2, pColor;
|
||||||
|
for (var i=0; i<vboxes.size(); i++) {
|
||||||
|
d2 = Math.sqrt(
|
||||||
|
Math.pow(color[0] - vboxes.peek(i).color[0], 2) +
|
||||||
|
Math.pow(color[1] - vboxes.peek(i).color[1], 2) +
|
||||||
|
Math.pow(color[2] - vboxes.peek(i).color[2], 2)
|
||||||
|
);
|
||||||
|
if (d2 < d1 || d1 === undefined) {
|
||||||
|
d1 = d2;
|
||||||
|
pColor = vboxes.peek(i).color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pColor;
|
||||||
|
},
|
||||||
|
forcebw: function() {
|
||||||
|
// XXX: won't work yet
|
||||||
|
var vboxes = this.vboxes;
|
||||||
|
vboxes.sort(function(a,b) { return pv.naturalOrder(pv.sum(a.color), pv.sum(b.color));});
|
||||||
|
|
||||||
|
// force darkest color to black if everything < 5
|
||||||
|
var lowest = vboxes[0].color;
|
||||||
|
if (lowest[0] < 5 && lowest[1] < 5 && lowest[2] < 5)
|
||||||
|
vboxes[0].color = [0,0,0];
|
||||||
|
|
||||||
|
// force lightest color to white if everything > 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
|
|
@ -12,12 +12,17 @@ module.exports = function (config) {
|
||||||
// http://karma-runner.github.io/0.13/config/browsers.html
|
// http://karma-runner.github.io/0.13/config/browsers.html
|
||||||
// 2. add it to the `browsers` array below.
|
// 2. add it to the `browsers` array below.
|
||||||
browsers: ['PhantomJS'],
|
browsers: ['PhantomJS'],
|
||||||
frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],
|
frameworks: ['mocha', 'sinon-stub-promise', 'sinon-chai', 'phantomjs-shim'],
|
||||||
reporters: ['spec', 'coverage'],
|
reporters: ['spec', 'coverage'],
|
||||||
files: ['./index.js'],
|
files: [
|
||||||
|
'../../node_modules/es6-promise/dist/es6-promise.auto.js',
|
||||||
|
'./index.js'
|
||||||
|
],
|
||||||
preprocessors: {
|
preprocessors: {
|
||||||
'./index.js': ['webpack', 'sourcemap']
|
'./index.js': ['webpack', 'sourcemap']
|
||||||
},
|
},
|
||||||
|
captureTimeout: 15000,
|
||||||
|
retryLimit: 1,
|
||||||
webpack: webpackConfig,
|
webpack: webpackConfig,
|
||||||
webpackMiddleware: {
|
webpackMiddleware: {
|
||||||
noInfo: true
|
noInfo: true
|
||||||
|
|
|
@ -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')
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -0,0 +1,200 @@
|
||||||
|
var sinon = require('sinon')
|
||||||
|
import moxios from 'moxios'
|
||||||
|
import store from '@/store/auth'
|
||||||
|
|
||||||
|
import { testAction } from '../../utils'
|
||||||
|
|
||||||
|
describe('store/auth', () => {
|
||||||
|
var sandbox
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
sandbox = sinon.sandbox.create()
|
||||||
|
moxios.install()
|
||||||
|
})
|
||||||
|
afterEach(function () {
|
||||||
|
sandbox.restore()
|
||||||
|
moxios.uninstall()
|
||||||
|
})
|
||||||
|
|
||||||
|
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})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('getters', () => {
|
||||||
|
it('header', () => {
|
||||||
|
const state = { token: 'helloworld' }
|
||||||
|
expect(store.getters['header'](state)).to.equal('JWT helloworld')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
it('login success', (done) => {
|
||||||
|
moxios.stubRequest('token/', {
|
||||||
|
status: 200,
|
||||||
|
response: {
|
||||||
|
token: 'test'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const credentials = {
|
||||||
|
username: 'bob'
|
||||||
|
}
|
||||||
|
testAction({
|
||||||
|
action: store.actions.login,
|
||||||
|
payload: {credentials: credentials},
|
||||||
|
expectedMutations: [
|
||||||
|
{ type: 'token', payload: 'test' },
|
||||||
|
{ type: 'username', payload: 'bob' },
|
||||||
|
{ type: 'authenticated', payload: true }
|
||||||
|
],
|
||||||
|
expectedActions: [
|
||||||
|
{ type: 'fetchProfile' }
|
||||||
|
]
|
||||||
|
}, done)
|
||||||
|
})
|
||||||
|
it('login error', (done) => {
|
||||||
|
moxios.stubRequest('token/', {
|
||||||
|
status: 500,
|
||||||
|
response: {
|
||||||
|
token: 'test'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const credentials = {
|
||||||
|
username: 'bob'
|
||||||
|
}
|
||||||
|
let spy = sandbox.spy()
|
||||||
|
testAction({
|
||||||
|
action: store.actions.login,
|
||||||
|
payload: {credentials: credentials, onError: spy}
|
||||||
|
}, () => {
|
||||||
|
expect(spy.calledOnce).to.equal(true)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
it('fetchProfile', (done) => {
|
||||||
|
const profile = {
|
||||||
|
username: 'bob',
|
||||||
|
permissions: {
|
||||||
|
admin: {
|
||||||
|
status: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
moxios.stubRequest('users/users/me/', {
|
||||||
|
status: 200,
|
||||||
|
response: profile
|
||||||
|
})
|
||||||
|
testAction({
|
||||||
|
action: store.actions.fetchProfile,
|
||||||
|
expectedMutations: [
|
||||||
|
{ type: 'profile', payload: profile },
|
||||||
|
{ type: 'permission', payload: {key: 'admin', status: true} }
|
||||||
|
],
|
||||||
|
expectedActions: [
|
||||||
|
{ type: 'favorites/fetch', payload: null, options: {root: true} }
|
||||||
|
]
|
||||||
|
}, done)
|
||||||
|
})
|
||||||
|
it('refreshToken', (done) => {
|
||||||
|
moxios.stubRequest('token/refresh/', {
|
||||||
|
status: 200,
|
||||||
|
response: {token: 'newtoken'}
|
||||||
|
})
|
||||||
|
testAction({
|
||||||
|
action: store.actions.refreshToken,
|
||||||
|
params: {state: {token: 'oldtoken'}},
|
||||||
|
expectedMutations: [
|
||||||
|
{ type: 'token', payload: 'newtoken' }
|
||||||
|
]
|
||||||
|
}, done)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,70 @@
|
||||||
|
var sinon = require('sinon')
|
||||||
|
import moxios from 'moxios'
|
||||||
|
import store from '@/store/instance'
|
||||||
|
import { testAction } from '../../utils'
|
||||||
|
|
||||||
|
describe('store/instance', () => {
|
||||||
|
var sandbox
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
sandbox = sinon.sandbox.create()
|
||||||
|
moxios.install()
|
||||||
|
})
|
||||||
|
afterEach(function () {
|
||||||
|
sandbox.restore()
|
||||||
|
moxios.uninstall()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mutations', () => {
|
||||||
|
it('settings', () => {
|
||||||
|
const state = {settings: {raven: {front_dsn: {value: 'test'}}}}
|
||||||
|
let settings = {raven: {front_enabled: {value: true}}}
|
||||||
|
store.mutations.settings(state, settings)
|
||||||
|
expect(state.settings).to.deep.equal({
|
||||||
|
raven: {front_dsn: {value: 'test'}, front_enabled: {value: true}}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('actions', () => {
|
||||||
|
it('fetchSettings', (done) => {
|
||||||
|
moxios.stubRequest('instance/settings/', {
|
||||||
|
status: 200,
|
||||||
|
response: [
|
||||||
|
{
|
||||||
|
section: 'raven',
|
||||||
|
name: 'front_dsn',
|
||||||
|
value: 'test'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
section: 'raven',
|
||||||
|
name: 'front_enabled',
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
testAction({
|
||||||
|
action: store.actions.fetchSettings,
|
||||||
|
payload: null,
|
||||||
|
expectedMutations: [
|
||||||
|
{
|
||||||
|
type: 'settings',
|
||||||
|
payload: {
|
||||||
|
raven: {
|
||||||
|
front_dsn: {
|
||||||
|
section: 'raven',
|
||||||
|
name: 'front_dsn',
|
||||||
|
value: 'test'
|
||||||
|
},
|
||||||
|
front_enabled: {
|
||||||
|
section: 'raven',
|
||||||
|
name: 'front_enabled',
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, done)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,333 @@
|
||||||
|
var sinon = require('sinon')
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
import store from '@/store/queue'
|
||||||
|
import { testAction } from '../../utils'
|
||||||
|
|
||||||
|
describe('store/queue', () => {
|
||||||
|
var sandbox
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
// Create a sandbox for the test
|
||||||
|
sandbox = sinon.sandbox.create()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
// Restore all the things made through the sandbox
|
||||||
|
sandbox.restore()
|
||||||
|
})
|
||||||
|
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 = sandbox.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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,86 @@
|
||||||
|
var sinon = require('sinon')
|
||||||
|
import moxios from 'moxios'
|
||||||
|
import store from '@/store/radios'
|
||||||
|
import { testAction } from '../../utils'
|
||||||
|
|
||||||
|
describe('store/radios', () => {
|
||||||
|
var sandbox
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
sandbox = sinon.sandbox.create()
|
||||||
|
moxios.install()
|
||||||
|
})
|
||||||
|
afterEach(function () {
|
||||||
|
sandbox.restore()
|
||||||
|
moxios.uninstall()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mutations', () => {
|
||||||
|
it('current', () => {
|
||||||
|
const state = {}
|
||||||
|
store.mutations.current(state, 1)
|
||||||
|
expect(state.current).to.equal(1)
|
||||||
|
})
|
||||||
|
it('running', () => {
|
||||||
|
const state = {}
|
||||||
|
store.mutations.running(state, false)
|
||||||
|
expect(state.running).to.equal(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('actions', () => {
|
||||||
|
it('start', (done) => {
|
||||||
|
moxios.stubRequest('radios/sessions/', {
|
||||||
|
status: 200,
|
||||||
|
response: {id: 2}
|
||||||
|
})
|
||||||
|
testAction({
|
||||||
|
action: store.actions.start,
|
||||||
|
payload: {type: 'favorites', objectId: 0, customRadioId: null},
|
||||||
|
expectedMutations: [
|
||||||
|
{
|
||||||
|
type: 'current',
|
||||||
|
payload: {
|
||||||
|
type: 'favorites',
|
||||||
|
objectId: 0,
|
||||||
|
customRadioId: null,
|
||||||
|
session: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ type: 'running', payload: true }
|
||||||
|
],
|
||||||
|
expectedActions: [
|
||||||
|
{ type: 'populateQueue' }
|
||||||
|
]
|
||||||
|
}, done)
|
||||||
|
})
|
||||||
|
it('stop', (done) => {
|
||||||
|
testAction({
|
||||||
|
action: store.actions.stop,
|
||||||
|
expectedMutations: [
|
||||||
|
{ type: 'current', payload: null },
|
||||||
|
{ type: 'running', payload: false }
|
||||||
|
]
|
||||||
|
}, done)
|
||||||
|
})
|
||||||
|
it('populateQueue', (done) => {
|
||||||
|
moxios.stubRequest('radios/tracks/', {
|
||||||
|
status: 201,
|
||||||
|
response: {track: {id: 1}}
|
||||||
|
})
|
||||||
|
testAction({
|
||||||
|
action: store.actions.populateQueue,
|
||||||
|
params: {state: {running: true, current: {session: 1}}},
|
||||||
|
expectedActions: [
|
||||||
|
{ type: 'queue/append', payload: {track: {id: 1}}, options: {root: true} }
|
||||||
|
]
|
||||||
|
}, done)
|
||||||
|
})
|
||||||
|
it('populateQueue does nothing when not running', (done) => {
|
||||||
|
testAction({
|
||||||
|
action: store.actions.populateQueue,
|
||||||
|
params: {state: {running: false}},
|
||||||
|
expectedActions: []
|
||||||
|
}, done)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,73 @@
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let end = function () {
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// call the action with mocked store and arguments
|
||||||
|
let promise = action({ commit, dispatch, ...params }, payload)
|
||||||
|
if (promise) {
|
||||||
|
return promise.then(end)
|
||||||
|
} else {
|
||||||
|
return end()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue