Merge branch 'master' into develop

merge-requests/757/head
Eliot Berriot 2019-03-20 20:45:06 +01:00
commit 9a162c57ca
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: DD6965E2476E5C27
22 zmienionych plików z 495 dodań i 43 usunięć

Wyświetl plik

@ -358,6 +358,344 @@ Internationalization
--------------------
We're using https://github.com/Polyconseil/vue-gettext to manage i18n in the project.
<<<<<<< HEAD
When working on the front-end, any end-user string should be marked as a translatable string,
with the proper context, as described below.
Translations in HTML
^^^^^^^^^^^^^^^^^^^^
Translations in HTML use the ``<translate>`` tag::
<template>
<div>
<h1><translate translate-context="Content/Profile/Header">User profile</translate></h1>
<p>
<translate
translate-context="Content/Profile/Paragraph"
:translate-params="{username: 'alice'}">
You are logged in as %{ username }
</translate>
</p>
<p>
<translate
translate-context="Content/Profile/Paragraph"
translate-plural="You have %{ count } new messages, that's a lot!"
:translate-n="unreadMessagesCount"
:translate-params="{count: unreadMessagesCount}">
You have 1 new message
</translate>
</p>
</div>
</template>
Anything between the `<translate>` and `</translate>` delimiters will be considered as a translatable string.
You can use variables in the translated string via the ``:translate-params="{var: 'value'}"`` directive, and reference them like this:
``val value is %{ value }``.
For pluralization, you need to use ``translate-params`` in conjunction with ``translate-plural`` and ``translate-n``:
- ``translate-params`` should contain the variable you're using for pluralization (which is usually shown to the user)
- ``translate-n`` should match the same variable
- The ``<translate>`` delimiters contain the non-pluralized version of your string
- The ``translate-plural`` directive contains the pluralized version of your string
Translations in javascript
^^^^^^^^^^^^^^^^^^^^^^^^^^
Translations in javascript work by calling the ``this.$*gettext`` functions::
export default {
computed: {
strings () {
let tracksCount = 42
let playButton = this.$pgettext('Sidebar/Player/Button/Verb, Short', 'Play')
let loginMessage = this.$pgettext('*/Login/Message', 'Welcome back %{ username }')
let addedMessage = this.$npgettext('*/Player/Message', 'One track was queued', '%{ count } tracks were queued', tracksCount)
console.log(this.$gettextInterpolate(addedMessage, {count: tracksCount}))
console.log(this.$gettextInterpolate(loginMessage, {username: 'alice'}))
}
}
}
The first argument of the ``$pgettext`` and ``$npgettext`` functions is the string context.
Contextualization
^^^^^^^^^^^^^^^^^
Translation contexts provided via the ``translate-context`` directive and the ``$pgettext`` and ``$npgettext`` are never shown to end users
but visible by Funkwhale translators. They help translators where and how the strings are used,
especially with short or ambiguous strings, like ``May``, which can refer a month or a verb.
While we could in theory use free form context, like ``This string is inside a button, in the main page, and is a call to action``,
Funkwhale use a hierarchical structure to write contexts and keep them short and consistents accross the app. The previous context,
rewritten correctly would be: ``Content/Home/Button/Call to action``.
This hierarchical structure is made of several parts:
- The location part, which is required and refers to the big blocks found in Funkwhale UI where the translated string is displayed:
- ``Content``
- ``Footer``
- ``Head``
- ``Menu``
- ``Popup``
- ``Sidebar``
- ``*`` for strings that are not tied to a specific location
- The feature part, which is required, and refers to the feature associated with the translated string:
- ``About``
- ``Admin``
- ``Album``
- ``Artist``
- ``Embed``
- ``Home``
- ``Login``
- ``Library``
- ``Moderation``
- ``Player``
- ``Playlist``
- ``Profile``
- ``Favorites``
- ``Notifications``
- ``Radio``
- ``Search``
- ``Settings``
- ``Signup``
- ``Track``
- ``Queue``
- ``*`` for strings that are not tied to a specific feature
- The component part, which is required and refers to the type of element that contain the string:
- ``Button``
- ``Card``
- ``Checkbox``
- ``Dropdown``
- ``Error message``
- ``Form``
- ``Header``
- ``Help text``
- ``Hidden text``
- ``Icon``
- ``Input``
- ``Image``
- ``Label``
- ``Link``
- ``List item``
- ``Menu``
- ``Message``
- ``Paragraph``
- ``Placeholder``
- ``Tab``
- ``Table``
- ``Title``
- ``Tooltip``
- ``*`` for strings that are not tied to a specific component
The detail part, which is optional and refers to the contents of the string itself, such as:
- ``Adjective``
- ``Call to action``
- ``Noun``
- ``Short``
- ``Unit``
- ``Verb``
Here are a few examples of valid context hierarchies:
- ``Sidebar/Player/Button``
- ``Content/Home/Button/Call to action``
- ``Footer/*/Help text``
- ``*/*/*/Verb, Short``
- ``Popup/Playlist/Button``
- ``Content/Admin/Table.Label/Short, Noun (Value is a date)``
It's possible to nest multiple component parts to reach a higher level of detail. The component parts are then separated by a dot:
- ``Sidebar/Queue/Tab.Title``
- ``Content/*/Button.Title``
- ``Content/*/Table.Header``
- ``Footer/*/List item.Link``
- ``Content/*/Form.Help text``
Collecting translatable strings
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If you want to ensure your translatable strings are correctly marked for translation,
you can try to extract them.
||||||| merged common ancestors
When working on the front-end, any end-user string should be translated
using either ``<translate>yourstring</translate>`` or ``$gettext('yourstring')``
function.
=======
<<<<<<< HEAD
When working on the front-end, any end-user string should be translated
using either ``<translate>yourstring</translate>`` or ``$gettext('yourstring')``
function.
||||||| parent of 21fb39dd... Update docs/developers/index.rst, docs/developers/subsonic.rst files
When working on the front-end, any end-user string should be marked as a translatable string,
with the proper context, as described below.
Translations in HTML
^^^^^^^^^^^^^^^^^^^^
Translations in HTML use the ``<translate>`` tag::
<template>
<div>
<h1><translate translate-context="Content/Profile/Header">User profile</translate></h1>
<p>
<translate
translate-context="Content/Profile/Paragraph"
:translate-params="{username: 'alice'}">
You are logged in as %{ username }
</translate>
</p>
<p>
<translate
translate-context="Content/Profile/Paragraph"
translate-plural="You have %{ count } new messages, that's a lot!"
:translate-n="unreadMessagesCount"
:translate-params="{count: unreadMessagesCount}">
You have 1 new message
</translate>
</p>
</div>
</template>
Anything between the `<translate>` and `</translate>` delimiters will be considered as a translatable string.
You can use variables in the translated string via the ``:translate-params="{var: 'value'}"`` directive, and reference them like this:
``val value is %{ value }``.
For pluralization, you need to use ``translate-params`` in conjunction with ``translate-plural`` and ``translate-n``:
- ``translate-params`` should contain the variable you're using for pluralization (which is usually shown to the user)
- ``translate-n`` should match the same variable
- The ``<translate>`` delimiters contain the non-pluralized version of your string
- The ``translate-plural`` directive contains the pluralized version of your string
Translations in javascript
^^^^^^^^^^^^^^^^^^^^^^^^^^
Translations in javascript work by calling the ``this.$*gettext`` functions::
export default {
computed: {
strings () {
let tracksCount = 42
let playButton = this.$pgettext('Sidebar/Player/Button/Verb, Short', 'Play')
let loginMessage = this.$pgettext('*/Login/Message', 'Welcome back %{ username }')
let addedMessage = this.$npgettext('*/Player/Message', 'One track was queued', '%{ count } tracks were queued', tracksCount)
console.log(this.$gettextInterpolate(addedMessage, {count: tracksCount}))
console.log(this.$gettextInterpolate(loginMessage, {username: 'alice'}))
}
}
}
The first argument of the ``$pgettext`` and ``$npgettext`` functions is the string context.
Contextualization
^^^^^^^^^^^^^^^^^
Translation contexts provided via the ``translate-context`` directive and the ``$pgettext`` and ``$npgettext`` are never shown to end users
but visible by Funkwhale translators. They help translators where and how the strings are used,
especially with short or ambiguous strings, like ``May``, which can refer a month or a verb.
While we could in theory use free form context, like ``This string is inside a button, in the main page, and is a call to action``,
Funkwhale use a hierarchical structure to write contexts and keep them short and consistents accross the app. The previous context,
rewritten correctly would be: ``Content/Home/Button/Call to action``.
This hierarchical structure is made of several parts:
- The location part, which is required and refers to the big blocks found in Funkwhale UI where the translated string is displayed:
- ``Content``
- ``Footer``
- ``Head``
- ``Menu``
- ``Popup``
- ``Sidebar``
- ``*`` for strings that are not tied to a specific location
- The feature part, which is required, and refers to the feature associated with the translated string:
- ``About``
- ``Admin``
- ``Album``
- ``Artist``
- ``Embed``
- ``Home``
- ``Login``
- ``Library``
- ``Moderation``
- ``Player``
- ``Playlist``
- ``Profile``
- ``Favorites``
- ``Notifications``
- ``Radio``
- ``Search``
- ``Settings``
- ``Signup``
- ``Track``
- ``Queue``
- ``*`` for strings that are not tied to a specific feature
- The component part, which is required and refers to the type of element that contain the string:
- ``Button``
- ``Card``
- ``Checkbox``
- ``Dropdown``
- ``Error message``
- ``Form``
- ``Header``
- ``Help text``
- ``Hidden text``
- ``Icon``
- ``Input``
- ``Image``
- ``Label``
- ``Link``
- ``List item``
- ``Menu``
- ``Message``
- ``Paragraph``
- ``Placeholder``
- ``Tab``
- ``Table``
- ``Title``
- ``Tooltip``
- ``*`` for strings that are not tied to a specific component
The detail part, which is optional and refers to the contents of the string itself, such as:
- ``Adjective``
- ``Call to action``
- ``Noun``
- ``Short``
- ``Unit``
- ``Verb``
Here are a few examples of valid context hierarchies:
- ``Sidebar/Player/Button``
- ``Content/Home/Button/Call to action``
- ``Footer/*/Help text``
- ``*/*/*/Verb, Short``
- ``Popup/Playlist/Button``
- ``Content/Admin/Table.Label/Short, Noun (Value is a date)``
It's possible to nest multiple component parts to reach a higher level of detail. The component parts are then separated by a dot:
- ``Sidebar/Queue/Tab.Title``
- ``Content/*/Button.Title``
- ``Content/*/Table.Header``
- ``Footer/*/List item.Link``
- ``Content/*/Form.Help text``
Collecting translatable strings
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If you want to ensure your translatable strings are correctly marked for translation,
you can try to extract them.
When working on the front-end, any end-user string should be marked as a translatable string,
with the proper context, as described below.

Wyświetl plik

@ -33,8 +33,8 @@ class DomainAdmin(admin.ModelAdmin):
@admin.register(models.Activity)
class ActivityAdmin(admin.ModelAdmin):
list_display = ["type", "fid", "url", "actor", "creation_date"]
search_fields = ["payload", "fid", "url", "actor__domain"]
list_filter = ["type", "actor__domain"]
search_fields = ["payload", "fid", "url", "actor__domain__name"]
list_filter = ["type", "actor__domain__name"]
actions = [redeliver_activities]
list_select_related = True
@ -49,7 +49,7 @@ class ActorAdmin(admin.ModelAdmin):
"creation_date",
"last_fetch_date",
]
search_fields = ["fid", "domain", "preferred_username"]
search_fields = ["fid", "domain__name", "preferred_username"]
list_filter = ["type"]

Wyświetl plik

@ -859,7 +859,7 @@ class TrackSerializer(MusicEntitySerializer):
from_activity = self.context.get("activity")
if from_activity:
metadata["from_activity_id"] = from_activity.pk
track = music_tasks.get_track_from_import_metadata(metadata)
track = music_tasks.get_track_from_import_metadata(metadata, update_cover=True)
return track

Wyświetl plik

@ -26,7 +26,9 @@ from . import serializers
logger = logging.getLogger(__name__)
def update_album_cover(album, source=None, cover_data=None, replace=False):
def update_album_cover(
album, source=None, cover_data=None, musicbrainz=True, replace=False
):
if album.cover and not replace:
return
if cover_data:
@ -39,7 +41,7 @@ def update_album_cover(album, source=None, cover_data=None, replace=False):
cover = get_cover_from_fs(path)
if cover:
return album.get_image(data=cover)
if album.mbid:
if musicbrainz and album.mbid:
try:
logger.info(
"[Album %s] Fetching cover from musicbrainz release %s",
@ -179,8 +181,8 @@ def process_upload(upload):
import_metadata = upload.import_metadata or {}
old_status = upload.import_status
audio_file = upload.get_audio_file()
additional_data = {}
try:
additional_data = {}
if not audio_file:
# we can only rely on user proveded data
final_metadata = import_metadata
@ -241,6 +243,15 @@ def process_upload(upload):
"bitrate",
]
)
# update album cover, if needed
if not track.album.cover:
update_album_cover(
track.album,
source=final_metadata.get("upload_source"),
cover_data=final_metadata.get("cover_data"),
)
broadcast = getter(
import_metadata, "funkwhale", "config", "broadcast", default=True
)
@ -369,7 +380,18 @@ def sort_candidates(candidates, important_fields):
@transaction.atomic
def get_track_from_import_metadata(data):
def get_track_from_import_metadata(data, update_cover=False):
track = _get_track(data)
if update_cover and track and not track.album.cover:
update_album_cover(
track.album,
source=data.get("upload_source"),
cover_data=data.get("cover_data"),
)
return track
def _get_track(data):
track_uuid = getter(data, "funkwhale", "track", "uuid")
if track_uuid:
@ -380,12 +402,6 @@ def get_track_from_import_metadata(data):
except models.Track.DoesNotExist:
raise UploadImportError(code="track_uuid_not_found")
if not track.album.cover:
update_album_cover(
track.album,
source=data.get("upload_source"),
cover_data=data.get("cover_data"),
)
return track
from_activity_id = data.get("from_activity_id", None)
@ -479,10 +495,6 @@ def get_track_from_import_metadata(data):
album = get_best_candidate_or_create(
models.Album, query, defaults=defaults, sort_fields=["mbid", "fid"]
)[0]
if not album.cover:
update_album_cover(
album, source=data.get("upload_source"), cover_data=data.get("cover_data")
)
# get / create track
track_title = data["title"]

Wyświetl plik

@ -70,6 +70,7 @@ def get_track_data(album, track, upload):
"album": album.title,
"artist": album.artist.name,
"track": track.position or 1,
"discNumber": track.disc_number or 1,
"contentType": upload.mimetype,
"suffix": upload.extension or "",
"duration": upload.duration or 0,

Wyświetl plik

@ -153,7 +153,7 @@ def test_can_create_track_from_file_metadata_federation(factories, mocker, r_moc
r_mock.get(metadata["cover_data"]["url"], body=io.BytesIO(b"coucou"))
mocker.patch("funkwhale_api.music.metadata.Metadata.all", return_value=metadata)
track = tasks.get_track_from_import_metadata(metadata)
track = tasks.get_track_from_import_metadata(metadata, update_cover=True)
assert track.title == metadata["title"]
assert track.fid == metadata["fid"]
@ -183,7 +183,9 @@ def test_sort_candidates(factories):
def test_upload_import(now, factories, temp_signal, mocker):
outbox = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
track = factories["music.Track"]()
update_album_cover = mocker.patch("funkwhale_api.music.tasks.update_album_cover")
get_picture = mocker.patch("funkwhale_api.music.metadata.Metadata.get_picture")
track = factories["music.Track"](album__cover="")
upload = factories["music.Upload"](
track=None, import_metadata={"funkwhale": {"track": {"uuid": str(track.uuid)}}}
)
@ -196,6 +198,10 @@ def test_upload_import(now, factories, temp_signal, mocker):
assert upload.track == track
assert upload.import_status == "finished"
assert upload.import_date == now
get_picture.assert_called_once_with("cover_front", "other")
update_album_cover.assert_called_once_with(
upload.track.album, cover_data=get_picture.return_value, source=upload.source
)
handler.assert_called_once_with(
upload=upload,
old_status="pending",

Wyświetl plik

@ -64,7 +64,7 @@ def test_get_artist_serializer(factories):
def test_get_album_serializer(factories):
artist = factories["music.Artist"]()
album = factories["music.Album"](artist=artist)
track = factories["music.Track"](album=album)
track = factories["music.Track"](album=album, disc_number=42)
upload = factories["music.Upload"](track=track, bitrate=42000, duration=43, size=44)
expected = {
@ -85,6 +85,7 @@ def test_get_album_serializer(factories):
"album": album.title,
"artist": artist.name,
"track": track.position,
"discNumber": track.disc_number,
"year": track.album.release_date.year,
"contentType": upload.mimetype,
"suffix": upload.extension or "",

Wyświetl plik

@ -0,0 +1 @@
Ensure cover art from uploaded files is picked up properly on existing albums (#757)

Wyświetl plik

@ -0,0 +1 @@
Fixed broken sample apache configuration (#764)

Wyświetl plik

@ -0,0 +1 @@
Include disc number in Subsonic responses (#765)

Wyświetl plik

@ -0,0 +1 @@
Added title on hover for truncated content (#766)

Wyświetl plik

@ -0,0 +1 @@
Fixed broken Activity and Actor modules in django admin (#767)

Wyświetl plik

@ -65,7 +65,9 @@ Define MUSIC_DIRECTORY_PATH /srv/funkwhale/data/music
</Proxy>
# Activating WebSockets
ProxyPass "/api/v1/activity" ${funkwhale-api-ws}/api/v1/activity
<Location "/api/v1/activity">
ProxyPass ${funkwhale-api-ws}/api/v1/activity
</Location>
<Location "/">
# similar to nginx 'client_max_body_size 100M;'
@ -90,13 +92,19 @@ Define MUSIC_DIRECTORY_PATH /srv/funkwhale/data/music
ProxyPassReverse ${funkwhale-api}/.well-known/
</Location>
ProxyPass "/front" "!"
<Location "/front">
ProxyPass "!"
</Location>
Alias /front /srv/funkwhale/front/dist
ProxyPass "/media" "!"
<Location "/media">
ProxyPass "!"
</Location>
Alias /media /srv/funkwhale/data/media
ProxyPass "/staticfiles" "!"
<Location "/staticfiles">
ProxyPass "!"
</Location>
Alias /staticfiles /srv/funkwhale/data/static
# Setting appropriate access levels to serve frontend

Wyświetl plik

@ -52,7 +52,7 @@ services:
command: python /app/manage.py runserver 0.0.0.0:${FUNKWHALE_API_PORT-5000}
volumes:
- ./api:/app
- "${MUSIC_DIRECTORY_PATH-./data/music}:/music:ro"
- "${MUSIC_DIRECTORY_SERVE_PATH-./data/music}:/music:ro"
environment:
- "FUNKWHALE_HOSTNAME=${FUNKWHALE_HOSTNAME-localhost}"
- "FUNKWHALE_HOSTNAME_SUFFIX=funkwhale.test"
@ -87,7 +87,7 @@ services:
- "CACHE_URL=redis://redis:6379/0"
volumes:
- ./api:/app
- "${MUSIC_DIRECTORY_PATH-./data/music}:/music:ro"
- "${MUSIC_DIRECTORY_SERVE_PATH-./data/music}:/music:ro"
networks:
- internal
nginx:
@ -112,7 +112,7 @@ services:
volumes:
- ./docker/nginx/conf.dev:/etc/nginx/nginx.conf.template:ro
- ./docker/nginx/entrypoint.sh:/entrypoint.sh:ro
- "${MUSIC_DIRECTORY_PATH-./data/music}:/music:ro"
- "${MUSIC_DIRECTORY_SERVE_PATH-./data/music}:/music:ro"
- ./deploy/funkwhale_proxy.conf:/etc/nginx/funkwhale_proxy.conf:ro
- "${MEDIA_ROOT-./api/funkwhale_api/media}:/protected/media:ro"
networks:

79
docs/backup.rst 100644
Wyświetl plik

@ -0,0 +1,79 @@
Backup your Funkwhale instance
==============================
.. note::
Before upgrading your instance, we strongly advise you to make at least a database backup. Ideally, you should make a full backup, including the database and the media files.
Docker setup
------------
If you've followed the setup instructions in :doc:`../installation/docker`, here is the backup path:
Multi-container installation
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Backup the db
^^^^^^^^^^^^^
On docker setups, you have to ``pg_dumpall`` in container ``funkwhale_postgres_1``:
.. code-block:: shell
docker exec -t funkwhale_postgres_1 pg_dumpall -c -U postgres > dump_`date +%d-%m-%Y"_"%H_%M_%S`.sql
Backup the media files
^^^^^^^^^^^^^^^^^^^^^^
To backup docker data volumes, as the volumes are bound mounted to the host, the ``rsync`` way would go like this:
.. code-block:: shell
rsync -avzhP /srv/funkwhale/data/media /path/to/your/backup/media
rsync -avzhP /srv/funkwhale/data/music /path/to/your/backup/music
Backup the configuration files
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
On docker setups, the configuration file is located at the root level:
.. code-block:: shell
rsync -avzhP /srv/funkwhale/.env /path/to/your/backup/.env
Non-docker setup
----------------
Backup the db
^^^^^^^^^^^^^
On non-docker setups, you have to ``pg_dump`` as user ``postgres``:
.. code-block:: shell
sudo -u postgres -H pg_dump funkwhale > /path/to/your/backup/dump_`date +%d-%m-%Y"_"%H_%M_%S`.sql
Backup the media files
^^^^^^^^^^^^^^^^^^^^^^
A simple way to backup your media files is to use ``rsync``:
.. code-block:: shell
rsync -avzhP /srv/funkwhale/data/media /path/to/your/backup/media
rsync -avzhP /srv/funkwhale/data/music /path/to/your/backup/music
Backup the configuration files
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: shell
rsync -avzhP /srv/funkwhale/config/.env /path/to/your/backup/.env
.. note::
You may also want to backup your proxy configuration file.
For frequent backups, you may want to use deduplication and compression to keep the backup size low. In this case, a tool like ``borg`` will be more appropriate.

Wyświetl plik

@ -34,5 +34,5 @@ Troubleshooting Issues
.. toctree::
:maxdepth: 2
troubleshooting

Wyświetl plik

@ -1,5 +1,5 @@
Uploading and removing content on Funkwhale
===========================================
Uploading Content To Funkwhale
==============================
To upload content to any Funkwhale instance, you need:
@ -149,7 +149,7 @@ can vary depending on server load.
Removing files
--------------
If you want to remove some of the files you have uploaded, visit ``/content/libraries/tracks/`` or click "Add content" in the sidebar then "Tracks" in the top menu.
If you want to remove some of the files you have uploaded, visit ``/content/libraries/tracks/`` or click "Add content" in the sidebar then "Tracks" in the top menu.
Then select the files you want to delete using the checkboxes on the left ; you can filter the list of files using a search pattern.
Finally, select "Delete" in the "Action" menu and click "Go".

Wyświetl plik

@ -146,9 +146,11 @@
<img class="ui mini image" v-else src="../assets/audio/default-cover.png">
</td>
<td colspan="4">
<button class="title reset ellipsis" :aria-label="labels.selectTrack">
<button class="title reset ellipsis" :title="track.title" :aria-label="labels.selectTrack">
<strong>{{ track.title }}</strong><br />
{{ track.artist.name }}
<span>
{{ track.artist.name }}
</span>
</button>
</td>
<td>

Wyświetl plik

@ -10,7 +10,7 @@
</div>
<div class="meta">
<span>
<router-link tag="span" :to="{name: 'library.artists.detail', params: {id: album.artist.id }}">
<router-link :title="album.artist.name" tag="span" :to="{name: 'library.artists.detail', params: {id: album.artist.id }}">
<span v-translate="{artist: album.artist.name}" translate-context="Content/Album/Card" :translate-params="{artist: album.artist.name}">By %{ artist }</span>
</router-link>
</span><span class="time" v-if="album.release_date"> {{ album.release_date | year }}</span>
@ -24,7 +24,7 @@
</td>
<td class="content-cell" colspan="5">
<track-favorite-icon :track="track"></track-favorite-icon>
<router-link class="track discrete link" :to="{name: 'library.tracks.detail', params: {id: track.id }}">
<router-link :title="track.title" class="track discrete link" :to="{name: 'library.tracks.detail', params: {id: track.id }}">
<template v-if="track.position">
{{ track.position }}.
</template>

Wyświetl plik

@ -15,7 +15,7 @@
<img class="ui mini image" v-else src="../../../assets/audio/default-cover.png">
</td>
<td colspan="4">
<router-link class="discrete link" :to="{name: 'library.albums.detail', params: {id: album.id }}">
<router-link :title="album.title" class="discrete link" :to="{name: 'library.albums.detail', params: {id: album.id }}">
<strong>{{ album.title }}</strong>
</router-link><br />
{{ album.tracks_count }} tracks

Wyświetl plik

@ -8,7 +8,7 @@
<img class="ui mini image" v-else src="../../../assets/audio/default-cover.png">
</td>
<td colspan="6">
<router-link class="track" :to="{name: 'library.tracks.detail', params: {id: track.id }}">
<router-link class="track" :title="track.title" :to="{name: 'library.tracks.detail', params: {id: track.id }}">
<template v-if="displayPosition && track.position">
{{ track.position }}.
</template>
@ -16,21 +16,21 @@
</router-link>
</td>
<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 }}">
<router-link v-if="track.artist.id === albumArtist.id" :title="track.artist.name" class="artist discrete link" :to="{name: 'library.artists.detail', params: {id: track.artist.id }}">
{{ track.artist.name }}
</router-link>
<template v-else>
<router-link class="artist discrete link" :to="{name: 'library.artists.detail', params: {id: albumArtist.id }}">
<router-link class="artist discrete link" :title="albumArtist.name" :to="{name: 'library.artists.detail', params: {id: albumArtist.id }}">
{{ albumArtist.name }}
</router-link>
/
<router-link class="artist discrete link" :to="{name: 'library.artists.detail', params: {id: track.artist.id }}">
<router-link class="artist discrete link" :title="track.artist.name" :to="{name: 'library.artists.detail', params: {id: track.artist.id }}">
{{ track.artist.name }}
</router-link>
</template>
</td>
<td colspan="4">
<router-link class="album discrete link" :to="{name: 'library.albums.detail', params: {id: track.album.id }}">
<router-link class="album discrete link" :title="track.album.title" :to="{name: 'library.albums.detail', params: {id: track.album.id }}">
{{ track.album.title }}
</router-link>
</td>

Wyświetl plik

@ -43,7 +43,7 @@
class="ui icon basic small button"
:to="{name: 'library.playlists.detail', params: {id: playlist.id }, query: {mode: 'edit'}}"><i class="ui pencil icon"></i></router-link>
</td>
<td>
<td :title="playlist.name">
<router-link :to="{name: 'library.playlists.detail', params: {id: playlist.id }}">{{ playlist.name }}</router-link></td>
<td><human-date :date="playlist.modification_date"></human-date></td>
<td>{{ playlist.tracks_count }}</td>