Merge branch 'release/0.16.1'

This commit is contained in:
Eliot Berriot 2018-08-19 19:04:59 +02:00
commit 0913b716e5
No known key found for this signature in database
GPG Key ID: DD6965E2476E5C27
106 changed files with 9971 additions and 5155 deletions

View File

@ -7,7 +7,7 @@ C_FORCE_ROOT=true
FUNKWHALE_HOSTNAME=localhost
FUNKWHALE_PROTOCOL=http
PYTHONDONTWRITEBYTECODE=true
WEBPACK_DEVSERVER_PORT=8080
VUE_PORT=8080
MUSIC_DIRECTORY_PATH=/music
BROWSABLE_API_ENABLED=True
FORWARDED_PROTO=http

7
.gitignore vendored
View File

@ -72,16 +72,17 @@ api/media
api/staticfiles
api/static
api/.pytest_cache
api/celerybeat-*
# Front
oldfront/node_modules/
front/static/translations
front/node_modules/
front/dist/
front/npm-debug.log*
front/yarn-debug.log*
front/yarn-error.log*
front/test/unit/coverage
front/test/e2e/reports
front/tests/unit/coverage
front/tests/e2e/reports
front/selenium-debug.log
docs/_build

View File

@ -156,7 +156,6 @@ test_api:
tags:
- docker
test_front:
stage: test
image: node:9
@ -166,7 +165,7 @@ test_front:
- branches
script:
- yarn install
- yarn run unit
- yarn test:unit
cache:
key: "funkwhale__front_dependencies"
paths:
@ -179,7 +178,6 @@ test_front:
tags:
- docker
build_front:
stage: build
image: node:9
@ -192,8 +190,8 @@ build_front:
- yarn run i18n-compile
# this is to ensure we don't have any errors in the output,
# cf https://code.eliotberriot.com/funkwhale/funkwhale/issues/169
- yarn run build | tee /dev/stderr | (! grep -i 'ERROR in')
- chmod -R 750 dist
- yarn build | tee /dev/stderr | (! grep -i 'ERROR in')
- chmod -R 755 dist
cache:
key: "funkwhale__front_dependencies"
paths:
@ -210,7 +208,6 @@ build_front:
tags:
- docker
pages:
stage: test
image: python:3.6

View File

@ -10,7 +10,82 @@ This changelog is viewable on the web at https://docs.funkwhale.audio/changelog.
.. towncrier
0.16 (unreleased)
0.16.1 (2018-08-19)
-------------------
Upgrade instructions are available at
https://docs.funkwhale.audio/upgrading.html
Features:
- Make funkwhale themable by loading external stylesheets (#456)
Enhancements:
- Add link to admin on "Staff member" button (#202)
- Can now add a description to radios and better radio cards (#331)
- Display track duration in track tables (#461)
- More permissive default permissions for front-end files (#388)
- Simpler configuration and toolchain for the front-end using vue-cli (!375)
- Use Howler to manage audio instead of our own dirty/untested code (#392)
Bugfixes:
- Fix alignment issue on top bar in Admin tabs (#395)
- Fix Apache2 permission issue preventing `/media` folder from being served
correctly (#389)
- Fix loading on browse page lists causing them to go down, and dimming over
the top bar (#468)
- Fixed (again): administration section not showing up in sidebar after login
(#245)
- Fixed audio mimetype not showing up on track detail and list (#459)
- Fixed broken audio playback on Chrome and invisible volume control (#390)
- Fixed broken federation import on big imports due to missing transaction
logic (#397)
- Fixed crash on artist pages when no cover is available (#457)
- Fixed favorited status of tracks not appearing in interface (#398)
- Fixed invitation code not prefilled in form when accessing invitation link
(#476)
- Fixed typos in scheduled tasks configuration (#487)
- Removed release date error in case of empty date (#478)
- Removed white on white artist button on hover, on Album page (#393)
- Smarter date parsing during import by replacing arrow with pendulum (#376)
- Display public playlists properly for anonymous users (#488)
i18n:
- Added portuguese, spanish and german translations
Custom themes for Funkwhale
^^^^^^^^^^^^^^^^^^^^^^^^^^^
If you ever wanted to give a custom look and feel to your instance, this is now possible.
Check https://docs.funkwhale.audio/configuration.html#theming if you want to know more!
Fix Apache2 configuration file for media block [Manual action required]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The permission scope on the current Apache2 configuration file is too narrow, preventing thumbnails from being served.
On Apache2 setups, you have to replace the following line::
<Directory /srv/funkwhale/data/media/albums>
with::
<Directory /srv/funkwhale/data/media>
You can now restart your server::
sudo systemctl restart apache2
0.16 (2018-07-22)
-----------------
Upgrade instructions are available at
@ -232,7 +307,7 @@ In the end, the ``volumes`` directives of your containers should look like that:
Removed Cacheops dependency
---------------------------
^^^^^^^^^^^^^^^^^^^^^^^^^^^
We removed one of our dependency named django-cacheops. It was unly used in a few places,
and not playing nice with other dependencies.

View File

@ -23,7 +23,7 @@ As the front-end can work with any Funkwhale server, you can work with the front
and make it talk with an existing instance (like the demo one, or you own instance, if you have one).
If even that is too much for you, you can also make your changes without any development environment,
and open a merge request. We will be able to to review your work easily by spawning automatically a
and open a merge request. We will be able to review your work easily by spawning automatically a
live version of your changes, thanks to Gitlab Review apps.
Setup front-end only development environment
@ -43,7 +43,7 @@ Setup front-end only development environment
4. Launch the development server::
# this will serve the front-end on http://localhost:8000
WEBPACK_DEVSERVER_PORT=8000 yarn dev
VUE_PORT=8000 yarn serve
5. Make the front-end talk with an existing server (like https://demo.funkwhale.audio),
by clicking on the corresponding link in the footer
@ -264,7 +264,7 @@ When working on federation with traefik, ensure you have this in your ``env``::
# This will ensure we don't bind any port on the host, and thus enable
# multiple instances of funkwhale to be spawned concurrently.
WEBPACK_DEVSERVER_PORT_BINDING=
VUE_PORT_BINDING=
# This disable certificate verification
EXTERNAL_REQUESTS_VERIFY_SSL=false
# this ensure you don't have incorrect urls pointing to http resources
@ -466,12 +466,12 @@ Running tests
To run the front-end test suite, use the following command::
docker-compose -f dev.yml run --rm front yarn run unit
docker-compose -f dev.yml run --rm front yarn test:unit
We also support a "watch and test" mode were we continually relaunch
tests when changes are recorded on the file system::
docker-compose -f dev.yml run --rm front yarn run unit-watch
docker-compose -f dev.yml run --rm front yarn test:unit -w
The latter is especially useful when you are debugging failing tests.

View File

@ -26,7 +26,7 @@ Contribute
----------
Contribution guidelines as well as development installation instructions
are outlined in `CONTRIBUTING <CONTRIBUTING>`_.
are outlined in `CONTRIBUTING <CONTRIBUTING.rst>`_.
Translate
^^^^^^^^^

View File

@ -344,9 +344,9 @@ CELERY_BROKER_URL = env(
# Your common stuff: Below this line define 3rd party library settings
CELERY_TASK_DEFAULT_RATE_LIMIT = 1
CELERY_TASK_TIME_LIMIT = 300
CELERYBEAT_SCHEDULE = {
CELERY_BEAT_SCHEDULE = {
"federation.clean_music_cache": {
"task": "funkwhale_api.federation.tasks.clean_music_cache",
"task": "federation.clean_music_cache",
"schedule": crontab(hour="*/2"),
"options": {"expires": 60 * 2},
}

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
__version__ = "0.16"
__version__ = "0.16.1"
__version_info__ = tuple(
[
int(num) if num.isdigit() else num

View File

@ -796,6 +796,8 @@ class LibraryTrackActionSerializer(common_serializers.ActionSerializer):
jobs.append(job)
music_models.ImportJob.objects.bulk_create(jobs)
music_tasks.import_batch_run.delay(import_batch_id=batch.pk)
funkwhale_utils.on_commit(
music_tasks.import_batch_run.delay, import_batch_id=batch.pk
)
return {"batch": {"id": batch.pk}}

View File

@ -1,5 +1,6 @@
import arrow
import datetime
import mutagen
import pendulum
from django import forms
NODEFAULT = object()
@ -101,6 +102,11 @@ class FirstUUIDField(forms.UUIDField):
return super().to_python(value)
def get_date(value):
parsed = pendulum.parse(str(value))
return datetime.date(parsed.year, parsed.month, parsed.day)
VALIDATION = {
"musicbrainz_artistid": FirstUUIDField(),
"musicbrainz_albumid": FirstUUIDField(),
@ -118,7 +124,7 @@ CONF = {
"title": {},
"artist": {},
"album": {},
"date": {"field": "date", "to_application": lambda v: arrow.get(v).date()},
"date": {"field": "date", "to_application": get_date},
"musicbrainz_albumid": {},
"musicbrainz_artistid": {},
"musicbrainz_recordingid": {"field": "musicbrainz_trackid"},
@ -134,7 +140,7 @@ CONF = {
"title": {},
"artist": {},
"album": {},
"date": {"field": "date", "to_application": lambda v: arrow.get(v).date()},
"date": {"field": "date", "to_application": get_date},
"musicbrainz_albumid": {"field": "MusicBrainz Album Id"},
"musicbrainz_artistid": {"field": "MusicBrainz Artist Id"},
"musicbrainz_recordingid": {"field": "MusicBrainz Track Id"},
@ -148,10 +154,7 @@ CONF = {
"title": {"field": "TIT2"},
"artist": {"field": "TPE1"},
"album": {"field": "TALB"},
"date": {
"field": "TDRC",
"to_application": lambda v: arrow.get(str(v)).date(),
},
"date": {"field": "TDRC", "to_application": get_date},
"musicbrainz_albumid": {"field": "MusicBrainz Album Id"},
"musicbrainz_artistid": {"field": "MusicBrainz Artist Id"},
"musicbrainz_recordingid": {
@ -172,10 +175,7 @@ CONF = {
"title": {},
"artist": {},
"album": {},
"date": {
"field": "date",
"to_application": lambda v: arrow.get(str(v)).date(),
},
"date": {"field": "date", "to_application": get_date},
"musicbrainz_albumid": {},
"musicbrainz_artistid": {},
"musicbrainz_recordingid": {"field": "musicbrainz_trackid"},

View File

@ -1,11 +1,10 @@
import datetime
import os
import shutil
import tempfile
import uuid
import arrow
import markdown
import pendulum
from django.conf import settings
from django.core.files import File
from django.core.files.base import ContentFile
@ -125,9 +124,7 @@ def import_artist(v):
def parse_date(v):
if len(v) == 4:
return datetime.date(int(v), 1, 1)
d = arrow.get(v).date()
d = pendulum.parse(v).date()
return d
@ -147,7 +144,7 @@ class AlbumQuerySet(models.QuerySet):
class Album(APIModelMixin):
title = models.CharField(max_length=255)
artist = models.ForeignKey(Artist, related_name="albums", on_delete=models.CASCADE)
release_date = models.DateField(null=True)
release_date = models.DateField(null=True, blank=True)
release_group_id = models.UUIDField(null=True, blank=True)
cover = VersatileImageField(
upload_to="albums/covers/%Y/%m/%d", null=True, blank=True

View File

@ -35,7 +35,7 @@ djangorestframework>=3.7,<3.8
djangorestframework-jwt>=1.11,<1.12
oauth2client<4
google-api-python-client>=1.6,<1.7
arrow>=0.12,<0.13
pendulum>=2,<3
persisting-theory>=0.2,<0.3
django-versatileimagefield>=1.9,<1.10
django-filter>=1.1,<1.2

View File

@ -1,4 +1,4 @@
import arrow
import pendulum
import pytest
from django.urls import reverse
from django.utils import timezone
@ -455,7 +455,7 @@ def test_library_actor_handle_create_audio(mocker, factories):
assert lt.title == a["metadata"]["recording"]["title"]
assert lt.artist_name == a["metadata"]["artist"]["name"]
assert lt.album_title == a["metadata"]["release"]["title"]
assert lt.published_date == arrow.get(a["published"])
assert lt.published_date == pendulum.parse(a["published"])
def test_library_actor_handle_create_audio_autoimport(mocker, factories):
@ -494,7 +494,7 @@ def test_library_actor_handle_create_audio_autoimport(mocker, factories):
assert lt.title == a["metadata"]["recording"]["title"]
assert lt.artist_name == a["metadata"]["artist"]["name"]
assert lt.album_title == a["metadata"]["release"]["title"]
assert lt.published_date == arrow.get(a["published"])
assert lt.published_date == pendulum.parse(a["published"])
batch = music_models.ImportBatch.objects.latest("id")

View File

@ -1,4 +1,4 @@
import arrow
import pendulum
import pytest
from django.core.paginator import Paginator
@ -492,7 +492,7 @@ def test_activity_pub_audio_serializer_to_library_track(factories):
assert lt.title == audio["metadata"]["recording"]["title"]
assert lt.artist_name == audio["metadata"]["artist"]["name"]
assert lt.album_title == audio["metadata"]["release"]["title"]
assert lt.published_date == arrow.get(audio["published"])
assert lt.published_date == pendulum.parse(audio["published"])
def test_activity_pub_audio_serializer_to_library_track_no_duplicate(factories):

View File

@ -12,6 +12,7 @@ from funkwhale_api.federation import (
views,
webfinger,
)
from funkwhale_api.music import tasks as music_tasks
@pytest.mark.parametrize(
@ -398,7 +399,7 @@ def test_library_track_action_import(factories, superuser_api_client, mocker):
lt2 = factories["federation.LibraryTrack"](library=lt1.library)
lt3 = factories["federation.LibraryTrack"]()
factories["federation.LibraryTrack"](library=lt3.library)
mocked_run = mocker.patch("funkwhale_api.music.tasks.import_batch_run.delay")
mocked_run = mocker.patch("funkwhale_api.common.utils.on_commit")
payload = {
"objects": "all",
@ -416,7 +417,9 @@ def test_library_track_action_import(factories, superuser_api_client, mocker):
assert batch.jobs.count() == 2
for i, job in enumerate(batch.jobs.all()):
assert job.library_track == imported_lts[i]
mocked_run.assert_called_once_with(import_batch_id=batch.pk)
mocked_run.assert_called_once_with(
music_tasks.import_batch_run.delay, import_batch_id=batch.pk
)
def test_local_actor_detail(factories, api_client):

View File

@ -122,3 +122,11 @@ def test_mbid_clean_keeps_only_first(field_name):
result = field.to_python("/".join([u1, u2]))
assert str(result) == u1
@pytest.mark.parametrize(
"raw,expected",
[("2017", datetime.date(2017, 1, 1)), ("2017-12-31", datetime.date(2017, 12, 31))],
)
def test_date_parsing(raw, expected):
assert metadata.get_date(raw) == expected

View File

@ -111,7 +111,7 @@ Define MUSIC_DIRECTORY_PATH /srv/funkwhale/data/music
Require all granted
</Directory>
<Directory /srv/funkwhale/data/media/albums>
<Directory /srv/funkwhale/data/media>
Options FollowSymLinks
AllowOverride None
Require all granted

34
dev.yml
View File

@ -1,4 +1,4 @@
version: '3'
version: "3"
services:
front:
@ -8,22 +8,22 @@ services:
- .env
environment:
- "HOST=0.0.0.0"
- "WEBPACK_DEVSERVER_PORT=${WEBPACK_DEVSERVER_PORT-8080}"
- "VUE_PORT=${VUE_PORT-8080}"
ports:
- "${WEBPACK_DEVSERVER_PORT_BINDING-8080:}${WEBPACK_DEVSERVER_PORT-8080}"
- "${VUE_PORT_BINDING-8080:}${VUE_PORT-8080}"
volumes:
- './front:/app'
- '/app/node_modules'
- './po:/po'
- "./front:/app"
- "/app/node_modules"
- "./po:/po"
networks:
- federation
- internal
labels:
traefik.backend: "${COMPOSE_PROJECT_NAME-node1}"
traefik.frontend.rule: "Host:${COMPOSE_PROJECT_NAME-node1}.funkwhale.test,${NODE_IP-127.0.0.1}"
traefik.enable: 'true'
traefik.federation.protocol: 'http'
traefik.federation.port: "${WEBPACK_DEVSERVER_PORT-8080}"
traefik.enable: "true"
traefik.federation.protocol: "http"
traefik.federation.port: "${VUE_PORT-8080}"
postgres:
env_file:
@ -53,9 +53,9 @@ services:
context: ./api
dockerfile: docker/Dockerfile.test
links:
- postgres
- redis
command: celery -A funkwhale_api.taskapp worker -l debug
- postgres
- redis
command: celery -A funkwhale_api.taskapp worker -l debug -B
environment:
- "FUNKWHALE_HOSTNAME=${FUNKWHALE_HOSTNAME-localhost}"
- "FUNKWHALE_HOSTNAME_SUFFIX=funkwhale.test"
@ -98,7 +98,7 @@ services:
- .env
image: nginx
environment:
- "WEBPACK_DEVSERVER_PORT=${WEBPACK_DEVSERVER_PORT-8080}"
- "VUE_PORT=${VUE_PORT-8080}"
- "COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME- }"
- "FUNKWHALE_HOSTNAME=${FUNKWHALE_HOSTNAME-localhost}"
links:
@ -120,20 +120,20 @@ services:
volumes:
- ".:/app/"
ports:
- '35730:35730'
- '8001:8001'
- "35730:35730"
- "8001:8001"
api-docs:
image: swaggerapi/swagger-ui
environment:
- "API_URL=/swagger.yml"
ports:
- '8002:8080'
- "8002:8080"
volumes:
- "./docs/swagger.yml:/usr/share/nginx/html/swagger.yml"
networks:
internal:
? internal
federation:
external:
name: federation

View File

@ -1,6 +1,6 @@
#!/bin/bash -eux
FORWARDED_PORT="$WEBPACK_DEVSERVER_PORT"
FORWARDED_PORT="$VUE_PORT"
COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME// /}"
if [ -n "$COMPOSE_PROJECT_NAME" ]; then
echo

View File

@ -158,3 +158,79 @@ permissions are:
There is no dedicated interface to manage users permissions, but superusers
can login on the Django's admin at ``/api/admin/`` and grant permissions
to users at ``/api/admin/users/user/``.
Theming
-------
Funkwhale supports custom themes, which are great if you want to personnalize the
look and feel of your instance. Theming is achieved by declaring
additionnal stylesheets you want to load in the front-end.
Customize the settings
^^^^^^^^^^^^^^^^^^^^^^
In order to know what stylesheets to load, the front-end requests the following
url: ``https://your.instance/settings.json``. On typical deployments, this url
returns a 404 error, which is simply ignored.
However, if you return the appropriate payload on this url, you can make the magic
work. We will store the necessary files in the ``/srv/funkwhale/custom`` directory:
.. code-block:: shell
cd /srv/funkwhale/
mkdir custom
cat <<EOF > custom/settings.json
{
"additionalStylesheets": ["/custom/custom.css"]
}
EOF
cat <<EOF > custom/custom.css
body {
background-color: red;
}
EOF
By executing the previous commands, you will end up with two files in your ``/srv/funkwhale/custom``
directory:
- ``settings.json`` will tell the front-end what stylesheets you want to load (``/custom/custom.css`` in this example)
- ``custom.css`` will hold your custom CSS
The last step to make this work is to ensure both files are served by the reverse proxy.
On nginx, add the following snippet to your vhost config::
location /settings.json {
alias /srv/funkwhale/custom/settings.json;
}
location /custom {
alias /srv/funkwhale/custom;
}
On apache, use the following one::
Alias /settings.json /srv/funkwhale/custom/settings.json
Alias /custom /srv/funkwhale/custom
<Directory "/srv/funkwhale/custom">
Options FollowSymLinks
AllowOverride None
Require all granted
</Directory>
Once done, reload your reverse proxy, refresh Funkwhale in your web browser, and you should see
a red background.
.. note::
You can reference external urls as well in ``settings.json``, simply use
the full urls. Be especially careful with external urls as they may affect your users
privacy.
.. warning::
Loading additional stylesheets and CSS rules can affect the performance and
usability of your instance. If you encounter issues with the interfaces and use
custom stylesheets, try to disable those to ensure the issue is not caused
by your customizations.

View File

@ -1 +1 @@
.. include:: ../CONTRIBUTING
.. include:: ../CONTRIBUTING.rst

View File

@ -1,14 +0,0 @@
{
"presets": [
["env", { "modules": false }],
"stage-2"
],
"plugins": ["transform-runtime"],
"comments": false,
"env": {
"test": {
"presets": ["env", "stage-2"],
"plugins": [ "istanbul" ]
}
}
}

View File

@ -1,2 +0,0 @@
build/*.js
config/*.js

View File

@ -1,27 +0,0 @@
// http://eslint.org/docs/user-guide/configuring
module.exports = {
root: true,
parser: 'babel-eslint',
parserOptions: {
sourceType: 'module'
},
env: {
browser: true,
},
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
extends: 'standard',
// required to lint *.vue files
plugins: [
'html'
],
// add your custom rules here
'rules': {
// allow paren-less arrow functions
'arrow-parens': 0,
// allow async-await
'generator-star-spacing': 0,
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
}
}

21
front/.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

View File

@ -1,8 +0,0 @@
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
"plugins": {
// to edit target browsers: use "browserlist" field in package.json
"autoprefixer": {}
}
}

View File

@ -5,9 +5,9 @@ RUN curl -L -o /usr/local/bin/jq https://github.com/stedolan/jq/releases/downloa
EXPOSE 8080
WORKDIR /app/
ADD package.json .
ADD package.json yarn.lock ./
RUN yarn install
COPY . .
CMD ["npm", "run", "dev"]
CMD ["yarn", "serve"]

5
front/babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/app'
]
}

View File

@ -1,35 +0,0 @@
require('./check-versions')()
process.env.NODE_ENV = 'production'
var ora = require('ora')
var rm = require('rimraf')
var path = require('path')
var chalk = require('chalk')
var webpack = require('webpack')
var config = require('../config')
var webpackConfig = require('./webpack.prod.conf')
var spinner = ora('building for production...')
spinner.start()
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, function (err, stats) {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n\n')
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
})

View File

@ -1,48 +0,0 @@
var chalk = require('chalk')
var semver = require('semver')
var packageConfig = require('../package.json')
var shell = require('shelljs')
function exec (cmd) {
return require('child_process').execSync(cmd).toString().trim()
}
var versionRequirements = [
{
name: 'node',
currentVersion: semver.clean(process.version),
versionRequirement: packageConfig.engines.node
},
]
if (shell.which('npm')) {
versionRequirements.push({
name: 'npm',
currentVersion: exec('npm --version'),
versionRequirement: packageConfig.engines.npm
})
}
module.exports = function () {
var warnings = []
for (var i = 0; i < versionRequirements.length; i++) {
var mod = versionRequirements[i]
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' +
chalk.red(mod.currentVersion) + ' should be ' +
chalk.green(mod.versionRequirement)
)
}
}
if (warnings.length) {
console.log('')
console.log(chalk.yellow('To use this template, you must update following to modules:'))
console.log()
for (var i = 0; i < warnings.length; i++) {
var warning = warnings[i]
console.log(' ' + warning)
}
console.log()
process.exit(1)
}
}

View File

@ -1,9 +0,0 @@
/* eslint-disable */
require('eventsource-polyfill')
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
hotClient.subscribe(function (event) {
if (event.action === 'reload') {
window.location.reload()
}
})

View File

@ -1,92 +0,0 @@
require('./check-versions')()
var config = require('../config')
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
}
var opn = require('opn')
var path = require('path')
var express = require('express')
var webpack = require('webpack')
var proxyMiddleware = require('http-proxy-middleware')
var webpackConfig = process.env.NODE_ENV === 'testing'
? require('./webpack.prod.conf')
: require('./webpack.dev.conf')
// default port where dev server listens for incoming traffic
var port = process.env.PORT || config.dev.port
var host = process.env.HOST || config.dev.host
// automatically open browser, if not set will be false
var autoOpenBrowser = !!config.dev.autoOpenBrowser
// Define HTTP proxies to your custom API backend
// https://github.com/chimurai/http-proxy-middleware
var proxyTable = config.dev.proxyTable
var app = express()
var compiler = webpack(webpackConfig)
var devMiddleware = require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath,
quiet: true
})
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
log: () => {}
})
// force page reload when html-webpack-plugin template changes
compiler.plugin('compilation', function (compilation) {
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
hotMiddleware.publish({ action: 'reload' })
cb()
})
})
// proxy api requests
Object.keys(proxyTable).forEach(function (context) {
var options = proxyTable[context]
if (typeof options === 'string') {
options = { target: options }
}
app.use(proxyMiddleware(options.filter || context, options))
})
// handle fallback for HTML5 history API
app.use(require('connect-history-api-fallback')())
// serve webpack bundle output
app.use(devMiddleware)
// enable hot-reload and state-preserving
// compilation error display
app.use(hotMiddleware)
// serve pure static assets
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(staticPath, express.static('./static'))
var uri = 'http://' + host + ':' + port
var _resolve
var readyPromise = new Promise(resolve => {
_resolve = resolve
})
console.log('> Starting dev server...')
devMiddleware.waitUntilValid(() => {
console.log('> Listening at ' + uri + '\n')
// when env is testing, don't need open it
if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
opn(uri)
}
_resolve()
})
var server = app.listen(port, host)
module.exports = {
ready: readyPromise,
close: () => {
server.close()
}
}

View File

@ -1,71 +0,0 @@
var path = require('path')
var config = require('../config')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
exports.assetsPath = function (_path) {
var assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
exports.cssLoaders = function (options) {
options = options || {}
var cssLoader = {
loader: 'css-loader',
options: {
minimize: process.env.NODE_ENV === 'production',
sourceMap: options.sourceMap
}
}
// generate loader string to be used with extract text plugin
function generateLoaders (loader, loaderOptions) {
var loaders = [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
var output = []
var loaders = exports.cssLoaders(options)
for (var extension in loaders) {
var loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}

View File

@ -1,12 +0,0 @@
var utils = require('./utils')
var config = require('../config')
var isProduction = process.env.NODE_ENV === 'production'
module.exports = {
loaders: utils.cssLoaders({
sourceMap: isProduction
? config.build.productionSourceMap
: config.dev.cssSourceMap,
extract: isProduction
})
}

View File

@ -1,67 +0,0 @@
var path = require('path')
var utils = require('./utils')
var config = require('../config')
var vueLoaderConfig = require('./vue-loader.conf')
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
entry: {
app: './src/main.js'
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src')
}
},
module: {
rules: [
{
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter')
}
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
}
}

View File

@ -1,35 +0,0 @@
var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
// add hot-reload related code to entry chunks
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
})
module.exports = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
},
// cheap-module-eval-source-map is faster for development
devtool: '#cheap-module-eval-source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': config.dev.env
}),
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
new FriendlyErrorsPlugin()
]
})

View File

@ -1,124 +0,0 @@
var path = require('path')
var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var CopyWebpackPlugin = require('copy-webpack-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
var env = process.env.NODE_ENV === 'testing'
? require('../config/test.env')
: config.build.env
var webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true
})
},
devtool: config.build.productionSourceMap ? '#source-map' : false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({
'process.env': env
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
sourceMap: true
}),
// extract css into its own file
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css')
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
cssProcessorOptions: {
safe: true
}
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: process.env.NODE_ENV === 'testing'
? 'index.html'
: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
}),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module, count) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
if (config.build.productionGzip) {
var CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
threshold: 10240,
minRatio: 0.8
})
)
}
if (config.build.bundleAnalyzerReport) {
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig

View File

@ -1,31 +0,0 @@
// This is the webpack config used for unit tests.
var utils = require('./utils')
var webpack = require('webpack')
var merge = require('webpack-merge')
var baseConfig = require('./webpack.base.conf')
var webpackConfig = merge(baseConfig, {
// use inline sourcemap for karma-sourcemap-loader
module: {
rules: utils.styleLoaders()
},
devtool: '#inline-source-map',
resolveLoader: {
alias: {
// necessary to to make lang="scss" work in test when using vue-loader's ?inject option
// see discussion at https://github.com/vuejs/vue-loader/issues/724
'scss-loader': 'sass-loader'
}
},
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/test.env')
})
]
})
// no need for app entry during tests
delete webpackConfig.entry
module.exports = webpackConfig

View File

@ -1,6 +0,0 @@
var merge = require('webpack-merge')
var prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})

View File

@ -1,65 +0,0 @@
// see http://vuejs-templates.github.io/webpack for documentation.
var path = require('path')
module.exports = {
build: {
env: require('./prod.env'),
index: path.resolve(__dirname, '../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: '/',
productionSourceMap: false,
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report
},
dev: {
env: require('./dev.env'),
port: parseInt(process.env.WEBPACK_DEVSERVER_PORT),
host: '127.0.0.1',
autoOpenBrowser: true,
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {
'**': {
target: 'http://nginx:6001',
changeOrigin: true,
ws: true,
filter: function (pathname, req) {
let proxified = ['rest', '.well-known', 'staticfiles', 'media', 'federation', 'api']
let matches = proxified.filter(e => {
return pathname.match(`^/${e}`)
})
return matches.length > 0
}
},
// '/.well-known': {
// target: 'http://nginx:6001',
// changeOrigin: true
// },
// '/media': {
// target: 'http://nginx:6001',
// changeOrigin: true,
// },
// '/staticfiles': {
// target: 'http://nginx:6001',
// changeOrigin: true,
// },
},
// CSS Sourcemaps off by default because relative paths are "buggy"
// with this option, according to the CSS-Loader README
// (https://github.com/webpack/css-loader#sourcemaps)
// In our experience, they generally work as expected,
// just be aware of this issue when enabling this option.
cssSourceMap: false
}
}

View File

@ -1,5 +0,0 @@
let url = process.env.INSTANCE_URL || '/'
module.exports = {
NODE_ENV: '"production"',
INSTANCE_URL: `"${url}"`
}

View File

@ -1,6 +0,0 @@
var merge = require('webpack-merge')
var devEnv = require('./dev.env')
module.exports = merge(devEnv, {
NODE_ENV: '"testing"'
})

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Funkwhale</title>
<link rel="shortcut icon" type="image/png" href="/static/favicon.png"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: Arabic (FunkWhale)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-07-22 14:12+0200\n"
"PO-Revision-Date: 2018-07-22 15:50+0000\n"
"PO-Revision-Date: 2018-07-24 19:50+0000\n"
"Last-Translator: ButterflyOfFire <butterflyoffire+funkwhale@protonmail.com>\n"
"Language-Team: Arabic <https://translate.funkwhale.audio/projects/funkwhale/"
"funkwhale/front/ar/>\n"
@ -81,19 +81,19 @@ msgstr[5] "%{ count } مَقاطِع"
#: front/src/components/common/Duration.vue:2
msgid "%{ hours } h %{ minutes } min"
msgstr ""
msgstr "%{ hours } سا %{ minutes } د"
#: front/src/components/common/Duration.vue:5
msgid "%{ minutes } min"
msgstr ""
msgstr "%{ minutes } د"
#: front/src/components/activity/Like.vue:7
msgid "%{ user } favorited a track"
msgstr ""
msgstr "أُعجِب %{ user } بمقطع"
#: front/src/components/activity/Listen.vue:7
msgid "%{ user } listened to a track"
msgstr ""
msgstr "قام %{ user } بالاستماع إلى مَقطَع"
#: front/src/components/auth/Profile.vue:49
msgid "%{ username }'s profile"
@ -131,9 +131,8 @@ msgstr[4] "%{ count } مَقاطِع"
msgstr[5] "%{ count } مَقاطِع"
#: front/src/components/About.vue:5
#, fuzzy
msgid "About %{ instance }"
msgstr "عن مثيل الخادوم هذا"
msgstr "عن %{ instance }"
#: front/src/App.vue:54
msgid "About Funkwhale"
@ -146,11 +145,11 @@ msgstr "عن مثيل الخادوم هذا"
#: front/src/components/manage/library/RequestsTable.vue:28
#: front/src/components/manage/library/RequestsTable.vue:62
msgid "Accepted"
msgstr ""
msgstr "تم قبوله"
#: front/src/components/auth/SubsonicTokenForm.vue:111
msgid "Access disabled"
msgstr ""
msgstr "عُطّل النفاذ"
#: front/src/components/Home.vue:109
msgid "Access your music from a clean interface that focus on what really matters"
@ -502,7 +501,7 @@ msgstr ""
#: front/src/components/auth/Settings.vue:98
msgid "Changing your password will have the following consequences"
msgstr ""
msgstr "سوف ينجرّ ما يلي عند تعديل كلمتك السرية"
#: front/src/App.vue:4
msgid "Choose your instance"
@ -615,9 +614,8 @@ msgid "Creation date"
msgstr "تاريخ الإنشاء"
#: front/src/components/auth/Settings.vue:54
#, fuzzy
msgid "Current avatar"
msgstr "المَقطَع الحالي"
msgstr "الصورة الرمزية الحالية"
#: front/src/components/playlists/PlaylistModal.vue:8
msgid "Current track"
@ -701,9 +699,8 @@ msgid "Do you want to delete the playlist \"%{ playlist }\"?"
msgstr "متأكّد مِن أنك تريد حذف قائمة المَقاطِع الموسيقية \"%{ playlist }\" ؟"
#: front/src/views/radios/Detail.vue:26
#, fuzzy
msgid "Do you want to delete the radio \"%{ radio }\"?"
msgstr "أتريد حقا حذف إذاعة \"{{ radio }}\" ؟"
msgstr "أتريد حقا حذف إذاعة \"%{ radio }\" ؟"
#: front/src/components/common/ActionTable.vue:29
msgid "Do you want to launch %{ action } on %{ count } element?"
@ -908,7 +905,7 @@ msgstr "الفديرالية"
#: front/src/views/federation/LibraryDetail.vue:3
msgid "File mirroring"
msgstr ""
msgstr "النسخ المتماثل للملفات"
#: front/src/components/library/import/FileUpload.vue:43
msgid "File name"
@ -1037,7 +1034,7 @@ msgstr "جلب البيانات الوصفية ذات الصّلة"
#: front/src/App.vue:74
msgid "Help us translate Funkwhale"
msgstr ""
msgstr "ساعدنا على ترجمة فانك وايل"
#: front/src/components/library/Home.vue:65
msgid "Home"
@ -1192,9 +1189,8 @@ msgid "Instance information"
msgstr "معلومات عن مثيل الخادوم"
#: front/src/components/library/Radios.vue:9
#, fuzzy
msgid "Instance radios"
msgstr "معلومات عن مثيل الخادوم"
msgstr "إذاعات مثيل الخادوم"
#: front/src/views/admin/Settings.vue:75
msgid "Instance settings"
@ -1395,6 +1391,13 @@ msgid ""
" </a>\n"
" project, which you can think about as the Wikipedia of music."
msgstr ""
"البيانات الوصفية هي الم البيانات المتعلقة بالموسيقى التي تريد استيرادها. و "
"هي تحتوي على معلومات عن الفنانين و الألبومات و المَقاطِع الموسيقية. و بغرض "
"إنشاء مكتبة بها جودة، يُستحسن جلب البيانات مِن \n"
" <a href=\"https://musicbrainz.org\" target=\"_blank\">\n"
" MusicBrainz\n"
" </a>\n"
" مشروع بمثابة ويكيبيديا للموسيقى."
#: front/src/components/Sidebar.vue:48
#: src/components/library/import/Main.vue:18
@ -1498,6 +1501,8 @@ msgstr "الكلمة السرية الجديدة"
#: front/src/components/library/import/FileUpload.vue:36
msgid "Once all your files are uploaded, simply click the following button to check the import status."
msgstr ""
"حينما تتم عملية إرسال كافة ملفاتك، إضغط على الزر التالي للتحقق مِن حالة "
"الإستيراد."
#: front/src/components/federation/LibraryCard.vue:21
#: front/src/components/manage/users/InvitationsTable.vue:20
@ -1683,6 +1688,8 @@ msgstr "الرجاء التأكّد مِن صحة اسم المستخدِم و
#: front/src/components/auth/Settings.vue:46
msgid "PNG, GIF or JPG. At most 2MB. Will be downscaled to 400x400px."
msgstr ""
"نسق PNG أو GIF أو JPG. الحجم الأقصى 2 ميغابيت. سيتم تغيير حجمها إلى 400×400 "
"بكسل."
#: front/src/components/library/import/Main.vue:26
msgid "Previous step"
@ -1722,18 +1729,16 @@ msgid "Radio Builder"
msgstr "مُنشِئ الإذاعات و الراديو"
#: front/src/components/library/radios/Builder.vue:15
#, fuzzy
msgid "Radio created"
msgstr "إسم الإذاعة"
msgstr "تم إنشاء الإذاعة"
#: front/src/components/library/radios/Builder.vue:21
msgid "Radio name"
msgstr "إسم الإذاعة"
#: front/src/components/library/radios/Builder.vue:12
#, fuzzy
msgid "Radio updated"
msgstr "إسم الإذاعة"
msgstr "تم تحديث الإذاعة"
#: front/src/components/library/Library.vue:10
#: src/components/library/Radios.vue:141
@ -1746,12 +1751,11 @@ msgstr "أحدث الأنشطة على مثيل الخادوم هذا"
#: front/src/components/library/Home.vue:24
msgid "Recently added"
msgstr ""
msgstr "تمت إضافتها مؤخرا"
#: front/src/components/library/Home.vue:11
#, fuzzy
msgid "Recently favorited"
msgstr "في المفضلة"
msgstr "تمت إضافتها إلى المفضلة حديثا"
#: front/src/components/library/Home.vue:6
msgid "Recently listened"
@ -1787,9 +1791,8 @@ msgid "Remove"
msgstr "حذف"
#: front/src/components/auth/Settings.vue:58
#, fuzzy
msgid "Remove avatar"
msgstr "حذف"
msgstr "حذف الصورة الرمزية"
#: front/src/components/favorites/TrackFavoriteIcon.vue:19
msgid "Remove from favorites"
@ -2257,9 +2260,8 @@ msgid "Tracks available in this library"
msgstr "المَقاطِع المتوفّرة في هذه المكتبة"
#: front/src/components/library/Artist.vue:54
#, fuzzy
msgid "Tracks by this artist"
msgstr "مِن ألبومات هذا الفنان"
msgstr "مَقاطِع لهذا الفنان"
#: front/src/components/instance/Stats.vue:25
msgid "Tracks favorited"
@ -2275,7 +2277,7 @@ msgstr ""
#: front/src/components/manage/library/FilesTable.vue:41
msgid "Type"
msgstr ""
msgstr "النوع"
#: front/src/components/About.vue:15
msgid "Unfortunately, owners of this instance did not yet take the time to complete this page."
@ -2294,9 +2296,8 @@ msgid "Unmute"
msgstr "إلغاء الكتم"
#: front/src/components/auth/Settings.vue:50
#, fuzzy
msgid "Update avatar"
msgstr "تحديث قائمة المَقاطِع الموسيقية"
msgstr "تحديث الصورة الرمزية"
#: front/src/components/playlists/Form.vue:33
msgid "Update playlist"
@ -2317,7 +2318,7 @@ msgstr "أرسل"
#: front/src/components/auth/Settings.vue:45
msgid "Upload a new avatar"
msgstr ""
msgstr "إرسال صورة رمزية جديدة"
#: front/src/components/library/import/Main.vue:7
msgid "Uploaded files or external source"
@ -2349,7 +2350,7 @@ msgstr ""
#: front/src/components/manage/users/InvitationsTable.vue:49
msgid "Used"
msgstr ""
msgstr "مُستخدَم"
#: front/src/components/manage/library/RequestsTable.vue:47
msgid "User"
@ -2360,9 +2361,8 @@ msgid "User activity"
msgstr "نشاط المستخدِم"
#: front/src/components/library/Radios.vue:20
#, fuzzy
msgid "User radios"
msgstr "المستخدِمون"
msgstr "إذاعات المستخدِمين"
#: front/src/components/auth/Signup.vue:19
#: front/src/components/manage/users/UsersTable.vue:37
@ -2394,24 +2394,23 @@ msgstr "إطّلع عليه على ميوزيك براينز"
#: front/src/components/playlists/PlaylistModal.vue:20
msgid "We cannot add the track to a playlist"
msgstr ""
msgstr "لا يمكننا إضافة المَقطَع إلى قائمة التشغيل"
#: front/src/components/playlists/Form.vue:14
msgid "We cannot create the playlist"
msgstr ""
msgstr "لا يمكننا إنشاء قائمة التشغيل"
#: front/src/components/auth/Signup.vue:13
msgid "We cannot create your account"
msgstr ""
msgstr "لا يمكننا إنشاء حسابك"
#: front/src/components/auth/Login.vue:7
msgid "We cannot log you in"
msgstr ""
msgstr "تعذر علينا تسجيل دخولك"
#: front/src/components/auth/Settings.vue:38
#, fuzzy
msgid "We cannot save your avatar"
msgstr "تعذّر علينا حفظ إعداداتك"
msgstr "تعذّر علينا حفظ صورتك الرمزية"
#: front/src/components/auth/Settings.vue:14
msgid "We cannot save your settings"
@ -2431,7 +2430,7 @@ msgstr "نعتقد أنّ الاستماع إلى الموسيقى ينبغي أ
#: front/src/components/PageNotFound.vue:10
msgid "We're sorry, the page you asked for does not exist:"
msgstr ""
msgstr "المعذرة، إنّ الصفحة التي قمت بطلبها غير موجودة :"
#: front/src/components/requests/Form.vue:21
msgid "We've received your request, you'll get some groove soon ;)"
@ -2463,15 +2462,15 @@ msgstr "نعم"
#: front/src/components/auth/Logout.vue:8
msgid "Yes, log me out!"
msgstr ""
msgstr "نعم، أؤكد الخروج !"
#: front/src/components/auth/Logout.vue:7
msgid "You are currently logged in as %{ username }"
msgstr ""
msgstr "أنت متّصل حاليا بصفة %{ username }"
#: front/src/components/library/import/Main.vue:111
msgid "You can also skip this step and enter metadata manually."
msgstr ""
msgstr "يمكنك طبعًا تخطي هذه الخطوة و إدخال البيانات الوصفية يدويًا."
#: front/src/components/Home.vue:136
msgid "You can invite friends and family to your instance so they can enjoy your music"
@ -2487,7 +2486,7 @@ msgstr ""
#: front/src/components/Sidebar.vue:156
msgid "You have a radio playing"
msgstr ""
msgstr "إنك تستمع إلى إذاعة"
#: front/src/App.vue:6
msgid "You need to select an instance in order to continue"

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: front 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-07-22 14:12+0200\n"
"PO-Revision-Date: 2018-07-22 15:50+0000\n"
"PO-Revision-Date: 2018-07-24 19:50+0000\n"
"Last-Translator: Baptiste Gelez <baptiste@gelez.xyz>\n"
"Language-Team: none\n"
"Language: eo\n"
@ -34,8 +34,8 @@ msgstr "(malplena)"
#: front/src/components/common/ActionTable.vue:51
msgid "%{ count } on %{ total } selected"
msgid_plural "%{ count } on %{ total } selected"
msgstr[0] ""
msgstr[1] ""
msgstr[0] "Unu el %{total} estas selekta"
msgstr[1] "%{count} el %{total} estas selektaj"
#: front/src/components/Sidebar.vue:116
#: src/views/federation/LibraryDetail.vue:87
@ -739,11 +739,11 @@ msgstr "Tajpu nomon de artisto…"
#: front/src/views/federation/LibraryList.vue:122
msgid "Enter an library domain name..."
msgstr ""
msgstr "Tajpu domajna nomo de instance…"
#: front/src/views/playlists/List.vue:104
msgid "Enter an playlist name..."
msgstr ""
msgstr "Tajpu ludlistan nomon…"
#: front/src/components/auth/Signup.vue:98
msgid "Enter your email"
@ -884,86 +884,91 @@ msgstr "Filtri albumtipoj"
#: front/src/components/library/radios/Builder.vue:56
msgid "Filter name"
msgstr ""
msgstr "Filtri nomon"
#: front/src/components/library/import/Main.vue:52
msgid "Finish import"
msgstr ""
msgstr "Fini importadon"
#: front/src/components/library/import/BatchDetail.vue:54
msgid "Finished"
msgstr ""
msgstr "Finanto"
#: front/src/components/library/import/Main.vue:59
msgid "First, choose where you want to import the music from"
msgstr ""
msgstr "Unue, elekti ejo el vi volas importi muzikon"
#: front/src/components/federation/LibraryCard.vue:44
msgid "Follow"
msgstr ""
msgstr "Sekvi"
#: front/src/components/federation/LibraryCard.vue:36
msgid "Follow request pending approval"
msgstr ""
msgstr "Peto da sekvado atendis konsenton"
#: front/src/views/federation/LibraryDetail.vue:21
msgid "Follow status"
msgstr ""
msgstr "Sekva statuso"
#: front/src/views/federation/Base.vue:13
#: front/src/views/federation/LibraryFollowersList.vue:24
msgid "Followers"
msgstr ""
msgstr "Sekvantoj"
#: front/src/components/federation/LibraryCard.vue:18
msgid "Followers only"
msgstr ""
msgstr "Nur sekvantoj"
#: front/src/components/federation/LibraryCard.vue:15
#: front/src/views/federation/LibraryDetail.vue:29
msgid "Following"
msgstr ""
msgstr "Sekvata"
#: front/src/components/activity/Like.vue:12
#: src/components/activity/Listen.vue:12
msgid "from %{ album } by %{ artist }"
msgstr ""
msgstr "el %{album} je %{artist}"
#: front/src/components/library/Track.vue:13
msgid "From album %{ album } by %{ artist }"
msgstr ""
msgstr "El %{album} albumo je %{artist}"
#: front/src/App.vue:56
msgid "Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!"
msgstr ""
"Funkwhale estas senpaga kaj kun libera fontkodo projekto, ke viglas dankon "
"al vonlontuloj. Vi povas helpi nin plibonigi tiun kun cimosignaladoj, "
"trajtosugestoj kaj diskonigado de la projekto al viaj amikoj!"
#: front/src/components/auth/SubsonicTokenForm.vue:7
msgid "Funkwhale is compatible with other music players that support the Subsonic API."
msgstr ""
msgstr "Funkwhale funkcias kun aliaj muzikludiloj ke apogas la Subsonic API."
#: front/src/components/Home.vue:98
msgid "Funkwhale is dead simple to use."
msgstr ""
msgstr "Uzi Funkwhale facilegas."
#: front/src/components/Home.vue:39
msgid "Funkwhale is designed to make it easy to listen to music you like, or to discover new artists."
msgstr ""
"Funkwhale estas dizajna tiel ke estas facile aŭskulti muzikon vi ŝatas, aŭ "
"malkovri novajn artistojn."
#: front/src/components/Home.vue:119
msgid "Funkwhale is free and gives you control on your music."
msgstr ""
msgstr "Funkwhale estas senpaga kaj lasis vin estri vian muzikon."
#: front/src/components/Home.vue:66
msgid "Funkwhale takes care of handling your music"
msgstr ""
msgstr "Funkwhale atentas manipuli vian muzikon"
#: front/src/components/manage/users/InvitationForm.vue:16
msgid "Get a new invitation"
msgstr ""
msgstr "Akiri novan inviton"
#: front/src/components/Home.vue:13
msgid "Get me to the library"
msgstr ""
msgstr "Iru al la muzikejo"
#: front/src/components/Home.vue:77
msgid ""
@ -972,19 +977,21 @@ msgid ""
" MusicBrainz\n"
" </a>"
msgstr ""
"Akiri bonegaj metadatumoj pri vian muzikon kun <a href=\"https://musicbrainz."
"org\" target=\"_blank\">MusicBrainz</a>"
#: front/src/components/common/ActionTable.vue:21
#: front/src/components/common/ActionTable.vue:27
msgid "Go"
msgstr ""
msgstr "Komenci"
#: front/src/components/PageNotFound.vue:14
msgid "Go to home page"
msgstr ""
msgstr "Iru hejme"
#: front/src/components/library/import/Main.vue:13
msgid "Grab corresponding metadata"
msgstr ""
msgstr "Kolekti la koncernajn metadatumojn"
#: front/src/App.vue:74
msgid "Help us translate Funkwhale"
@ -992,237 +999,241 @@ msgstr ""
#: front/src/components/library/Home.vue:65
msgid "Home"
msgstr ""
msgstr "Hejmo"
#: front/src/components/instance/Stats.vue:36
msgid "Hours of music"
msgstr ""
msgstr "Muzikhoroj"
#: front/src/components/auth/SubsonicTokenForm.vue:11
msgid "However, accessing Funkwhale from those clients require a separate password you can set below."
msgstr ""
"Tamen, atingi Funkwhale el tiuj aplikaĵo bezonas alian pasvorton ke vi povas "
"difini malsupre."
#: front/src/components/library/import/BatchList.vue:34
msgid "ID"
msgstr ""
msgstr "ID"
#: front/src/views/auth/PasswordResetConfirm.vue:24
msgid "If the email address provided in the previous step is valid and binded to a user account, you should receive an email with reset instructions in the next couple of minutes."
msgstr ""
"Se la retadreso provizanta dum la antaŭa etapo korektas kaj bindas al "
"uzantkonto, vi baldaŭ ricevus retmesaĝon kun renuligadaj instrukcioj."
#: front/src/components/federation/LibraryTrackTable.vue:196
#: front/src/components/library/Library.vue:17
msgid "Import"
msgstr ""
msgstr "Importi"
#: front/src/components/federation/LibraryTrackTable.vue:57
msgid "Import #%{ id } launched"
msgstr ""
msgstr "Importado #%{id} komencis"
#: front/src/components/library/import/Main.vue:38
msgid "Import %{ count } track"
msgid_plural "Import %{ count } tracks"
msgstr[0] ""
msgstr[1] ""
msgstr[0] "Importi unu kanto"
msgstr[1] "Importi %{count} kantoj"
#: front/src/components/library/import/BatchDetail.vue:10
msgid "Import batch"
msgstr ""
msgstr "Importaro"
#: front/src/components/library/import/BatchDetail.vue:185
msgid "Import Batch #%{ id }"
msgstr ""
msgstr "Importaro #%{id}"
#: front/src/components/library/Library.vue:20
msgid "Import batches"
msgstr ""
msgstr "Importaroj"
#: front/src/components/library/import/BatchList.vue:117
msgid "Import Batches"
msgstr ""
msgstr "Importaroj"
#: front/src/components/manage/library/FilesTable.vue:40
#: front/src/components/manage/library/RequestsTable.vue:53
msgid "Import date"
msgstr ""
msgstr "Importdato"
#: front/src/components/library/import/FileUpload.vue:38
msgid "Import detail page"
msgstr ""
msgstr "Importado detalpaĝo"
#: front/src/components/Sidebar.vue:81
msgid "Import music"
msgstr ""
msgstr "Importi muzikon"
#: front/src/components/library/import/Main.vue:267
msgid "Import Music"
msgstr ""
msgstr "Importi muzikon"
#: front/src/components/Home.vue:71
msgid "Import music from various platforms, such as YouTube or SoundCloud"
msgstr ""
msgstr "Importi muzikon el multe da servicoj, kiel YouTube aŭ SoundCloud"
#: front/src/components/federation/LibraryTrackTable.vue:14
#: front/src/components/federation/LibraryTrackTable.vue:66
msgid "Import pending"
msgstr ""
msgstr "Importado atendas"
#: front/src/views/admin/library/Base.vue:9
#: front/src/views/admin/library/RequestsList.vue:3
#: front/src/views/admin/library/RequestsList.vue:21
msgid "Import requests"
msgstr ""
msgstr "Importpetoj"
#: front/src/components/library/import/BatchList.vue:20
#: front/src/components/library/import/Main.vue:6
msgid "Import source"
msgstr ""
msgstr "Importfonto"
#: front/src/components/federation/LibraryTrackTable.vue:9
msgid "Import status"
msgstr ""
msgstr "Importstato"
#: front/src/components/library/import/ReleaseImport.vue:14
msgid "Import this release"
msgstr ""
msgstr "Importi tiun albumon"
#: front/src/components/library/import/TrackImport.vue:11
msgid "Import this track"
msgstr ""
msgstr "Importi tiun kanton"
#: front/src/components/federation/LibraryTrackTable.vue:12
#: front/src/components/manage/library/RequestsTable.vue:29
#: front/src/components/manage/library/RequestsTable.vue:61
msgid "Imported"
msgstr ""
msgstr "Importata"
#: front/src/components/library/import/TrackImport.vue:44
msgid "Imported URL"
msgstr ""
msgstr "Importanta URL"
#: front/src/views/admin/Settings.vue:82
msgid "Imports"
msgstr ""
msgstr "Importadoj"
#: front/src/components/favorites/TrackFavoriteIcon.vue:3
msgid "In favorites"
msgstr ""
msgstr "En stelumoj"
#: front/src/components/federation/LibraryTrackTable.vue:65
msgid "In library"
msgstr ""
msgstr "En muzikejo"
#: front/src/components/manage/users/UsersTable.vue:54
msgid "Inactive"
msgstr ""
msgstr "Malaktiva"
#: front/src/components/library/import/Main.vue:96
msgid "Input a MusicBrainz ID manually:"
msgstr ""
msgstr "Tajpu MusicBrainz ID mane:"
#: front/src/views/auth/PasswordReset.vue:53
msgid "Input the email address binded to your account"
msgstr ""
msgstr "Tajpu la retadreson bindanta al via konto"
#: front/src/components/playlists/Editor.vue:31
msgid "Insert from queue (%{ count } track)"
msgid_plural "Insert from queue (%{ count } tracks)"
msgstr[0] ""
msgstr[1] ""
msgstr[0] "Internigi el atendovico (unu kanto)"
msgstr[1] "Internigi el atendovico (%{count} kantoj)"
#: front/src/views/admin/Settings.vue:80
msgid "Instance information"
msgstr ""
msgstr "Instanca informo"
#: front/src/components/library/Radios.vue:9
msgid "Instance radios"
msgstr ""
msgstr "Instancaj radioj"
#: front/src/views/admin/Settings.vue:75
msgid "Instance settings"
msgstr ""
msgstr "Instancaj preferoj"
#: front/src/views/instance/Timeline.vue:57
msgid "Instance Timeline"
msgstr ""
msgstr "Instanca tempolino"
#: front/src/components/auth/Signup.vue:42
#: front/src/components/manage/users/InvitationForm.vue:11
msgid "Invitation code"
msgstr ""
msgstr "Invita kodo"
#: front/src/components/auth/Signup.vue:43
msgid "Invitation code (optional)"
msgstr ""
msgstr "Invita kodo (nedeviga)"
#: front/src/views/admin/users/Base.vue:8
#: src/views/admin/users/InvitationsList.vue:3
#: front/src/views/admin/users/InvitationsList.vue:24
msgid "Invitations"
msgstr ""
msgstr "Invitoj"
#: front/src/App.vue:43
msgid "Issue tracker"
msgstr ""
msgstr "Cimspuradilo"
#: front/src/components/library/import/BatchDetail.vue:80
msgid "Job ID"
msgstr ""
msgstr "Labora ID"
#: front/src/components/library/import/BatchList.vue:36
msgid "Jobs"
msgstr ""
msgstr "Laboroj"
#: front/src/components/Home.vue:50
msgid "Keep a track of your favorite songs"
msgstr ""
msgstr "Konservi postsignojn de viaj preferitaj kantoj"
#: front/src/components/audio/track/Table.vue:33
msgid "Keep your PRIVATE_TOKEN secret as it gives access to your account."
msgstr ""
msgstr "Konservu vian PRIVATE_TOKEN sekreta, ĉar ĝi atingeblas vian konton."
#: front/src/components/manage/users/UsersTable.vue:41
#: front/src/views/admin/users/UsersDetail.vue:45
msgid "Last activity"
msgstr ""
msgstr "Lasta akto"
#: front/src/views/federation/LibraryDetail.vue:101
msgid "Last fetched"
msgstr ""
msgstr "Lasta kolektado"
#: front/src/components/playlists/PlaylistModal.vue:32
msgid "Last modification"
msgstr ""
msgstr "Lasta redakto"
#: front/src/components/common/ActionTable.vue:39
msgid "Launch"
msgstr ""
msgstr "Lanĉi"
#: front/src/components/library/import/BatchDetail.vue:18
#: front/src/components/library/import/BatchList.vue:35
msgid "Launch date"
msgstr ""
msgstr "Lanĉa dato"
#: front/src/components/federation/LibraryForm.vue:31
msgid "Launch scan"
msgstr ""
msgstr "Lanĉi skanon"
#: front/src/components/Home.vue:10
msgid "Learn more about this instance"
msgstr ""
msgstr "Lerni pli pri tiu instanco"
#: front/src/components/manage/users/InvitationForm.vue:58
msgid "Leave empty for a random code"
msgstr ""
msgstr "Lasu malplena por hazarda kodo"
#: front/src/components/requests/Form.vue:10
msgid "Leave this field empty if you're requesting the whole discography."
msgstr ""
msgstr "Lasu malplena se vi volas la tutan albumaron."
#: front/src/views/federation/Base.vue:5
#: src/views/federation/LibraryList.vue:123
msgid "Libraries"
msgstr ""
msgstr "Muzikejoj"
#: front/src/components/Sidebar.vue:70
#: front/src/components/federation/LibraryTrackTable.vue:51
@ -1231,103 +1242,104 @@ msgstr ""
#: front/src/views/admin/users/UsersDetail.vue:157
#: front/src/views/federation/LibraryDetail.vue:194
msgid "Library"
msgstr ""
msgstr "Muzikejo"
#: front/src/views/admin/library/FilesList.vue:3
msgid "Library files"
msgstr ""
msgstr "Muzikejaj dosieroj"
#: front/src/components/federation/LibraryForm.vue:20
msgid "Library name"
msgstr ""
msgstr "Nomo de muzikejo"
#: front/src/views/federation/LibraryDetail.vue:84
msgid "Library size"
msgstr ""
msgstr "Muzikejgrando"
#: front/src/components/federation/LibraryForm.vue:96
msgid "library@demo.funkwhale.audio"
msgstr ""
msgstr "library@demo.funkwhale.audio"
#: front/src/App.vue:29
msgid "Links"
msgstr ""
msgstr "Ligiloj"
#: front/src/views/instance/Timeline.vue:4
msgid "Loading timeline..."
msgstr ""
msgstr "Tempolino ŝarĝas…"
#: front/src/components/favorites/List.vue:5
#, fuzzy
msgid "Loading your favorites..."
msgstr ""
msgstr "Ŝarĝantas stelumojn"
#: front/src/components/auth/Login.vue:78
msgid "Log In"
msgstr ""
msgstr "Ensaluti"
#: front/src/components/auth/Login.vue:4
msgid "Log in to your Funkwhale account"
msgstr ""
msgstr "Ensaluti en via Funkwhale konto"
#: front/src/components/auth/Logout.vue:20
msgid "Log Out"
msgstr ""
msgstr "Elsaluti"
#: front/src/components/Sidebar.vue:38
msgid "Logged in as %{ username }"
msgstr ""
msgstr "Elsuta je %{username}"
#: front/src/components/Sidebar.vue:44 src/components/auth/Login.vue:41
msgid "Login"
msgstr ""
msgstr "Ensaluti"
#: front/src/components/Sidebar.vue:43
msgid "Logout"
msgstr ""
msgstr "Elsaluti"
#: front/src/components/audio/Player.vue:266
msgid "Looping disabled. Click to switch to single-track looping."
msgstr ""
msgstr "Ripeto malaktivas. Alklaki por aktivi ripetado de la aktuala kanto."
#: front/src/components/audio/Player.vue:267
msgid "Looping on a single track. Click to switch to whole queue looping."
msgstr ""
msgstr "Ripetas unu kanton. Alklaki por aktivi ripetado de la tutan atendovico."
#: front/src/components/audio/Player.vue:268
msgid "Looping on whole queue. Click to disable looping."
msgstr ""
msgstr "Ripetas la tutan atendovicon. Alklaki por malaktivi ripeto."
#: front/src/components/library/Track.vue:94
msgid "Lyrics"
msgstr ""
msgstr "Teksto"
#: front/src/views/admin/library/Base.vue:25
msgid "Manage library"
msgstr ""
msgstr "Manipuli muzikejon"
#: front/src/components/playlists/PlaylistModal.vue:3
msgid "Manage playlists"
msgstr ""
msgstr "Manipuli ludlistojn"
#: front/src/views/admin/users/Base.vue:20
msgid "Manage users"
msgstr ""
msgstr "Manipuli uzantojn"
#: front/src/views/playlists/List.vue:8
msgid "Manage your playlists"
msgstr ""
msgstr "Manipuli viajn ludlistojn"
#: front/src/components/manage/library/RequestsTable.vue:197
msgid "Mark as closed"
msgstr ""
msgstr "Marki fermata"
#: front/src/components/manage/library/RequestsTable.vue:196
msgid "Mark as imported"
msgstr ""
msgstr "Marki importata"
#: front/src/components/library/import/Main.vue:12
msgid "Metadata"
msgstr ""
msgstr "Metadatumoj"
#: front/src/components/library/import/Main.vue:115
msgid ""
@ -1337,31 +1349,36 @@ msgid ""
" </a>\n"
" project, which you can think about as the Wikipedia of music."
msgstr ""
"Metadatumoj estas datumoj rilatanta al muziko vi volas importi. Ĝi enhavas "
"ĉiu informo pri artistoj, albumoj kaj kantoj. Por havi bonega muzikejo, "
"estas rekomendata kolekti datumojn el la <a href=\"https://musicbrainz.org\" "
"target=\"_blank\">MusicBrainz</a> projekto, ke similas al Vikipedio sed por "
"muziko."
#: front/src/components/Sidebar.vue:48
#: src/components/library/import/Main.vue:18
msgid "Music"
msgstr ""
msgstr "Muziko"
#: front/src/components/library/import/Main.vue:147
msgid "Music request"
msgstr ""
msgstr "Muzikpeto"
#: front/src/components/audio/Player.vue:265
msgid "Mute"
msgstr ""
msgstr "Silentigi"
#: front/src/components/Sidebar.vue:34
msgid "My account"
msgstr ""
msgstr "Mia konto"
#: front/src/components/playlists/Form.vue:74
msgid "My awesome playlist"
msgstr ""
msgstr "Mia mojosa ludlisto"
#: front/src/components/library/radios/Builder.vue:227
msgid "My awesome radio"
msgstr ""
msgstr "Mia mojosa radio"
#: front/src/components/library/Track.vue:64
#: src/components/library/Track.vue:75
@ -1376,83 +1393,85 @@ msgstr ""
#: front/src/components/manage/users/UsersTable.vue:61
#: front/src/views/admin/users/UsersDetail.vue:49
msgid "N/A"
msgstr ""
msgstr "ND"
#: front/src/components/playlists/PlaylistModal.vue:31
#: front/src/views/admin/users/UsersDetail.vue:21
msgid "Name"
msgstr ""
msgstr "Nomo"
#: front/src/components/auth/Settings.vue:88
#: front/src/views/auth/PasswordResetConfirm.vue:14
msgid "New password"
msgstr ""
msgstr "Nova pasvorto"
#: front/src/components/Sidebar.vue:158
msgid "New tracks will be appended here automatically."
msgstr ""
msgstr "Novaj kantoj estos aldonataj ĉi-tie aŭtomate."
#: front/src/components/library/import/Main.vue:29
msgid "Next step"
msgstr ""
msgstr "Baldaŭa etapo"
#: front/src/components/audio/Player.vue:263
msgid "Next track"
msgstr ""
msgstr "Baldaŭa kanto"
#: front/src/components/Sidebar.vue:125
msgid "No"
msgstr ""
msgstr "Ne"
#: front/src/components/Home.vue:103
msgid "No add-ons, no plugins : you only need a web library"
msgstr ""
msgstr "Nek aldonaĵoj, nek kromprogramoj: vi nur bezonas retmuzikejo"
#: front/src/components/library/Track.vue:102
msgid "No lyrics available for this track."
msgstr ""
msgstr "Nenio teksto disponeblas por tiu kanto."
#: front/src/components/playlists/Form.vue:81
msgid "Nobody except me"
msgstr ""
msgstr "Neniu krom mi"
#: front/src/views/federation/LibraryDetail.vue:32
msgid "Not following"
msgstr ""
msgstr "Ne sekvas"
#: front/src/components/federation/LibraryTrackTable.vue:13
#: front/src/components/federation/LibraryTrackTable.vue:67
msgid "Not imported"
msgstr ""
msgstr "Ne importintas"
#: front/src/components/manage/users/InvitationsTable.vue:51
msgid "Not used"
msgstr ""
msgstr "Ne uzantata"
#: front/src/App.vue:37
msgid "Official website"
msgstr ""
msgstr "Oficiala retejo"
#: front/src/components/auth/Settings.vue:83
msgid "Old password"
msgstr ""
msgstr "Malnova pasvorto"
#: front/src/components/library/import/FileUpload.vue:36
msgid "Once all your files are uploaded, simply click the following button to check the import status."
msgstr ""
"Kiam viaj dosieroj estas tute elŝutinta, ĵus alklaki tiu butono por vidi la "
"staton de la importo."
#: front/src/components/federation/LibraryCard.vue:21
#: front/src/components/manage/users/InvitationsTable.vue:20
msgid "Open"
msgstr ""
msgstr "Malferma"
#: front/src/App.vue:63
msgid "Options"
msgstr ""
msgstr "Preferoj"
#: front/src/components/library/import/Main.vue:93
msgid "Or"
msgstr ""
msgstr ""
#: front/src/components/favorites/List.vue:23
#: front/src/components/federation/LibraryTrackTable.vue:18
@ -1465,7 +1484,7 @@ msgstr ""
#: front/src/views/federation/LibraryList.vue:18
#: src/views/playlists/List.vue:17
msgid "Ordering"
msgstr ""
msgstr "Ordo"
#: front/src/components/favorites/List.vue:31
#: front/src/components/federation/LibraryTrackTable.vue:26
@ -1477,35 +1496,35 @@ msgstr ""
#: front/src/views/federation/LibraryList.vue:26
#: src/views/playlists/List.vue:25
msgid "Ordering direction"
msgstr ""
msgstr "Orda direkto"
#: front/src/components/manage/users/InvitationsTable.vue:38
msgid "Owner"
msgstr ""
msgstr "Proprietulo"
#: front/src/components/PageNotFound.vue:33
msgid "Page Not Found"
msgstr ""
msgstr "Ne eblas trovi tiun paĝon"
#: front/src/components/PageNotFound.vue:7
msgid "Page not found!"
msgstr ""
msgstr "Maltrovitas paĝon!"
#: front/src/components/auth/Login.vue:32 src/components/auth/Signup.vue:38
msgid "Password"
msgstr ""
msgstr "Pasvorto"
#: front/src/components/auth/SubsonicTokenForm.vue:95
msgid "Password updated"
msgstr ""
msgstr "Pasvorto aktuliginta"
#: front/src/views/auth/PasswordResetConfirm.vue:28
msgid "Password updated successfully"
msgstr ""
msgstr "Pasvorto sukcese aktualiginta"
#: front/src/components/audio/Player.vue:262
msgid "Pause track"
msgstr ""
msgstr "Paŭzi kanton"
#: front/src/components/federation/LibraryFollowTable.vue:46
#: front/src/components/library/import/BatchDetail.vue:33
@ -1515,86 +1534,86 @@ msgstr ""
#: front/src/components/manage/library/RequestsTable.vue:27
#: front/src/components/manage/library/RequestsTable.vue:63
msgid "Pending"
msgstr ""
msgstr "Atendas"
#: front/src/components/federation/LibraryFollowTable.vue:11
#: front/src/views/federation/LibraryDetail.vue:26
msgid "Pending approval"
msgstr ""
msgstr "Atendas aprobon"
#: front/src/components/Sidebar.vue:217
msgid "Pending follow requests"
msgstr ""
msgstr "Atendantaj petoj da sekvado"
#: front/src/components/Sidebar.vue:216 src/views/admin/library/Base.vue:26
msgid "Pending import requests"
msgstr ""
msgstr "Atendantaj importpetoj"
#: front/src/components/requests/Form.vue:26 src/views/federation/Base.vue:36
msgid "Pending requests"
msgstr ""
msgstr "Atendantaj petoj"
#: front/src/components/manage/users/UsersTable.vue:42
#: front/src/views/admin/users/UsersDetail.vue:68
msgid "Permissions"
msgstr ""
msgstr "Rajtoj"
#: front/src/components/audio/PlayButton.vue:9
#: src/components/library/Track.vue:30
msgid "Play"
msgstr ""
msgstr "Ludi"
#: front/src/components/audio/album/Card.vue:50
#: front/src/components/audio/artist/Card.vue:44
#: src/components/library/Album.vue:28 front/src/views/playlists/Detail.vue:23
msgid "Play all"
msgstr ""
msgstr "Ludi ĉiu"
#: front/src/components/library/Artist.vue:26
msgid "Play all albums"
msgstr ""
msgstr "Ludi ĉiuj albumoj"
#: front/src/components/audio/PlayButton.vue:58
msgid "Play immediatly"
msgstr ""
msgstr "Ludi tuj"
#: front/src/components/audio/PlayButton.vue:15
msgid "Play next"
msgstr ""
msgstr "Ludi baldaŭe"
#: front/src/components/audio/PlayButton.vue:16
msgid "Play now"
msgstr ""
msgstr "Ludi tuj"
#: front/src/components/audio/Player.vue:261
msgid "Play track"
msgstr ""
msgstr "Ludi kanton"
#: front/src/views/playlists/Detail.vue:90
msgid "Playlist"
msgstr ""
msgstr "Ludlisto"
#: front/src/views/playlists/Detail.vue:12
msgid "Playlist containing %{ count } track, by %{ username }"
msgid_plural "Playlist containing %{ count } tracks, by %{ username }"
msgstr[0] ""
msgstr[1] ""
msgstr[0] "Ludisto enhavanta unu kanto, je %{username}"
msgstr[1] "Ludisto enhavanta %{count} kantoj, je %{username}"
#: front/src/components/playlists/Form.vue:9
msgid "Playlist created"
msgstr ""
msgstr "Ludlisto kreiintas"
#: front/src/components/playlists/Editor.vue:4
msgid "Playlist editor"
msgstr ""
msgstr "Ludlista redaktilo"
#: front/src/components/playlists/Form.vue:21
msgid "Playlist name"
msgstr ""
msgstr "Nomo de la ludlisto"
#: front/src/components/playlists/Form.vue:6
msgid "Playlist updated"
msgstr ""
msgstr "Ludlisto aktualigintas"
#: front/src/components/playlists/Form.vue:25
msgid "Playlist visibility"

File diff suppressed because it is too large Load Diff

View File

@ -3,9 +3,10 @@ msgstr ""
"Project-Id-Version: French (Funkwhale)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-07-22 14:12+0200\n"
"PO-Revision-Date: 2018-07-21 21:30+0000\n"
"Last-Translator: Baptiste Gelez <baptiste@gelez.xyz>\n"
"Language-Team: French <https://translate.funkwhale.audio/projects/funkwhale/front/fr/>\n"
"PO-Revision-Date: 2018-07-30 16:28+0000\n"
"Last-Translator: Eliot Berriot <contact@eliotberriot.com>\n"
"Language-Team: French <https://translate.funkwhale.audio/projects/funkwhale/"
"front/fr/>\n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -98,7 +99,7 @@ msgstr[1] "%{ count } pistes"
#: front/src/components/About.vue:5
msgid "About %{ instance }"
msgstr "À propos de cette instance"
msgstr "À propos de %{instance}"
#: front/src/App.vue:54
msgid "About Funkwhale"
@ -155,9 +156,8 @@ msgid "Actions"
msgstr "Actions"
#: front/src/components/manage/users/UsersTable.vue:53
#, fuzzy
msgid "Active"
msgstr "Activité"
msgstr "Actif"
#: front/src/components/Sidebar.vue:60
msgid "Activity"
@ -260,7 +260,9 @@ msgstr "Une erreur s'est produite pendant l'enregistrement de vos modifications"
#: front/src/components/auth/Login.vue:10
msgid "An unknown error happend, this can mean the server is down or cannot be reached"
msgstr "Une erreur inconnue a été rencontrée, ce qui peut signifier que le serveur est en panne ou na pas pu être atteint"
msgstr ""
"Une erreur inconnue s'est produite, cela pourrait signifier que le serveur "
"ne peut pas être accedé"
#: front/src/components/federation/LibraryTrackTable.vue:11
#: front/src/components/library/import/BatchDetail.vue:68
@ -985,7 +987,7 @@ msgstr "Récupérer les métadonnées correspondantes"
#: front/src/App.vue:74
msgid "Help us translate Funkwhale"
msgstr ""
msgstr "Aidez nous à traduire Funkwhale"
#: front/src/components/library/Home.vue:65
msgid "Home"
@ -1111,7 +1113,7 @@ msgstr "Dans la bibliothèque"
#: front/src/components/manage/users/UsersTable.vue:54
msgid "Inactive"
msgstr "Actif"
msgstr "Inactif"
#: front/src/components/library/import/Main.vue:96
msgid "Input a MusicBrainz ID manually:"
@ -1436,7 +1438,7 @@ msgstr "Site officiel"
#: front/src/components/auth/Settings.vue:83
msgid "Old password"
msgstr "Ancien mot de passe"
msgstr "Vieux mot de passe"
#: front/src/components/library/import/FileUpload.vue:36
msgid "Once all your files are uploaded, simply click the following button to check the import status."
@ -1621,7 +1623,7 @@ msgstr "Merci de vérifier que votre nom d'utilisateur et mot de passe sont corr
#: front/src/components/auth/Settings.vue:46
msgid "PNG, GIF or JPG. At most 2MB. Will be downscaled to 400x400px."
msgstr "PNG, GIF ou JPG. 2Mb au plus. L'image sera réduite à 400×400 pixels."
msgstr "PNG, GIF ou JPG. 2Mo au plus. L'image sera réduite à 400×400 pixels."
#: front/src/components/library/import/Main.vue:26
msgid "Previous step"
@ -1661,18 +1663,16 @@ msgid "Radio Builder"
msgstr "Éditeur de radio"
#: front/src/components/library/radios/Builder.vue:15
#, fuzzy
msgid "Radio created"
msgstr "Nom de la radio"
msgstr "Radio créée"
#: front/src/components/library/radios/Builder.vue:21
msgid "Radio name"
msgstr "Nom de la radio"
#: front/src/components/library/radios/Builder.vue:12
#, fuzzy
msgid "Radio updated"
msgstr "Nom de la radio"
msgstr "Radio à jour"
#: front/src/components/library/Library.vue:10
#: src/components/library/Radios.vue:141
@ -1689,7 +1689,7 @@ msgstr "Ajoutés récemment"
#: front/src/components/library/Home.vue:11
msgid "Recently favorited"
msgstr "Ajoutées à vos favoris récemment"
msgstr "Récemment ajouté aux favoris"
#: front/src/components/library/Home.vue:6
msgid "Recently listened"
@ -2297,7 +2297,7 @@ msgstr "Nom d'utilisateur ou email"
#: front/src/components/instance/Stats.vue:13
msgid "users"
msgstr "tilisateur·ice·s"
msgstr "utilisateur·rice·s"
#: front/src/components/Sidebar.vue:103 src/views/admin/Settings.vue:81
#: front/src/views/admin/users/Base.vue:5 src/views/admin/users/UsersList.vue:3
@ -2352,7 +2352,7 @@ msgstr "Nous pensons que l'accès à la musique devrait être simple."
#: front/src/components/PageNotFound.vue:10
msgid "We're sorry, the page you asked for does not exist:"
msgstr "Désolé, la page demandée nexiste pas :"
msgstr "Désolé, la page demandée nexiste pas :"
#: front/src/components/requests/Form.vue:21
msgid "We've received your request, you'll get some groove soon ;)"
@ -2368,7 +2368,7 @@ msgstr "Bienvenue sur Funkwhale"
#: front/src/components/library/import/Main.vue:114
msgid "What is metadata?"
msgstr "Qu'est-ce que les métadonnées ?"
msgstr "Qu'est-ce que les métadonnées ?"
#: front/src/views/federation/LibraryDetail.vue:197
msgid "When enabled, auto importing will automatically import new tracks published in this library"

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: front 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-07-22 14:12+0200\n"
"PO-Revision-Date: 2018-07-22 20:29+0000\n"
"PO-Revision-Date: 2018-07-24 17:46+0000\n"
"Last-Translator: Quentí <quentin_antonin@hotmail.com>\n"
"Language-Team: none\n"
"Language: oc\n"
@ -28,7 +28,7 @@ msgstr "(%{ index } sus %{ length })"
#: front/src/components/Sidebar.vue:22
msgid "(empty)"
msgstr "(void)"
msgstr "(voida)"
#: front/src/components/common/ActionTable.vue:43
#: front/src/components/common/ActionTable.vue:51
@ -2364,7 +2364,7 @@ msgstr "Nom dutilizaire o corrièl"
#: front/src/components/instance/Stats.vue:13
msgid "users"
msgstr "utilizaire"
msgstr "utilizaires"
#: front/src/components/Sidebar.vue:103 src/views/admin/Settings.vue:81
#: front/src/views/admin/users/Base.vue:5 src/views/admin/users/UsersList.vue:3

View File

@ -8,14 +8,15 @@ msgstr ""
"Project-Id-Version: front 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-07-22 14:12+0200\n"
"PO-Revision-Date: 2018-07-20 19:03+0000\n"
"PO-Revision-Date: 2018-07-31 13:32+0000\n"
"Last-Translator: Marcin Mikołajczak <me@m4sk.in>\n"
"Language-Team: \n"
"Language: pl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 2.20\n"
#: front/src/components/playlists/PlaylistModal.vue:9
@ -669,9 +670,8 @@ msgid "Do you want to delete the playlist \"%{ playlist }\"?"
msgstr "Czy chcesz usunąć listę odtwarzania „%{ playlist }”?"
#: front/src/views/radios/Detail.vue:26
#, fuzzy
msgid "Do you want to delete the radio \"%{ radio }\"?"
msgstr "Czy chcesz usunąć radio „{{ radio }}”?"
msgstr "Czy chcesz usunąć radio „%{ radio }”?"
#: front/src/components/common/ActionTable.vue:29
msgid "Do you want to launch %{ action } on %{ count } element?"
@ -1002,7 +1002,7 @@ msgstr "Uzyskaj odpowiednie metadane"
#: front/src/App.vue:74
msgid "Help us translate Funkwhale"
msgstr ""
msgstr "Pomóż nam tłumaczyć Funkwhale"
#: front/src/components/library/Home.vue:65
msgid "Home"
@ -1682,18 +1682,16 @@ msgid "Radio Builder"
msgstr "Tworzenie radia"
#: front/src/components/library/radios/Builder.vue:15
#, fuzzy
msgid "Radio created"
msgstr "Nazwa radia"
msgstr "Utworzono radio"
#: front/src/components/library/radios/Builder.vue:21
msgid "Radio name"
msgstr "Nazwa radia"
#: front/src/components/library/radios/Builder.vue:12
#, fuzzy
msgid "Radio updated"
msgstr "Nazwa radia"
msgstr "Zaktualizowano radio"
#: front/src/components/library/Library.vue:10
#: src/components/library/Radios.vue:141

View File

@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: front 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-07-17 19:29+0200\n"
"PO-Revision-Date: 2018-07-21 21:30+0000\n"
"PO-Revision-Date: 2018-07-23 16:49+0000\n"
"Last-Translator: Tim Stahel <gitlab@swedneck.xyz>\n"
"Language-Team: none\n"
"Language: sv\n"
@ -459,11 +459,11 @@ msgstr ""
#: front/src/components/auth/Settings.vue:98
msgid "Changing your password will have the following consequences"
msgstr ""
msgstr "Ändring av lösenord har följande konsekvenser"
#: front/src/App.vue:4
msgid "Choose your instance"
msgstr ""
msgstr "Välj din instans"
#: front/src/components/Home.vue:64
msgid "Clean library"

View File

@ -1,124 +1,92 @@
{
"name": "front",
"version": "1.0.0",
"description": "Funkwhale front-end",
"author": "Eliot Berriot <contact@eliotberriot.com>",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "scripts/i18n-compile.sh && node build/dev-server.js",
"start": "scripts/i18n-compile.sh && node build/dev-server.js",
"build": "node build/build.js",
"serve": "scripts/i18n-compile.sh && vue-cli-service serve --port ${VUE_PORT:-8000} --host ${VUE_HOST:-0.0.0.0}",
"build": "scripts/i18n-compile.sh && vue-cli-service build",
"lint": "vue-cli-service lint",
"i18n-extract": "scripts/i18n-extract.sh",
"i18n-compile": "scripts/i18n-compile.sh",
"unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
"unit-watch": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js",
"e2e": "node test/e2e/runner.js",
"test": "npm run unit && npm run e2e",
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs"
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"@panter/vue-i18next": "^0.9.1",
"axios": "^0.17.1",
"dateformat": "^2.0.0",
"axios": "^0.18.0",
"dateformat": "^3.0.3",
"django-channels": "^1.1.6",
"js-logger": "^1.3.0",
"howler": "^2.0.14",
"js-logger": "^1.4.1",
"jwt-decode": "^2.2.0",
"lodash": "^4.17.4",
"masonry-layout": "^4.2.1",
"moment": "^2.20.1",
"moxios": "^0.4.0",
"raven-js": "^3.22.3",
"semantic-ui-css": "^2.2.10",
"lodash": "^4.17.10",
"masonry-layout": "^4.2.2",
"moment": "^2.22.2",
"raven-js": "^3.26.4",
"semantic-ui-css": "^2.3.3",
"showdown": "^1.8.6",
"vue": "^2.5.16",
"vue-gettext": "^2.0.31",
"vue-lazyload": "^1.1.4",
"vue-masonry": "^0.10.16",
"vue-router": "^2.3.1",
"vue-upload-component": "^2.7.4",
"vuedraggable": "^2.14.1",
"vue": "^2.5.17",
"vue-gettext": "^2.1.0",
"vue-lazyload": "^1.2.6",
"vue-masonry": "^0.11.5",
"vue-router": "^3.0.1",
"vue-upload-component": "^2.8.11",
"vuedraggable": "^2.16.0",
"vuex": "^3.0.1",
"vuex-persistedstate": "^2.5.2",
"vuex-persistedstate": "^2.5.4",
"vuex-router-sync": "^5.0.0"
},
"devDependencies": {
"autoprefixer": "^6.7.2",
"babel-core": "^6.22.1",
"babel-eslint": "^7.1.1",
"babel-loader": "7",
"babel-plugin-istanbul": "^4.1.1",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"babel-register": "^6.22.0",
"chai": "^3.5.0",
"chalk": "^1.1.3",
"chromedriver": "^2.27.2",
"connect-history-api-fallback": "^1.3.0",
"copy-webpack-plugin": "^4.0.1",
"cross-env": "^4.0.0",
"cross-spawn": "^5.0.1",
"css-loader": "^0.28.0",
"easygettext": "^2.5.0",
"es6-promise": "^4.2.2",
"eslint": "^3.19.0",
"eslint-config-standard": "^6.2.1",
"eslint-friendly-formatter": "^2.0.7",
"eslint-loader": "^1.7.1",
"eslint-plugin-html": "^2.0.0",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^2.0.1",
"eventsource-polyfill": "^0.9.6",
"express": "^4.14.1",
"extract-text-webpack-plugin": "^2.0.0",
"file-loader": "^0.11.1",
"friendly-errors-webpack-plugin": "^1.1.3",
"html-webpack-plugin": "^2.28.0",
"http-proxy-middleware": "^0.17.3",
"inject-loader": "^3.0.0",
"karma": "^1.4.1",
"karma-coverage": "^1.1.1",
"karma-mocha": "^1.3.0",
"karma-phantomjs-launcher": "^1.0.2",
"karma-phantomjs-shim": "^1.4.0",
"karma-sinon-chai": "^1.3.1",
"karma-sinon-stub-promise": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.30",
"karma-webpack": "^2.0.2",
"lolex": "^1.5.2",
"mocha": "^3.2.0",
"nightwatch": "^0.9.12",
"node-sass": "^4.5.3",
"opn": "^4.0.2",
"optimize-css-assets-webpack-plugin": "^1.3.0",
"ora": "^1.2.0",
"phantomjs-prebuilt": "^2.1.14",
"rimraf": "^2.6.0",
"sass-loader": "^6.0.5",
"selenium-server": "^3.0.1",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"sinon": "^2.1.0",
"sinon-chai": "^2.8.0",
"sinon-stub-promise": "^4.0.0",
"url-loader": "^0.5.8",
"vue-loader": "^12.1.0",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.3.3",
"webpack": "3",
"webpack-bundle-analyzer": "^2.2.1",
"webpack-dev-middleware": "^1.10.0",
"webpack-hot-middleware": "^2.18.0",
"webpack-merge": "^4.1.0"
"@vue/cli-plugin-babel": "^3.0.0",
"@vue/cli-plugin-eslint": "^3.0.0",
"@vue/cli-plugin-unit-mocha": "^3.0.0",
"@vue/cli-service": "^3.0.0",
"@vue/test-utils": "^1.0.0-beta.20",
"chai": "^4.1.2",
"easygettext": "^2.6.3",
"eslint-plugin-html": "^4.0.5",
"mocha": "^5.2.0",
"moxios": "^0.4.0",
"node-sass": "^4.9.3",
"sass-loader": "^7.1.0",
"sinon": "^6.1.5",
"vue-template-compiler": "^2.5.17"
},
"engines": {
"node": ">= 4.0.0",
"npm": ">= 3.0.0"
"eslintConfig": {
"root": true,
"env": {
"browser": true,
"node": true
},
"plugins": [
"html"
],
"rules": {
"no-console": 0,
"no-unused-vars": [
2,
{
"vars": "all",
"args": "none"
}
]
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
],
"author": "Eliot Berriot <contact@eliotberriot.com>",
"description": "Funkwhale front-end"
}

1
front/public/custom.css Normal file
View File

@ -0,0 +1 @@
/* This is a custom CSS file that can be loaded thanks to settings.json */

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

20
front/public/index.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.png">
<title>Funkwhale</title>
</head>
<body>
<noscript>
<strong>We're sorry but front doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -0,0 +1,3 @@
{
"additionalStylesheets": ["/custom.css"]
}

View File

@ -1,5 +1,7 @@
<template>
<div id="app">
<!-- here, we display custom stylesheets, if any -->
<link v-for="url in customStylesheets" rel="stylesheet" property="stylesheet" :href="url" :key="url">
<div class="ui main text container instance-chooser" v-if="!$store.state.instance.instanceUrl">
<div class="ui padded segment">
<h1 class="ui header"><translate>Choose your instance</translate></h1>
@ -163,11 +165,7 @@ export default {
messages: state => state.ui.messages
}),
suggestedInstances () {
let rootUrl = (
window.location.protocol + '//' + window.location.hostname +
(window.location.port ? ':' + window.location.port : '')
)
let instances = [rootUrl, 'https://demo.funkwhale.audio']
let instances = [this.$store.getters['instance/defaultUrl'](), 'https://demo.funkwhale.audio']
return instances
},
version () {
@ -175,6 +173,11 @@ export default {
return null
}
return _.get(this.nodeinfo, 'software.version')
},
customStylesheets () {
if (this.$store.state.instance.frontSettings) {
return this.$store.state.instance.frontSettings.additionalStylesheets || []
}
}
},
watch: {
@ -243,7 +246,7 @@ html, body {
left: 350px;
right: 0px;
top: 0px;
z-index: 1;
z-index: 2000;
}
background-color: white;
.item {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

@ -0,0 +1,5 @@
This work is provided under the terms of the Attribution-ShareAlike 4.0 International (CC BY-SA 3.0) license.
The terms of this license can be found here : https://creativecommons.org/licenses/by-sa/4.0/legalcode.
The logo, the favicon and its derivatives were designed by Francis Gading.

View File

@ -84,9 +84,10 @@ export default {
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.ui.menu {
border: none;
box-shadow: none;
.ui.pagination.menu .item {
cursor: pointer;
}
</style>

View File

@ -191,7 +191,8 @@ export default {
backend: backend,
tracksChangeBuffer: null,
isCollapsed: true,
fetchInterval: null
fetchInterval: null,
showAdmin: this.getShowAdmin()
}
},
mounted () {
@ -220,16 +221,6 @@ export default {
pendingFollows
}
},
showAdmin () {
let adminPermissions = [
this.$store.state.auth.availablePermissions['federation'],
this.$store.state.auth.availablePermissions['library'],
this.$store.state.auth.availablePermissions['upload']
]
return adminPermissions.filter(e => {
return e
}).length > 0
},
tracks: {
get () {
return this.$store.state.queue.tracks
@ -250,6 +241,17 @@ export default {
...mapActions({
cleanTrack: 'queue/cleanTrack'
}),
getShowAdmin () {
let adminPermissions = [
this.$store.state.auth.availablePermissions['federation'],
this.$store.state.auth.availablePermissions['library'],
this.$store.state.auth.availablePermissions['upload']
]
return adminPermissions.filter(e => {
return e
}).length > 0
},
fetchNotificationsCount () {
this.$store.dispatch('ui/fetchFederationNotificationsCount')
this.$store.dispatch('ui/fetchImportRequestsCount')
@ -289,6 +291,7 @@ export default {
},
'$store.state.auth.availablePermissions': {
handler () {
this.showAdmin = this.getShowAdmin()
this.fetchNotificationsCount()
},
deep: true

View File

@ -110,7 +110,7 @@ export default {
resolve(self.tracks)
} else if (self.playlist) {
let url = 'playlists/' + self.playlist.id + '/'
axios.get(url + 'tracks').then((response) => {
axios.get(url + 'tracks/').then((response) => {
resolve(response.data.results.map(plt => {
return plt.track
}))

View File

@ -1,16 +1,15 @@
<template>
<div class="ui inverted segment player-wrapper" :style="style">
<div class="player">
<keep-alive>
<audio-track
ref="currentAudio"
v-if="renderAudio && currentTrack"
:is-current="true"
:start-time="$store.state.player.currentTime"
:autoplay="$store.state.player.playing"
:track="currentTrack">
</audio-track>
</keep-alive>
<audio-track
ref="currentAudio"
v-if="currentTrack"
:is-current="true"
:start-time="$store.state.player.currentTime"
:autoplay="$store.state.player.playing"
:key="audioKey"
:track="currentTrack">
</audio-track>
<div v-if="currentTrack" class="track-area ui unstackable items">
<div class="ui inverted item">
<div class="ui tiny image">
@ -160,13 +159,13 @@
import {mapState, mapGetters, mapActions} from 'vuex'
import GlobalEvents from '@/components/utils/global-events'
import ColorThief from '@/vendor/color-thief'
import {Howl} from 'howler'
import AudioTrack from '@/components/audio/Track'
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon'
export default {
name: 'player',
components: {
TrackFavoriteIcon,
TrackPlaylistIcon,
@ -177,16 +176,28 @@ export default {
let defaultAmbiantColors = [[46, 46, 46], [46, 46, 46], [46, 46, 46], [46, 46, 46]]
return {
isShuffling: false,
renderAudio: true,
sliderVolume: this.volume,
defaultAmbiantColors: defaultAmbiantColors,
showVolume: false,
ambiantColors: defaultAmbiantColors
ambiantColors: defaultAmbiantColors,
audioKey: String(new Date()),
dummyAudio: null
}
},
mounted () {
// we trigger the watcher explicitely it does not work otherwise
this.sliderVolume = this.volume
// this is needed to unlock audio playing under some browsers,
// cf https://github.com/goldfire/howler.js#mobilechrome-playback
// but we never actually load those audio files
this.dummyAudio = new Howl({
preload: false,
autoplay: false,
src: ['noop.webm', 'noop.mp3']
})
},
destroyed () {
this.dummyAudio.unload()
},
methods: {
...mapActions({
@ -305,21 +316,13 @@ export default {
},
watch: {
currentTrack (newValue) {
if (!this.isShuffling) {
this.audioKey = String(new Date())
}
if (!newValue || !newValue.album.cover) {
this.ambiantColors = this.defaultAmbiantColors
}
},
currentIndex (newValue, oldValue) {
if (newValue !== oldValue) {
// why this? to ensure the audio tag is deleted and fully
// rerendered, so we don't have any issues with cached position
// or whatever
this.renderAudio = false
this.$nextTick(() => {
this.renderAudio = true
})
}
},
volume (newValue) {
this.sliderVolume = newValue
},
@ -385,9 +388,6 @@ export default {
.volume-control {
position: relative;
width: 12.5% !important;
.icon {
// margin: 0;
}
[type="range"] {
max-width: 70%;
position: absolute;
@ -395,16 +395,11 @@ export default {
left: 25%;
cursor: pointer;
}
input[type=range] {
-webkit-appearance: none;
}
input[type=range]:focus {
outline: none;
}
input[type=range]::-webkit-slider-runnable-track {
cursor: pointer;
background: white;
opacity: 0.3;
}
input[type=range]::-webkit-slider-thumb {
background: white;
@ -413,10 +408,6 @@ export default {
border-radius: 3px;
width: 10px;
}
input[type=range]:focus::-webkit-slider-runnable-track {
background: #white;
opacity: 0.3;
}
input[type=range]::-moz-range-track {
cursor: pointer;
background: white;
@ -455,7 +446,7 @@ export default {
background: white;
}
input[type=range]:focus::-ms-fill-upper {
background: #white;
background: white;
}
}

View File

@ -1,24 +1,13 @@
<template>
<audio
ref="audio"
@error="errored"
@loadeddata="loaded"
@durationchange="updateDuration"
@timeupdate="updateProgressThrottled"
@ended="ended"
preload>
<source
@error="sourceErrored"
v-for="src in srcs"
:src="src.url"
:type="src.type">
</audio>
<i />
</template>
<script>
import {mapState} from 'vuex'
import url from '@/utils/url'
import _ from 'lodash'
import url from '@/utils/url'
import {Howl} from 'howler'
// import logger from '@/logging'
export default {
@ -30,11 +19,44 @@ export default {
},
data () {
return {
realTrack: this.track,
sourceErrors: 0,
isUpdatingTime: false
sound: null,
isUpdatingTime: false,
progressInterval: null
}
},
mounted () {
let self = this
this.sound = new Howl({
src: this.srcs.map((s) => { return s.url }),
autoplay: false,
loop: false,
html5: true,
preload: true,
volume: this.volume,
onend: function () {
self.ended()
},
onunlock: function () {
if (this.$store.state.player.playing) {
self.sound.play()
}
},
onload: function () {
self.$store.commit('player/resetErrorCount')
self.$store.commit('player/duration', self.sound.duration())
}
})
if (this.autoplay) {
this.sound.play()
this.$store.commit('player/playing', true)
this.observeProgress(true)
}
},
destroyed () {
this.observeProgress(false)
this.sound.unload()
},
computed: {
...mapState({
playing: state => state.player.playing,
@ -44,7 +66,7 @@ export default {
looping: state => state.player.looping
}),
srcs: function () {
let file = this.realTrack.files[0]
let file = this.track.files[0]
if (!file) {
this.$store.dispatch('player/trackErrored')
return []
@ -68,90 +90,58 @@ export default {
}
},
methods: {
errored: function () {
let self = this
setTimeout(
() => { self.$store.dispatch('player/trackErrored') }
, 1000)
},
sourceErrored: function () {
this.sourceErrors += 1
if (this.sourceErrors >= this.srcs.length) {
// all sources failed
this.errored()
}
},
updateDuration: function (e) {
if (!this.$refs.audio) {
return
}
this.$store.commit('player/duration', this.$refs.audio.duration)
},
loaded: function () {
if (!this.$refs.audio) {
return
}
this.$refs.audio.volume = this.volume
this.$store.commit('player/resetErrorCount')
if (this.isCurrent) {
this.$store.commit('player/duration', this.$refs.audio.duration)
if (this.startTime) {
this.setCurrentTime(this.startTime)
}
if (this.autoplay) {
this.$store.commit('player/playing', true)
this.$refs.audio.play()
}
}
},
updateProgress: function () {
this.isUpdatingTime = true
if (this.$refs.audio) {
this.$store.dispatch('player/updateProgress', this.$refs.audio.currentTime)
if (this.sound && this.sound.state() === 'loaded') {
this.$store.dispatch('player/updateProgress', this.sound.seek())
}
},
ended: function () {
let onlyTrack = this.$store.state.queue.tracks.length === 1
if (this.looping === 1 || (onlyTrack && this.looping === 2)) {
this.setCurrentTime(0)
this.$refs.audio.play()
observeProgress: function (enable) {
let self = this
if (enable) {
if (self.progressInterval) {
clearInterval(self.progressInterval)
}
self.progressInterval = setInterval(() => {
self.updateProgress()
}, 1000)
} else {
this.$store.dispatch('player/trackEnded', this.realTrack)
clearInterval(self.progressInterval)
}
},
setCurrentTime (t) {
if (t < 0 | t > this.duration) {
return
}
if (t === this.$refs.audio.currentTime) {
if (t === this.sound.seek()) {
return
}
if (t === 0) {
this.updateProgressThrottled.cancel()
}
this.$refs.audio.currentTime = t
this.sound.seek(t)
},
ended: function () {
let onlyTrack = this.$store.state.queue.tracks.length === 1
if (this.looping === 1 || (onlyTrack && this.looping === 2)) {
this.sound.seek(0)
this.sound.play()
} else {
this.$store.dispatch('player/trackEnded', this.track)
}
}
},
watch: {
track: _.debounce(function (newValue) {
this.realTrack = newValue
this.setCurrentTime(0)
this.$refs.audio.load()
}, 1000, {leading: true, trailing: true}),
playing: function (newValue) {
if (newValue === true) {
this.$refs.audio.play()
this.sound.play()
} else {
this.$refs.audio.pause()
}
},
'$store.state.queue.currentIndex' () {
if (this.$store.state.player.playing) {
this.$refs.audio.play()
this.sound.pause()
}
this.observeProgress(newValue)
},
volume: function (newValue) {
this.$refs.audio.volume = newValue
this.sound.volume(newValue)
},
currentTime (newValue) {
if (!this.isUpdatingTime) {

View File

@ -15,7 +15,7 @@
{{ track.title }}
</router-link>
</td>
<td colspan="6">
<td colspan="4">
<router-link v-if="track.artist.id === albumArtist.id" class="artist discrete link" :to="{name: 'library.artists.detail', params: {id: track.artist.id }}">
{{ track.artist.name }}
</router-link>
@ -29,11 +29,17 @@
</router-link>
</template>
</td>
<td colspan="6">
<td colspan="4">
<router-link class="album discrete link" :to="{name: 'library.albums.detail', params: {id: track.album.id }}">
{{ track.album.title }}
</router-link>
</td>
<td colspan="4" v-if="file && file.duration">
{{ time.parse(file.duration) }}
</td>
<td colspan="4" v-else>
<translate>N/A</translate>
</td>
<td>
<track-favorite-icon class="favorite-icon" :track="track"></track-favorite-icon>
<track-playlist-icon
@ -44,6 +50,8 @@
</template>
<script>
import time from '@/utils/time'
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon'
import PlayButton from '@/components/audio/PlayButton'
@ -59,6 +67,11 @@ export default {
TrackPlaylistIcon,
PlayButton
},
data () {
return {
time
}
},
computed: {
albumArtist () {
if (this.artist) {
@ -66,6 +79,9 @@ export default {
} else {
return this.track.album.artist
}
},
file () {
return this.track.files[0]
}
}
}

View File

@ -5,8 +5,9 @@
<th></th>
<th></th>
<th colspan="6"><translate>Title</translate></th>
<th colspan="6"><translate>Artist</translate></th>
<th colspan="6"><translate>Album</translate></th>
<th colspan="4"><translate>Artist</translate></th>
<th colspan="4"><translate>Album</translate></th>
<th colspan="4"><translate>Duration</translate></th>
<th></th>
</tr>
</thead>

View File

@ -10,9 +10,6 @@
<i @click="fetchData(url)" :class="['ui', 'circular', 'medium', 'refresh', 'icon']">
</i>
<div class="ui divided unstackable items">
<div v-if="isLoading" class="ui inverted active dimmer">
<div class="ui loader"></div>
</div>
<div class="item" v-for="object in objects" :key="object.id">
<div class="ui tiny image">
<img v-if="object.track.album.cover.original" v-lazy="$store.getters['instance/absoluteUrl'](object.track.album.cover.medium_square_crop)">
@ -45,6 +42,9 @@
</div>
</div>
</div>
<div v-if="isLoading" class="ui inverted active dimmer">
<div class="ui loader"></div>
</div>
</div>
</div>
</template>
@ -126,4 +126,7 @@ export default {
.refresh.icon {
float: right;
}
.ui.divided.items > .item:last-child {
padding-bottom: 1em !important;
}
</style>

View File

@ -16,10 +16,13 @@
<div class="ui basic green label">
<translate>This is you!</translate>
</div>
<div v-if="profile.is_staff" class="ui yellow label">
<a v-if="profile.is_staff"
class="ui yellow label"
:href="$store.getters['instance/absoluteUrl']('/api/admin')"
target="_blank">
<i class="star icon"></i>
<translate>Staff member</translate>
</div>
</a>
<router-link class="ui tiny basic button" :to="{path: '/settings'}">
<i class="setting icon"></i>
<translate>Settings...</translate>

View File

@ -1,6 +1,6 @@
<template>
<div>
<div v-if="isLoading" class="ui vertical segment" v-title="labels.title">
<div v-title="labels.title">
<div v-if="isLoading" class="ui vertical segment">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div>
<template v-if="artist">
@ -102,7 +102,7 @@ export default {
self.artist = response.data
self.isLoading = false
self.isLoadingAlbums = true
axios.get('albums/', {params: {artist: this.id, ordering: '-release_date'}}).then((response) => {
axios.get('albums/', {params: {artist: self.id, ordering: '-release_date'}}).then((response) => {
let parsed = JSON.parse(JSON.stringify(response.data.results))
self.albums = parsed.map((album) => {
return backend.Album.clean(album)
@ -158,7 +158,7 @@ export default {
})[0]
},
headerStyle () {
if (!this.cover.original) {
if (!this.cover || !this.cover.original) {
return ''
}
return 'background-image: url(' + this.$store.getters['instance/absoluteUrl'](this.cover.original) + ')'

View File

@ -54,7 +54,7 @@ export default {
}
&.with-background {
.header {
&, .sub, a {
&, .sub {
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.8);
color: white !important;
}

View File

@ -87,6 +87,17 @@
<translate>N/A</translate>
</td>
</tr>
<tr>
<td>
<translate>Type</translate>
</td>
<td v-if="file.mimetype">
{{ file.mimetype }}
</td>
<td v-else>
<translate>N/A</translate>
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -17,12 +17,16 @@
</template>
</div>
</div>
<div class="inline fields">
<div class="">
<div class="field">
<label for="name"><translate>Radio name</translate></label>
<input id="name" type="text" v-model="radioName" :placeholder="labels.placeholder" />
<input id="name" type="text" v-model="radioName" :placeholder="labels.placeholder.name" />
</div>
<div class="field">
<label for="description"><translate>Description</translate></label>
<textarea rows="2" id="description" type="text" v-model="radioDesc" :placeholder="labels.placeholder.description" />
</div>
<div class="inline field">
<input id="public" type="checkbox" v-model="isPublic" />
<label for="public"><translate>Display publicly</translate></label>
</div>
@ -113,6 +117,7 @@ export default {
filters: [],
checkResult: null,
radioName: '',
radioDesc: '',
isPublic: true
}
},
@ -164,6 +169,7 @@ export default {
}
})
self.radioName = response.data.name
self.radioDesc = response.data.description
self.isPublic = response.data.is_public
self.isLoading = false
})
@ -197,6 +203,7 @@ export default {
})
final = {
'name': this.radioName,
'description': this.radioDesc,
'is_public': this.isPublic,
'config': final
}
@ -224,7 +231,10 @@ export default {
computed: {
labels () {
let title = this.$gettext('Radio Builder')
let placeholder = this.$gettext('My awesome radio')
let placeholder = {
'name': this.$gettext('My awesome radio'),
'description': this.$gettext('My awesome description')
}
return {
title,
placeholder

View File

@ -57,8 +57,8 @@
<td>
<human-date :date="scope.obj.creation_date"></human-date>
</td>
<td v-if="scope.obj.audio_mimetype">
{{ scope.obj.audio_mimetype }}
<td v-if="scope.obj.mimetype">
{{ scope.obj.mimetype }}
</td>
<td v-else>
<translate>N/A</translate>

View File

@ -14,13 +14,14 @@
</div>
</div>
<div class="extra content">
<user-link :user="radio.user" class="left floated" />
<radio-button class="right floated button" :type="type" :custom-radio-id="customRadioId"></radio-button>
<router-link
class="ui basic yellow button"
v-if="$store.state.auth.authenticated && type === 'custom' && customRadio.user === $store.state.auth.profile.id"
class="ui basic yellow button right floated"
v-if="$store.state.auth.authenticated && type === 'custom' && radio.user.id === $store.state.auth.profile.id"
:to="{name: 'library.radios.edit', params: {id: customRadioId }}">
<translate>Edit...</translate>
</router-link>
<radio-button class="right floated button" :type="type" :custom-radio-id="customRadioId"></radio-button>
</div>
</div>
</template>

View File

@ -12,6 +12,14 @@ export default {
"code": "eo",
"label": "Esperanto"
},
{
"code": "de",
"label": "Deutsch"
},
{
"code": "es",
"label": "Español"
},
{
"code": "fr_FR",
"label": "Français"
@ -27,6 +35,10 @@ export default {
{
"code": "pl",
"label": "Polszczyzna"
},
{
"code": "pt_PT",
"label": "Português (Portugal)"
}
]
}

View File

@ -43,6 +43,10 @@ if (availableLanguages[store.state.ui.currentLanguage]) {
Vue.use(GetTextPlugin, {
availableLanguages: availableLanguages,
defaultLanguage: defaultLanguage,
// cf https://github.com/Polyconseil/vue-gettext#configuration
// not recommended but this is fixing weird bugs with translation nodes
// not being updated when in v-if/v-else clauses
autoAddKeyAttributes: true,
languageVmMixin: {
computed: {
currentKebabCase: function () {
@ -126,6 +130,8 @@ axios.interceptors.response.use(function (response) {
return Promise.reject(error)
})
store.dispatch('instance/fetchFrontSettings')
/* eslint-disable no-new */
new Vue({
el: '#app',

View File

@ -100,7 +100,7 @@ export default new Router({
name: 'signup',
component: Signup,
props: (route) => ({
invitation: route.query.invitation
defaultInvitation: route.query.invitation
})
},
{

View File

@ -56,13 +56,20 @@ export default {
fetch ({dispatch, state, commit, rootState}, url) {
// will fetch favorites by batches from API to have them locally
let params = {
user: rootState.auth.profile.id
user: rootState.auth.profile.id,
page_size: 50,
ordering: '-creation_date'
}
url = url || 'favorites/tracks/'
return axios.get(url, {params: params}).then((response) => {
let promise
if (url) {
promise = axios.get(url)
} else {
promise = axios.get('favorites/tracks/', {params: params})
}
return promise.then((response) => {
logger.default.info('Fetched a batch of ' + response.data.results.length + ' favorites')
response.data.results.forEach(result => {
commit('track', {id: result.track, value: true})
commit('track', {id: result.track.id, value: true})
})
if (response.data.next) {
dispatch('fetch', response.data.next)

View File

@ -2,10 +2,18 @@ import axios from 'axios'
import logger from '@/logging'
import _ from 'lodash'
function getDefaultUrl () {
return (
window.location.protocol + '//' + window.location.hostname +
(window.location.port ? ':' + window.location.port : '')
)
}
export default {
namespaced: true,
state: {
maxEvents: 200,
frontSettings: {},
instanceUrl: process.env.INSTANCE_URL,
events: [],
settings: {
@ -53,6 +61,9 @@ export default {
events: (state, value) => {
state.events = value
},
frontSettings: (state, value) => {
state.frontSettings = value
},
instanceUrl: (state, value) => {
if (value && !value.endsWith('/')) {
value = value + '/'
@ -67,6 +78,9 @@ export default {
}
},
getters: {
defaultUrl: (state) => () => {
return getDefaultUrl()
},
absoluteUrl: (state) => (relativeUrl) => {
if (relativeUrl.startsWith('http')) {
return relativeUrl
@ -74,7 +88,9 @@ export default {
if (state.instanceUrl.endsWith('/') && relativeUrl.startsWith('/')) {
relativeUrl = relativeUrl.slice(1)
}
return state.instanceUrl + relativeUrl
let instanceUrl = state.instanceUrl || getDefaultUrl()
return instanceUrl + relativeUrl
}
},
actions: {
@ -110,6 +126,13 @@ export default {
}, response => {
logger.default.error('Error while fetching settings', response.data)
})
},
fetchFrontSettings ({commit}) {
return axios.get('/settings.json').then(response => {
commit('frontSettings', response.data)
}, response => {
logger.default.error('Error when fetching front-end configuration (or no customization available)')
})
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -32,3 +32,12 @@ export default {
}
}
</script>
<style scoped>
.ui.menu .item > .label {
position: absolute;
right: -2em;
}
</style>

View File

@ -50,3 +50,12 @@ export default {
}
}
</script>
<style scoped>
.ui.menu .item > .label {
position: absolute;
right: -2em;
}
</style>

View File

@ -24,13 +24,13 @@
<play-button class="orange" :tracks="tracks"><translate>Play all</translate></play-button>
<button
class="ui icon button"
v-if="playlist.user.id === $store.state.auth.profile.id"
v-if="$store.state.auth.profile && playlist.user.id === $store.state.auth.profile.id"
@click="edit = !edit">
<i class="pencil icon"></i>
<template v-if="edit"><translate>End edition</translate></template>
<template v-else><translate>Edit...</translate></template>
</button>
<dangerous-button v-if="playlist.user.id === $store.state.auth.profile.id" class="labeled icon" :action="deletePlaylist">
<dangerous-button v-if="$store.state.auth.profile && playlist.user.id === $store.state.auth.profile.id" class="labeled icon" :action="deletePlaylist">
<i class="trash icon"></i> <translate>Delete</translate>
<p slot="modal-header">
<translate :translate-params="{playlist: playlist.name}">Do you want to delete the playlist "%{ playlist }"?</translate>

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 991 B

View File

@ -1,26 +0,0 @@
// A custom Nightwatch assertion.
// the name of the method is the filename.
// can be used in tests like this:
//
// browser.assert.elementCount(selector, count)
//
// for how to write custom assertions see
// http://nightwatchjs.org/guide#writing-custom-assertions
exports.assertion = function (selector, count) {
this.message = 'Testing if element <' + selector + '> has count: ' + count
this.expected = count
this.pass = function (val) {
return val === this.expected
}
this.value = function (res) {
return res.value
}
this.command = function (cb) {
var self = this
return this.api.execute(function (selector) {
return document.querySelectorAll(selector).length
}, [selector], function (res) {
cb.call(self, res)
})
}
}

View File

@ -1,46 +0,0 @@
require('babel-register')
var config = require('../../config')
// http://nightwatchjs.org/gettingstarted#settings-file
module.exports = {
src_folders: ['test/e2e/specs'],
output_folder: 'test/e2e/reports',
custom_assertions_path: ['test/e2e/custom-assertions'],
selenium: {
start_process: true,
server_path: require('selenium-server').path,
host: '127.0.0.1',
port: 4444,
cli_args: {
'webdriver.chrome.driver': require('chromedriver').path
}
},
test_settings: {
default: {
selenium_port: 4444,
selenium_host: 'localhost',
silent: true,
globals: {
devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port)
}
},
chrome: {
desiredCapabilities: {
browserName: 'chrome',
javascriptEnabled: true,
acceptSslCerts: true
}
},
firefox: {
desiredCapabilities: {
browserName: 'firefox',
javascriptEnabled: true,
acceptSslCerts: true
}
}
}
}

View File

@ -1,33 +0,0 @@
// 1. start the dev server using production config
process.env.NODE_ENV = 'testing'
var server = require('../../build/dev-server.js')
server.ready.then(() => {
// 2. run the nightwatch test suite against it
// to run in additional browsers:
// 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings"
// 2. add it to the --env flag below
// or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
// For more information on Nightwatch's config file, see
// http://nightwatchjs.org/guide#settings-file
var opts = process.argv.slice(2)
if (opts.indexOf('--config') === -1) {
opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js'])
}
if (opts.indexOf('--env') === -1) {
opts = opts.concat(['--env', 'chrome'])
}
var spawn = require('cross-spawn')
var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' })
runner.on('exit', function (code) {
server.close()
process.exit(code)
})
runner.on('error', function (err) {
server.close()
throw err
})
})

View File

@ -1,19 +0,0 @@
// For authoring Nightwatch tests, see
// http://nightwatchjs.org/guide#usage
module.exports = {
'default e2e tests': function (browser) {
// automatically uses dev Server port from /config.index.js
// default: http://localhost:8080
// see nightwatch.conf.js
const devServer = browser.globals.devServerURL
browser
.url(devServer)
.waitForElementVisible('#app', 5000)
.assert.elementPresent('.hello')
.assert.containsText('h1', 'Welcome to Your Vue.js App')
.assert.elementCount('img', 1)
.end()
}
}

View File

@ -1,13 +0,0 @@
import Vue from 'vue'
Vue.config.productionTip = false
// require all test files (files that ends with .spec.js)
const testsContext = require.context('./specs', true, /\.spec$/)
testsContext.keys().forEach(testsContext)
// require all src files except main.js for coverage.
// you can also change this to match only the subset of files that
// you want coverage for.
const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/)
srcContext.keys().forEach(srcContext)

View File

@ -1,38 +0,0 @@
// This is a karma config file. For more details see
// http://karma-runner.github.io/0.13/config/configuration-file.html
// we are also using it with karma-webpack
// https://github.com/webpack/karma-webpack
var webpackConfig = require('../../build/webpack.test.conf')
module.exports = function (config) {
config.set({
// to run in additional browsers:
// 1. install corresponding karma launcher
// http://karma-runner.github.io/0.13/config/browsers.html
// 2. add it to the `browsers` array below.
browsers: ['PhantomJS'],
frameworks: ['mocha', 'sinon-stub-promise', 'sinon-chai', 'phantomjs-shim'],
reporters: ['spec', 'coverage'],
files: [
'../../node_modules/es6-promise/dist/es6-promise.auto.js',
'./index.js'
],
preprocessors: {
'./index.js': ['webpack', 'sourcemap']
},
captureTimeout: 15000,
retryLimit: 1,
webpack: webpackConfig,
webpackMiddleware: {
noInfo: true
},
coverageReporter: {
dir: './coverage',
reporters: [
{ type: 'lcov', subdir: '.' },
{ type: 'text-summary' }
]
}
})
}

View File

@ -1,3 +1,5 @@
import {expect} from 'chai'
import Username from '@/components/common/Username.vue'
import { render } from '../../utils'

View File

@ -1,3 +1,5 @@
import {expect} from 'chai'
import {truncate, markdown, ago, capitalize, year} from '@/filters'
describe('filters', () => {

View File

@ -1,4 +1,6 @@
var sinon = require('sinon')
import {expect} from 'chai'
import moxios from 'moxios'
import store from '@/store/auth'
@ -8,7 +10,7 @@ describe('store/auth', () => {
var sandbox
beforeEach(function () {
sandbox = sinon.sandbox.create()
sandbox = sinon.createSandbox()
moxios.install()
})
afterEach(function () {
@ -84,7 +86,7 @@ describe('store/auth', () => {
})
})
describe('actions', () => {
it('logout', (done) => {
it('logout', () => {
testAction({
action: store.actions.logout,
params: {state: {}},
@ -96,18 +98,18 @@ describe('store/auth', () => {
{ type: 'queue/reset', payload: null, options: {root: true} },
{ type: 'radios/reset', payload: null, options: {root: true} }
]
}, done)
})
})
it('check jwt null', (done) => {
it('check jwt null', () => {
testAction({
action: store.actions.check,
params: {state: {}},
expectedMutations: [
{ type: 'authenticated', payload: false }
]
}, done)
})
})
it('check jwt set', (done) => {
it('check jwt set', () => {
testAction({
action: store.actions.check,
params: {state: {token: 'test', username: 'user'}},
@ -118,9 +120,9 @@ describe('store/auth', () => {
{ type: 'fetchProfile' },
{ type: 'refreshToken' }
]
}, done)
})
})
it('login success', (done) => {
it('login success', () => {
moxios.stubRequest('token/', {
status: 200,
response: {
@ -139,9 +141,9 @@ describe('store/auth', () => {
expectedActions: [
{ type: 'fetchProfile' }
]
}, done)
})
})
it('login error', (done) => {
it('login error', () => {
moxios.stubRequest('token/', {
status: 500,
response: {
@ -160,7 +162,7 @@ describe('store/auth', () => {
done()
})
})
it('fetchProfile', (done) => {
it('fetchProfile', () => {
const profile = {
username: 'bob',
permissions: {
@ -183,9 +185,9 @@ describe('store/auth', () => {
{ type: 'favorites/fetch', payload: null, options: {root: true} },
{ type: 'playlists/fetchOwn', payload: null, options: {root: true} }
]
}, done)
})
})
it('refreshToken', (done) => {
it('refreshToken', () => {
moxios.stubRequest('token/refresh/', {
status: 200,
response: {token: 'newtoken'}
@ -196,7 +198,7 @@ describe('store/auth', () => {
expectedMutations: [
{ type: 'token', payload: 'newtoken' }
]
}, done)
})
})
})
})

View File

@ -1,3 +1,5 @@
import {expect} from 'chai'
import store from '@/store/favorites'
import { testAction } from '../../utils'
@ -28,7 +30,7 @@ describe('store/favorites', () => {
})
})
describe('actions', () => {
it('toggle true', (done) => {
it('toggle true', () => {
testAction({
action: store.actions.toggle,
payload: 1,
@ -36,9 +38,9 @@ describe('store/favorites', () => {
expectedActions: [
{ type: 'set', payload: {id: 1, value: true} }
]
}, done)
})
})
it('toggle true', (done) => {
it('toggle true', () => {
testAction({
action: store.actions.toggle,
payload: 1,
@ -46,7 +48,7 @@ describe('store/favorites', () => {
expectedActions: [
{ type: 'set', payload: {id: 1, value: false} }
]
}, done)
})
})
})
})

View File

@ -1,3 +1,4 @@
import {expect} from 'chai'
var sinon = require('sinon')
import moxios from 'moxios'
import store from '@/store/instance'
@ -7,7 +8,7 @@ describe('store/instance', () => {
var sandbox
beforeEach(function () {
sandbox = sinon.sandbox.create()
sandbox = sinon.createSandbox()
moxios.install()
})
afterEach(function () {
@ -26,7 +27,7 @@ describe('store/instance', () => {
})
})
describe('actions', () => {
it('fetchSettings', (done) => {
it('fetchSettings', () => {
moxios.stubRequest('instance/settings/', {
status: 200,
response: [
@ -64,7 +65,7 @@ describe('store/instance', () => {
}
}
]
}, done)
})
})
})
})

View File

@ -1,3 +1,5 @@
import {expect} from 'chai'
import store from '@/store/player'
import { testAction } from '../../utils'
@ -100,7 +102,7 @@ describe('store/player', () => {
})
})
describe('actions', () => {
it('incrementVolume', (done) => {
it('incrementVolume', () => {
testAction({
action: store.actions.incrementVolume,
payload: 0.2,
@ -108,27 +110,27 @@ describe('store/player', () => {
expectedMutations: [
{ type: 'volume', payload: 0.7 + 0.2 }
]
}, done)
})
})
it('toggle play false', (done) => {
it('toggle play false', () => {
testAction({
action: store.actions.togglePlay,
params: {state: {playing: false}},
expectedMutations: [
{ type: 'playing', payload: true }
]
}, done)
})
})
it('toggle play true', (done) => {
it('toggle play true', () => {
testAction({
action: store.actions.togglePlay,
params: {state: {playing: true}},
expectedMutations: [
{ type: 'playing', payload: false }
]
}, done)
})
})
it('trackEnded', (done) => {
it('trackEnded', () => {
testAction({
action: store.actions.trackEnded,
payload: {test: 'track'},
@ -137,9 +139,9 @@ describe('store/player', () => {
{ type: 'trackListened', payload: {test: 'track'} },
{ type: 'queue/next', payload: null, options: {root: true} }
]
}, done)
})
})
it('trackEnded calls populateQueue if last', (done) => {
it('trackEnded calls populateQueue if last', () => {
testAction({
action: store.actions.trackEnded,
payload: {test: 'track'},
@ -149,9 +151,9 @@ describe('store/player', () => {
{ type: 'radios/populateQueue', payload: null, options: {root: true} },
{ type: 'queue/next', payload: null, options: {root: true} }
]
}, done)
})
})
it('trackErrored', (done) => {
it('trackErrored', () => {
testAction({
action: store.actions.trackErrored,
payload: {test: 'track'},
@ -163,16 +165,16 @@ describe('store/player', () => {
expectedActions: [
{ type: 'queue/next', payload: null, options: {root: true} }
]
}, done)
})
})
it('updateProgress', (done) => {
it('updateProgress', () => {
testAction({
action: store.actions.updateProgress,
payload: 1,
expectedMutations: [
{ type: 'currentTime', payload: 1 }
]
}, done)
})
})
})
})

View File

@ -1,3 +1,4 @@
import {expect} from 'chai'
var sinon = require('sinon')
import moxios from 'moxios'
import store from '@/store/playlists'
@ -8,7 +9,7 @@ describe('store/playlists', () => {
var sandbox
beforeEach(function () {
sandbox = sinon.sandbox.create()
sandbox = sinon.createSandbox()
moxios.install()
})
afterEach(function () {
@ -24,13 +25,13 @@ describe('store/playlists', () => {
})
})
describe('actions', () => {
it('fetchOwn does nothing with no user', (done) => {
it('fetchOwn does nothing with no user', () => {
testAction({
action: store.actions.fetchOwn,
payload: null,
params: {state: { playlists: [] }, rootState: {auth: {profile: {}}}},
expectedMutations: []
}, done)
})
})
})
})

Some files were not shown because too many files have changed in this diff Show More