Lint the frontend code
This commit is contained in:
parent
869fc20536
commit
8ee9a536e1
|
@ -136,10 +136,7 @@ eslint:
|
|||
- cd front
|
||||
- yarn install
|
||||
script:
|
||||
# We search for all files ending with .vue or .js in src which changed in relation to develop
|
||||
# and lint them. This way we focus on some errors instead of checking the hole repository
|
||||
- export changedFiles=$(git diff --relative --name-only --diff-filter=d origin/develop -- src/ | grep -E "\.(vue|js)$")
|
||||
- yarn run eslint --quiet -f table $(echo $changedFiles | tr '\n' ' ')
|
||||
- yarn lint
|
||||
cache:
|
||||
key: "$CI_PROJECT_ID__eslint_npm_cache"
|
||||
paths:
|
||||
|
|
|
@ -20,5 +20,7 @@ module.exports = {
|
|||
'vue'
|
||||
],
|
||||
rules: {
|
||||
"vue/no-v-html": "off", // TODO: tackle this properly
|
||||
"vue/no-use-v-if-with-v-for": "off"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"serve": "[ ! -d src/translations ] && npm run i18n-compile; vue-cli-service serve --port ${VUE_PORT:-8080} --host ${VUE_HOST:-0.0.0.0}",
|
||||
"build": "scripts/i18n-compile.sh && vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit --reporter mocha-junit-reporter",
|
||||
"lint": "eslint $(git status --porcelain --untracked-files=no | grep -E '(A|M) ' | cut -d' ' -f3 | sed s_front/__ | grep -E '.(js|vue)$')",
|
||||
"lint": "eslint --ext .js,.vue src",
|
||||
"i18n-compile": "scripts/i18n-compile.sh",
|
||||
"i18n-extract": "scripts/i18n-extract.sh",
|
||||
"fix-fomantic-css": "scripts/fix-fomantic-css.sh",
|
||||
|
|
|
@ -2,57 +2,110 @@
|
|||
<template>
|
||||
<main :class="[theme]">
|
||||
<!-- SVG from https://cdn.plyr.io/3.4.7/plyr.svg -->
|
||||
<svg aria-hidden="true" style="display: none" xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="plyr-download"><path d="M9 13c.3 0 .5-.1.7-.3L15.4 7 14 5.6l-4 4V1H8v8.6l-4-4L2.6 7l5.7 5.7c.2.2.4.3.7.3zM2 15h14v2H2z"/></symbol>
|
||||
<symbol id="plyr-enter-fullscreen"><path d="M10 3h3.6l-4 4L11 8.4l4-4V8h2V1h-7zM7 9.6l-4 4V10H1v7h7v-2H4.4l4-4z"/></symbol>
|
||||
<symbol id="plyr-exit-fullscreen"><path d="M1 12h3.6l-4 4L2 17.4l4-4V17h2v-7H1zM16 .6l-4 4V1h-2v7h7V6h-3.6l4-4z"/></symbol>
|
||||
<symbol id="plyr-fast-forward"><path d="M7.875 7.171L0 1v16l7.875-6.171V17L18 9 7.875 1z"/></symbol>
|
||||
<symbol id="plyr-muted"><path d="M12.4 12.5l2.1-2.1 2.1 2.1 1.4-1.4L15.9 9 18 6.9l-1.4-1.4-2.1 2.1-2.1-2.1L11 6.9 13.1 9 11 11.1zM3.786 6.008H.714C.286 6.008 0 6.31 0 6.76v4.512c0 .452.286.752.714.752h3.072l4.071 3.858c.5.3 1.143 0 1.143-.602V2.752c0-.601-.643-.977-1.143-.601L3.786 6.008z"/></symbol>
|
||||
<symbol id="plyr-pause"><path d="M6 1H3c-.6 0-1 .4-1 1v14c0 .6.4 1 1 1h3c.6 0 1-.4 1-1V2c0-.6-.4-1-1-1zM12 1c-.6 0-1 .4-1 1v14c0 .6.4 1 1 1h3c.6 0 1-.4 1-1V2c0-.6-.4-1-1-1h-3z"/></symbol>
|
||||
<symbol id="plyr-pip"><path d="M13.293 3.293L7.022 9.564l1.414 1.414 6.271-6.271L17 7V1h-6z"/><path d="M13 15H3V5h5V3H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-6h-2v5z"/></symbol>
|
||||
<symbol id="plyr-play"><path d="M15.562 8.1L3.87.225C3.052-.337 2 .225 2 1.125v15.75c0 .9 1.052 1.462 1.87.9L15.563 9.9c.584-.45.584-1.35 0-1.8z"/></symbol>
|
||||
<symbol id="plyr-restart"><path d="M9.7 1.2l.7 6.4 2.1-2.1c1.9 1.9 1.9 5.1 0 7-.9 1-2.2 1.5-3.5 1.5-1.3 0-2.6-.5-3.5-1.5-1.9-1.9-1.9-5.1 0-7 .6-.6 1.4-1.1 2.3-1.3l-.6-1.9C6 2.6 4.9 3.2 4 4.1 1.3 6.8 1.3 11.2 4 14c1.3 1.3 3.1 2 4.9 2 1.9 0 3.6-.7 4.9-2 2.7-2.7 2.7-7.1 0-9.9L16 1.9l-6.3-.7z"/></symbol>
|
||||
<symbol id="plyr-rewind"><path d="M10.125 1L0 9l10.125 8v-6.171L18 17V1l-7.875 6.171z"/></symbol>
|
||||
<symbol id="plyr-settings"><path d="M16.135 7.784a2 2 0 0 1-1.23-2.969c.322-.536.225-.998-.094-1.316l-.31-.31c-.318-.318-.78-.415-1.316-.094a2 2 0 0 1-2.969-1.23C10.065 1.258 9.669 1 9.219 1h-.438c-.45 0-.845.258-.997.865a2 2 0 0 1-2.969 1.23c-.536-.322-.999-.225-1.317.093l-.31.31c-.318.318-.415.781-.093 1.317a2 2 0 0 1-1.23 2.969C1.26 7.935 1 8.33 1 8.781v.438c0 .45.258.845.865.997a2 2 0 0 1 1.23 2.969c-.322.536-.225.998.094 1.316l.31.31c.319.319.782.415 1.316.094a2 2 0 0 1 2.969 1.23c.151.607.547.865.997.865h.438c.45 0 .845-.258.997-.865a2 2 0 0 1 2.969-1.23c.535.321.997.225 1.316-.094l.31-.31c.318-.318.415-.781.094-1.316a2 2 0 0 1 1.23-2.969c.607-.151.865-.547.865-.997v-.438c0-.451-.26-.846-.865-.997zM9 12a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol>
|
||||
<symbol id="plyr-volume"><path d="M15.6 3.3c-.4-.4-1-.4-1.4 0-.4.4-.4 1 0 1.4C15.4 5.9 16 7.4 16 9c0 1.6-.6 3.1-1.8 4.3-.4.4-.4 1 0 1.4.2.2.5.3.7.3.3 0 .5-.1.7-.3C17.1 13.2 18 11.2 18 9s-.9-4.2-2.4-5.7z"/><path d="M11.282 5.282a.909.909 0 0 0 0 1.316c.735.735.995 1.458.995 2.402 0 .936-.425 1.917-.995 2.487a.909.909 0 0 0 0 1.316c.145.145.636.262 1.018.156a.725.725 0 0 0 .298-.156C13.773 11.733 14.13 10.16 14.13 9c0-.17-.002-.34-.011-.51-.053-.992-.319-2.005-1.522-3.208a.909.909 0 0 0-1.316 0zM3.786 6.008H.714C.286 6.008 0 6.31 0 6.76v4.512c0 .452.286.752.714.752h3.072l4.071 3.858c.5.3 1.143 0 1.143-.602V2.752c0-.601-.643-.977-1.143-.601L3.786 6.008z"/></symbol>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
style="display: none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<symbol id="plyr-download"><path d="M9 13c.3 0 .5-.1.7-.3L15.4 7 14 5.6l-4 4V1H8v8.6l-4-4L2.6 7l5.7 5.7c.2.2.4.3.7.3zM2 15h14v2H2z" /></symbol>
|
||||
<symbol id="plyr-enter-fullscreen"><path d="M10 3h3.6l-4 4L11 8.4l4-4V8h2V1h-7zM7 9.6l-4 4V10H1v7h7v-2H4.4l4-4z" /></symbol>
|
||||
<symbol id="plyr-exit-fullscreen"><path d="M1 12h3.6l-4 4L2 17.4l4-4V17h2v-7H1zM16 .6l-4 4V1h-2v7h7V6h-3.6l4-4z" /></symbol>
|
||||
<symbol id="plyr-fast-forward"><path d="M7.875 7.171L0 1v16l7.875-6.171V17L18 9 7.875 1z" /></symbol>
|
||||
<symbol id="plyr-muted"><path d="M12.4 12.5l2.1-2.1 2.1 2.1 1.4-1.4L15.9 9 18 6.9l-1.4-1.4-2.1 2.1-2.1-2.1L11 6.9 13.1 9 11 11.1zM3.786 6.008H.714C.286 6.008 0 6.31 0 6.76v4.512c0 .452.286.752.714.752h3.072l4.071 3.858c.5.3 1.143 0 1.143-.602V2.752c0-.601-.643-.977-1.143-.601L3.786 6.008z" /></symbol>
|
||||
<symbol id="plyr-pause"><path d="M6 1H3c-.6 0-1 .4-1 1v14c0 .6.4 1 1 1h3c.6 0 1-.4 1-1V2c0-.6-.4-1-1-1zM12 1c-.6 0-1 .4-1 1v14c0 .6.4 1 1 1h3c.6 0 1-.4 1-1V2c0-.6-.4-1-1-1h-3z" /></symbol>
|
||||
<symbol id="plyr-pip"><path d="M13.293 3.293L7.022 9.564l1.414 1.414 6.271-6.271L17 7V1h-6z" /><path d="M13 15H3V5h5V3H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-6h-2v5z" /></symbol>
|
||||
<symbol id="plyr-play"><path d="M15.562 8.1L3.87.225C3.052-.337 2 .225 2 1.125v15.75c0 .9 1.052 1.462 1.87.9L15.563 9.9c.584-.45.584-1.35 0-1.8z" /></symbol>
|
||||
<symbol id="plyr-restart"><path d="M9.7 1.2l.7 6.4 2.1-2.1c1.9 1.9 1.9 5.1 0 7-.9 1-2.2 1.5-3.5 1.5-1.3 0-2.6-.5-3.5-1.5-1.9-1.9-1.9-5.1 0-7 .6-.6 1.4-1.1 2.3-1.3l-.6-1.9C6 2.6 4.9 3.2 4 4.1 1.3 6.8 1.3 11.2 4 14c1.3 1.3 3.1 2 4.9 2 1.9 0 3.6-.7 4.9-2 2.7-2.7 2.7-7.1 0-9.9L16 1.9l-6.3-.7z" /></symbol>
|
||||
<symbol id="plyr-rewind"><path d="M10.125 1L0 9l10.125 8v-6.171L18 17V1l-7.875 6.171z" /></symbol>
|
||||
<symbol id="plyr-settings"><path d="M16.135 7.784a2 2 0 0 1-1.23-2.969c.322-.536.225-.998-.094-1.316l-.31-.31c-.318-.318-.78-.415-1.316-.094a2 2 0 0 1-2.969-1.23C10.065 1.258 9.669 1 9.219 1h-.438c-.45 0-.845.258-.997.865a2 2 0 0 1-2.969 1.23c-.536-.322-.999-.225-1.317.093l-.31.31c-.318.318-.415.781-.093 1.317a2 2 0 0 1-1.23 2.969C1.26 7.935 1 8.33 1 8.781v.438c0 .45.258.845.865.997a2 2 0 0 1 1.23 2.969c-.322.536-.225.998.094 1.316l.31.31c.319.319.782.415 1.316.094a2 2 0 0 1 2.969 1.23c.151.607.547.865.997.865h.438c.45 0 .845-.258.997-.865a2 2 0 0 1 2.969-1.23c.535.321.997.225 1.316-.094l.31-.31c.318-.318.415-.781.094-1.316a2 2 0 0 1 1.23-2.969c.607-.151.865-.547.865-.997v-.438c0-.451-.26-.846-.865-.997zM9 12a3 3 0 1 1 0-6 3 3 0 0 1 0 6z" /></symbol>
|
||||
<symbol id="plyr-volume"><path d="M15.6 3.3c-.4-.4-1-.4-1.4 0-.4.4-.4 1 0 1.4C15.4 5.9 16 7.4 16 9c0 1.6-.6 3.1-1.8 4.3-.4.4-.4 1 0 1.4.2.2.5.3.7.3.3 0 .5-.1.7-.3C17.1 13.2 18 11.2 18 9s-.9-4.2-2.4-5.7z" /><path d="M11.282 5.282a.909.909 0 0 0 0 1.316c.735.735.995 1.458.995 2.402 0 .936-.425 1.917-.995 2.487a.909.909 0 0 0 0 1.316c.145.145.636.262 1.018.156a.725.725 0 0 0 .298-.156C13.773 11.733 14.13 10.16 14.13 9c0-.17-.002-.34-.011-.51-.053-.992-.319-2.005-1.522-3.208a.909.909 0 0 0-1.316 0zM3.786 6.008H.714C.286 6.008 0 6.31 0 6.76v4.512c0 .452.286.752.714.752h3.072l4.071 3.858c.5.3 1.143 0 1.143-.602V2.752c0-.601-.643-.977-1.143-.601L3.786 6.008z" /></symbol>
|
||||
<!-- those ones are from fork-awesome -->
|
||||
<symbol id="plyr-step-backward"><path d="M979 141c25-25 45-16 45 19v1472c0 35-20 44-45 19L269 941c-6-6-10-12-13-19v678c0 35-29 64-64 64H64c-35 0-64-29-64-64V192c0-35 29-64 64-64h128c35 0 64 29 64 64v678c3-7 7-13 13-19z"/></symbol>
|
||||
<symbol id="plyr-step-forward"><path d="M45 1651c-25 25-45 16-45-19V160c0-35 20-44 45-19l710 710c6 6 10 12 13 19V192c0-35 29-64 64-64h128c35 0 64 29 64 64v1408c0 35-29 64-64 64H832c-35 0-64-29-64-64V922c-3 7-7 13-13 19z"/></symbol>
|
||||
<symbol id="plyr-step-backward"><path d="M979 141c25-25 45-16 45 19v1472c0 35-20 44-45 19L269 941c-6-6-10-12-13-19v678c0 35-29 64-64 64H64c-35 0-64-29-64-64V192c0-35 29-64 64-64h128c35 0 64 29 64 64v678c3-7 7-13 13-19z" /></symbol>
|
||||
<symbol id="plyr-step-forward"><path d="M45 1651c-25 25-45 16-45-19V160c0-35 20-44 45-19l710 710c6 6 10 12 13 19V192c0-35 29-64 64-64h128c35 0 64 29 64 64v1408c0 35-29 64-64 64H832c-35 0-64-29-64-64V922c-3 7-7 13-13 19z" /></symbol>
|
||||
</svg>
|
||||
<article>
|
||||
<aside class="cover main" v-if="currentTrack">
|
||||
<img height="120" v-if="currentTrack.cover" :src="currentTrack.cover" alt="Cover" />
|
||||
<img height="120" v-else src="./assets/embed/default-cover.jpeg" alt="Cover" />
|
||||
<aside
|
||||
v-if="currentTrack"
|
||||
class="cover main"
|
||||
>
|
||||
<img
|
||||
v-if="currentTrack.cover"
|
||||
height="120"
|
||||
:src="currentTrack.cover"
|
||||
alt="Cover"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
height="120"
|
||||
src="./assets/embed/default-cover.jpeg"
|
||||
alt="Cover"
|
||||
>
|
||||
</aside>
|
||||
<div class="content" aria-label="Track information">
|
||||
<div
|
||||
class="content"
|
||||
aria-label="Track information"
|
||||
>
|
||||
<header v-if="currentTrack">
|
||||
<h3><a :href="fullUrl('/library/tracks/' + currentTrack.id)" target="_blank" rel="noopener noreferrer">{{ currentTrack.title }}</a></h3>
|
||||
<a :href="fullUrl('/library/artists/' + currentTrack.artist.id)" target="_blank" rel="noopener noreferrer">{{ currentTrack.artist.name }}</a>
|
||||
<h3>
|
||||
<a
|
||||
:href="fullUrl('/library/tracks/' + currentTrack.id)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ currentTrack.title }}</a>
|
||||
</h3>
|
||||
<a
|
||||
:href="fullUrl('/library/artists/' + currentTrack.artist.id)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ currentTrack.artist.name }}</a>
|
||||
</header>
|
||||
<section v-if="!isLoading" class="controls" aria-label="Audio player">
|
||||
<section
|
||||
v-if="!isLoading"
|
||||
class="controls"
|
||||
aria-label="Audio player"
|
||||
>
|
||||
<template v-if="currentTrack && currentTrack.sources.length > 0">
|
||||
<div class="queue-controls plyr--audio" v-if="tracks.length > 1">
|
||||
<div
|
||||
v-if="tracks.length > 1"
|
||||
class="queue-controls plyr--audio"
|
||||
>
|
||||
<div class="plyr__controls">
|
||||
<button
|
||||
type="button"
|
||||
class="plyr__control"
|
||||
aria-label="Play previous track"
|
||||
@focus="setControlFocus($event, true)"
|
||||
@blur="setControlFocus($event, false)"
|
||||
@click="previous()"
|
||||
type="button"
|
||||
class="plyr__control"
|
||||
aria-label="Play previous track">
|
||||
<svg class="icon--not-pressed" role="presentation" focusable="false" viewBox="0 0 1100 1650" width="80" height="80">
|
||||
<use xlink:href="#plyr-step-backward"></use>
|
||||
>
|
||||
<svg
|
||||
class="icon--not-pressed"
|
||||
role="presentation"
|
||||
focusable="false"
|
||||
viewBox="0 0 1100 1650"
|
||||
width="80"
|
||||
height="80"
|
||||
>
|
||||
<use xlink:href="#plyr-step-backward" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="plyr__control"
|
||||
aria-label="Play next track"
|
||||
@click="next()"
|
||||
@focus="setControlFocus($event, true)"
|
||||
@blur="setControlFocus($event, false)"
|
||||
type="button"
|
||||
class="plyr__control"
|
||||
aria-label="Play next track">
|
||||
<svg class="icon--not-pressed" role="presentation" focusable="false" viewBox="0 0 1100 1650" width="80" height="80">
|
||||
<use xlink:href="#plyr-step-forward"></use>
|
||||
>
|
||||
<svg
|
||||
class="icon--not-pressed"
|
||||
role="presentation"
|
||||
focusable="false"
|
||||
viewBox="0 0 1100 1650"
|
||||
width="80"
|
||||
height="80"
|
||||
>
|
||||
<use xlink:href="#plyr-step-forward" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -62,51 +115,122 @@
|
|||
:key="currentIndex"
|
||||
ref="player"
|
||||
class="player"
|
||||
:options="{loadSprite: false, controls: controls, duration: currentTrack.sources[0].duration, autoplay}">
|
||||
:options="{loadSprite: false, controls: controls, duration: currentTrack.sources[0].duration, autoplay}"
|
||||
>
|
||||
<audio preload="none">
|
||||
<source v-for="source in currentTrack.sources" :src="source.src" :type="source.type"/>
|
||||
<source
|
||||
v-for="(source, key) in currentTrack.sources"
|
||||
:key="key"
|
||||
:src="source.src"
|
||||
:type="source.type"
|
||||
>
|
||||
</audio>
|
||||
</vue-plyr>
|
||||
</template>
|
||||
<div v-else class="player">
|
||||
<span v-if="error === 'invalid_type'" class="error">Widget improperly configured (bad resource type {{ type }}).</span>
|
||||
<span v-else-if="error === 'invalid_id'" class="error">Widget improperly configured (missing resource id).</span>
|
||||
<span v-else-if="error === 'server_not_found'" class="error">Track not found.</span>
|
||||
<span v-else-if="error === 'server_requires_auth'" class="error">You need to login to access this resource.</span>
|
||||
<span v-else-if="error === 'server_error'" class="error">A server error occurred.</span>
|
||||
<span v-else-if="error === 'server_error'" class="error">An unknown error occurred while loading track data from server.</span>
|
||||
<span v-else-if="currentTrack && currentTrack.sources.length === 0" class="error">This track is unavailable.</span>
|
||||
<span v-else class="error">An unknown error occurred while loading track data.</span>
|
||||
<div
|
||||
v-else
|
||||
class="player"
|
||||
>
|
||||
<span
|
||||
v-if="error === 'invalid_type'"
|
||||
class="error"
|
||||
>Widget improperly configured (bad resource type {{ type }}).</span>
|
||||
<span
|
||||
v-else-if="error === 'invalid_id'"
|
||||
class="error"
|
||||
>Widget improperly configured (missing resource id).</span>
|
||||
<span
|
||||
v-else-if="error === 'server_not_found'"
|
||||
class="error"
|
||||
>Track not found.</span>
|
||||
<span
|
||||
v-else-if="error === 'server_requires_auth'"
|
||||
class="error"
|
||||
>You need to login to access this resource.</span>
|
||||
<span
|
||||
v-else-if="error === 'server_error'"
|
||||
class="error"
|
||||
>An unknown error occurred while loading track data from server.</span>
|
||||
<span
|
||||
v-else-if="currentTrack && currentTrack.sources.length === 0"
|
||||
class="error"
|
||||
>This track is unavailable.</span>
|
||||
<span
|
||||
v-else
|
||||
class="error"
|
||||
>An unknown error occurred while loading track data.</span>
|
||||
</div>
|
||||
<a title="Funkwhale" href="https://funkwhale.audio" target="_blank" rel="noopener noreferrer" class="logo-wrapper">
|
||||
<logo :fill="currentTheme.textColor" class="logo"></logo>
|
||||
<a
|
||||
title="Funkwhale"
|
||||
href="https://funkwhale.audio"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="logo-wrapper"
|
||||
>
|
||||
<logo
|
||||
:fill="currentTheme.textColor"
|
||||
class="logo"
|
||||
/>
|
||||
</a>
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
<div v-if="tracks.length > 1" class="queue-wrapper" id="queue">
|
||||
<div
|
||||
v-if="tracks.length > 1"
|
||||
id="queue"
|
||||
class="queue-wrapper"
|
||||
>
|
||||
<table class="queue">
|
||||
<tbody>
|
||||
<tr
|
||||
:id="'queue-item-' + index"
|
||||
role="button"
|
||||
v-for="(track, index) in tracks"
|
||||
v-if="track.sources.length > 0"
|
||||
:id="'queue-item-' + index"
|
||||
:key="index"
|
||||
role="button"
|
||||
:class="[{active: index === currentIndex}]"
|
||||
@click="play(index)"
|
||||
@keyup.enter="play(index)"
|
||||
v-for="(track, index) in tracks">
|
||||
<td class="position-cell" width="40">
|
||||
>
|
||||
<td
|
||||
class="position-cell"
|
||||
width="40"
|
||||
>
|
||||
<span class="position">
|
||||
{{ index + 1 }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="title" :title="track.title" ><div colspan="2" class="ellipsis">{{ track.title }}</div></td>
|
||||
<td class="artist" :title="track.artist.name" ><div class="ellipsis">{{ track.artist.name }}</div></td>
|
||||
<td class="album">
|
||||
<div class="ellipsis" v-if="track.album" :title="track.album.title">{{ track.album.title }}</div>
|
||||
<td
|
||||
class="title"
|
||||
:title="track.title"
|
||||
>
|
||||
<div
|
||||
colspan="2"
|
||||
class="ellipsis"
|
||||
>
|
||||
{{ track.title }}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="artist"
|
||||
:title="track.artist.name"
|
||||
>
|
||||
<div class="ellipsis">
|
||||
{{ track.artist.name }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="album">
|
||||
<div
|
||||
v-if="track.album"
|
||||
class="ellipsis"
|
||||
:title="track.album.title"
|
||||
>
|
||||
{{ track.album.title }}
|
||||
</div>
|
||||
</td>
|
||||
<td width="50">
|
||||
{{ time.durationFormatted(track.sources[0].duration) }}
|
||||
</td>
|
||||
<td width="50">{{ time.durationFormatted(track.sources[0].duration) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -116,26 +240,24 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import Logo from "@/components/Logo"
|
||||
import Logo from '@/components/Logo'
|
||||
import url from '@/utils/url'
|
||||
import time from '@/utils/time'
|
||||
|
||||
function getURLParams () {
|
||||
var urlParams
|
||||
var match,
|
||||
pl = /\+/g, // Regex for replacing addition symbol with a space
|
||||
search = /([^&=]+)=?([^&]*)/g,
|
||||
decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); },
|
||||
query = window.location.search.substring(1);
|
||||
let match
|
||||
const pl = /\+/g // Regex for replacing addition symbol with a space
|
||||
const urlParams = {}
|
||||
const search = /([^&=]+)=?([^&]*)/g
|
||||
const decode = function (s) { return decodeURIComponent(s.replace(pl, ' ')) }
|
||||
const query = window.location.search.substring(1)
|
||||
|
||||
urlParams = {};
|
||||
while (match = search.exec(query))
|
||||
urlParams[decode(match[1])] = decode(match[2]);
|
||||
while (match === search.exec(query)) { urlParams[decode(match[1])] = decode(match[2]) }
|
||||
return urlParams
|
||||
}
|
||||
export default {
|
||||
name: 'app',
|
||||
components: {Logo},
|
||||
name: 'App',
|
||||
components: { Logo },
|
||||
data () {
|
||||
return {
|
||||
time,
|
||||
|
@ -152,38 +274,11 @@ export default {
|
|||
currentIndex: -1,
|
||||
themes: {
|
||||
dark: {
|
||||
textColor: 'white',
|
||||
textColor: 'white'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
let params = getURLParams()
|
||||
this.baseUrl = params.b || ''
|
||||
this.type = params.type
|
||||
if (this.supportedTypes.indexOf(this.type) === -1) {
|
||||
this.error = 'invalid_type'
|
||||
}
|
||||
this.id = params.id
|
||||
if (!this.id) {
|
||||
this.error = 'invalid_id'
|
||||
}
|
||||
if (this.error) {
|
||||
this.isLoading = false
|
||||
return
|
||||
}
|
||||
if (!!params.instance) {
|
||||
this.baseUrl = params.instance
|
||||
}
|
||||
|
||||
this.autoplay = params.autoplay != undefined || params.auto_play != undefined
|
||||
this.fetch(this.type, this.id)
|
||||
},
|
||||
mounted () {
|
||||
var parser = document.createElement('a')
|
||||
parser.href = this.baseUrl
|
||||
this.url = parser
|
||||
},
|
||||
computed: {
|
||||
currentTrack () {
|
||||
if (this.tracks.length === 0) {
|
||||
|
@ -200,7 +295,7 @@ export default {
|
|||
'progress', // The progress bar and scrubber for playback and buffering
|
||||
'current-time', // The current time of playback
|
||||
'mute', // Toggle mute
|
||||
'volume', // Volume control
|
||||
'volume' // Volume control
|
||||
]
|
||||
},
|
||||
hasPrevious () {
|
||||
|
@ -208,7 +303,54 @@ export default {
|
|||
},
|
||||
hasNext () {
|
||||
return this.currentIndex < this.tracks.length - 1
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentIndex (v) {
|
||||
// we bind player events
|
||||
const self = this
|
||||
this.$nextTick(() => {
|
||||
self.bindEvents()
|
||||
if (self.tracks.length > 0) {
|
||||
const el = document.getElementById(`queue-item-${v}`)
|
||||
if (!el) {
|
||||
return
|
||||
}
|
||||
const topPos = el.offsetTop
|
||||
document.getElementById('queue').scrollTop = topPos - 10
|
||||
}
|
||||
})
|
||||
},
|
||||
tracks () {
|
||||
this.currentIndex = 0
|
||||
}
|
||||
},
|
||||
created () {
|
||||
const params = getURLParams()
|
||||
this.baseUrl = params.b || ''
|
||||
this.type = params.type
|
||||
if (this.supportedTypes.indexOf(this.type) === -1) {
|
||||
this.error = 'invalid_type'
|
||||
}
|
||||
this.id = params.id
|
||||
if (!this.id) {
|
||||
this.error = 'invalid_id'
|
||||
}
|
||||
if (this.error) {
|
||||
this.isLoading = false
|
||||
return
|
||||
}
|
||||
if (params.instance) {
|
||||
this.baseUrl = params.instance
|
||||
}
|
||||
|
||||
this.autoplay = params.autoplay !== undefined || params.auto_play !== undefined
|
||||
this.fetch(this.type, this.id)
|
||||
},
|
||||
mounted () {
|
||||
const parser = document.createElement('a')
|
||||
parser.href = this.baseUrl
|
||||
this.url = parser
|
||||
},
|
||||
methods: {
|
||||
next () {
|
||||
|
@ -221,11 +363,11 @@ export default {
|
|||
this.play(this.currentIndex - 1)
|
||||
}
|
||||
},
|
||||
setControlFocus(event, enable) {
|
||||
setControlFocus (event, enable) {
|
||||
if (enable) {
|
||||
event.target.classList.add("plyr__tab-focus");
|
||||
event.target.classList.add('plyr__tab-focus')
|
||||
} else {
|
||||
event.target.classList.remove("plyr__tab-focus");
|
||||
event.target.classList.remove('plyr__tab-focus')
|
||||
}
|
||||
},
|
||||
fetch (type, id) {
|
||||
|
@ -233,13 +375,13 @@ export default {
|
|||
this.fetchTrack(id)
|
||||
}
|
||||
if (type === 'album') {
|
||||
this.fetchTracks({album: id, playable: true, ordering: "disc_number,position"})
|
||||
this.fetchTracks({ album: id, playable: true, ordering: 'disc_number,position' })
|
||||
}
|
||||
if (type === 'channel') {
|
||||
this.fetchTracks({channel: id, playable: true, include_channels: 'true', ordering: "-creation_date"})
|
||||
this.fetchTracks({ channel: id, playable: true, include_channels: 'true', ordering: '-creation_date' })
|
||||
}
|
||||
if (type === 'artist') {
|
||||
this.fetchTracks({artist: id, playable: true, include_channels: 'true', ordering: "-album__release_date,disc_number,position"})
|
||||
this.fetchTracks({ artist: id, playable: true, include_channels: 'true', ordering: '-album__release_date,disc_number,position' })
|
||||
}
|
||||
if (type === 'playlist') {
|
||||
this.fetchTracks({}, `/api/v1/playlists/${id}/tracks/`)
|
||||
|
@ -247,67 +389,61 @@ export default {
|
|||
},
|
||||
play (index) {
|
||||
this.currentIndex = index
|
||||
let self = this
|
||||
const self = this
|
||||
this.$nextTick(() => {
|
||||
self.$refs.player.player.play()
|
||||
})
|
||||
},
|
||||
fetchTrack (id) {
|
||||
let self = this
|
||||
let url = `${this.baseUrl}/api/v1/tracks/${id}/`
|
||||
const self = this
|
||||
const url = `${this.baseUrl}/api/v1/tracks/${id}/`
|
||||
axios.get(url).then(response => {
|
||||
self.tracks = self.parseTracks([response.data])
|
||||
self.isLoading = false;
|
||||
self.isLoading = false
|
||||
}).catch(error => {
|
||||
if (error.response) {
|
||||
if (error.response.status === 404) {
|
||||
self.error = 'server_not_found'
|
||||
}
|
||||
else if (error.response.status === 403) {
|
||||
} else if (error.response.status === 403) {
|
||||
self.error = 'server_requires_auth'
|
||||
}
|
||||
else if (error.response.status === 500) {
|
||||
} else if (error.response.status === 500) {
|
||||
self.error = 'server_error'
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
self.error = 'server_unknown_error'
|
||||
}
|
||||
} else {
|
||||
self.error = 'server_unknown_error'
|
||||
}
|
||||
self.isLoading = false;
|
||||
self.isLoading = false
|
||||
})
|
||||
},
|
||||
fetchTracks (filters, path) {
|
||||
path = path || "/api/v1/tracks/"
|
||||
filters.include_channels = "true"
|
||||
let self = this
|
||||
let url = `${this.baseUrl}${path}`
|
||||
axios.get(url, {params: filters}).then(response => {
|
||||
path = path || '/api/v1/tracks/'
|
||||
filters.include_channels = 'true'
|
||||
const self = this
|
||||
const url = `${this.baseUrl}${path}`
|
||||
axios.get(url, { params: filters }).then(response => {
|
||||
self.tracks = self.parseTracks(response.data.results)
|
||||
self.isLoading = false;
|
||||
self.isLoading = false
|
||||
}).catch(error => {
|
||||
if (error.response) {
|
||||
if (error.response.status === 404) {
|
||||
self.error = 'server_not_found'
|
||||
}
|
||||
else if (error.response.status === 403) {
|
||||
} else if (error.response.status === 403) {
|
||||
self.error = 'server_requires_auth'
|
||||
}
|
||||
else if (error.response.status === 500) {
|
||||
} else if (error.response.status === 500) {
|
||||
self.error = 'server_error'
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
self.error = 'server_unknown_error'
|
||||
}
|
||||
} else {
|
||||
self.error = 'server_unknown_error'
|
||||
}
|
||||
self.isLoading = false;
|
||||
self.isLoading = false
|
||||
})
|
||||
},
|
||||
parseTracks (tracks) {
|
||||
let self = this
|
||||
const self = this
|
||||
if (this.type === 'playlist') {
|
||||
tracks = tracks.map((t) => {
|
||||
return t.track
|
||||
|
@ -325,7 +461,7 @@ export default {
|
|||
})
|
||||
},
|
||||
bindEvents () {
|
||||
let self = this
|
||||
const self = this
|
||||
this.$refs.player.player.on('ended', () => {
|
||||
self.next()
|
||||
})
|
||||
|
@ -336,17 +472,17 @@ export default {
|
|||
}
|
||||
return path
|
||||
},
|
||||
getCover(albumCover) {
|
||||
getCover (albumCover) {
|
||||
if (albumCover) {
|
||||
return albumCover.urls.medium_square_crop
|
||||
}
|
||||
},
|
||||
getSources (uploads) {
|
||||
let self = this;
|
||||
let a = document.createElement('audio')
|
||||
let allowed = ['probably', 'maybe']
|
||||
let sources = uploads.filter(u => {
|
||||
let canPlay = a.canPlayType(u.mimetype)
|
||||
const self = this
|
||||
const a = document.createElement('audio')
|
||||
const allowed = ['probably', 'maybe']
|
||||
const sources = uploads.filter(u => {
|
||||
const canPlay = a.canPlayType(u.mimetype)
|
||||
return allowed.indexOf(canPlay) > -1
|
||||
}).map(u => {
|
||||
return {
|
||||
|
@ -371,26 +507,6 @@ export default {
|
|||
}
|
||||
return sources
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentIndex (v) {
|
||||
// we bind player events
|
||||
let self = this
|
||||
this.$nextTick(() => {
|
||||
self.bindEvents()
|
||||
if (self.tracks.length > 0) {
|
||||
let el = document.getElementById(`queue-item-${v}`);
|
||||
if (!el) {
|
||||
return
|
||||
}
|
||||
var topPos = el.offsetTop;
|
||||
document.getElementById('queue').scrollTop = topPos-10;
|
||||
}
|
||||
})
|
||||
},
|
||||
tracks () {
|
||||
this.currentIndex = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
var Album = {
|
||||
const Album = {
|
||||
clean (album) {
|
||||
// we manually rebind the album and artist to each child track
|
||||
album.tracks = album.tracks.map((track) => {
|
||||
|
@ -8,7 +8,7 @@ var Album = {
|
|||
return album
|
||||
}
|
||||
}
|
||||
var Artist = {
|
||||
const Artist = {
|
||||
clean (artist) {
|
||||
// clean data as given by the API
|
||||
artist.albums = artist.albums.map((album) => {
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
const DYNAMIC_RANGE = 40 // dB
|
||||
|
||||
function toLinearVolumeScale(v) {
|
||||
function toLinearVolumeScale (v) {
|
||||
if (v <= 0.0) {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// (1.0; 0.0) -> (0; -DYNAMIC_RANGE) dB
|
||||
let dB = (v-1)*DYNAMIC_RANGE
|
||||
const dB = (v - 1) * DYNAMIC_RANGE
|
||||
|
||||
return Math.pow(10, dB / 20)
|
||||
}
|
||||
|
||||
function toLogarithmicVolumeScale(v) {
|
||||
function toLogarithmicVolumeScale (v) {
|
||||
if (v <= 0.0) {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
let dB = 20 * Math.log10(v)
|
||||
const dB = 20 * Math.log10(v)
|
||||
|
||||
// (0; -DYNAMIC_RANGE) [dB] -> (1.0; 0.0)
|
||||
return 1 - (dB / -DYNAMIC_RANGE)
|
||||
|
|
|
@ -1,77 +1,208 @@
|
|||
<template>
|
||||
<footer id="footer" role="contentinfo" class="ui vertical footer segment" aria-labelledby="footer-label">
|
||||
<h1 id="footer-label" class="visually-hidden">
|
||||
<translate translate-context="*/*/*">Application footer</translate>
|
||||
<footer
|
||||
id="footer"
|
||||
role="contentinfo"
|
||||
class="ui vertical footer segment"
|
||||
aria-labelledby="footer-label"
|
||||
>
|
||||
<h1
|
||||
id="footer-label"
|
||||
class="visually-hidden"
|
||||
>
|
||||
<translate translate-context="*/*/*">
|
||||
Application footer
|
||||
</translate>
|
||||
</h1>
|
||||
<div class="ui container">
|
||||
<div class="ui stackable equal height stackable grid">
|
||||
<section class="four wide column">
|
||||
<h4 v-if="podName" class="ui header ellipsis">
|
||||
<span v-translate="{instanceName: podName}" translate-context="Footer/About/Title">About %{instanceName}</span>
|
||||
<h4
|
||||
v-if="podName"
|
||||
class="ui header ellipsis"
|
||||
>
|
||||
<span
|
||||
v-translate="{instanceName: podName}"
|
||||
translate-context="Footer/About/Title"
|
||||
>About %{instanceName}</span>
|
||||
</h4>
|
||||
<h4 v-else class="ui header ellipsis">
|
||||
<span v-translate="{instanceUrl: instanceHostname}" translate-context="Footer/About/Title">About %{instanceUrl}</span>
|
||||
<h4
|
||||
v-else
|
||||
class="ui header ellipsis"
|
||||
>
|
||||
<span
|
||||
v-translate="{instanceUrl: instanceHostname}"
|
||||
translate-context="Footer/About/Title"
|
||||
>About %{instanceUrl}</span>
|
||||
</h4>
|
||||
<div class="ui list">
|
||||
<router-link v-if="this.$route.path != '/about'" class="link item" to="/about">
|
||||
<translate translate-context="Footer/About/List item.Link">About</translate>
|
||||
<router-link
|
||||
v-if="$route.path != '/about'"
|
||||
class="link item"
|
||||
to="/about"
|
||||
>
|
||||
<translate translate-context="Footer/About/List item.Link">
|
||||
About
|
||||
</translate>
|
||||
</router-link>
|
||||
<router-link v-else-if="this.$route.path == '/about' && $store.state.auth.authenticated" class="link item" to="/library">
|
||||
<translate translate-context="Footer/*/List item.Link">Go to Library</translate>
|
||||
<router-link
|
||||
v-else-if="$route.path == '/about' && $store.state.auth.authenticated"
|
||||
class="link item"
|
||||
to="/library"
|
||||
>
|
||||
<translate translate-context="Footer/*/List item.Link">
|
||||
Go to Library
|
||||
</translate>
|
||||
</router-link>
|
||||
<router-link v-else class="link item" to="/">
|
||||
<translate translate-context="Footer/*/List item.Link">Home Page</translate>
|
||||
<router-link
|
||||
v-else
|
||||
class="link item"
|
||||
to="/"
|
||||
>
|
||||
<translate translate-context="Footer/*/List item.Link">
|
||||
Home Page
|
||||
</translate>
|
||||
</router-link>
|
||||
<a v-if="version" class="link item" href="https://docs.funkwhale.audio/changelog.html" target="_blank">
|
||||
<translate translate-context="Footer/*/List item" :translate-params="{version: version}" >Version %{version}</translate>
|
||||
<a
|
||||
v-if="version"
|
||||
class="link item"
|
||||
href="https://docs.funkwhale.audio/changelog.html"
|
||||
target="_blank"
|
||||
>
|
||||
<translate
|
||||
translate-context="Footer/*/List item"
|
||||
:translate-params="{version: version}"
|
||||
>Version %{version}</translate>
|
||||
</a>
|
||||
<a role="button" href="" class="link item" @click.prevent="$emit('show:set-instance-modal')" >
|
||||
<a
|
||||
role="button"
|
||||
href=""
|
||||
class="link item"
|
||||
@click.prevent="$emit('show:set-instance-modal')"
|
||||
>
|
||||
<translate translate-context="Footer/*/List item.Link">Use another instance</translate>
|
||||
</a>
|
||||
</div>
|
||||
<div class="ui form">
|
||||
<div class="ui field">
|
||||
<label for="language-select"><translate translate-context="Footer/Settings/Dropdown.Label/Short, Verb">Change language</translate></label>
|
||||
<select id="language-select" class="ui dropdown" :value="$language.current" @change="$store.dispatch('ui/currentLanguage', $event.target.value)">
|
||||
<option v-for="(language, key) in $language.available" :key="key" :value="key">{{ language }}</option>
|
||||
<select
|
||||
id="language-select"
|
||||
class="ui dropdown"
|
||||
:value="$language.current"
|
||||
@change="$store.dispatch('ui/currentLanguage', $event.target.value)"
|
||||
>
|
||||
<option
|
||||
v-for="(language, key) in $language.available"
|
||||
:key="key"
|
||||
:value="key"
|
||||
>
|
||||
{{ language }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="four wide column">
|
||||
<h4 v-translate class="ui header" translate-context="Footer/*/Title">Using Funkwhale</h4>
|
||||
<h4
|
||||
v-translate
|
||||
class="ui header"
|
||||
translate-context="Footer/*/Title"
|
||||
>
|
||||
Using Funkwhale
|
||||
</h4>
|
||||
<div class="ui list">
|
||||
<a href="https://docs.funkwhale.audio" class="link item" target="_blank"><translate translate-context="Footer/*/List item.Link/Short, Noun">Documentation</translate></a>
|
||||
<a href="https://funkwhale.audio/apps" class="link item" target="_blank"><translate translate-context="Footer/*/List item.Link">Mobile and desktop apps</translate></a>
|
||||
<a hrelf="" class="link item" @click.prevent="$emit('show:shortcuts-modal')"><translate translate-context="*/*/*/Noun">Keyboard shortcuts</translate></a>
|
||||
<a
|
||||
href="https://docs.funkwhale.audio"
|
||||
class="link item"
|
||||
target="_blank"
|
||||
><translate translate-context="Footer/*/List item.Link/Short, Noun">Documentation</translate></a>
|
||||
<a
|
||||
href="https://funkwhale.audio/apps"
|
||||
class="link item"
|
||||
target="_blank"
|
||||
><translate translate-context="Footer/*/List item.Link">Mobile and desktop apps</translate></a>
|
||||
<a
|
||||
hrelf=""
|
||||
class="link item"
|
||||
@click.prevent="$emit('show:shortcuts-modal')"
|
||||
><translate translate-context="*/*/*/Noun">Keyboard shortcuts</translate></a>
|
||||
</div>
|
||||
<div class="ui form">
|
||||
<div class="ui field">
|
||||
<label for="theme-select"><translate translate-context="Footer/Settings/Dropdown.Label/Short, Verb">Change theme</translate></label>
|
||||
<select id="theme-select" class="ui dropdown" :value="$store.state.ui.theme" @change="$store.dispatch('ui/theme', $event.target.value)">
|
||||
<option v-for="theme in themes" :key="theme.key" :value="theme.key">{{ theme.name }}</option>
|
||||
<select
|
||||
id="theme-select"
|
||||
class="ui dropdown"
|
||||
:value="$store.state.ui.theme"
|
||||
@change="$store.dispatch('ui/theme', $event.target.value)"
|
||||
>
|
||||
<option
|
||||
v-for="theme in themes"
|
||||
:key="theme.key"
|
||||
:value="theme.key"
|
||||
>
|
||||
{{ theme.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="four wide column">
|
||||
<h4 v-translate translate-context="Footer/*/Link" class="ui header">Getting help</h4>
|
||||
<h4
|
||||
v-translate
|
||||
translate-context="Footer/*/Link"
|
||||
class="ui header"
|
||||
>
|
||||
Getting help
|
||||
</h4>
|
||||
<div class="ui list">
|
||||
<a href="https://forum.funkwhale.audio/" class="link item" target="_blank"><translate translate-context="Footer/*/Listitem.Link">Support forum</translate></a>
|
||||
<a href="https://matrix.to/#/#funkwhale-troubleshooting:matrix.org" class="link item" target="_blank"><translate translate-context="Footer/*/List item.Link">Chat room</translate></a>
|
||||
<a href="https://dev.funkwhale.audio/funkwhale/funkwhale/issues" class="link item" target="_blank"><translate translate-context="Footer/*/List item.Link">Issue tracker</translate></a>
|
||||
<a
|
||||
href="https://forum.funkwhale.audio/"
|
||||
class="link item"
|
||||
target="_blank"
|
||||
><translate translate-context="Footer/*/Listitem.Link">Support forum</translate></a>
|
||||
<a
|
||||
href="https://matrix.to/#/#funkwhale-troubleshooting:matrix.org"
|
||||
class="link item"
|
||||
target="_blank"
|
||||
><translate translate-context="Footer/*/List item.Link">Chat room</translate></a>
|
||||
<a
|
||||
href="https://dev.funkwhale.audio/funkwhale/funkwhale/issues"
|
||||
class="link item"
|
||||
target="_blank"
|
||||
><translate translate-context="Footer/*/List item.Link">Issue tracker</translate></a>
|
||||
</div>
|
||||
</section>
|
||||
<section class="four wide column">
|
||||
<h4 v-translate class="ui header" translate-context="Footer/*/Title/Short">About Funkwhale</h4>
|
||||
<h4
|
||||
v-translate
|
||||
class="ui header"
|
||||
translate-context="Footer/*/Title/Short"
|
||||
>
|
||||
About Funkwhale
|
||||
</h4>
|
||||
<div class="ui list">
|
||||
<a href="https://funkwhale.audio" class="link item" target="_blank"><translate translate-context="Footer/*/List item.Link">Official website</translate></a>
|
||||
<a href="https://contribute.funkwhale.audio" class="link item" target="_blank"><translate translate-context="Footer/*/List item.Link">Contribute</translate></a>
|
||||
<a href="https://dev.funkwhale.audio/funkwhale/funkwhale" class="link item" target="_blank"><translate translate-context="Footer/*/List item.Link">Source code</translate></a>
|
||||
<a
|
||||
href="https://funkwhale.audio"
|
||||
class="link item"
|
||||
target="_blank"
|
||||
><translate translate-context="Footer/*/List item.Link">Official website</translate></a>
|
||||
<a
|
||||
href="https://contribute.funkwhale.audio"
|
||||
class="link item"
|
||||
target="_blank"
|
||||
><translate translate-context="Footer/*/List item.Link">Contribute</translate></a>
|
||||
<a
|
||||
href="https://dev.funkwhale.audio/funkwhale/funkwhale"
|
||||
class="link item"
|
||||
target="_blank"
|
||||
><translate translate-context="Footer/*/List item.Link">Source code</translate></a>
|
||||
</div>
|
||||
<div class="ui hidden divider"></div>
|
||||
<div class="ui hidden divider" />
|
||||
<p>
|
||||
<translate translate-context="Footer/*/List item.Link">The Funkwhale logo was kindly designed and provided by Francis Gading.</translate>
|
||||
<translate translate-context="Footer/*/List item.Link">
|
||||
The Funkwhale logo was kindly designed and provided by Francis Gading.
|
||||
</translate>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
@ -80,24 +211,22 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import { mapState } from "vuex"
|
||||
import axios from 'axios'
|
||||
import { mapState } from 'vuex'
|
||||
import _ from '@/lodash'
|
||||
|
||||
export default {
|
||||
props: ["version"],
|
||||
props: { version: { type: String, required: true } },
|
||||
computed: {
|
||||
...mapState({
|
||||
messages: state => state.ui.messages,
|
||||
nodeinfo: state => state.instance.nodeinfo,
|
||||
nodeinfo: state => state.instance.nodeinfo
|
||||
}),
|
||||
podName() {
|
||||
podName () {
|
||||
return _.get(this.nodeinfo, 'metadata.nodeName')
|
||||
},
|
||||
instanceHostname() {
|
||||
let url = this.$store.state.instance.instanceUrl
|
||||
let parser = document.createElement("a")
|
||||
instanceHostname () {
|
||||
const url = this.$store.state.instance.instanceUrl
|
||||
const parser = document.createElement('a')
|
||||
parser.href = url
|
||||
return parser.hostname
|
||||
},
|
||||
|
|
|
@ -1,15 +1,25 @@
|
|||
<template>
|
||||
<main class="main pusher page-home" v-title="labels.title">
|
||||
<section :class="['ui', 'head', {'with-background': banner}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" :style="headerStyle">
|
||||
<main
|
||||
v-title="labels.title"
|
||||
class="main pusher page-home"
|
||||
>
|
||||
<section
|
||||
:class="['ui', 'head', {'with-background': banner}, 'vertical', 'center', 'aligned', 'stripe', 'segment']"
|
||||
:style="headerStyle"
|
||||
>
|
||||
<div class="segment-content">
|
||||
<h1 class="ui center aligned large header">
|
||||
<span
|
||||
v-translate="{podName: podName}"
|
||||
translate-context="Content/Home/Header"
|
||||
:translate-params="{podName: podName}">
|
||||
:translate-params="{podName: podName}"
|
||||
>
|
||||
Welcome to %{ podName }!
|
||||
</span>
|
||||
<div v-if="shortDescription" class="sub header">
|
||||
<div
|
||||
v-if="shortDescription"
|
||||
class="sub header"
|
||||
>
|
||||
{{ shortDescription }}
|
||||
</div>
|
||||
</h1>
|
||||
|
@ -19,31 +29,61 @@
|
|||
<div class="ui stackable grid">
|
||||
<div class="ten wide column">
|
||||
<h2 class="header">
|
||||
<translate translate-context="Content/Home/Header">About this Funkwhale pod</translate>
|
||||
<translate translate-context="Content/Home/Header">
|
||||
About this Funkwhale pod
|
||||
</translate>
|
||||
</h2>
|
||||
<div class="ui raised segment" id="pod">
|
||||
<div
|
||||
id="pod"
|
||||
class="ui raised segment"
|
||||
>
|
||||
<div class="ui stackable grid">
|
||||
<div class="eight wide column">
|
||||
<p v-if="!truncatedDescription">
|
||||
<translate translate-context="Content/Home/Paragraph">No description available.</translate>
|
||||
<translate translate-context="Content/Home/Paragraph">
|
||||
No description available.
|
||||
</translate>
|
||||
</p>
|
||||
<template v-if="truncatedDescription || rules">
|
||||
<div v-if="truncatedDescription" v-html="truncatedDescription"></div>
|
||||
<div v-if="truncatedDescription" class="ui hidden divider"></div>
|
||||
<div
|
||||
v-if="truncatedDescription"
|
||||
v-html="truncatedDescription"
|
||||
/>
|
||||
<div
|
||||
v-if="truncatedDescription"
|
||||
class="ui hidden divider"
|
||||
/>
|
||||
<div class="ui relaxed list">
|
||||
<div class="item" v-if="truncatedDescription">
|
||||
<i class="arrow right icon"></i>
|
||||
<div
|
||||
v-if="truncatedDescription"
|
||||
class="item"
|
||||
>
|
||||
<i class="arrow right icon" />
|
||||
<div class="content">
|
||||
<router-link class="ui link" :to="{name: 'about'}">
|
||||
<translate translate-context="Content/Home/Link">Learn more</translate>
|
||||
<router-link
|
||||
class="ui link"
|
||||
:to="{name: 'about'}"
|
||||
>
|
||||
<translate translate-context="Content/Home/Link">
|
||||
Learn more
|
||||
</translate>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item" v-if="rules">
|
||||
<i class="book open icon"></i>
|
||||
<div
|
||||
v-if="rules"
|
||||
class="item"
|
||||
>
|
||||
<i class="book open icon" />
|
||||
<div class="content">
|
||||
<router-link class="ui link" v-if="rules" :to="{name: 'about', hash: '#rules'}">
|
||||
<translate translate-context="Content/Home/Link">Server rules</translate>
|
||||
<router-link
|
||||
v-if="rules"
|
||||
class="ui link"
|
||||
:to="{name: 'about', hash: '#rules'}"
|
||||
>
|
||||
<translate translate-context="Content/Home/Link">
|
||||
Server rules
|
||||
</translate>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -53,71 +93,130 @@
|
|||
<div class="eight wide column">
|
||||
<template v-if="stats">
|
||||
<h3 class="sub header">
|
||||
<translate translate-context="Content/Home/Header">Statistics</translate>
|
||||
<translate translate-context="Content/Home/Header">
|
||||
Statistics
|
||||
</translate>
|
||||
</h3>
|
||||
<p>
|
||||
<i class="user icon"></i><translate translate-context="Content/Home/Stat" :translate-params="{count: stats.users.toLocaleString($store.state.ui.momentLocale) }" :translate-n="stats.users" translate-plural="%{ count } active users">%{ count } active user</translate>
|
||||
<i class="user icon" /><translate
|
||||
translate-context="Content/Home/Stat"
|
||||
:translate-params="{count: stats.users.toLocaleString($store.state.ui.momentLocale) }"
|
||||
:translate-n="stats.users"
|
||||
translate-plural="%{ count } active users"
|
||||
>
|
||||
%{ count } active user
|
||||
</translate>
|
||||
</p>
|
||||
<p>
|
||||
<i class="music icon"></i><translate translate-context="Content/Home/Stat" :translate-params="{count: parseInt(stats.hours).toLocaleString($store.state.ui.momentLocale)}" :translate-n="parseInt(stats.hours)" translate-plural="%{ count } hours of music">%{ count } hour of music</translate>
|
||||
<i class="music icon" /><translate
|
||||
translate-context="Content/Home/Stat"
|
||||
:translate-params="{count: parseInt(stats.hours).toLocaleString($store.state.ui.momentLocale)}"
|
||||
:translate-n="parseInt(stats.hours)"
|
||||
translate-plural="%{ count } hours of music"
|
||||
>
|
||||
%{ count } hour of music
|
||||
</translate>
|
||||
</p>
|
||||
|
||||
</template>
|
||||
<template v-if="contactEmail">
|
||||
<h3 class="sub header">
|
||||
<translate translate-context="Content/Home/Header/Name">Contact</translate>
|
||||
<translate translate-context="Content/Home/Header/Name">
|
||||
Contact
|
||||
</translate>
|
||||
</h3>
|
||||
<i class="at icon"></i>
|
||||
<i class="at icon" />
|
||||
<a :href="`mailto:${contactEmail}`">{{ contactEmail }}</a>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="six wide column">
|
||||
<img class="ui image" src="../assets/network.png" alt=""/>
|
||||
<img
|
||||
class="ui image"
|
||||
src="../assets/network.png"
|
||||
alt=""
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui hidden divider"></div>
|
||||
<div class="ui hidden divider"></div>
|
||||
<div class="ui hidden divider" />
|
||||
<div class="ui hidden divider" />
|
||||
<div class="ui stackable grid">
|
||||
<div class="four wide column">
|
||||
<h3 class="header">
|
||||
<translate translate-context="Footer/*/Title/Short">About Funkwhale</translate>
|
||||
<translate translate-context="Footer/*/Title/Short">
|
||||
About Funkwhale
|
||||
</translate>
|
||||
</h3>
|
||||
<p v-translate translate-context="Content/Home/Paragraph">This pod runs Funkwhale, a community-driven project that lets you listen and share music and audio within a decentralized, open network.</p>
|
||||
<p v-translate translate-context="Content/Home/Paragraph">Funkwhale is free and developed by a friendly community of volunteers.</p>
|
||||
<a target="_blank" rel="noopener" href="https://funkwhale.audio">
|
||||
<i class="external alternate icon"></i>
|
||||
<p
|
||||
v-translate
|
||||
translate-context="Content/Home/Paragraph"
|
||||
>
|
||||
This pod runs Funkwhale, a community-driven project that lets you listen and share music and audio within a decentralized, open network.
|
||||
</p>
|
||||
<p
|
||||
v-translate
|
||||
translate-context="Content/Home/Paragraph"
|
||||
>
|
||||
Funkwhale is free and developed by a friendly community of volunteers.
|
||||
</p>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href="https://funkwhale.audio"
|
||||
>
|
||||
<i class="external alternate icon" />
|
||||
<translate translate-context="Content/Home/Link">Visit funkwhale.audio</translate>
|
||||
</a>
|
||||
</div>
|
||||
<div class="four wide column">
|
||||
<h3 class="header">
|
||||
<translate translate-context="Head/Login/Title">Log In</translate>
|
||||
<translate translate-context="Head/Login/Title">
|
||||
Log In
|
||||
</translate>
|
||||
</h3>
|
||||
<login-form button-classes="success" :show-signup="false"></login-form>
|
||||
<div class="ui hidden clearing divider"></div>
|
||||
<login-form
|
||||
button-classes="success"
|
||||
:show-signup="false"
|
||||
/>
|
||||
<div class="ui hidden clearing divider" />
|
||||
</div>
|
||||
<div class="four wide column">
|
||||
<h3 class="header">
|
||||
<translate translate-context="*/Signup/Title">Sign up</translate>
|
||||
<translate translate-context="*/Signup/Title">
|
||||
Sign up
|
||||
</translate>
|
||||
</h3>
|
||||
<template v-if="openRegistrations">
|
||||
<p>
|
||||
<translate translate-context="Content/Home/Paragraph">Sign up now to keep track of your favorites, create playlists, discover new content and much more!</translate>
|
||||
<translate translate-context="Content/Home/Paragraph">
|
||||
Sign up now to keep track of your favorites, create playlists, discover new content and much more!
|
||||
</translate>
|
||||
</p>
|
||||
<p v-if="defaultUploadQuota">
|
||||
<translate translate-context="Content/Home/Paragraph" :translate-params="{quota: humanSize(defaultUploadQuota * 1000 * 1000)}">Users on this pod also get %{ quota } of free storage to upload their own content!</translate>
|
||||
<translate
|
||||
translate-context="Content/Home/Paragraph"
|
||||
:translate-params="{quota: humanSize(defaultUploadQuota * 1000 * 1000)}"
|
||||
>
|
||||
Users on this pod also get %{ quota } of free storage to upload their own content!
|
||||
</translate>
|
||||
</p>
|
||||
<signup-form button-classes="success" :show-login="false"></signup-form>
|
||||
<signup-form
|
||||
button-classes="success"
|
||||
:show-login="false"
|
||||
/>
|
||||
</template>
|
||||
<div v-else>
|
||||
<p translate-context="Content/Home/Paragraph">Registrations are closed on this pod. You can signup on another pod using the link below.</p>
|
||||
<a target="_blank" rel="noopener" href="https://funkwhale.audio/#get-started">
|
||||
<i class="external alternate icon"></i>
|
||||
<p translate-context="Content/Home/Paragraph">
|
||||
Registrations are closed on this pod. You can signup on another pod using the link below.
|
||||
</p>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href="https://funkwhale.audio/#get-started"
|
||||
>
|
||||
<i class="external alternate icon" />
|
||||
<translate translate-context="Content/Home/Link">Find another pod</translate>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -125,39 +224,63 @@
|
|||
|
||||
<div class="four wide column">
|
||||
<h3 class="header">
|
||||
<translate translate-context="Content/Home/Header">Useful links</translate>
|
||||
<translate translate-context="Content/Home/Header">
|
||||
Useful links
|
||||
</translate>
|
||||
</h3>
|
||||
<div class="ui relaxed list">
|
||||
<div class="item">
|
||||
<i class="headphones icon"></i>
|
||||
<i class="headphones icon" />
|
||||
<div class="content">
|
||||
<router-link v-if="anonymousCanListen" class="header" to="/library">
|
||||
<translate translate-context="Content/Home/Link">Browse public content</translate>
|
||||
<router-link
|
||||
v-if="anonymousCanListen"
|
||||
class="header"
|
||||
to="/library"
|
||||
>
|
||||
<translate translate-context="Content/Home/Link">
|
||||
Browse public content
|
||||
</translate>
|
||||
</router-link>
|
||||
<div class="description">
|
||||
<translate translate-context="Content/Home/Link">Listen to public albums and playlists shared on this pod</translate>
|
||||
<translate translate-context="Content/Home/Link">
|
||||
Listen to public albums and playlists shared on this pod
|
||||
</translate>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<i class="mobile alternate icon"></i>
|
||||
<i class="mobile alternate icon" />
|
||||
<div class="content">
|
||||
<a class="header" href="https://funkwhale.audio/apps" target="_blank" rel="noopener">
|
||||
<a
|
||||
class="header"
|
||||
href="https://funkwhale.audio/apps"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<translate translate-context="Content/Home/Link">Mobile apps</translate>
|
||||
</a>
|
||||
<div class="description">
|
||||
<translate translate-context="Content/Home/Link">Use Funkwhale on other devices with our apps</translate>
|
||||
<translate translate-context="Content/Home/Link">
|
||||
Use Funkwhale on other devices with our apps
|
||||
</translate>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<i class="book icon"></i>
|
||||
<i class="book icon" />
|
||||
<div class="content">
|
||||
<a class="header" href="https://docs.funkwhale.audio/users/index.html" target="_blank" rel="noopener">
|
||||
<a
|
||||
class="header"
|
||||
href="https://docs.funkwhale.audio/users/index.html"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<translate translate-context="Content/Home/Link">User guides</translate>
|
||||
</a>
|
||||
<div class="description">
|
||||
<translate translate-context="Content/Home/Link">Discover everything you need to know about Funkwhale and its features</translate>
|
||||
<translate translate-context="Content/Home/Link">
|
||||
Discover everything you need to know about Funkwhale and its features
|
||||
</translate>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -165,20 +288,37 @@
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="anonymousCanListen" class="ui vertical stripe segment">
|
||||
<album-widget :filters="{playable: true, ordering: '-creation_date'}" :limit="10">
|
||||
<template slot="title"><translate translate-context="Content/Home/Title">Recently added albums</translate></template>
|
||||
<section
|
||||
v-if="anonymousCanListen"
|
||||
class="ui vertical stripe segment"
|
||||
>
|
||||
<album-widget
|
||||
:filters="{playable: true, ordering: '-creation_date'}"
|
||||
:limit="10"
|
||||
>
|
||||
<template slot="title">
|
||||
<translate translate-context="Content/Home/Title">
|
||||
Recently added albums
|
||||
</translate>
|
||||
</template>
|
||||
<router-link to="/library">
|
||||
<translate translate-context="Content/Home/Link">View more…</translate>
|
||||
<div class="ui hidden divider"></div>
|
||||
<translate translate-context="Content/Home/Link">
|
||||
View more…
|
||||
</translate>
|
||||
<div class="ui hidden divider" />
|
||||
</router-link>
|
||||
</album-widget>
|
||||
<div class="ui hidden section divider"></div>
|
||||
<h3 class="ui header" >
|
||||
<translate translate-context="*/*/*">New channels</translate>
|
||||
<div class="ui hidden section divider" />
|
||||
<h3 class="ui header">
|
||||
<translate translate-context="*/*/*">
|
||||
New channels
|
||||
</translate>
|
||||
</h3>
|
||||
<channels-widget :show-modification-date="true" :limit="10" :filters="{ordering: '-creation_date', external: 'false'}"></channels-widget>
|
||||
|
||||
<channels-widget
|
||||
:show-modification-date="true"
|
||||
:limit="10"
|
||||
:filters="{ordering: '-creation_date', external: 'false'}"
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
@ -186,20 +326,20 @@
|
|||
<script>
|
||||
import $ from 'jquery'
|
||||
import _ from '@/lodash'
|
||||
import {mapState} from 'vuex'
|
||||
import { mapState } from 'vuex'
|
||||
import showdown from 'showdown'
|
||||
import AlbumWidget from "@/components/audio/album/Widget"
|
||||
import ChannelsWidget from "@/components/audio/ChannelsWidget"
|
||||
import LoginForm from "@/components/auth/LoginForm"
|
||||
import SignupForm from "@/components/auth/SignupForm"
|
||||
import {humanSize } from '@/filters'
|
||||
import AlbumWidget from '@/components/audio/album/Widget'
|
||||
import ChannelsWidget from '@/components/audio/ChannelsWidget'
|
||||
import LoginForm from '@/components/auth/LoginForm'
|
||||
import SignupForm from '@/components/auth/SignupForm'
|
||||
import { humanSize } from '@/filters'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AlbumWidget,
|
||||
ChannelsWidget,
|
||||
LoginForm,
|
||||
SignupForm,
|
||||
SignupForm
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
@ -210,15 +350,15 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
nodeinfo: state => state.instance.nodeinfo,
|
||||
nodeinfo: state => state.instance.nodeinfo
|
||||
}),
|
||||
labels() {
|
||||
labels () {
|
||||
return {
|
||||
title: this.$pgettext('Head/Home/Title', "Home")
|
||||
title: this.$pgettext('Head/Home/Title', 'Home')
|
||||
}
|
||||
},
|
||||
podName() {
|
||||
return _.get(this.nodeinfo, 'metadata.nodeName') || "Funkwhale"
|
||||
podName () {
|
||||
return _.get(this.nodeinfo, 'metadata.nodeName') || 'Funkwhale'
|
||||
},
|
||||
banner () {
|
||||
return _.get(this.nodeinfo, 'metadata.banner')
|
||||
|
@ -236,12 +376,12 @@ export default {
|
|||
if (!this.longDescription) {
|
||||
return
|
||||
}
|
||||
let doc = this.markdown.makeHtml(this.longDescription)
|
||||
let nodes = $.parseHTML(doc)
|
||||
let excerptParts = []
|
||||
const doc = this.markdown.makeHtml(this.longDescription)
|
||||
const nodes = $.parseHTML(doc)
|
||||
const excerptParts = []
|
||||
let handled = 0
|
||||
nodes.forEach((n) => {
|
||||
let content = n.innerHTML || n.nodeValue
|
||||
const content = n.innerHTML || n.nodeValue
|
||||
if (handled < this.excerptLength && content.trim()) {
|
||||
excerptParts.push(n)
|
||||
handled += 1
|
||||
|
@ -250,9 +390,9 @@ export default {
|
|||
return excerptParts.map((p) => { return p.outerHTML }).join('')
|
||||
},
|
||||
stats () {
|
||||
let data = {
|
||||
const data = {
|
||||
users: _.get(this.nodeinfo, 'usage.users.activeMonth', null),
|
||||
hours: _.get(this.nodeinfo, 'metadata.library.music.hours', null),
|
||||
hours: _.get(this.nodeinfo, 'metadata.library.music.hours', null)
|
||||
}
|
||||
if (data.users === null || data.artists === null) {
|
||||
return
|
||||
|
@ -271,16 +411,16 @@ export default {
|
|||
openRegistrations () {
|
||||
return _.get(this.nodeinfo, 'openRegistrations')
|
||||
},
|
||||
headerStyle() {
|
||||
headerStyle () {
|
||||
if (!this.banner) {
|
||||
return ""
|
||||
return ''
|
||||
}
|
||||
return (
|
||||
"background-image: url(" +
|
||||
this.$store.getters["instance/absoluteUrl"](this.banner) +
|
||||
")"
|
||||
'background-image: url(' +
|
||||
this.$store.getters['instance/absoluteUrl'](this.banner) +
|
||||
')'
|
||||
)
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$store.state.auth.authenticated': {
|
||||
|
|
|
@ -1,31 +1,50 @@
|
|||
<template>
|
||||
<svg version="1.1" id="layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 141.7 141.7" enable-background="new 0 0 141.7 141.7" xml:space="preserve">
|
||||
<svg
|
||||
id="layer_1"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 141.7 141.7"
|
||||
enable-background="new 0 0 141.7 141.7"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<g>
|
||||
<g>
|
||||
<path :fill="fill" d="M70.9,86.1c11.7,0,21.2-9.5,21.2-21.2c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1c0,6-4.9,11-11,11
|
||||
c-6,0-11-4.9-11-11c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1C49.7,76.6,59.2,86.1,70.9,86.1z"/>
|
||||
<path :fill="fill" d="M70.9,106.1c22.7,0,41.2-18.5,41.2-41.2c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1
|
||||
<path
|
||||
:fill="fill"
|
||||
d="M70.9,86.1c11.7,0,21.2-9.5,21.2-21.2c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1c0,6-4.9,11-11,11
|
||||
c-6,0-11-4.9-11-11c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1C49.7,76.6,59.2,86.1,70.9,86.1z"
|
||||
/>
|
||||
<path
|
||||
:fill="fill"
|
||||
d="M70.9,106.1c22.7,0,41.2-18.5,41.2-41.2c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1
|
||||
c0,17.1-13.9,31-31,31c-17.1,0-31-13.9-31-31c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1C29.6,87.6,48.1,106.1,70.9,106.1z"
|
||||
/>
|
||||
<path :fill="fill" d="M131.1,63.8h-8c-0.6,0-1.1,0.5-1.1,1.1C122,93.1,99,116,70.9,116c-28.2,0-51.1-22.9-51.1-51.1
|
||||
<path
|
||||
:fill="fill"
|
||||
d="M131.1,63.8h-8c-0.6,0-1.1,0.5-1.1,1.1C122,93.1,99,116,70.9,116c-28.2,0-51.1-22.9-51.1-51.1
|
||||
c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1c0,33.8,27.5,61.3,61.3,61.3c33.8,0,61.3-27.5,61.3-61.3
|
||||
C132.2,64.3,131.7,63.8,131.1,63.8z"/>
|
||||
C132.2,64.3,131.7,63.8,131.1,63.8z"
|
||||
/>
|
||||
</g>
|
||||
<path :fill="fill" d="M43.3,37.3c4.1,2.1,8.5,2.5,12.5,4.8c2.6,1.5,4.2,3.2,5.8,5.7c2.5,3.8,2.4,8.5,2.4,8.5l0.3,5.2
|
||||
<path
|
||||
:fill="fill"
|
||||
d="M43.3,37.3c4.1,2.1,8.5,2.5,12.5,4.8c2.6,1.5,4.2,3.2,5.8,5.7c2.5,3.8,2.4,8.5,2.4,8.5l0.3,5.2
|
||||
c0,0,2,5.2,6.4,5.2c4.7,0,6.4-5.2,6.4-5.2l0.3-5.2c0,0-0.1-4.7,2.4-8.5c1.6-2.5,3.2-4.3,5.8-5.7c4-2.3,8.4-2.7,12.5-4.8
|
||||
c4.1-2.1,8.1-4.8,10.8-8.6c2.7-3.8,4-8.8,2.5-13.2c-7.8-0.4-16.8,0.5-23.7,4.2c-9.6,5.1-15.4,3.3-17.1,10.9h-0.1
|
||||
c-1.7-7.7-7.5-5.8-17.1-10.9c-6.9-3.7-15.9-4.6-23.7-4.2c-1.5,4.4-0.2,9.4,2.5,13.2C35.2,32.5,39.2,35.2,43.3,37.3z"/>
|
||||
c-1.7-7.7-7.5-5.8-17.1-10.9c-6.9-3.7-15.9-4.6-23.7-4.2c-1.5,4.4-0.2,9.4,2.5,13.2C35.2,32.5,39.2,35.2,43.3,37.3z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
fill: {type: String, default: '#222222'}
|
||||
fill: { type: String, default: '#222222' }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<svg viewBox="0 0 271.66678 53.49133" version="1.1">
|
||||
<svg
|
||||
viewBox="0 0 271.66678 53.49133"
|
||||
version="1.1"
|
||||
>
|
||||
<g transform="translate(34.65295 -109.48195)">
|
||||
<g>
|
||||
<g transform="matrix(.3191 0 0 .3191 -45.91741 93.47184)">
|
||||
|
@ -14,7 +17,11 @@
|
|||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-.75595 -.75595)" :fill="text" stroke-width=".74383">
|
||||
<g
|
||||
transform="translate(-.75595 -.75595)"
|
||||
:fill="text"
|
||||
stroke-width=".74383"
|
||||
>
|
||||
<path d="M32.84591 132.89252c0-6.69443 2.6034-9.29781 10.41356-9.29781 1.63641 0 3.71912.14876 4.83486.37191.59506.14876 1.11574.59506 1.11574 1.11574v2.00832c0 .59506-.4463 1.11574-1.11574 1.11574h-.66944c-.8182 0-1.48765-.29753-2.529-.29753-4.83487 0-5.80184.96698-5.80184 4.98363v.29753h6.62004c.59506 0 1.11574.4463 1.11574 1.11574v2.15709c0 .66945-.4463 1.11574-1.11574 1.11574h-6.62004v11.30614c0 .59506-.4463 1.11574-1.11574 1.11574h-4.01666c-.59506 0-1.11574-.52068-1.11574-1.11574z" />
|
||||
<path d="M57.02023 141.59528c0 3.04968 1.41327 4.31418 3.49598 4.31418 1.78518 0 3.49598-1.2645 4.83487-2.60339v-12.12435c0-.59506.52068-1.11573 1.11574-1.11573h4.09103c.59506 0 1.11574.52067 1.11574 1.11573v17.70304c0 .59506-.4463 1.11574-1.11574 1.11574h-4.09104c-.59505 0-1.11573-.52068-1.11573-1.11574v-1.19012c-1.7108 1.48765-3.57036 2.67777-6.32252 2.67777-4.83486 0-8.25646-2.529-8.25646-8.70275v-10.41355c0-.59506.4463-1.11574 1.11574-1.11574h4.09104c.59506 0 1.11574.52068 1.11574 1.11574v10.33917z" />
|
||||
<path d="M90.71552 138.47121c0-3.04968-1.41327-4.31419-3.49598-4.31419-1.78518 0-3.57036 1.26451-4.90925 2.60339v12.19874c0 .59506-.4463 1.11573-1.11573 1.11573h-4.09104c-.66945 0-1.11574-.52067-1.11574-1.11573v-17.77743c0-.59506.4463-1.11573 1.11574-1.11573h4.16542c.59506 0 1.11574.52067 1.11574 1.11573v1.19012c1.7108-1.48765 3.57036-2.67777 6.3969-2.67777 4.83486 0 8.25645 2.52901 8.25645 8.70276v10.41355c0 .59506-.4463 1.11574-1.11573 1.11574h-4.09104c-.59506 0-1.11574-.52068-1.11574-1.11574z" />
|
||||
|
@ -33,9 +40,9 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
primary: {type: String, default: '#009fe3'},
|
||||
secondary: {type: String, default: 'var(--text-color)'},
|
||||
text: {type: String, default: 'var(--text-color)'},
|
||||
primary: { type: String, default: '#009fe3' },
|
||||
secondary: { type: String, default: 'var(--text-color)' },
|
||||
text: { type: String, default: 'var(--text-color)' }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,19 +1,33 @@
|
|||
<template>
|
||||
<main class="main pusher" :v-title="labels.title">
|
||||
<main
|
||||
class="main pusher"
|
||||
:v-title="labels.title"
|
||||
>
|
||||
<section class="ui vertical stripe segment">
|
||||
<div class="ui text container">
|
||||
<h1 class="ui huge header">
|
||||
<i class="warning icon"></i>
|
||||
<i class="warning icon" />
|
||||
<div class="content">
|
||||
<translate translate-context="Content/*/Title">Page not found!</translate>
|
||||
<translate translate-context="Content/*/Title">
|
||||
Page not found!
|
||||
</translate>
|
||||
</div>
|
||||
</h1>
|
||||
<p><translate translate-context="Content/*/Paragraph">Sorry, the page you asked for does not exist:</translate></p>
|
||||
<p>
|
||||
<translate translate-context="Content/*/Paragraph">
|
||||
Sorry, the page you asked for does not exist:
|
||||
</translate>
|
||||
</p>
|
||||
<a :href="path">{{ path }}</a>
|
||||
<div class="ui hidden divider"></div>
|
||||
<router-link class="ui icon labeled right button" to="/">
|
||||
<translate translate-context="Content/*/Button.Label/Verb">Go to home page</translate>
|
||||
<i class="right arrow icon"></i>
|
||||
<div class="ui hidden divider" />
|
||||
<router-link
|
||||
class="ui icon labeled right button"
|
||||
to="/"
|
||||
>
|
||||
<translate translate-context="Content/*/Button.Label/Verb">
|
||||
Go to home page
|
||||
</translate>
|
||||
<i class="right arrow icon" />
|
||||
</router-link>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -22,15 +36,15 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
data: function() {
|
||||
data: function () {
|
||||
return {
|
||||
path: window.location.href
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels() {
|
||||
labels () {
|
||||
return {
|
||||
title: this.$pgettext('Head/*/Title', "Page Not Found")
|
||||
title: this.$pgettext('Head/*/Title', 'Page Not Found')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,57 +1,68 @@
|
|||
<template>
|
||||
<div v-if='maxPage > 1' class="ui pagination menu component-pagination" role="navigation" :aria-label="labels.pagination">
|
||||
<a href
|
||||
<div
|
||||
v-if="maxPage > 1"
|
||||
class="ui pagination menu component-pagination"
|
||||
role="navigation"
|
||||
:aria-label="labels.pagination"
|
||||
>
|
||||
<a
|
||||
href
|
||||
:disabled="current - 1 < 1"
|
||||
role="button"
|
||||
:aria-label="labels.previousPage"
|
||||
:class="[{'disabled': current - 1 < 1}, 'item']"
|
||||
@click.prevent.stop="selectPage(current - 1)"
|
||||
:class="[{'disabled': current - 1 < 1}, 'item']"><i class="angle left icon"></i></a>
|
||||
><i class="angle left icon" /></a>
|
||||
<template v-if="!compact">
|
||||
<a href
|
||||
<a
|
||||
v-for="page in pages"
|
||||
:key="page"
|
||||
href
|
||||
:class="[{'active': page === current}, {'disabled': page === 'skip'}, 'item']"
|
||||
@click.prevent.stop="selectPage(page)"
|
||||
:class="[{'active': page === current}, {'disabled': page === 'skip'}, 'item']">
|
||||
>
|
||||
<span v-if="page !== 'skip'">{{ page }}</span>
|
||||
<span v-else>…</span>
|
||||
</a>
|
||||
</template>
|
||||
<a href
|
||||
<a
|
||||
href
|
||||
:disabled="current + 1 > maxPage"
|
||||
role="button"
|
||||
:aria-label="labels.nextPage"
|
||||
:class="[{'disabled': current + 1 > maxPage}, 'item']"
|
||||
@click.prevent.stop="selectPage(current + 1)"
|
||||
:class="[{'disabled': current + 1 > maxPage}, 'item']"><i class="angle right icon"></i></a>
|
||||
><i class="angle right icon" /></a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from "@/lodash"
|
||||
import _ from '@/lodash'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
current: { type: Number, default: 1 },
|
||||
paginateBy: { type: Number, default: 25 },
|
||||
total: { type: Number },
|
||||
total: { type: Number, required: true },
|
||||
compact: { type: Boolean, default: false }
|
||||
},
|
||||
computed: {
|
||||
labels() {
|
||||
labels () {
|
||||
return {
|
||||
pagination: this.$pgettext('Content/*/Hidden text/Noun', "Pagination"),
|
||||
previousPage: this.$pgettext('Content/*/Link', "Previous Page"),
|
||||
nextPage: this.$pgettext('Content/*/Link', "Next Page")
|
||||
pagination: this.$pgettext('Content/*/Hidden text/Noun', 'Pagination'),
|
||||
previousPage: this.$pgettext('Content/*/Link', 'Previous Page'),
|
||||
nextPage: this.$pgettext('Content/*/Link', 'Next Page')
|
||||
}
|
||||
},
|
||||
pages: function() {
|
||||
let range = 2
|
||||
let current = this.current
|
||||
let beginning = _.range(1, Math.min(this.maxPage, 1 + range))
|
||||
let middle = _.range(
|
||||
pages: function () {
|
||||
const range = 2
|
||||
const current = this.current
|
||||
const beginning = _.range(1, Math.min(this.maxPage, 1 + range))
|
||||
const middle = _.range(
|
||||
Math.max(1, current - range + 1),
|
||||
Math.min(this.maxPage, current + range)
|
||||
)
|
||||
let end = _.range(this.maxPage, Math.max(1, this.maxPage - range))
|
||||
const end = _.range(this.maxPage, Math.max(1, this.maxPage - range))
|
||||
let allowed = beginning.concat(middle, end)
|
||||
allowed = _.uniq(allowed)
|
||||
allowed = _.sortBy(allowed, [
|
||||
|
@ -59,11 +70,11 @@ export default {
|
|||
return e
|
||||
}
|
||||
])
|
||||
let final = []
|
||||
const final = []
|
||||
allowed.forEach(p => {
|
||||
let last = final.slice(-1)[0]
|
||||
const last = final.slice(-1)[0]
|
||||
let consecutive = true
|
||||
if (last === "skip") {
|
||||
if (last === 'skip') {
|
||||
consecutive = false
|
||||
} else {
|
||||
if (!last) {
|
||||
|
@ -75,25 +86,25 @@ export default {
|
|||
if (consecutive) {
|
||||
final.push(p)
|
||||
} else {
|
||||
if (p !== "skip") {
|
||||
final.push("skip")
|
||||
if (p !== 'skip') {
|
||||
final.push('skip')
|
||||
final.push(p)
|
||||
}
|
||||
}
|
||||
})
|
||||
return final
|
||||
},
|
||||
maxPage: function() {
|
||||
maxPage: function () {
|
||||
return Math.ceil(this.total / this.paginateBy)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectPage: function(page) {
|
||||
selectPage: function (page) {
|
||||
if (page > this.maxPage || page < 1) {
|
||||
return
|
||||
}
|
||||
if (this.current !== page) {
|
||||
this.$emit("page-changed", page)
|
||||
this.$emit('page-changed', page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,77 +1,148 @@
|
|||
<template>
|
||||
<section class="main with-background component-queue" :aria-label="labels.queue">
|
||||
<section
|
||||
class="main with-background component-queue"
|
||||
:aria-label="labels.queue"
|
||||
>
|
||||
<div :class="['ui vertical stripe queue segment', playerFocused ? 'player-focused' : '']">
|
||||
<div class="ui fluid container">
|
||||
<div class="ui stackable grid" id="queue-grid">
|
||||
<div
|
||||
id="queue-grid"
|
||||
class="ui stackable grid"
|
||||
>
|
||||
<div class="ui six wide column current-track">
|
||||
<div class="ui basic segment" id="player">
|
||||
<div
|
||||
id="player"
|
||||
class="ui basic segment"
|
||||
>
|
||||
<template v-if="currentTrack">
|
||||
<img ref="cover" alt="" v-if="currentTrack.cover && currentTrack.cover.urls.large_square_crop" :src="$store.getters['instance/absoluteUrl'](currentTrack.cover.urls.large_square_crop)">
|
||||
<img ref="cover" alt="" v-else-if="currentTrack.album && currentTrack.album.cover && currentTrack.album.cover.urls.large_square_crop" :src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.urls.large_square_crop)">
|
||||
<img class="ui image" alt="" v-else src="../assets/audio/default-cover.png">
|
||||
<img
|
||||
v-if="currentTrack.cover && currentTrack.cover.urls.large_square_crop"
|
||||
ref="cover"
|
||||
alt=""
|
||||
:src="$store.getters['instance/absoluteUrl'](currentTrack.cover.urls.large_square_crop)"
|
||||
>
|
||||
<img
|
||||
v-else-if="currentTrack.album && currentTrack.album.cover && currentTrack.album.cover.urls.large_square_crop"
|
||||
ref="cover"
|
||||
alt=""
|
||||
:src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.urls.large_square_crop)"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
class="ui image"
|
||||
alt=""
|
||||
src="../assets/audio/default-cover.png"
|
||||
>
|
||||
<h1 class="ui header">
|
||||
<div class="content ellipsis">
|
||||
<router-link class="small header discrete link track" :to="{name: 'library.tracks.detail', params: {id: currentTrack.id }}">
|
||||
<router-link
|
||||
class="small header discrete link track"
|
||||
:to="{name: 'library.tracks.detail', params: {id: currentTrack.id }}"
|
||||
>
|
||||
{{ currentTrack.title }}
|
||||
</router-link>
|
||||
<div class="sub header ellipsis">
|
||||
<router-link class="discrete link artist" :to="{name: 'library.artists.detail', params: {id: currentTrack.artist.id }}">{{ currentTrack.artist.name }}</router-link>
|
||||
<template v-if="currentTrack.album"> /
|
||||
<router-link class="discrete link album" :to="{name: 'library.albums.detail', params: {id: currentTrack.album.id }}">{{ currentTrack.album.title }}</router-link>
|
||||
<router-link
|
||||
class="discrete link artist"
|
||||
:to="{name: 'library.artists.detail', params: {id: currentTrack.artist.id }}"
|
||||
>
|
||||
{{ currentTrack.artist.name }}
|
||||
</router-link>
|
||||
<template v-if="currentTrack.album">
|
||||
/
|
||||
<router-link
|
||||
class="discrete link album"
|
||||
:to="{name: 'library.albums.detail', params: {id: currentTrack.album.id }}"
|
||||
>
|
||||
{{ currentTrack.album.title }}
|
||||
</router-link>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</h1>
|
||||
<div class="ui small warning message" v-if="currentTrack && errored">
|
||||
<div
|
||||
v-if="currentTrack && errored"
|
||||
class="ui small warning message"
|
||||
>
|
||||
<h3 class="header">
|
||||
<translate translate-context="Sidebar/Player/Error message.Title">The track cannot be loaded</translate>
|
||||
<translate translate-context="Sidebar/Player/Error message.Title">
|
||||
The track cannot be loaded
|
||||
</translate>
|
||||
</h3>
|
||||
<p v-if="hasNext && playing && $store.state.player.errorCount < $store.state.player.maxConsecutiveErrors">
|
||||
<translate translate-context="Sidebar/Player/Error message.Paragraph">The next track will play automatically in a few seconds…</translate>
|
||||
<i class="loading spinner icon"></i>
|
||||
<translate translate-context="Sidebar/Player/Error message.Paragraph">
|
||||
The next track will play automatically in a few seconds…
|
||||
</translate>
|
||||
<i class="loading spinner icon" />
|
||||
</p>
|
||||
<p>
|
||||
<translate translate-context="Sidebar/Player/Error message.Paragraph">You may have a connectivity issue.</translate>
|
||||
<translate translate-context="Sidebar/Player/Error message.Paragraph">
|
||||
You may have a connectivity issue.
|
||||
</translate>
|
||||
</p>
|
||||
</div>
|
||||
<div class="additional-controls tablet-and-below">
|
||||
<track-favorite-icon
|
||||
v-if="$store.state.auth.authenticated"
|
||||
:track="currentTrack"></track-favorite-icon>
|
||||
:track="currentTrack"
|
||||
/>
|
||||
<track-playlist-icon
|
||||
v-if="$store.state.auth.authenticated"
|
||||
:track="currentTrack"></track-playlist-icon>
|
||||
:track="currentTrack"
|
||||
/>
|
||||
<button
|
||||
v-if="$store.state.auth.authenticated"
|
||||
@click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})"
|
||||
:class="['ui', 'really', 'basic', 'circular', 'icon', 'button']"
|
||||
:aria-label="labels.addArtistContentFilter"
|
||||
:title="labels.addArtistContentFilter">
|
||||
<i :class="['eye slash outline', 'basic', 'icon']"></i>
|
||||
:title="labels.addArtistContentFilter"
|
||||
@click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})"
|
||||
>
|
||||
<i :class="['eye slash outline', 'basic', 'icon']" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="progress-wrapper">
|
||||
<div class="progress-area" v-if="currentTrack && !errored">
|
||||
<div
|
||||
v-if="currentTrack && !errored"
|
||||
class="progress-area"
|
||||
>
|
||||
<div
|
||||
ref="progress"
|
||||
:class="['ui', 'small', 'vibrant', {'indicating': isLoadingAudio}, 'progress']"
|
||||
@click="touchProgress">
|
||||
<div class="buffer bar" :data-percent="bufferProgress" :style="{ 'width': bufferProgress + '%' }"></div>
|
||||
<div class="position bar" :data-percent="progress" :style="{ 'width': progress + '%' }"></div>
|
||||
@click="touchProgress"
|
||||
>
|
||||
<div
|
||||
class="buffer bar"
|
||||
:data-percent="bufferProgress"
|
||||
:style="{ 'width': bufferProgress + '%' }"
|
||||
/>
|
||||
<div
|
||||
class="position bar"
|
||||
:data-percent="progress"
|
||||
:style="{ 'width': progress + '%' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-area" v-else>
|
||||
<div
|
||||
v-else
|
||||
class="progress-area"
|
||||
>
|
||||
<div
|
||||
ref="progress"
|
||||
:class="['ui', 'small', 'vibrant', 'progress']">
|
||||
<div class="buffer bar"></div>
|
||||
<div class="position bar"></div>
|
||||
:class="['ui', 'small', 'vibrant', 'progress']"
|
||||
>
|
||||
<div class="buffer bar" />
|
||||
<div class="position bar" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<template v-if="!isLoadingAudio">
|
||||
<a href="" :aria-label="labels.restart" class="left floated timer discrete start" @click.prevent="setCurrentTime(0)">{{currentTimeFormatted}}</a>
|
||||
<span class="right floated timer total">{{durationFormatted}}</span>
|
||||
<a
|
||||
href=""
|
||||
:aria-label="labels.restart"
|
||||
class="left floated timer discrete start"
|
||||
@click.prevent="setCurrentTime(0)"
|
||||
>{{ currentTimeFormatted }}</a>
|
||||
<span class="right floated timer total">{{ durationFormatted }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="left floated timer">00:00</span>
|
||||
|
@ -80,45 +151,47 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="player-controls tablet-and-below">
|
||||
<template>
|
||||
<span
|
||||
role="button"
|
||||
:title="labels.previousTrack"
|
||||
:aria-label="labels.previousTrack"
|
||||
class="control"
|
||||
:disabled="emptyQueue"
|
||||
@click.prevent.stop="$store.dispatch('queue/previous')"
|
||||
:disabled="emptyQueue">
|
||||
<i :class="['ui', 'backward step', {'disabled': emptyQueue}, 'icon']"></i>
|
||||
>
|
||||
<i :class="['ui', 'backward step', {'disabled': emptyQueue}, 'icon']" />
|
||||
</span>
|
||||
|
||||
<span
|
||||
role="button"
|
||||
v-if="!playing"
|
||||
role="button"
|
||||
:title="labels.play"
|
||||
:aria-label="labels.play"
|
||||
class="control"
|
||||
@click.prevent.stop="resumePlayback"
|
||||
class="control">
|
||||
<i :class="['ui', 'play', {'disabled': !currentTrack}, 'icon']"></i>
|
||||
>
|
||||
<i :class="['ui', 'play', {'disabled': !currentTrack}, 'icon']" />
|
||||
</span>
|
||||
<span
|
||||
role="button"
|
||||
v-else
|
||||
role="button"
|
||||
:title="labels.pause"
|
||||
:aria-label="labels.pause"
|
||||
class="control"
|
||||
@click.prevent.stop="pausePlayback"
|
||||
class="control">
|
||||
<i :class="['ui', 'pause', {'disabled': !currentTrack}, 'icon']"></i>
|
||||
>
|
||||
<i :class="['ui', 'pause', {'disabled': !currentTrack}, 'icon']" />
|
||||
</span>
|
||||
<span
|
||||
role="button"
|
||||
:title="labels.next"
|
||||
:aria-label="labels.next"
|
||||
class="control"
|
||||
:disabled="!hasNext"
|
||||
@click.prevent.stop="$store.dispatch('queue/next')"
|
||||
:disabled="!hasNext">
|
||||
<i :class="['ui', {'disabled': !hasNext}, 'forward step', 'icon']" ></i>
|
||||
>
|
||||
<i :class="['ui', {'disabled': !hasNext}, 'forward step', 'icon']" />
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -129,20 +202,30 @@
|
|||
<div class="content">
|
||||
<button
|
||||
class="ui right floated basic button"
|
||||
@click="$store.commit('ui/queueFocused', null)">
|
||||
<translate translate-context="*/Queue/*/Verb">Close</translate>
|
||||
@click="$store.commit('ui/queueFocused', null)"
|
||||
>
|
||||
<translate translate-context="*/Queue/*/Verb">
|
||||
Close
|
||||
</translate>
|
||||
</button>
|
||||
<button
|
||||
class="ui right floated basic button danger"
|
||||
@click="$store.dispatch('queue/clean')">
|
||||
<translate translate-context="*/Queue/*/Verb">Clear</translate>
|
||||
@click="$store.dispatch('queue/clean')"
|
||||
>
|
||||
<translate translate-context="*/Queue/*/Verb">
|
||||
Clear
|
||||
</translate>
|
||||
</button>
|
||||
{{ labels.queue }}
|
||||
<div class="sub header">
|
||||
<div>
|
||||
<translate translate-context="Sidebar/Queue/Text" :translate-params="{index: queue.currentIndex + 1, length: queue.tracks.length}">
|
||||
<translate
|
||||
translate-context="Sidebar/Queue/Text"
|
||||
:translate-params="{index: queue.currentIndex + 1, length: queue.tracks.length}"
|
||||
>
|
||||
Track %{ index } of %{ length }
|
||||
</translate><template v-if="!$store.state.radios.running"> -
|
||||
</translate><template v-if="!$store.state.radios.running">
|
||||
-
|
||||
<span :title="labels.duration">
|
||||
{{ timeLeft }}
|
||||
</span>
|
||||
|
@ -153,22 +236,53 @@
|
|||
</h2>
|
||||
</div>
|
||||
<table class="ui compact very basic fixed single line selectable unstackable table">
|
||||
<draggable v-model="tracks" tag="tbody" @update="reorder" handle=".handle">
|
||||
<draggable
|
||||
v-model="tracks"
|
||||
tag="tbody"
|
||||
handle=".handle"
|
||||
@update="reorder"
|
||||
>
|
||||
<tr
|
||||
v-for="(track, index) in tracks"
|
||||
:key="index"
|
||||
:class="['queue-item', {'active': index === queue.currentIndex}]">
|
||||
:class="['queue-item', {'active': index === queue.currentIndex}]"
|
||||
>
|
||||
<td class="handle">
|
||||
<i class="grip lines icon"></i>
|
||||
<i class="grip lines icon" />
|
||||
</td>
|
||||
<td class="image-cell" @click="$store.dispatch('queue/currentIndex', index)">
|
||||
<img class="ui mini image" alt="" v-if="track.cover && track.cover.urls.original" :src="$store.getters['instance/absoluteUrl'](track.cover.urls.medium_square_crop)">
|
||||
<img class="ui mini image" alt="" v-else-if="track.album && track.album.cover && track.album.cover.urls.original" :src="$store.getters['instance/absoluteUrl'](track.album.cover.urls.medium_square_crop)">
|
||||
<img class="ui mini image" alt="" v-else src="../assets/audio/default-cover.png">
|
||||
<td
|
||||
class="image-cell"
|
||||
@click="$store.dispatch('queue/currentIndex', index)"
|
||||
>
|
||||
<img
|
||||
v-if="track.cover && track.cover.urls.original"
|
||||
class="ui mini image"
|
||||
alt=""
|
||||
:src="$store.getters['instance/absoluteUrl'](track.cover.urls.medium_square_crop)"
|
||||
>
|
||||
<img
|
||||
v-else-if="track.album && track.album.cover && track.album.cover.urls.original"
|
||||
class="ui mini image"
|
||||
alt=""
|
||||
:src="$store.getters['instance/absoluteUrl'](track.album.cover.urls.medium_square_crop)"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
class="ui mini image"
|
||||
alt=""
|
||||
src="../assets/audio/default-cover.png"
|
||||
>
|
||||
</td>
|
||||
<td colspan="3" @click="$store.dispatch('queue/currentIndex', index)">
|
||||
<button class="title reset ellipsis" :title="track.title" :aria-label="labels.selectTrack">
|
||||
<strong>{{ track.title }}</strong><br />
|
||||
<td
|
||||
colspan="3"
|
||||
@click="$store.dispatch('queue/currentIndex', index)"
|
||||
>
|
||||
<button
|
||||
class="title reset ellipsis"
|
||||
:title="track.title"
|
||||
:aria-label="labels.selectTrack"
|
||||
>
|
||||
<strong>{{ track.title }}</strong><br>
|
||||
<span>
|
||||
{{ track.artist.name }}
|
||||
</span>
|
||||
|
@ -181,23 +295,44 @@
|
|||
</td>
|
||||
<td class="controls">
|
||||
<template v-if="$store.getters['favorites/isFavorite'](track.id)">
|
||||
<i class="pink heart icon"></i>
|
||||
<i class="pink heart icon" />
|
||||
</template>
|
||||
<button :aria-label="labels.removeFromQueue" :title="labels.removeFromQueue" @click.stop="cleanTrack(index)" :class="['ui', 'really', 'tiny', 'basic', 'circular', 'icon', 'button']">
|
||||
<i class="x icon"></i>
|
||||
<button
|
||||
:aria-label="labels.removeFromQueue"
|
||||
:title="labels.removeFromQueue"
|
||||
:class="['ui', 'really', 'tiny', 'basic', 'circular', 'icon', 'button']"
|
||||
@click.stop="cleanTrack(index)"
|
||||
>
|
||||
<i class="x icon" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</draggable>
|
||||
</table>
|
||||
|
||||
<div v-if="$store.state.radios.running" class="ui info message">
|
||||
<div
|
||||
v-if="$store.state.radios.running"
|
||||
class="ui info message"
|
||||
>
|
||||
<div class="content">
|
||||
<h3 class="header">
|
||||
<i class="feed icon"></i> <translate translate-context="Sidebar/Player/Title">You have a radio playing</translate>
|
||||
<i class="feed icon" /> <translate translate-context="Sidebar/Player/Title">
|
||||
You have a radio playing
|
||||
</translate>
|
||||
</h3>
|
||||
<p><translate translate-context="Sidebar/Player/Paragraph">New tracks will be appended here automatically.</translate></p>
|
||||
<button @click="$store.dispatch('radios/stop')" class="ui basic primary button"><translate translate-context="*/Player/Button.Label/Short, Verb">Stop radio</translate></button>
|
||||
<p>
|
||||
<translate translate-context="Sidebar/Player/Paragraph">
|
||||
New tracks will be appended here automatically.
|
||||
</translate>
|
||||
</p>
|
||||
<button
|
||||
class="ui basic primary button"
|
||||
@click="$store.dispatch('radios/stop')"
|
||||
>
|
||||
<translate translate-context="*/Player/Button.Label/Short, Verb">
|
||||
Stop radio
|
||||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -229,16 +364,6 @@ export default {
|
|||
time
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.focusTrap = createFocusTrap(this.$el, { allowOutsideClick: () => { return true } })
|
||||
this.focusTrap.activate()
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
this.scrollToCurrent()
|
||||
// delay is to let transition work
|
||||
}, 400)
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentIndex: state => state.queue.currentIndex,
|
||||
|
@ -298,6 +423,46 @@ export default {
|
|||
return this.$store.state.ui.queueFocused === 'player'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$store.state.ui.queueFocused': {
|
||||
handler (v) {
|
||||
if (v === 'queue') {
|
||||
this.$nextTick(() => {
|
||||
this.scrollToCurrent()
|
||||
})
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
'$store.state.queue.currentIndex': {
|
||||
handler () {
|
||||
this.$nextTick(() => {
|
||||
this.scrollToCurrent()
|
||||
})
|
||||
}
|
||||
},
|
||||
'$store.state.queue.tracks': {
|
||||
handler (v) {
|
||||
if (!v || v.length === 0) {
|
||||
this.$store.commit('ui/queueFocused', null)
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
'$route.fullPath' () {
|
||||
this.$store.commit('ui/queueFocused', null)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.focusTrap = createFocusTrap(this.$el, { allowOutsideClick: () => { return true } })
|
||||
this.focusTrap.activate()
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
this.scrollToCurrent()
|
||||
// delay is to let transition work
|
||||
}, 400)
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
cleanTrack: 'queue/cleanTrack',
|
||||
|
@ -348,36 +513,6 @@ export default {
|
|||
})
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$store.state.ui.queueFocused': {
|
||||
handler (v) {
|
||||
if (v === 'queue') {
|
||||
this.$nextTick(() => {
|
||||
this.scrollToCurrent()
|
||||
})
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
'$store.state.queue.currentIndex': {
|
||||
handler () {
|
||||
this.$nextTick(() => {
|
||||
this.scrollToCurrent()
|
||||
})
|
||||
}
|
||||
},
|
||||
'$store.state.queue.tracks': {
|
||||
handler (v) {
|
||||
if (!v || v.length === 0) {
|
||||
this.$store.commit('ui/queueFocused', null)
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
'$route.fullPath' () {
|
||||
this.$store.commit('ui/queueFocused', null)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,19 +1,51 @@
|
|||
<template>
|
||||
<div v-if="type === 'both' || type === undefined" class="two ui buttons">
|
||||
<button class="ui left floated labeled icon button" @click.prevent="changeType('rss')"><i class="feed icon"></i>
|
||||
<translate translate-context="Content/Search/Input.Label/Noun">RSS</translate>
|
||||
<div
|
||||
v-if="type === 'both' || type === undefined"
|
||||
class="two ui buttons"
|
||||
>
|
||||
<button
|
||||
class="ui left floated labeled icon button"
|
||||
@click.prevent="changeType('rss')"
|
||||
>
|
||||
<i class="feed icon" />
|
||||
<translate translate-context="Content/Search/Input.Label/Noun">
|
||||
RSS
|
||||
</translate>
|
||||
</button>
|
||||
<div class="or"></div>
|
||||
<button class="ui right floated right labeled icon button" @click.prevent="changeType('artists')"><i class="globe icon"></i>
|
||||
<translate translate-context="Content/Search/Input.Label/Noun">Fediverse</translate>
|
||||
<div class="or" />
|
||||
<button
|
||||
class="ui right floated right labeled icon button"
|
||||
@click.prevent="changeType('artists')"
|
||||
>
|
||||
<i class="globe icon" />
|
||||
<translate translate-context="Content/Search/Input.Label/Noun">
|
||||
Fediverse
|
||||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<form id="remote-search" :class="['ui', {loading: isLoading}, 'form']" @submit.stop.prevent="submit">
|
||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
||||
<h3 class="header"><translate translate-context="Content/*/Error message.Title">Error while fetching object</translate></h3>
|
||||
<form
|
||||
id="remote-search"
|
||||
:class="['ui', {loading: isLoading}, 'form']"
|
||||
@submit.stop.prevent="submit"
|
||||
>
|
||||
<div
|
||||
v-if="errors.length > 0"
|
||||
role="alert"
|
||||
class="ui negative message"
|
||||
>
|
||||
<h3 class="header">
|
||||
<translate translate-context="Content/*/Error message.Title">
|
||||
Error while fetching object
|
||||
</translate>
|
||||
</h3>
|
||||
<ul class="list">
|
||||
<li v-for="error in errors">{{ error }}</li>
|
||||
<li
|
||||
v-for="(error, key) in errors"
|
||||
:key="key"
|
||||
>
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ui required field">
|
||||
|
@ -21,19 +53,45 @@
|
|||
{{ labels.fieldLabel }}
|
||||
</label>
|
||||
<p v-if="type === 'rss'">
|
||||
<translate translate-context="Content/Fetch/Paragraph">Use this form to subscribe to an RSS feed from its URL.</translate>
|
||||
<translate translate-context="Content/Fetch/Paragraph">
|
||||
Use this form to subscribe to an RSS feed from its URL.
|
||||
</translate>
|
||||
</p>
|
||||
<p v-else-if="type === 'artists'">
|
||||
<translate translate-context="Content/Fetch/Paragraph">Use this form to subscribe to a channel hosted somewhere else on the Fediverse.</translate>
|
||||
<translate translate-context="Content/Fetch/Paragraph">
|
||||
Use this form to subscribe to a channel hosted somewhere else on the Fediverse.
|
||||
</translate>
|
||||
</p>
|
||||
<input type="text" name="object-id" id="object-id" :placeholder="labels.fieldPlaceholder" v-model="id" required>
|
||||
<input
|
||||
id="object-id"
|
||||
v-model="id"
|
||||
type="text"
|
||||
name="object-id"
|
||||
:placeholder="labels.fieldPlaceholder"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<button v-if="showSubmit" type="submit" :class="['ui', 'primary', {loading: isLoading}, 'button']" :disabled="isLoading || !id || id.length === 0">
|
||||
<translate translate-context="Content/Search/Input.Label/Noun">Search</translate>
|
||||
<button
|
||||
v-if="showSubmit"
|
||||
type="submit"
|
||||
:class="['ui', 'primary', {loading: isLoading}, 'button']"
|
||||
:disabled="isLoading || !id || id.length === 0"
|
||||
>
|
||||
<translate translate-context="Content/Search/Input.Label/Noun">
|
||||
Search
|
||||
</translate>
|
||||
</button>
|
||||
</form>
|
||||
<div v-if="!isLoading && fetch && fetch.status === 'finished' && !redirectRoute" role="alert" class="ui warning message">
|
||||
<p><translate translate-context="Content/*/Error message.Title">This kind of object isn't supported yet</translate></p>
|
||||
<div
|
||||
v-if="!isLoading && fetch && fetch.status === 'finished' && !redirectRoute"
|
||||
role="alert"
|
||||
class="ui warning message"
|
||||
>
|
||||
<p>
|
||||
<translate translate-context="Content/*/Error message.Title">
|
||||
This kind of object isn't supported yet
|
||||
</translate>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -42,20 +100,87 @@ import axios from 'axios'
|
|||
|
||||
export default {
|
||||
props: {
|
||||
initialId: { type: String, required: false},
|
||||
type: { type: String, required: false},
|
||||
redirect: { type: Boolean, default: true},
|
||||
showSubmit: { type: Boolean, default: true},
|
||||
standalone: { type: Boolean, default: true},
|
||||
initialId: { type: String, required: false, default: '' },
|
||||
initialType: { type: String, required: false, default: '' },
|
||||
redirect: { type: Boolean, default: true },
|
||||
showSubmit: { type: Boolean, default: true },
|
||||
standalone: { type: Boolean, default: true }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
type: this.initialType,
|
||||
id: this.initialId,
|
||||
fetch: null,
|
||||
obj: null,
|
||||
isLoading: false,
|
||||
errors: [],
|
||||
errors: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
let title = ''
|
||||
let fieldLabel = ''
|
||||
let fieldPlaceholder = ''
|
||||
if (this.type === 'rss') {
|
||||
title = this.$pgettext('Head/Fetch/Title', 'Subscribe to a podcast RSS feed')
|
||||
fieldLabel = this.$pgettext('*/*/*', 'RSS feed location')
|
||||
fieldPlaceholder = this.$pgettext('Head/Fetch/Field.Placeholder', 'https://website.example.com/rss.xml')
|
||||
} else if (this.type === 'artists') {
|
||||
title = this.$pgettext('Head/Fetch/Title', 'Subscribe to a podcast hosted on the Fediverse')
|
||||
fieldLabel = this.$pgettext('*/*/*', 'Fediverse object')
|
||||
fieldPlaceholder = this.$pgettext('Head/Fetch/Field.Placeholder', '@username@example.com')
|
||||
}
|
||||
return {
|
||||
title,
|
||||
fieldLabel,
|
||||
fieldPlaceholder
|
||||
}
|
||||
},
|
||||
objInfo () {
|
||||
if (this.fetch && this.fetch.status === 'finished') {
|
||||
return this.fetch.object
|
||||
}
|
||||
return null
|
||||
},
|
||||
redirectRoute () {
|
||||
if (!this.objInfo) {
|
||||
return
|
||||
}
|
||||
switch (this.objInfo.type) {
|
||||
case 'account': {
|
||||
const [username, domain] = this.objInfo.full_username.split('@')
|
||||
return { name: 'profile.full', params: { username, domain } }
|
||||
}
|
||||
case 'library':
|
||||
return { name: 'library.detail', params: { id: this.objInfo.uuid } }
|
||||
case 'artist':
|
||||
return { name: 'library.artists.detail', params: { id: this.objInfo.id } }
|
||||
case 'album':
|
||||
return { name: 'library.albums.detail', params: { id: this.objInfo.id } }
|
||||
case 'track':
|
||||
return { name: 'library.tracks.detail', params: { id: this.objInfo.id } }
|
||||
case 'upload':
|
||||
return { name: 'library.uploads.detail', params: { id: this.objInfo.uuid } }
|
||||
case 'channel':
|
||||
return { name: 'channels.detail', params: { id: this.objInfo.uuid } }
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
initialId (v) {
|
||||
this.id = v
|
||||
this.createFetch()
|
||||
},
|
||||
redirectRoute (v) {
|
||||
if (v && this.redirect) {
|
||||
this.$router.push(v)
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
|
@ -67,60 +192,9 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels() {
|
||||
let title = ""
|
||||
let fieldLabel = ""
|
||||
let fieldPlaceholder = ""
|
||||
if (this.type === "rss") {
|
||||
title = this.$pgettext('Head/Fetch/Title', "Subscribe to a podcast RSS feed")
|
||||
fieldLabel = this.$pgettext('*/*/*', "RSS feed location")
|
||||
fieldPlaceholder = this.$pgettext('Head/Fetch/Field.Placeholder', "https://website.example.com/rss.xml")
|
||||
} else if (this.type === 'artists') {
|
||||
title = this.$pgettext('Head/Fetch/Title', "Subscribe to a podcast hosted on the Fediverse")
|
||||
fieldLabel = this.$pgettext('*/*/*', "Fediverse object")
|
||||
fieldPlaceholder = this.$pgettext('Head/Fetch/Field.Placeholder', "@username@example.com")
|
||||
}
|
||||
return {
|
||||
title,
|
||||
fieldLabel,
|
||||
fieldPlaceholder,
|
||||
}
|
||||
},
|
||||
objInfo () {
|
||||
if (this.fetch && this.fetch.status === 'finished') {
|
||||
return this.fetch.object
|
||||
}
|
||||
},
|
||||
redirectRoute () {
|
||||
if (!this.objInfo) {
|
||||
return
|
||||
}
|
||||
switch (this.objInfo.type) {
|
||||
case 'account':
|
||||
let [username, domain] = this.objInfo.full_username.split('@')
|
||||
return {name: 'profile.full', params: {username, domain}}
|
||||
case 'library':
|
||||
return {name: 'library.detail', params: {id: this.objInfo.uuid}}
|
||||
case 'artist':
|
||||
return {name: 'library.artists.detail', params: {id: this.objInfo.id}}
|
||||
case 'album':
|
||||
return {name: 'library.albums.detail', params: {id: this.objInfo.id}}
|
||||
case 'track':
|
||||
return {name: 'library.tracks.detail', params: {id: this.objInfo.id}}
|
||||
case 'upload':
|
||||
return {name: 'library.uploads.detail', params: {id: this.objInfo.uuid}}
|
||||
case 'channel':
|
||||
return {name: 'channels.detail', params: {id: this.objInfo.uuid}}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
changeType(newType) {
|
||||
changeType (newType) {
|
||||
this.type = newType
|
||||
},
|
||||
submit () {
|
||||
|
@ -135,13 +209,13 @@ export default {
|
|||
return
|
||||
}
|
||||
if (this.standalone) {
|
||||
this.$router.replace({name: "search", query: {id: this.id}})
|
||||
this.$router.replace({ name: 'search', query: { id: this.id } })
|
||||
}
|
||||
this.fetch = null
|
||||
let self = this
|
||||
const self = this
|
||||
self.errors = []
|
||||
self.isLoading = true
|
||||
let payload = {
|
||||
const payload = {
|
||||
object: this.id
|
||||
}
|
||||
|
||||
|
@ -150,7 +224,7 @@ export default {
|
|||
self.fetch = response.data
|
||||
if (self.fetch.status === 'errored' || self.fetch.status === 'skipped') {
|
||||
self.errors.push(
|
||||
self.$pgettext("Content/*/Error message.Title", "This object cannot be retrieved")
|
||||
self.$pgettext('Content/*/Error message.Title', 'This object cannot be retrieved')
|
||||
)
|
||||
}
|
||||
}, error => {
|
||||
|
@ -163,40 +237,27 @@ export default {
|
|||
return
|
||||
}
|
||||
if (this.standalone) {
|
||||
this.$router.replace({name: "search", query: {id: this.id, type: 'rss'}})
|
||||
this.$router.replace({ name: 'search', query: { id: this.id, type: 'rss' } })
|
||||
}
|
||||
this.fetch = null
|
||||
let self = this
|
||||
const self = this
|
||||
self.errors = []
|
||||
self.isLoading = true
|
||||
let payload = {
|
||||
const payload = {
|
||||
url: this.id
|
||||
}
|
||||
|
||||
axios.post('channels/rss-subscribe/', payload).then((response) => {
|
||||
self.isLoading = false
|
||||
self.$store.commit('channels/subscriptions', {uuid: response.data.channel.uuid, value: true})
|
||||
self.$store.commit('channels/subscriptions', { uuid: response.data.channel.uuid, value: true })
|
||||
self.$emit('subscribed', response.data)
|
||||
if (self.redirect) {
|
||||
self.$router.push({name: 'channels.detail', params: {id: response.data.channel.uuid}})
|
||||
self.$router.push({ name: 'channels.detail', params: { id: response.data.channel.uuid } })
|
||||
}
|
||||
|
||||
}, error => {
|
||||
self.isLoading = false
|
||||
self.errors = error.backendErrors
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
initialId (v) {
|
||||
this.id = v
|
||||
this.createFetch()
|
||||
},
|
||||
redirectRoute (v) {
|
||||
if (v && this.redirect) {
|
||||
this.$router.push(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
<template>
|
||||
<div class="ui toast-container">
|
||||
<message v-for="message in $store.state.ui.messages" :message="message" :key="message.key"></message>
|
||||
<slot></slot>
|
||||
<message
|
||||
v-for="message in $store.state.ui.messages"
|
||||
:key="message.key"
|
||||
:message="message"
|
||||
/>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,41 +1,105 @@
|
|||
<template>
|
||||
<modal @update:show="$emit('update:show', $event); isError = false" :show="show">
|
||||
<h3 class="header"><translate translate-context="Popup/Instance/Title">Choose your instance</translate></h3>
|
||||
<modal
|
||||
:show="show"
|
||||
@update:show="$emit('update:show', $event); isError = false"
|
||||
>
|
||||
<h3 class="header">
|
||||
<translate translate-context="Popup/Instance/Title">
|
||||
Choose your instance
|
||||
</translate>
|
||||
</h3>
|
||||
<div class="scrolling content">
|
||||
<div v-if="isError" role="alert" class="ui negative message">
|
||||
<h4 class="header"><translate translate-context="Popup/Instance/Error message.Title">It is not possible to connect to the given URL</translate></h4>
|
||||
<div
|
||||
v-if="isError"
|
||||
role="alert"
|
||||
class="ui negative message"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate translate-context="Popup/Instance/Error message.Title">
|
||||
It is not possible to connect to the given URL
|
||||
</translate>
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li><translate translate-context="Popup/Instance/Error message.List item">The server might be down</translate></li>
|
||||
<li><translate translate-context="Popup/Instance/Error message.List item">The given address is not a Funkwhale server</translate></li>
|
||||
<li>
|
||||
<translate translate-context="Popup/Instance/Error message.List item">
|
||||
The server might be down
|
||||
</translate>
|
||||
</li>
|
||||
<li>
|
||||
<translate translate-context="Popup/Instance/Error message.List item">
|
||||
The given address is not a Funkwhale server
|
||||
</translate>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<form class="ui form" @submit.prevent="checkAndSwitch(instanceUrl)">
|
||||
<p v-if="$store.state.instance.instanceUrl" class="description" translate-context="Popup/Login/Paragraph" v-translate="{url: $store.state.instance.instanceUrl, hostname: instanceHostname }">
|
||||
You are currently connected to <a href="%{ url }" target="_blank">%{ hostname } <i class="external icon"></i></a>. If you continue, you will be disconnected from your current instance and all your local data will be deleted.
|
||||
<form
|
||||
class="ui form"
|
||||
@submit.prevent="checkAndSwitch(instanceUrl)"
|
||||
>
|
||||
<p
|
||||
v-if="$store.state.instance.instanceUrl"
|
||||
v-translate="{url: $store.state.instance.instanceUrl, hostname: instanceHostname }"
|
||||
class="description"
|
||||
translate-context="Popup/Login/Paragraph"
|
||||
>
|
||||
You are currently connected to <a
|
||||
href="%{ url }"
|
||||
target="_blank"
|
||||
>%{ hostname } <i class="external icon" /></a>. If you continue, you will be disconnected from your current instance and all your local data will be deleted.
|
||||
</p>
|
||||
<p v-else>
|
||||
<translate translate-context="Popup/Instance/Paragraph">To continue, please select the Funkwhale instance you want to connect to. Enter the address directly, or select one of the suggested choices.</translate>
|
||||
<translate translate-context="Popup/Instance/Paragraph">
|
||||
To continue, please select the Funkwhale instance you want to connect to. Enter the address directly, or select one of the suggested choices.
|
||||
</translate>
|
||||
</p>
|
||||
<div class="field">
|
||||
<label for="instance-picker"><translate translate-context="Popup/Instance/Input.Label/Noun">Instance URL</translate></label>
|
||||
<div class="ui action input">
|
||||
<input id ="instance-picker" type="text" v-model="instanceUrl" placeholder="https://funkwhale.server">
|
||||
<button type="submit" :class="['ui', 'icon', {loading: isLoading}, 'button']">
|
||||
<translate translate-context="*/*/Button.Label/Verb">Submit</translate>
|
||||
<input
|
||||
id="instance-picker"
|
||||
v-model="instanceUrl"
|
||||
type="text"
|
||||
placeholder="https://funkwhale.server"
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
:class="['ui', 'icon', {loading: isLoading}, 'button']"
|
||||
>
|
||||
<translate translate-context="*/*/Button.Label/Verb">
|
||||
Submit
|
||||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="ui hidden divider"></div>
|
||||
<form class="ui form" @submit.prevent="">
|
||||
<div class="ui hidden divider" />
|
||||
<form
|
||||
class="ui form"
|
||||
@submit.prevent=""
|
||||
>
|
||||
<div class="field">
|
||||
<h4><translate translate-context="Popup/Instance/List.Label">Suggested choices</translate></h4>
|
||||
<button v-for="url in suggestedInstances" @click="checkAndSwitch(url)" class="ui basic button">{{ url }}</button>
|
||||
<h4>
|
||||
<translate translate-context="Popup/Instance/List.Label">
|
||||
Suggested choices
|
||||
</translate>
|
||||
</h4>
|
||||
<button
|
||||
v-for="(url, key) in suggestedInstances"
|
||||
:key="key"
|
||||
class="ui basic button"
|
||||
@click="checkAndSwitch(url)"
|
||||
>
|
||||
{{ url }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="ui basic cancel button"><translate translate-context="*/*/Button.Label/Verb">Cancel</translate></button>
|
||||
<button class="ui basic cancel button">
|
||||
<translate translate-context="*/*/Button.Label/Verb">
|
||||
Cancel
|
||||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
@ -43,25 +107,52 @@
|
|||
<script>
|
||||
import Modal from '@/components/semantic/Modal'
|
||||
import axios from 'axios'
|
||||
import _ from "@/lodash"
|
||||
import _ from '@/lodash'
|
||||
|
||||
export default {
|
||||
props: ['show'],
|
||||
components: {
|
||||
Modal,
|
||||
Modal
|
||||
},
|
||||
data() {
|
||||
props: { show: { type: Boolean, required: true } },
|
||||
data () {
|
||||
return {
|
||||
instanceUrl: null,
|
||||
nodeinfo: null,
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
path: 'api/v1/instance/nodeinfo/2.0/',
|
||||
path: 'api/v1/instance/nodeinfo/2.0/'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
suggestedInstances () {
|
||||
const instances = this.$store.state.instance.knownInstances.slice(0)
|
||||
if (this.$store.state.instance.frontSettings.defaultServerUrl) {
|
||||
let serverUrl = this.$store.state.instance.frontSettings.defaultServerUrl
|
||||
if (!serverUrl.endsWith('/')) {
|
||||
serverUrl = serverUrl + '/'
|
||||
}
|
||||
instances.push(serverUrl)
|
||||
}
|
||||
const self = this
|
||||
instances.push(this.$store.getters['instance/defaultUrl'](), 'https://demo.funkwhale.audio/')
|
||||
return _.uniq(instances.filter((e) => { return e !== self.$store.state.instance.instanceUrl }))
|
||||
},
|
||||
instanceHostname () {
|
||||
const url = this.$store.state.instance.instanceUrl
|
||||
const parser = document.createElement('a')
|
||||
parser.href = url
|
||||
return parser.hostname
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$store.state.instance.instanceUrl' () {
|
||||
this.$store.dispatch('instance/fetchSettings')
|
||||
this.fetchNodeInfo()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchNodeInfo () {
|
||||
let self = this
|
||||
const self = this
|
||||
axios.get('instance/nodeinfo/2.0/').then(response => {
|
||||
self.nodeinfo = response.data
|
||||
})
|
||||
|
@ -79,14 +170,14 @@ export default {
|
|||
return urlFetch
|
||||
},
|
||||
requestDistantNodeInfo (url) {
|
||||
var self = this
|
||||
const self = this
|
||||
axios.get(this.fetchUrl(url)).then(function (response) {
|
||||
self.isLoading = false
|
||||
if(!url.startsWith('https://') && !url.startsWith('http://')) {
|
||||
if (!url.startsWith('https://') && !url.startsWith('http://')) {
|
||||
url = `https://${url}`
|
||||
}
|
||||
self.switchInstance(url)
|
||||
}).catch(function (error) {
|
||||
}).catch(function () {
|
||||
self.isLoading = false
|
||||
self.isError = true
|
||||
})
|
||||
|
@ -95,12 +186,12 @@ export default {
|
|||
// Here we disconnect from the current instance and reconnect to the new one. No check is performed…
|
||||
this.$emit('update:show', false)
|
||||
this.isError = false
|
||||
let msg = this.$pgettext('*/Instance/Message', 'You are now using the Funkwhale instance at %{ url }')
|
||||
const msg = this.$pgettext('*/Instance/Message', 'You are now using the Funkwhale instance at %{ url }')
|
||||
this.$store.commit('ui/addMessage', {
|
||||
content: this.$gettextInterpolate(msg, {url: url}),
|
||||
content: this.$gettextInterpolate(msg, { url: url }),
|
||||
date: new Date()
|
||||
})
|
||||
let self = this
|
||||
const self = this
|
||||
this.$nextTick(() => {
|
||||
self.$store.commit('instance/instanceUrl', null)
|
||||
self.$store.dispatch('instance/setUrl', url)
|
||||
|
@ -111,34 +202,7 @@ export default {
|
|||
this.isError = false // Clear error message if any…
|
||||
this.isLoading = true
|
||||
this.requestDistantNodeInfo(url)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
suggestedInstances () {
|
||||
let instances = this.$store.state.instance.knownInstances.slice(0)
|
||||
if (this.$store.state.instance.frontSettings.defaultServerUrl) {
|
||||
let serverUrl = this.$store.state.instance.frontSettings.defaultServerUrl
|
||||
if (!serverUrl.endsWith('/')) {
|
||||
serverUrl = serverUrl + '/'
|
||||
}
|
||||
instances.push(serverUrl)
|
||||
}
|
||||
let self = this
|
||||
instances.push(this.$store.getters['instance/defaultUrl'](), 'https://demo.funkwhale.audio/')
|
||||
return _.uniq(instances.filter((e) => {return e != self.$store.state.instance.instanceUrl}))
|
||||
},
|
||||
instanceHostname() {
|
||||
let url = this.$store.state.instance.instanceUrl
|
||||
let parser = document.createElement("a")
|
||||
parser.href = url
|
||||
return parser.hostname
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'$store.state.instance.instanceUrl' () {
|
||||
this.$store.dispatch('instance/fetchSettings')
|
||||
this.fetchNodeInfo()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,18 +1,27 @@
|
|||
<template>
|
||||
<modal @update:show="$emit('update:show', $event)" :show="show">
|
||||
<modal
|
||||
:show="show"
|
||||
@update:show="$emit('update:show', $event)"
|
||||
>
|
||||
<header class="header">
|
||||
<translate translate-context="*/*/*/Noun">Keyboard shortcuts</translate>
|
||||
<translate translate-context="*/*/*/Noun">
|
||||
Keyboard shortcuts
|
||||
</translate>
|
||||
</header>
|
||||
<section class="scrolling content">
|
||||
<div class="ui stackable two column grid">
|
||||
<div class="column">
|
||||
<table
|
||||
class="ui compact basic table"
|
||||
v-for="section in player"
|
||||
:key="section.title">
|
||||
:key="section.title"
|
||||
class="ui compact basic table"
|
||||
>
|
||||
<caption>{{ section.title }}</caption>
|
||||
<tbody>
|
||||
<tr v-for="shortcut in section.shortcuts" :key="shortcut.summary">
|
||||
<tr
|
||||
v-for="shortcut in section.shortcuts"
|
||||
:key="shortcut.summary"
|
||||
>
|
||||
<td>{{ shortcut.summary }}</td>
|
||||
<td><span class="ui label">{{ shortcut.key }}</span></td>
|
||||
</tr>
|
||||
|
@ -21,12 +30,16 @@
|
|||
</div>
|
||||
<div class="column">
|
||||
<table
|
||||
class="ui compact basic table"
|
||||
v-for="section in general"
|
||||
:key="section.title">
|
||||
:key="section.title"
|
||||
class="ui compact basic table"
|
||||
>
|
||||
<caption>{{ section.title }}</caption>
|
||||
<tbody>
|
||||
<tr v-for="shortcut in section.shortcuts" :key="shortcut.summary">
|
||||
<tr
|
||||
v-for="shortcut in section.shortcuts"
|
||||
:key="shortcut.summary"
|
||||
>
|
||||
<td>{{ shortcut.summary }}</td>
|
||||
<td><span class="ui label">{{ shortcut.key }}</span></td>
|
||||
</tr>
|
||||
|
@ -36,7 +49,11 @@
|
|||
</div>
|
||||
</section>
|
||||
<footer class="actions">
|
||||
<button class="ui basic cancel button"><translate translate-context="*/*/Button.Label/Verb">Close</translate></button>
|
||||
<button class="ui basic cancel button">
|
||||
<translate translate-context="*/*/Button.Label/Verb">
|
||||
Close
|
||||
</translate>
|
||||
</button>
|
||||
</footer>
|
||||
</modal>
|
||||
</template>
|
||||
|
@ -44,10 +61,10 @@
|
|||
<script>
|
||||
|
||||
export default {
|
||||
props: ['show'],
|
||||
components: {
|
||||
Modal: () => import(/* webpackChunkName: "modal" */ "@/components/semantic/Modal"),
|
||||
Modal: () => import(/* webpackChunkName: "modal" */ '@/components/semantic/Modal')
|
||||
},
|
||||
props: { show: { type: Boolean, required: true } },
|
||||
computed: {
|
||||
general () {
|
||||
return [
|
||||
|
@ -65,9 +82,9 @@ export default {
|
|||
{
|
||||
key: 'esc',
|
||||
summary: this.$pgettext('Popup/Keyboard shortcuts/Table.Label/Verb', 'Unfocus searchbar')
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
@ -135,7 +152,7 @@ export default {
|
|||
{
|
||||
key: 'f',
|
||||
summary: this.$pgettext('Popup/Keyboard shortcuts/Table.Label/Verb', 'Toggle favorite')
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,86 +1,156 @@
|
|||
<template>
|
||||
<form :id="group.id" class="ui form component-settings-group" @submit.prevent="save">
|
||||
<form
|
||||
:id="group.id"
|
||||
class="ui form component-settings-group"
|
||||
@submit.prevent="save"
|
||||
>
|
||||
<div class="ui divider" />
|
||||
<h3 class="ui header">{{ group.label }}</h3>
|
||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
||||
<h4 class="header"><translate translate-context="Content/*/Error message.Title">Error while saving settings</translate></h4>
|
||||
<h3 class="ui header">
|
||||
{{ group.label }}
|
||||
</h3>
|
||||
<div
|
||||
v-if="errors.length > 0"
|
||||
role="alert"
|
||||
class="ui negative message"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate translate-context="Content/*/Error message.Title">
|
||||
Error while saving settings
|
||||
</translate>
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li v-for="error in errors">{{ error }}</li>
|
||||
<li
|
||||
v-for="(error, key) in errors"
|
||||
:key="key"
|
||||
>
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="result" class="ui positive message">
|
||||
<translate translate-context="Content/Settings/Paragraph">Settings updated successfully.</translate>
|
||||
<div
|
||||
v-if="result"
|
||||
class="ui positive message"
|
||||
>
|
||||
<translate translate-context="Content/Settings/Paragraph">
|
||||
Settings updated successfully.
|
||||
</translate>
|
||||
</div>
|
||||
<p v-if="group.help">{{ group.help }}</p>
|
||||
<div v-for="setting in settings" class="ui field">
|
||||
<p v-if="group.help">
|
||||
{{ group.help }}
|
||||
</p>
|
||||
<div
|
||||
v-for="(setting, key) in settings"
|
||||
:key="key"
|
||||
class="ui field"
|
||||
>
|
||||
<template v-if="setting.field.widget.class !== 'CheckboxInput'">
|
||||
<label :for="setting.identifier">{{ setting.verbose_name }}</label>
|
||||
<p v-if="setting.help_text">{{ setting.help_text }}</p>
|
||||
<p v-if="setting.help_text">
|
||||
{{ setting.help_text }}
|
||||
</p>
|
||||
</template>
|
||||
<content-form v-if="setting.fieldType === 'markdown'" v-model="values[setting.identifier]" v-bind="setting.fieldParams" />
|
||||
<content-form
|
||||
v-if="setting.fieldType === 'markdown'"
|
||||
v-model="values[setting.identifier]"
|
||||
v-bind="setting.fieldParams"
|
||||
/>
|
||||
<signup-form-builder
|
||||
v-else-if="setting.fieldType === 'formBuilder'"
|
||||
:value="values[setting.identifier]"
|
||||
:signup-approval-enabled="values.moderation__signup_approval_enabled"
|
||||
@input="set(setting.identifier, $event)" />
|
||||
@input="set(setting.identifier, $event)"
|
||||
/>
|
||||
<input
|
||||
:id="setting.identifier"
|
||||
:name="setting.identifier"
|
||||
v-else-if="setting.field.widget.class === 'PasswordInput'"
|
||||
:id="setting.identifier"
|
||||
v-model="values[setting.identifier]"
|
||||
:name="setting.identifier"
|
||||
type="password"
|
||||
class="ui input"
|
||||
v-model="values[setting.identifier]" />
|
||||
>
|
||||
<input
|
||||
:id="setting.identifier"
|
||||
:name="setting.identifier"
|
||||
v-else-if="setting.field.widget.class === 'TextInput'"
|
||||
:id="setting.identifier"
|
||||
v-model="values[setting.identifier]"
|
||||
:name="setting.identifier"
|
||||
type="text"
|
||||
class="ui input"
|
||||
v-model="values[setting.identifier]" />
|
||||
>
|
||||
<input
|
||||
:id="setting.identifier"
|
||||
:name="setting.identifier"
|
||||
v-else-if="setting.field.class === 'IntegerField'"
|
||||
:id="setting.identifier"
|
||||
v-model.number="values[setting.identifier]"
|
||||
:name="setting.identifier"
|
||||
type="number"
|
||||
class="ui input"
|
||||
v-model.number="values[setting.identifier]" />
|
||||
>
|
||||
<textarea
|
||||
:id="setting.identifier"
|
||||
:name="setting.identifier"
|
||||
v-else-if="setting.field.widget.class === 'Textarea'"
|
||||
:id="setting.identifier"
|
||||
v-model="values[setting.identifier]"
|
||||
:name="setting.identifier"
|
||||
type="text"
|
||||
class="ui input"
|
||||
v-model="values[setting.identifier]" />
|
||||
<div v-else-if="setting.field.widget.class === 'CheckboxInput'" class="ui toggle checkbox">
|
||||
/>
|
||||
<div
|
||||
v-else-if="setting.field.widget.class === 'CheckboxInput'"
|
||||
class="ui toggle checkbox"
|
||||
>
|
||||
<input
|
||||
:id="setting.identifier"
|
||||
:name="setting.identifier"
|
||||
v-model="values[setting.identifier]"
|
||||
type="checkbox" />
|
||||
:name="setting.identifier"
|
||||
type="checkbox"
|
||||
>
|
||||
<label :for="setting.identifier">{{ setting.verbose_name }}</label>
|
||||
<p v-if="setting.help_text">{{ setting.help_text }}</p>
|
||||
<p v-if="setting.help_text">
|
||||
{{ setting.help_text }}
|
||||
</p>
|
||||
</div>
|
||||
<select
|
||||
:id="setting.identifier"
|
||||
v-else-if="setting.field.class === 'MultipleChoiceField'"
|
||||
:id="setting.identifier"
|
||||
v-model="values[setting.identifier]"
|
||||
multiple
|
||||
class="ui search selection dropdown">
|
||||
<option v-for="v in setting.additional_data.choices" :value="v[0]">{{ v[1] }}</option>
|
||||
class="ui search selection dropdown"
|
||||
>
|
||||
<option
|
||||
v-for="(v, index) in setting.additional_data.choices"
|
||||
:key="index"
|
||||
:value="v[0]"
|
||||
>
|
||||
{{ v[1] }}
|
||||
</option>
|
||||
</select>
|
||||
<div v-else-if="setting.field.widget.class === 'ImageWidget'">
|
||||
<input :id="setting.identifier" type="file" :ref="setting.identifier">
|
||||
<input
|
||||
:id="setting.identifier"
|
||||
:ref="setting.identifier"
|
||||
type="file"
|
||||
>
|
||||
<div v-if="values[setting.identifier]">
|
||||
<div class="ui hidden divider"></div>
|
||||
<h3 class="ui header"><translate translate-context="Content/Settings/Title/Noun">Current image</translate></h3>
|
||||
<img class="ui image" alt="" v-if="values[setting.identifier]" :src="$store.getters['instance/absoluteUrl'](values[setting.identifier])" />
|
||||
<div class="ui hidden divider" />
|
||||
<h3 class="ui header">
|
||||
<translate translate-context="Content/Settings/Title/Noun">
|
||||
Current image
|
||||
</translate>
|
||||
</h3>
|
||||
<img
|
||||
v-if="values[setting.identifier]"
|
||||
class="ui image"
|
||||
alt=""
|
||||
:src="$store.getters['instance/absoluteUrl'](values[setting.identifier])"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']">
|
||||
<translate translate-context="Content/*/Button.Label/Verb">Save</translate>
|
||||
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']"
|
||||
>
|
||||
<translate translate-context="Content/*/Button.Label/Verb">
|
||||
Save
|
||||
</translate>
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
|
@ -91,12 +161,12 @@ import axios from 'axios'
|
|||
import lodash from '@/lodash'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
group: {type: Object, required: true},
|
||||
settingsData: {type: Array, required: true}
|
||||
},
|
||||
components: {
|
||||
SignupFormBuilder: () => import(/* webpackChunkName: "signup-form-builder" */ "@/components/admin/SignupFormBuilder"),
|
||||
SignupFormBuilder: () => import(/* webpackChunkName: "signup-form-builder" */ '@/components/admin/SignupFormBuilder')
|
||||
},
|
||||
props: {
|
||||
group: { type: Object, required: true },
|
||||
settingsData: { type: Array, required: true }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
@ -106,28 +176,44 @@ export default {
|
|||
isLoading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
settings () {
|
||||
const byIdentifier = {}
|
||||
this.settingsData.forEach(e => {
|
||||
byIdentifier[e.identifier] = e
|
||||
})
|
||||
return this.group.settings.map(e => {
|
||||
return { ...byIdentifier[e.name], fieldType: e.fieldType, fieldParams: e.fieldParams || {} }
|
||||
})
|
||||
},
|
||||
fileSettings () {
|
||||
return this.settings.filter((s) => {
|
||||
return s.field.widget.class === 'ImageWidget'
|
||||
})
|
||||
}
|
||||
},
|
||||
created () {
|
||||
let self = this
|
||||
const self = this
|
||||
this.settings.forEach(e => {
|
||||
self.values[e.identifier] = e.value
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
save () {
|
||||
let self = this
|
||||
const self = this
|
||||
this.isLoading = true
|
||||
self.errors = []
|
||||
self.result = null
|
||||
let postData = self.values
|
||||
let contentType = 'application/json'
|
||||
let fileSettingsIDs = this.fileSettings.map((s) => {return s.identifier})
|
||||
const fileSettingsIDs = this.fileSettings.map((s) => { return s.identifier })
|
||||
if (fileSettingsIDs.length > 0) {
|
||||
contentType = 'multipart/form-data'
|
||||
postData = new FormData()
|
||||
this.settings.forEach((s) => {
|
||||
if (fileSettingsIDs.indexOf(s.identifier) > -1) {
|
||||
let input = self.$refs[s.identifier][0]
|
||||
let files = input.files
|
||||
const input = self.$refs[s.identifier][0]
|
||||
const files = input.files
|
||||
console.log('ref', input, files)
|
||||
if (files && files.length > 0) {
|
||||
postData.append(s.identifier, files[0])
|
||||
|
@ -139,8 +225,8 @@ export default {
|
|||
}
|
||||
axios.post('instance/admin/settings/bulk/', postData, {
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
},
|
||||
'Content-Type': contentType
|
||||
}
|
||||
}).then((response) => {
|
||||
self.result = true
|
||||
response.data.forEach((s) => {
|
||||
|
@ -158,22 +244,6 @@ export default {
|
|||
this.values = lodash.cloneDeep(this.values)
|
||||
this.$set(this.values, key, value)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
settings () {
|
||||
let byIdentifier = {}
|
||||
this.settingsData.forEach(e => {
|
||||
byIdentifier[e.identifier] = e
|
||||
})
|
||||
return this.group.settings.map(e => {
|
||||
return {...byIdentifier[e.name], fieldType: e.fieldType, fieldParams: e.fieldParams || {}}
|
||||
})
|
||||
},
|
||||
fileSettings () {
|
||||
return this.settings.filter((s) => {
|
||||
return s.field.widget.class === 'ImageWidget'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,116 +1,173 @@
|
|||
<template>
|
||||
<div>
|
||||
|
||||
<div class="ui top attached tabular menu">
|
||||
<button :class="[{active: !isPreviewing}, 'item']" @click.stop.prevent="isPreviewing = false">
|
||||
<translate translate-context="Content/*/Button.Label/Verb">Edit form</translate>
|
||||
<button
|
||||
:class="[{active: !isPreviewing}, 'item']"
|
||||
@click.stop.prevent="isPreviewing = false"
|
||||
>
|
||||
<translate translate-context="Content/*/Button.Label/Verb">
|
||||
Edit form
|
||||
</translate>
|
||||
</button>
|
||||
<button :class="[{active: isPreviewing}, 'item']" @click.stop.prevent="isPreviewing = true">
|
||||
<translate translate-context="*/Form/Menu.item">Preview form</translate>
|
||||
<button
|
||||
:class="[{active: isPreviewing}, 'item']"
|
||||
@click.stop.prevent="isPreviewing = true"
|
||||
>
|
||||
<translate translate-context="*/Form/Menu.item">
|
||||
Preview form
|
||||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="isPreviewing" class="ui bottom attached segment">
|
||||
<div
|
||||
v-if="isPreviewing"
|
||||
class="ui bottom attached segment"
|
||||
>
|
||||
<signup-form
|
||||
:customization="local"
|
||||
:signup-approval-enabled="signupApprovalEnabled"
|
||||
:fetch-description-html="true"></signup-form>
|
||||
<div class="ui clearing hidden divider"></div>
|
||||
:fetch-description-html="true"
|
||||
/>
|
||||
<div class="ui clearing hidden divider" />
|
||||
</div>
|
||||
<div v-else class="ui bottom attached segment">
|
||||
<div
|
||||
v-else
|
||||
class="ui bottom attached segment"
|
||||
>
|
||||
<div class="field">
|
||||
<label for="help-text">
|
||||
<translate translate-context="*/*/Label">Help text</translate>
|
||||
</label>
|
||||
<p>
|
||||
<translate translate-context="*/*/Help">An optional text to be displayed at the start of the sign-up form.</translate>
|
||||
<translate translate-context="*/*/Help">
|
||||
An optional text to be displayed at the start of the sign-up form.
|
||||
</translate>
|
||||
</p>
|
||||
<content-form
|
||||
field-id="help-text"
|
||||
:permissive="true"
|
||||
:value="(local.help_text || {}).text"
|
||||
@input="update('help_text.text', $event)"></content-form>
|
||||
@input="update('help_text.text', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>
|
||||
<translate translate-context="*/*/Label">Additional fields</translate>
|
||||
</label>
|
||||
<p>
|
||||
<translate translate-context="*/*/Help">Additional form fields to be displayed in the form. Only shown if manual sign-up validation is enabled.</translate>
|
||||
<translate translate-context="*/*/Help">
|
||||
Additional form fields to be displayed in the form. Only shown if manual sign-up validation is enabled.
|
||||
</translate>
|
||||
</p>
|
||||
<table v-if="local.fields.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<translate translate-context="*/*/Form-builder,Help">Field label</translate>
|
||||
<translate translate-context="*/*/Form-builder,Help">
|
||||
Field label
|
||||
</translate>
|
||||
</th>
|
||||
<th>
|
||||
<translate translate-context="*/*/Form-builder,Help">Field type</translate>
|
||||
<translate translate-context="*/*/Form-builder,Help">
|
||||
Field type
|
||||
</translate>
|
||||
</th>
|
||||
<th>
|
||||
<translate translate-context="*/*/Form-builder,Help">Required</translate>
|
||||
<translate translate-context="*/*/Form-builder,Help">
|
||||
Required
|
||||
</translate>
|
||||
</th>
|
||||
<th><span class="visually-hidden"><translate translate-context="*/*/Form-builder,Help">Actions</translate></span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(field, idx) in local.fields">
|
||||
<tr
|
||||
v-for="(field, idx) in local.fields"
|
||||
:key="idx"
|
||||
>
|
||||
<td>
|
||||
<input type="text" v-model="field.label" required>
|
||||
<input
|
||||
v-model="field.label"
|
||||
type="text"
|
||||
required
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<select v-model="field.input_type">
|
||||
<option value="short_text">
|
||||
<translate translate-context="*/*/Form-builder">Short text</translate>
|
||||
<translate translate-context="*/*/Form-builder">
|
||||
Short text
|
||||
</translate>
|
||||
</option>
|
||||
<option value="long_text">
|
||||
<translate translate-context="*/*/Form-builder">Long text</translate>
|
||||
<translate translate-context="*/*/Form-builder">
|
||||
Long text
|
||||
</translate>
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select v-model="field.required">
|
||||
<option :value="true">
|
||||
<translate translate-context="*/*/*">Yes</translate>
|
||||
<translate translate-context="*/*/*">
|
||||
Yes
|
||||
</translate>
|
||||
</option>
|
||||
<option :value="false">
|
||||
<translate translate-context="*/*/*">No</translate>
|
||||
<translate translate-context="*/*/*">
|
||||
No
|
||||
</translate>
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<i
|
||||
:disabled="idx === 0"
|
||||
@click="move(idx, -1)" role="button"
|
||||
role="button"
|
||||
:title="labels.up"
|
||||
:class="['up', 'arrow', {disabled: idx === 0}, 'icon']"></i>
|
||||
:class="['up', 'arrow', {disabled: idx === 0}, 'icon']"
|
||||
@click="move(idx, -1)"
|
||||
/>
|
||||
<i
|
||||
:disabled="idx >= local.fields.length - 1"
|
||||
@click="move(idx, 1)" role="button"
|
||||
role="button"
|
||||
:title="labels.down"
|
||||
:class="['down', 'arrow', {disabled: idx >= local.fields.length - 1}, 'icon']"></i>
|
||||
<i @click="remove(idx)" role="button" :title="labels.delete" class="x icon"></i>
|
||||
:class="['down', 'arrow', {disabled: idx >= local.fields.length - 1}, 'icon']"
|
||||
@click="move(idx, 1)"
|
||||
/>
|
||||
<i
|
||||
role="button"
|
||||
:title="labels.delete"
|
||||
class="x icon"
|
||||
@click="remove(idx)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="ui hidden divider"></div>
|
||||
<button v-if="local.fields.length < maxFields" class="ui basic button" @click.stop.prevent="addField">
|
||||
<translate translate-context="*/*/Form-builder">Add a new field</translate>
|
||||
<div class="ui hidden divider" />
|
||||
<button
|
||||
v-if="local.fields.length < maxFields"
|
||||
class="ui basic button"
|
||||
@click.stop.prevent="addField"
|
||||
>
|
||||
<translate translate-context="*/*/Form-builder">
|
||||
Add a new field
|
||||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui hidden divider"></div>
|
||||
<div class="ui hidden divider" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import lodash from '@/lodash'
|
||||
|
||||
import SignupForm from "@/components/auth/SignupForm"
|
||||
import SignupForm from '@/components/auth/SignupForm'
|
||||
|
||||
function arrayMove(arr, oldIndex, newIndex) {
|
||||
function arrayMove (arr, oldIndex, newIndex) {
|
||||
if (newIndex >= arr.length) {
|
||||
var k = newIndex - arr.length + 1
|
||||
let k = newIndex - arr.length + 1
|
||||
while (k--) {
|
||||
arr.push(undefined)
|
||||
}
|
||||
|
@ -122,40 +179,40 @@ function arrayMove(arr, oldIndex, newIndex) {
|
|||
// v-model with objects is complex, cf
|
||||
// https://simonkollross.de/posts/vuejs-using-v-model-with-objects-for-custom-components
|
||||
export default {
|
||||
props: {
|
||||
value: {type: Object},
|
||||
signupApprovalEnabled: {type: Boolean},
|
||||
},
|
||||
components: {
|
||||
SignupForm
|
||||
},
|
||||
props: {
|
||||
value: { type: Object, required: true },
|
||||
signupApprovalEnabled: { type: Boolean }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
maxFields: 10,
|
||||
isPreviewing: false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$emit('input', this.local)
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
return {
|
||||
delete: this.$pgettext('*/*/*', 'Delete'),
|
||||
up: this.$pgettext('*/*/*', 'Move up'),
|
||||
down: this.$pgettext('*/*/*', 'Move down'),
|
||||
down: this.$pgettext('*/*/*', 'Move down')
|
||||
}
|
||||
},
|
||||
local() {
|
||||
return (this.value && this.value.fields) ? this.value : { help_text: {text: null, content_type: "text/markdown"}, fields: [] }
|
||||
local () {
|
||||
return (this.value && this.value.fields) ? this.value : { help_text: { text: null, content_type: 'text/markdown' }, fields: [] }
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$emit('input', this.local)
|
||||
},
|
||||
methods: {
|
||||
addField () {
|
||||
let newValue = lodash.tap(lodash.cloneDeep(this.local), v => v.fields.push({
|
||||
const newValue = lodash.tap(lodash.cloneDeep(this.local), v => v.fields.push({
|
||||
label: this.$pgettext('*/*/Form-builder', 'Additional field') + ' ' + (this.local.fields.length + 1),
|
||||
required: true,
|
||||
input_type: 'short_text',
|
||||
input_type: 'short_text'
|
||||
}))
|
||||
this.$emit('input', newValue)
|
||||
},
|
||||
|
@ -169,10 +226,10 @@ export default {
|
|||
if (idx + incr >= this.local.fields.length) {
|
||||
return
|
||||
}
|
||||
let newFields = arrayMove(lodash.cloneDeep(this.local).fields, idx, idx + incr)
|
||||
const newFields = arrayMove(lodash.cloneDeep(this.local).fields, idx, idx + incr)
|
||||
this.update('fields', newFields)
|
||||
},
|
||||
update(key, value) {
|
||||
update (key, value) {
|
||||
if (key === 'help_text.text') {
|
||||
key = 'help_text'
|
||||
if (!value || value.length === 0) {
|
||||
|
@ -180,12 +237,12 @@ export default {
|
|||
} else {
|
||||
value = {
|
||||
text: value,
|
||||
content_type: "text/markdown"
|
||||
content_type: 'text/markdown'
|
||||
}
|
||||
}
|
||||
}
|
||||
this.$emit('input', lodash.tap(lodash.cloneDeep(this.local), v => lodash.set(v, key, value)))
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,25 +1,34 @@
|
|||
<template>
|
||||
<router-link class="artist-label ui image label" :to="route">
|
||||
<img alt="" :class="[{circular: artist.content_category != 'podcast'}]" v-if="artist.cover && artist.cover.urls.original" v-lazy="$store.getters['instance/absoluteUrl'](artist.cover.urls.medium_square_crop)" />
|
||||
<i :class="[artist.content_category != 'podcast' ? 'circular' : 'bordered', 'inverted violet users icon']" v-else />
|
||||
<router-link
|
||||
class="artist-label ui image label"
|
||||
:to="route"
|
||||
>
|
||||
<img
|
||||
v-if="artist.cover && artist.cover.urls.original"
|
||||
v-lazy="$store.getters['instance/absoluteUrl'](artist.cover.urls.medium_square_crop)"
|
||||
alt=""
|
||||
:class="[{circular: artist.content_category != 'podcast'}]"
|
||||
>
|
||||
<i
|
||||
v-else
|
||||
:class="[artist.content_category != 'podcast' ? 'circular' : 'bordered', 'inverted violet users icon']"
|
||||
/>
|
||||
{{ artist.name }}
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {momentFormat} from '@/filters'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
artist: Object,
|
||||
artist: { type: Object, required: true }
|
||||
},
|
||||
computed: {
|
||||
route () {
|
||||
if (this.artist.channel) {
|
||||
return {name: 'channels.detail', params: {id: this.artist.channel.uuid}}
|
||||
return { name: 'channels.detail', params: { id: this.artist.channel.uuid } }
|
||||
}
|
||||
return {name: 'library.artists.detail', params: {id: this.artist.id}}
|
||||
return { name: 'library.artists.detail', params: { id: this.artist.id } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,67 +1,100 @@
|
|||
<template>
|
||||
<div class="card app-card">
|
||||
<div
|
||||
v-lazy:background-image="imageUrl"
|
||||
:class="['ui', 'head-image', {'circular': object.artist.content_category != 'podcast'}, {'padded': object.artist.content_category === 'podcast'}, 'image', {'default-cover': !object.artist.cover}]"
|
||||
@click="$router.push({name: 'channels.detail', params: {id: urlId}})"
|
||||
:class="['ui', 'head-image', {'circular': object.artist.content_category != 'podcast'}, {'padded': object.artist.content_category === 'podcast'}, 'image', {'default-cover': !object.artist.cover}]" v-lazy:background-image="imageUrl">
|
||||
<play-button :icon-only="true" :is-playable="true" :button-classes="['ui', 'circular', 'large', 'vibrant', 'icon', 'button']" :artist="object.artist"></play-button>
|
||||
>
|
||||
<play-button
|
||||
:icon-only="true"
|
||||
:is-playable="true"
|
||||
:button-classes="['ui', 'circular', 'large', 'vibrant', 'icon', 'button']"
|
||||
:artist="object.artist"
|
||||
/>
|
||||
</div>
|
||||
<div class="content">
|
||||
<strong>
|
||||
<router-link class="discrete link" :to="{name: 'channels.detail', params: {id: urlId}}">
|
||||
<router-link
|
||||
class="discrete link"
|
||||
:to="{name: 'channels.detail', params: {id: urlId}}"
|
||||
>
|
||||
{{ object.artist.name }}
|
||||
</router-link>
|
||||
</strong>
|
||||
<div class="description">
|
||||
<translate class="meta ellipsis" translate-context="Content/Channel/Paragraph"
|
||||
key="1"
|
||||
<translate
|
||||
v-if="object.artist.content_category === 'podcast'"
|
||||
key="1"
|
||||
class="meta ellipsis"
|
||||
translate-context="Content/Channel/Paragraph"
|
||||
translate-plural="%{ count } episodes"
|
||||
:translate-n="object.artist.tracks_count"
|
||||
:translate-params="{count: object.artist.tracks_count}">
|
||||
:translate-params="{count: object.artist.tracks_count}"
|
||||
>
|
||||
%{ count } episode
|
||||
</translate>
|
||||
<translate key="2" v-else translate-context="*/*/*" :translate-params="{count: object.artist.tracks_count}" :translate-n="object.artist.tracks_count" translate-plural="%{ count } tracks">%{ count } track</translate>
|
||||
<tags-list label-classes="tiny" :truncate-size="20" :limit="2" :show-more="false" :tags="object.artist.tags"></tags-list>
|
||||
<translate
|
||||
v-else
|
||||
key="2"
|
||||
translate-context="*/*/*"
|
||||
:translate-params="{count: object.artist.tracks_count}"
|
||||
:translate-n="object.artist.tracks_count"
|
||||
translate-plural="%{ count } tracks"
|
||||
>
|
||||
%{ count } track
|
||||
</translate>
|
||||
<tags-list
|
||||
label-classes="tiny"
|
||||
:truncate-size="20"
|
||||
:limit="2"
|
||||
:show-more="false"
|
||||
:tags="object.artist.tags"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<time
|
||||
v-translate
|
||||
class="meta ellipsis"
|
||||
:datetime="object.artist.modification_date"
|
||||
:title="updatedTitle">
|
||||
:title="updatedTitle"
|
||||
>
|
||||
%{ updatedAgo }
|
||||
</time>
|
||||
<play-button
|
||||
class="right floated basic icon"
|
||||
:dropdown-only="true"
|
||||
:is-playable="true"
|
||||
:dropdown-icon-classes="['ellipsis', 'horizontal', 'large really discrete']" :artist="object.artist" :channel="object" :account="object.attributed_to"></play-button>
|
||||
:dropdown-icon-classes="['ellipsis', 'horizontal', 'large really discrete']"
|
||||
:artist="object.artist"
|
||||
:channel="object"
|
||||
:account="object.attributed_to"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PlayButton from '@/components/audio/PlayButton'
|
||||
import TagsList from "@/components/tags/List"
|
||||
import TagsList from '@/components/tags/List'
|
||||
|
||||
import {momentFormat} from '@/filters'
|
||||
import moment from "moment"
|
||||
import { momentFormat } from '@/filters'
|
||||
import moment from 'moment'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
object: {type: Object},
|
||||
},
|
||||
components: {
|
||||
PlayButton,
|
||||
TagsList
|
||||
},
|
||||
props: {
|
||||
object: { type: Object, required: true }
|
||||
},
|
||||
computed: {
|
||||
imageUrl () {
|
||||
if (this.object.artist.cover) {
|
||||
return this.$store.getters['instance/absoluteUrl'](this.object.artist.cover.urls.medium_square_crop)
|
||||
}
|
||||
return null
|
||||
},
|
||||
urlId () {
|
||||
if (this.object.actor && this.object.actor.is_local) {
|
||||
|
@ -73,9 +106,9 @@ export default {
|
|||
}
|
||||
},
|
||||
updatedTitle () {
|
||||
let d = momentFormat(this.object.artist.modification_date)
|
||||
let message = this.$pgettext('*/*/*', 'Updated on %{ date }')
|
||||
return this.$gettextInterpolate(message, {date: d})
|
||||
const d = momentFormat(this.object.artist.modification_date)
|
||||
const message = this.$pgettext('*/*/*', 'Updated on %{ date }')
|
||||
return this.$gettextInterpolate(message, { date: d })
|
||||
},
|
||||
updatedAgo () {
|
||||
return moment(this.object.artist.modification_date).fromNow()
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
<template>
|
||||
<div>
|
||||
<slot></slot>
|
||||
<div class="ui hidden divider"></div>
|
||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||
<div class="ui loader"></div>
|
||||
<slot />
|
||||
<div class="ui hidden divider" />
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui inverted active dimmer"
|
||||
>
|
||||
<div class="ui loader" />
|
||||
</div>
|
||||
<podcast-table
|
||||
v-if="isPodcast"
|
||||
|
@ -16,9 +19,10 @@
|
|||
:show-album="false"
|
||||
:paginate-results="true"
|
||||
:total="count"
|
||||
@page-changed="updatePage"
|
||||
:page="page"
|
||||
:paginate-by="limit"></podcast-table>
|
||||
:paginate-by="limit"
|
||||
@page-changed="updatePage"
|
||||
/>
|
||||
<track-table
|
||||
v-else
|
||||
:default-cover="defaultCover"
|
||||
|
@ -30,13 +34,19 @@
|
|||
:show-album="false"
|
||||
:paginate-results="true"
|
||||
:total="count"
|
||||
@page-changed="updatePage"
|
||||
:page="page"
|
||||
:paginate-by="limit"></track-table>
|
||||
:paginate-by="limit"
|
||||
@page-changed="updatePage"
|
||||
/>
|
||||
<template v-if="!isLoading && objects.length === 0">
|
||||
<empty-state @refresh="fetchData('tracks/')" :refresh="true">
|
||||
<empty-state
|
||||
:refresh="true"
|
||||
@refresh="fetchData('tracks/')"
|
||||
>
|
||||
<p>
|
||||
<translate translate-context="Content/Channels/*">You may need to subscribe to this channel to see its content.</translate>
|
||||
<translate translate-context="Content/Channels/*">
|
||||
You may need to subscribe to this channel to see its content.
|
||||
</translate>
|
||||
</p>
|
||||
</empty-state>
|
||||
</template>
|
||||
|
@ -50,26 +60,31 @@ import PodcastTable from '@/components/audio/podcast/Table'
|
|||
import TrackTable from '@/components/audio/track/Table'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
filters: {type: Object, required: true},
|
||||
limit: {type: Number, default: 10},
|
||||
defaultCover: {type: Object},
|
||||
isPodcast: {type: Boolean, required: true},
|
||||
},
|
||||
components: {
|
||||
PodcastTable,
|
||||
TrackTable,
|
||||
TrackTable
|
||||
},
|
||||
props: {
|
||||
filters: { type: Object, required: true },
|
||||
limit: { type: Number, default: 10 },
|
||||
defaultCover: { type: Object, required: true },
|
||||
isPodcast: { type: Boolean, required: true }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
objects: [],
|
||||
count: 0,
|
||||
isLoading: false,
|
||||
errors: null,
|
||||
errors: [],
|
||||
nextPage: null,
|
||||
page: 1
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
page () {
|
||||
this.fetchData('tracks/')
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchData('tracks/')
|
||||
},
|
||||
|
@ -79,31 +94,26 @@ export default {
|
|||
return
|
||||
}
|
||||
this.isLoading = true
|
||||
let self = this
|
||||
let params = _.clone(this.filters)
|
||||
const self = this
|
||||
const params = _.clone(this.filters)
|
||||
params.page_size = this.limit
|
||||
params.page = this.page
|
||||
params.include_channels = true
|
||||
try {
|
||||
let channelsPromise = await axios.get(url, {params: params})
|
||||
const channelsPromise = await axios.get(url, { params: params })
|
||||
self.nextPage = channelsPromise.data.next
|
||||
self.objects = channelsPromise.data.results
|
||||
self.count = channelsPromise.data.count
|
||||
self.$emit('fetched', channelsPromise.data)
|
||||
self.isLoading = false
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
self.isLoading = false
|
||||
self.errors = error.backendErrors
|
||||
self.errors = e.backendErrors
|
||||
}
|
||||
},
|
||||
updatePage: function(page) {
|
||||
updatePage: function (page) {
|
||||
this.page = page
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
page() {
|
||||
this.fetchData('tracks/')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,48 +1,77 @@
|
|||
<template>
|
||||
<div :class="[{active: currentTrack && isPlaying && entry.id === currentTrack.id}, 'channel-entry-card']">
|
||||
<div class="controls">
|
||||
<play-button class="basic circular icon" :discrete="true" :icon-only="true" :is-playable="true" :button-classes="['ui', 'circular', 'inverted vibrant', 'icon', 'button']" :track="entry"></play-button>
|
||||
<play-button
|
||||
class="basic circular icon"
|
||||
:discrete="true"
|
||||
:icon-only="true"
|
||||
:is-playable="true"
|
||||
:button-classes="['ui', 'circular', 'inverted vibrant', 'icon', 'button']"
|
||||
:track="entry"
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
|
||||
alt=""
|
||||
class="channel-image image"
|
||||
v-if="cover && cover.urls.original"
|
||||
v-lazy="$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)">
|
||||
<img
|
||||
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
|
||||
v-lazy="$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)"
|
||||
alt=""
|
||||
class="channel-image image"
|
||||
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
|
||||
>
|
||||
<img
|
||||
v-else-if="entry.artist.content_category === 'podcast' && defaultCover != undefined"
|
||||
v-lazy="$store.getters['instance/absoluteUrl'](defaultCover.urls.medium_square_crop)">
|
||||
<img
|
||||
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
|
||||
alt=""
|
||||
v-lazy="$store.getters['instance/absoluteUrl'](defaultCover.urls.medium_square_crop)"
|
||||
class="channel-image image"
|
||||
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
|
||||
>
|
||||
<img
|
||||
v-else-if="entry.album && entry.album.cover && entry.album.cover.urls.original"
|
||||
v-lazy="$store.getters['instance/absoluteUrl'](entry.album.cover.urls.medium_square_crop)">
|
||||
<img
|
||||
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
|
||||
v-lazy="$store.getters['instance/absoluteUrl'](entry.album.cover.urls.medium_square_crop)"
|
||||
alt=""
|
||||
class="channel-image image"
|
||||
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
src="../../assets/audio/default-cover.png">
|
||||
alt=""
|
||||
class="channel-image image"
|
||||
src="../../assets/audio/default-cover.png"
|
||||
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
|
||||
>
|
||||
<div class="ellipsis content">
|
||||
<strong>
|
||||
<router-link class="discrete link" :to="{name: 'library.tracks.detail', params: {id: entry.id}}">
|
||||
<router-link
|
||||
class="discrete link"
|
||||
:to="{name: 'library.tracks.detail', params: {id: entry.id}}"
|
||||
>
|
||||
{{ entry.title }}
|
||||
</router-link>
|
||||
</strong>
|
||||
<br>
|
||||
<human-date class="really discrete" :date="entry.creation_date"></human-date>
|
||||
<human-date
|
||||
class="really discrete"
|
||||
:date="entry.creation_date"
|
||||
/>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<template v-if="$store.state.auth.authenticated && $store.getters['favorites/isFavorite'](entry.id)">
|
||||
<track-favorite-icon class="tiny" :track="entry"></track-favorite-icon>
|
||||
<track-favorite-icon
|
||||
class="tiny"
|
||||
:track="entry"
|
||||
/>
|
||||
</template>
|
||||
<human-duration v-if="duration" :duration="duration"></human-duration>
|
||||
<human-duration
|
||||
v-if="duration"
|
||||
:duration="duration"
|
||||
/>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<play-button class="play-button basic icon" :dropdown-only="true" :is-playable="entry.is_playable" :dropdown-icon-classes="['ellipsis', 'vertical', 'large really discrete']" :track="entry"></play-button>
|
||||
<play-button
|
||||
class="play-button basic icon"
|
||||
:dropdown-only="true"
|
||||
:is-playable="entry.is_playable"
|
||||
:dropdown-icon-classes="['ellipsis', 'vertical', 'large really discrete']"
|
||||
:track="entry"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -50,19 +79,21 @@
|
|||
<script>
|
||||
import PlayButton from '@/components/audio/PlayButton'
|
||||
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
||||
import { mapGetters } from "vuex"
|
||||
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
props: ['entry', 'defaultCover'],
|
||||
components: {
|
||||
PlayButton,
|
||||
TrackFavoriteIcon,
|
||||
TrackFavoriteIcon
|
||||
},
|
||||
props: {
|
||||
entry: { type: Object, required: true },
|
||||
defaultCover: { type: Object, required: true }
|
||||
},
|
||||
computed: {
|
||||
|
||||
...mapGetters({
|
||||
currentTrack: "queue/currentTrack",
|
||||
currentTrack: 'queue/currentTrack'
|
||||
}),
|
||||
|
||||
isPlaying () {
|
||||
|
@ -72,14 +103,16 @@ export default {
|
|||
if (this.entry.cover) {
|
||||
return this.entry.cover
|
||||
}
|
||||
return null
|
||||
},
|
||||
duration () {
|
||||
let uploads = this.entry.uploads.filter((e) => {
|
||||
const uploads = this.entry.uploads.filter((e) => {
|
||||
return e.duration
|
||||
})
|
||||
if (uploads.length > 0) {
|
||||
return uploads[0].duration
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,55 @@
|
|||
<template>
|
||||
<form class="ui form" @submit.prevent.stop="submit">
|
||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
||||
<h4 class="header"><translate translate-context="Content/*/Error message.Title">Error while saving channel</translate></h4>
|
||||
<form
|
||||
class="ui form"
|
||||
@submit.prevent.stop="submit"
|
||||
>
|
||||
<div
|
||||
v-if="errors.length > 0"
|
||||
role="alert"
|
||||
class="ui negative message"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate translate-context="Content/*/Error message.Title">
|
||||
Error while saving channel
|
||||
</translate>
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li v-for="error in errors">{{ error }}</li>
|
||||
<li
|
||||
v-for="(error, key) in errors"
|
||||
:key="key"
|
||||
>
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<template v-if="metadataChoices">
|
||||
<fieldset v-if="creating && step === 1" class="ui grouped channel-type required field">
|
||||
<fieldset
|
||||
v-if="creating && step === 1"
|
||||
class="ui grouped channel-type required field"
|
||||
>
|
||||
<legend>
|
||||
<translate translate-context="Content/Channel/Paragraph">What will this channel be used for?</translate>
|
||||
<translate translate-context="Content/Channel/Paragraph">
|
||||
What will this channel be used for?
|
||||
</translate>
|
||||
</legend>
|
||||
<div class="ui hidden divider"></div>
|
||||
<div class="ui hidden divider" />
|
||||
<div class="field">
|
||||
<div :class="['ui', 'radio', 'checkbox', {selected: choice.value == newValues.content_category}]" v-for="choice in categoryChoices">
|
||||
<input type="radio" name="channel-category" :id="`category-${choice.value}`" :value="choice.value" v-model="newValues.content_category">
|
||||
<div
|
||||
v-for="(choice, key) in categoryChoices"
|
||||
:key="key"
|
||||
:class="['ui', 'radio', 'checkbox', {selected: choice.value == newValues.content_category}]"
|
||||
>
|
||||
<input
|
||||
:id="`category-${choice.value}`"
|
||||
v-model="newValues.content_category"
|
||||
type="radio"
|
||||
name="channel-category"
|
||||
:value="choice.value"
|
||||
>
|
||||
<label :for="`category-${choice.value}`">
|
||||
<span :class="['right floated', 'placeholder', 'image', {circular: choice.value === 'music'}]"></span>
|
||||
<span :class="['right floated', 'placeholder', 'image', {circular: choice.value === 'music'}]" />
|
||||
<strong>{{ choice.label }}</strong>
|
||||
<div class="ui small hidden divider"></div>
|
||||
<div class="ui small hidden divider" />
|
||||
{{ choice.helpText }}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -29,20 +60,35 @@
|
|||
<label for="channel-name">
|
||||
<translate translate-context="Content/Channel/*">Name</translate>
|
||||
</label>
|
||||
<input type="text" required v-model="newValues.name" :placeholder="labels.namePlaceholder">
|
||||
<input
|
||||
v-model="newValues.name"
|
||||
type="text"
|
||||
required
|
||||
:placeholder="labels.namePlaceholder"
|
||||
>
|
||||
</div>
|
||||
<div class="ui required field">
|
||||
<label for="channel-username">
|
||||
<translate translate-context="Content/Channel/*">Fediverse handle</translate>
|
||||
</label>
|
||||
<div class="ui left labeled input">
|
||||
<div class="ui basic label">@</div>
|
||||
<input type="text" :required="creating" :disabled="!creating" :placeholder="labels.usernamePlaceholder" v-model="newValues.username">
|
||||
<div class="ui basic label">
|
||||
@
|
||||
</div>
|
||||
<input
|
||||
v-model="newValues.username"
|
||||
type="text"
|
||||
:required="creating"
|
||||
:disabled="!creating"
|
||||
:placeholder="labels.usernamePlaceholder"
|
||||
>
|
||||
</div>
|
||||
<template v-if="creating">
|
||||
<div class="ui small hidden divider"></div>
|
||||
<div class="ui small hidden divider" />
|
||||
<p>
|
||||
<translate translate-context="Content/Channels/Paragraph">Used in URLs and to follow this channel in the Fediverse. It cannot be changed later.</translate>
|
||||
<translate translate-context="Content/Channels/Paragraph">
|
||||
Used in URLs and to follow this channel in the Fediverse. It cannot be changed later.
|
||||
</translate>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -51,12 +97,17 @@
|
|||
v-model="newValues.cover"
|
||||
:required="false"
|
||||
:image-class="newValues.content_category === 'podcast' ? '' : 'circular'"
|
||||
@delete="newValues.cover = null">
|
||||
<translate translate-context="Content/Channel/*" slot="label">Channel Picture</translate>
|
||||
@delete="newValues.cover = null"
|
||||
>
|
||||
<translate
|
||||
slot="label"
|
||||
translate-context="Content/Channel/*"
|
||||
>
|
||||
Channel Picture
|
||||
</translate>
|
||||
</attachment-input>
|
||||
|
||||
</div>
|
||||
<div class="ui small hidden divider"></div>
|
||||
<div class="ui small hidden divider" />
|
||||
<div class="ui stackable grid row">
|
||||
<div class="ten wide column">
|
||||
<div class="ui field">
|
||||
|
@ -64,46 +115,67 @@
|
|||
<translate translate-context="*/*/*">Tags</translate>
|
||||
</label>
|
||||
<tags-selector
|
||||
v-model="newValues.tags"
|
||||
id="channel-tags"
|
||||
:required="false"></tags-selector>
|
||||
v-model="newValues.tags"
|
||||
:required="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="six wide column" v-if="newValues.content_category === 'podcast'">
|
||||
<div
|
||||
v-if="newValues.content_category === 'podcast'"
|
||||
class="six wide column"
|
||||
>
|
||||
<div class="ui required field">
|
||||
<label for="channel-language">
|
||||
<translate translate-context="*/*/*">Language</translate>
|
||||
</label>
|
||||
<select
|
||||
name="channel-language"
|
||||
id="channel-language"
|
||||
v-model="newValues.metadata.language"
|
||||
name="channel-language"
|
||||
required
|
||||
class="ui search selection dropdown">
|
||||
<option v-for="v in metadataChoices.language" :value="v.value">{{ v.label }}</option>
|
||||
class="ui search selection dropdown"
|
||||
>
|
||||
<option
|
||||
v-for="(v, key) in metadataChoices.language"
|
||||
:key="key"
|
||||
:value="v.value"
|
||||
>
|
||||
{{ v.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui small hidden divider"></div>
|
||||
<div class="ui small hidden divider" />
|
||||
<div class="ui field">
|
||||
<label for="channel-name">
|
||||
<translate translate-context="*/*/*">Description</translate>
|
||||
</label>
|
||||
<content-form v-model="newValues.description"></content-form>
|
||||
<content-form v-model="newValues.description" />
|
||||
</div>
|
||||
<div class="ui two fields" v-if="newValues.content_category === 'podcast'">
|
||||
<div
|
||||
v-if="newValues.content_category === 'podcast'"
|
||||
class="ui two fields"
|
||||
>
|
||||
<div class="ui required field">
|
||||
<label for="channel-itunes-category">
|
||||
<translate translate-context="*/*/*">Category</translate>
|
||||
</label>
|
||||
<select
|
||||
name="itunes-category"
|
||||
id="itunes-category"
|
||||
v-model="newValues.metadata.itunes_category"
|
||||
name="itunes-category"
|
||||
required
|
||||
class="ui dropdown">
|
||||
<option v-for="v in metadataChoices.itunes_category" :value="v.value">{{ v.label }}</option>
|
||||
class="ui dropdown"
|
||||
>
|
||||
<option
|
||||
v-for="(v, key) in metadataChoices.itunes_category"
|
||||
:key="key"
|
||||
:value="v.value"
|
||||
>
|
||||
{{ v.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
|
@ -111,45 +183,64 @@
|
|||
<translate translate-context="*/*/*">Subcategory</translate>
|
||||
</label>
|
||||
<select
|
||||
name="itunes-category"
|
||||
id="itunes-category"
|
||||
v-model="newValues.metadata.itunes_subcategory"
|
||||
name="itunes-category"
|
||||
:disabled="!newValues.metadata.itunes_category"
|
||||
class="ui dropdown">
|
||||
<option v-for="v in itunesSubcategories" :value="v">{{ v }}</option>
|
||||
class="ui dropdown"
|
||||
>
|
||||
<option
|
||||
v-for="(v, key) in itunesSubcategories"
|
||||
:key="key"
|
||||
:value="v"
|
||||
>
|
||||
{{ v }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui two fields" v-if="newValues.content_category === 'podcast'">
|
||||
<div
|
||||
v-if="newValues.content_category === 'podcast'"
|
||||
class="ui two fields"
|
||||
>
|
||||
<div class="ui field">
|
||||
<label for="channel-itunes-email">
|
||||
<translate translate-context="*/*/*">Owner e-mail address</translate>
|
||||
</label>
|
||||
<input
|
||||
name="channel-itunes-email"
|
||||
id="channel-itunes-email"
|
||||
v-model="newValues.metadata.owner_email"
|
||||
name="channel-itunes-email"
|
||||
type="email"
|
||||
v-model="newValues.metadata.owner_email">
|
||||
>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<label for="channel-itunes-name">
|
||||
<translate translate-context="*/*/*">Owner name</translate>
|
||||
</label>
|
||||
<input
|
||||
name="channel-itunes-name"
|
||||
id="channel-itunes-name"
|
||||
v-model="newValues.metadata.owner_name"
|
||||
name="channel-itunes-name"
|
||||
maxlength="255"
|
||||
v-model="newValues.metadata.owner_name">
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<translate translate-context="*/*/*">Used for the itunes:email and itunes:name field required by certain platforms such as Spotify or iTunes.</translate>
|
||||
<translate translate-context="*/*/*">
|
||||
Used for the itunes:email and itunes:name field required by certain platforms such as Spotify or iTunes.
|
||||
</translate>
|
||||
</p>
|
||||
</template>
|
||||
</template>
|
||||
<div v-else class="ui active inverted dimmer">
|
||||
<div
|
||||
v-else
|
||||
class="ui active inverted dimmer"
|
||||
>
|
||||
<div class="ui text loader">
|
||||
<translate translate-context="*/*/*">Loading</translate>
|
||||
<translate translate-context="*/*/*">
|
||||
Loading
|
||||
</translate>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -161,29 +252,25 @@ import axios from 'axios'
|
|||
import AttachmentInput from '@/components/common/AttachmentInput'
|
||||
import TagsSelector from '@/components/library/TagsSelector'
|
||||
|
||||
function slugify(text) {
|
||||
function slugify (text) {
|
||||
return text.toString().toLowerCase()
|
||||
.replace(/\s+/g, '') // Remove spaces
|
||||
.replace(/[^\w]+/g, '') // Remove all non-word chars
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
object: {type: Object, required: false, default: null},
|
||||
step: {type: Number, required: false, default: 1},
|
||||
},
|
||||
components: {
|
||||
AttachmentInput,
|
||||
TagsSelector
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchMetadataChoices()
|
||||
props: {
|
||||
object: { type: Object, required: false, default: null },
|
||||
step: { type: Number, required: false, default: 1 }
|
||||
},
|
||||
data () {
|
||||
let oldValues = {}
|
||||
const oldValues = {}
|
||||
if (this.object) {
|
||||
oldValues.metadata = {...(this.object.metadata || {})}
|
||||
oldValues.metadata = { ...(this.object.metadata || {}) }
|
||||
oldValues.name = this.object.artist.name
|
||||
oldValues.description = this.object.artist.description
|
||||
oldValues.cover = this.object.artist.cover
|
||||
|
@ -196,13 +283,13 @@ export default {
|
|||
errors: [],
|
||||
metadataChoices: null,
|
||||
newValues: {
|
||||
name: oldValues.name || "",
|
||||
username: oldValues.username || "",
|
||||
name: oldValues.name || '',
|
||||
username: oldValues.username || '',
|
||||
tags: oldValues.tags || [],
|
||||
description: (oldValues.description || {}).text || "",
|
||||
description: (oldValues.description || {}).text || '',
|
||||
cover: (oldValues.cover || {}).uuid || null,
|
||||
content_category: oldValues.content_category || "podcast",
|
||||
metadata: oldValues.metadata || {},
|
||||
content_category: oldValues.content_category || 'podcast',
|
||||
metadata: oldValues.metadata || {}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -213,20 +300,20 @@ export default {
|
|||
categoryChoices () {
|
||||
return [
|
||||
{
|
||||
value: "podcast",
|
||||
label: this.$pgettext('*/*/*', "Podcasts"),
|
||||
helpText: this.$pgettext('Content/Channels/Help', "Host your episodes and keep your community updated."),
|
||||
value: 'podcast',
|
||||
label: this.$pgettext('*/*/*', 'Podcasts'),
|
||||
helpText: this.$pgettext('Content/Channels/Help', 'Host your episodes and keep your community updated.')
|
||||
},
|
||||
{
|
||||
value: "music",
|
||||
label: this.$pgettext('*/*/*', "Artist discography"),
|
||||
helpText: this.$pgettext('Content/Channels/Help', "Publish music you make as a nice discography of albums and singles."),
|
||||
value: 'music',
|
||||
label: this.$pgettext('*/*/*', 'Artist discography'),
|
||||
helpText: this.$pgettext('Content/Channels/Help', 'Publish music you make as a nice discography of albums and singles.')
|
||||
}
|
||||
]
|
||||
},
|
||||
itunesSubcategories () {
|
||||
for (let index = 0; index < this.metadataChoices.itunes_category.length; index++) {
|
||||
const element = this.metadataChoices.itunes_category[index];
|
||||
const element = this.metadataChoices.itunes_category[index]
|
||||
if (element.value === this.newValues.metadata.itunes_category) {
|
||||
return element.children || []
|
||||
}
|
||||
|
@ -235,8 +322,8 @@ export default {
|
|||
},
|
||||
labels () {
|
||||
return {
|
||||
namePlaceholder: this.$pgettext('Content/Channel/Form.Field.Placeholder', "Awesome channel name"),
|
||||
usernamePlaceholder: this.$pgettext('Content/Channel/Form.Field.Placeholder', "awesomechannelname"),
|
||||
namePlaceholder: this.$pgettext('Content/Channel/Form.Field.Placeholder', 'Awesome channel name'),
|
||||
usernamePlaceholder: this.$pgettext('Content/Channel/Form.Field.Placeholder', 'awesomechannelname')
|
||||
}
|
||||
},
|
||||
submittable () {
|
||||
|
@ -247,9 +334,41 @@ export default {
|
|||
return !!v
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'newValues.name' (v) {
|
||||
if (this.creating) {
|
||||
this.newValues.username = slugify(v)
|
||||
}
|
||||
},
|
||||
'newValues.metadata.itunes_category' (v) {
|
||||
this.newValues.metadata.itunes_subcategory = null
|
||||
},
|
||||
'newValues.content_category': {
|
||||
handler (v) {
|
||||
this.$emit('category', v)
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
isLoading: {
|
||||
handler (v) {
|
||||
this.$emit('loading', v)
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
submittable: {
|
||||
handler (v) {
|
||||
this.$emit('submittable', v)
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchMetadataChoices()
|
||||
},
|
||||
methods: {
|
||||
fetchMetadataChoices () {
|
||||
let self = this
|
||||
const self = this
|
||||
axios.get('channels/metadata-choices').then((response) => {
|
||||
self.metadataChoices = response.data
|
||||
}, error => {
|
||||
|
@ -258,21 +377,21 @@ export default {
|
|||
},
|
||||
submit () {
|
||||
this.isLoading = true
|
||||
let self = this
|
||||
let handler = this.creating ? axios.post : axios.patch
|
||||
let url = this.creating ? `channels/` : `channels/${this.object.uuid}`
|
||||
let payload = {
|
||||
const self = this
|
||||
const handler = this.creating ? axios.post : axios.patch
|
||||
const url = this.creating ? 'channels/' : `channels/${this.object.uuid}`
|
||||
const payload = {
|
||||
name: this.newValues.name,
|
||||
username: this.newValues.username,
|
||||
tags: this.newValues.tags,
|
||||
content_category: this.newValues.content_category,
|
||||
cover: this.newValues.cover,
|
||||
metadata: this.newValues.metadata,
|
||||
metadata: this.newValues.metadata
|
||||
}
|
||||
if (this.newValues.description) {
|
||||
payload.description = {
|
||||
content_type: 'text/markdown',
|
||||
text: this.newValues.description,
|
||||
text: this.newValues.description
|
||||
}
|
||||
} else {
|
||||
payload.description = null
|
||||
|
@ -291,34 +410,6 @@ export default {
|
|||
self.$emit('errored', self.errors)
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"newValues.name" (v) {
|
||||
if (this.creating) {
|
||||
this.newValues.username = slugify(v)
|
||||
}
|
||||
},
|
||||
"newValues.metadata.itunes_category" (v) {
|
||||
this.newValues.metadata.itunes_subcategory = null
|
||||
},
|
||||
"newValues.content_category": {
|
||||
handler (v) {
|
||||
this.$emit("category", v)
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
isLoading: {
|
||||
handler (v) {
|
||||
this.$emit("loading", v)
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
submittable: {
|
||||
handler (v) {
|
||||
this.$emit("submittable", v)
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,28 +1,62 @@
|
|||
<template>
|
||||
<div class="channel-serie-card">
|
||||
<div class="two-images">
|
||||
<img alt="" @click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})" class="channel-image" v-if="cover && cover.urls.original" v-lazy="$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)">
|
||||
<img alt="" @click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})" class="channel-image" v-else src="../../assets/audio/default-cover.png">
|
||||
<img alt="" @click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})" class="channel-image" v-if="cover && cover.urls.original" v-lazy="$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)">
|
||||
<img alt="" @click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})" class="channel-image" v-else src="../../assets/audio/default-cover.png">
|
||||
<img
|
||||
v-if="cover && cover.urls.original"
|
||||
v-lazy="$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)"
|
||||
alt=""
|
||||
class="channel-image"
|
||||
@click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
class="channel-image"
|
||||
src="../../assets/audio/default-cover.png"
|
||||
@click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})"
|
||||
>
|
||||
<img
|
||||
v-if="cover && cover.urls.original"
|
||||
v-lazy="$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)"
|
||||
alt=""
|
||||
class="channel-image"
|
||||
@click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
class="channel-image"
|
||||
src="../../assets/audio/default-cover.png"
|
||||
@click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})"
|
||||
>
|
||||
</div>
|
||||
<div class="content ellipsis">
|
||||
<strong>
|
||||
<router-link class="discrete link" :to="{name: 'library.albums.detail', params: {id: serie.id}}">
|
||||
<router-link
|
||||
class="discrete link"
|
||||
:to="{name: 'library.albums.detail', params: {id: serie.id}}"
|
||||
>
|
||||
{{ serie.title }}
|
||||
</router-link>
|
||||
</strong>
|
||||
<div class="description">
|
||||
<translate translate-context="Content/Channel/Paragraph"
|
||||
<translate
|
||||
translate-context="Content/Channel/Paragraph"
|
||||
translate-plural="%{ count } episodes"
|
||||
:translate-n="serie.tracks_count"
|
||||
:translate-params="{count: serie.tracks_count}">
|
||||
:translate-params="{count: serie.tracks_count}"
|
||||
>
|
||||
%{ count } episode
|
||||
</translate>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<play-button :icon-only="true" :is-playable="true" :button-classes="['ui', 'circular', 'vibrant', 'icon', 'button']" :album="serie"></play-button>
|
||||
<play-button
|
||||
:icon-only="true"
|
||||
:is-playable="true"
|
||||
:button-classes="['ui', 'circular', 'vibrant', 'icon', 'button']"
|
||||
:album="serie"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -31,18 +65,19 @@
|
|||
import PlayButton from '@/components/audio/PlayButton'
|
||||
|
||||
export default {
|
||||
props: ['serie'],
|
||||
components: {
|
||||
PlayButton,
|
||||
PlayButton
|
||||
},
|
||||
props: { serie: { type: Object, required: true } },
|
||||
computed: {
|
||||
cover () {
|
||||
if (this.serie.cover) {
|
||||
return this.serie.cover
|
||||
}
|
||||
return null
|
||||
},
|
||||
duration () {
|
||||
let uploads = this.serie.uploads.filter((e) => {
|
||||
const uploads = this.serie.uploads.filter((e) => {
|
||||
return e.duration
|
||||
})
|
||||
return uploads[0].duration
|
||||
|
|
|
@ -1,26 +1,51 @@
|
|||
<template>
|
||||
<div>
|
||||
<slot></slot>
|
||||
<div class="ui hidden divider"></div>
|
||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||
<div class="ui loader"></div>
|
||||
<slot />
|
||||
<div class="ui hidden divider" />
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui inverted active dimmer"
|
||||
>
|
||||
<div class="ui loader" />
|
||||
</div>
|
||||
<template v-if="isPodcast">
|
||||
<channel-serie-card v-for="serie in objects" :serie="serie" :key="serie.id" />
|
||||
<channel-serie-card
|
||||
v-for="serie in objects"
|
||||
:key="serie.id"
|
||||
:serie="serie"
|
||||
/>
|
||||
</template>
|
||||
<div v-else class="ui app-cards cards">
|
||||
<album-card v-for="album in objects" :album="album" :key="album.id" />
|
||||
<div
|
||||
v-else
|
||||
class="ui app-cards cards"
|
||||
>
|
||||
<album-card
|
||||
v-for="album in objects"
|
||||
:key="album.id"
|
||||
:album="album"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="nextPage">
|
||||
<div class="ui hidden divider"></div>
|
||||
<button v-if="nextPage" @click="fetchData(nextPage)" :class="['ui', 'basic', 'button']">
|
||||
<translate translate-context="*/*/Button,Label">Show more</translate>
|
||||
<div class="ui hidden divider" />
|
||||
<button
|
||||
v-if="nextPage"
|
||||
:class="['ui', 'basic', 'button']"
|
||||
@click="fetchData(nextPage)"
|
||||
>
|
||||
<translate translate-context="*/*/Button,Label">
|
||||
Show more
|
||||
</translate>
|
||||
</button>
|
||||
</template>
|
||||
<template v-if="!isLoading && objects.length === 0">
|
||||
<empty-state @refresh="fetchData('albums/')" :refresh="true">
|
||||
<empty-state
|
||||
:refresh="true"
|
||||
@refresh="fetchData('albums/')"
|
||||
>
|
||||
<p>
|
||||
<translate translate-context="Content/Channels/*">You may need to subscribe to this channel to see its contents.</translate>
|
||||
<translate translate-context="Content/Channels/*">
|
||||
You may need to subscribe to this channel to see its contents.
|
||||
</translate>
|
||||
</p>
|
||||
</empty-state>
|
||||
</template>
|
||||
|
@ -33,16 +58,15 @@ import axios from 'axios'
|
|||
import ChannelSerieCard from '@/components/audio/ChannelSerieCard'
|
||||
import AlbumCard from '@/components/audio/album/Card'
|
||||
|
||||
|
||||
export default {
|
||||
props: {
|
||||
filters: {type: Object, required: true},
|
||||
isPodcast: {type: Boolean, default: true},
|
||||
limit: {type: Number, default: 5},
|
||||
},
|
||||
components: {
|
||||
ChannelSerieCard,
|
||||
AlbumCard,
|
||||
AlbumCard
|
||||
},
|
||||
props: {
|
||||
filters: { type: Object, required: true },
|
||||
isPodcast: { type: Boolean, default: true },
|
||||
limit: { type: Number, default: 5 }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
@ -62,11 +86,11 @@ export default {
|
|||
return
|
||||
}
|
||||
this.isLoading = true
|
||||
let self = this
|
||||
let params = _.clone(this.filters)
|
||||
const self = this
|
||||
const params = _.clone(this.filters)
|
||||
params.page_size = this.limit
|
||||
params.include_channels = true
|
||||
axios.get(url, {params: params}).then((response) => {
|
||||
axios.get(url, { params: params }).then((response) => {
|
||||
self.nextPage = response.data.next
|
||||
self.isLoading = false
|
||||
self.objects = self.objects.concat(response.data.results)
|
||||
|
@ -75,7 +99,7 @@ export default {
|
|||
self.isLoading = false
|
||||
self.errors = error.backendErrors
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,21 +1,37 @@
|
|||
<template>
|
||||
<div>
|
||||
<slot></slot>
|
||||
<div class="ui hidden divider"></div>
|
||||
<slot />
|
||||
<div class="ui hidden divider" />
|
||||
<div class="ui app-cards cards">
|
||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||
<div class="ui loader"></div>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui inverted active dimmer"
|
||||
>
|
||||
<div class="ui loader" />
|
||||
</div>
|
||||
<channel-card v-for="object in objects" :object="object" :key="object.uuid" />
|
||||
<channel-card
|
||||
v-for="object in objects"
|
||||
:key="object.uuid"
|
||||
:object="object"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="nextPage">
|
||||
<div class="ui hidden divider"></div>
|
||||
<button v-if="nextPage" @click="fetchData(nextPage)" :class="['ui', 'basic', 'button']">
|
||||
<translate translate-context="*/*/Button,Label">Show more</translate>
|
||||
<div class="ui hidden divider" />
|
||||
<button
|
||||
v-if="nextPage"
|
||||
:class="['ui', 'basic', 'button']"
|
||||
@click="fetchData(nextPage)"
|
||||
>
|
||||
<translate translate-context="*/*/Button,Label">
|
||||
Show more
|
||||
</translate>
|
||||
</button>
|
||||
</template>
|
||||
<template v-if="!isLoading && objects.length === 0">
|
||||
<empty-state @refresh="fetchData('channels/')" :refresh="true"></empty-state>
|
||||
<empty-state
|
||||
:refresh="true"
|
||||
@refresh="fetchData('channels/')"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -26,13 +42,13 @@ import axios from 'axios'
|
|||
import ChannelCard from '@/components/audio/ChannelCard'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
filters: {type: Object, required: true},
|
||||
limit: {type: Number, default: 5},
|
||||
},
|
||||
components: {
|
||||
ChannelCard
|
||||
},
|
||||
props: {
|
||||
filters: { type: Object, required: true },
|
||||
limit: { type: Number, default: 5 }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
objects: [],
|
||||
|
@ -51,11 +67,11 @@ export default {
|
|||
return
|
||||
}
|
||||
this.isLoading = true
|
||||
let self = this
|
||||
let params = _.clone(this.filters)
|
||||
const self = this
|
||||
const params = _.clone(this.filters)
|
||||
params.page_size = this.limit
|
||||
params.include_channels = true
|
||||
axios.get(url, {params: params}).then((response) => {
|
||||
axios.get(url, { params: params }).then((response) => {
|
||||
self.nextPage = response.data.next
|
||||
self.isLoading = false
|
||||
self.objects = self.objects.concat(response.data.results)
|
||||
|
@ -65,7 +81,7 @@ export default {
|
|||
self.isLoading = false
|
||||
self.errors = error.backendErrors
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
<template>
|
||||
<div>
|
||||
<div role="alert" class="ui warning message" v-if="!anonymousCanListen">
|
||||
<div
|
||||
v-if="!anonymousCanListen"
|
||||
role="alert"
|
||||
class="ui warning message"
|
||||
>
|
||||
<p>
|
||||
<strong>
|
||||
<translate translate-context="Content/Embed/Message">Sharing will not work because this pod doesn't allow anonymous users to access content.</translate>
|
||||
</strong>
|
||||
</p>
|
||||
<p>
|
||||
<translate translate-context="Content/Embed/Message">Please contact your admins and ask them to update the corresponding setting.</translate>
|
||||
<translate translate-context="Content/Embed/Message">
|
||||
Please contact your admins and ask them to update the corresponding setting.
|
||||
</translate>
|
||||
</p>
|
||||
</div>
|
||||
<div class="ui form">
|
||||
|
@ -15,49 +21,100 @@
|
|||
<div class="field">
|
||||
<div class="field">
|
||||
<label for="embed-width"><translate translate-context="Popup/Embed/Input.Label">Widget width</translate></label>
|
||||
<p><translate translate-context="Popup/Embed/Paragraph">Leave empty for a responsive widget</translate></p>
|
||||
<input id="embed-width" type="number" v-model.number="width" min="0" step="10" />
|
||||
<p>
|
||||
<translate translate-context="Popup/Embed/Paragraph">
|
||||
Leave empty for a responsive widget
|
||||
</translate>
|
||||
</p>
|
||||
<input
|
||||
id="embed-width"
|
||||
v-model.number="width"
|
||||
type="number"
|
||||
min="0"
|
||||
step="10"
|
||||
>
|
||||
</div>
|
||||
<template v-if="type != 'track'">
|
||||
<br>
|
||||
<div class="field">
|
||||
<label for="embed-height"><translate translate-context="Popup/Embed/Input.Label">Widget height</translate></label>
|
||||
<input id="embed-height" type="number" v-model="height" :min="minHeight" max="1000" step="10" />
|
||||
<input
|
||||
id="embed-height"
|
||||
v-model="height"
|
||||
type="number"
|
||||
:min="minHeight"
|
||||
max="1000"
|
||||
step="10"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="field">
|
||||
<button @click="copy" class="ui right accent labeled icon floated button"><i class="copy icon"></i><translate translate-context="*/*/Button.Label/Short, Verb">Copy</translate></button>
|
||||
<button
|
||||
class="ui right accent labeled icon floated button"
|
||||
@click="copy"
|
||||
>
|
||||
<i class="copy icon" /><translate translate-context="*/*/Button.Label/Short, Verb">
|
||||
Copy
|
||||
</translate>
|
||||
</button>
|
||||
<label for="embed-width"><translate translate-context="Popup/Embed/Input.Label/Noun">Embed code</translate></label>
|
||||
<p><translate translate-context="Popup/Embed/Paragraph">Copy/paste this code in your website HTML</translate></p>
|
||||
<textarea ref="textarea" :value="embedCode" rows="5" readonly>
|
||||
</textarea>
|
||||
<p>
|
||||
<translate translate-context="Popup/Embed/Paragraph">
|
||||
Copy/paste this code in your website HTML
|
||||
</translate>
|
||||
</p>
|
||||
<textarea
|
||||
ref="textarea"
|
||||
:value="embedCode"
|
||||
rows="5"
|
||||
readonly
|
||||
/>
|
||||
<div class="ui right">
|
||||
<p class="message" v-if=copied><translate translate-context="Content/*/Paragraph">Text copied to clipboard!</translate></p>
|
||||
<p
|
||||
v-if="copied"
|
||||
class="message"
|
||||
>
|
||||
<translate translate-context="Content/*/Paragraph">
|
||||
Text copied to clipboard!
|
||||
</translate>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview">
|
||||
<h3>
|
||||
<a :href="iframeSrc" target="_blank">
|
||||
<a
|
||||
:href="iframeSrc"
|
||||
target="_blank"
|
||||
>
|
||||
<translate translate-context="Popup/Embed/Title/Noun">Preview</translate>
|
||||
</a>
|
||||
</h3>
|
||||
<iframe :width="frameWidth" :height="height" scrolling="no" frameborder="no" :src="iframeSrc"></iframe>
|
||||
<iframe
|
||||
:width="frameWidth"
|
||||
:height="height"
|
||||
scrolling="no"
|
||||
frameborder="no"
|
||||
:src="iframeSrc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { mapState } from "vuex"
|
||||
import { mapState } from 'vuex'
|
||||
import _ from '@/lodash'
|
||||
|
||||
export default {
|
||||
props: ['type', 'id'],
|
||||
props: {
|
||||
type: { type: String, required: true },
|
||||
id: { type: Number, required: true }
|
||||
},
|
||||
data () {
|
||||
let d = {
|
||||
const d = {
|
||||
width: null,
|
||||
height: 150,
|
||||
minHeight: 100,
|
||||
|
@ -71,7 +128,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
nodeinfo: state => state.instance.nodeinfo,
|
||||
nodeinfo: state => state.instance.nodeinfo
|
||||
}),
|
||||
anonymousCanListen () {
|
||||
return _.get(this.nodeinfo, 'metadata.library.anonymousCanListen', false)
|
||||
|
@ -82,7 +139,7 @@ export default {
|
|||
// include hostname/protocol too so that the iframe link is absolute
|
||||
base = `${window.location.protocol}//${window.location.host}${base}`
|
||||
}
|
||||
let instanceUrl = this.$store.state.instance.instanceUrl
|
||||
const instanceUrl = this.$store.state.instance.instanceUrl
|
||||
let b = ''
|
||||
if (!window.location.href.startsWith(instanceUrl)) {
|
||||
// the frontend is running on a separate domain, so we need to provide
|
||||
|
@ -98,15 +155,15 @@ export default {
|
|||
return '100%'
|
||||
},
|
||||
embedCode () {
|
||||
let src = this.iframeSrc.replace(/&/g, '&')
|
||||
const src = this.iframeSrc.replace(/&/g, '&')
|
||||
return `<iframe width="${this.frameWidth}" height="${this.height}" scrolling="no" frameborder="no" src="${src}"></iframe>`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copy () {
|
||||
this.$refs.textarea.select()
|
||||
document.execCommand("Copy")
|
||||
let self = this
|
||||
document.execCommand('Copy')
|
||||
const self = this
|
||||
self.copied = true
|
||||
this.timeout = setTimeout(() => {
|
||||
self.copied = false
|
||||
|
|
|
@ -1,16 +1,34 @@
|
|||
<template>
|
||||
<button @click.stop="toggle" :class="['ui', 'pink', {'inverted': isApproved || isPending}, {'favorited': isApproved}, 'icon', 'labeled', 'button']">
|
||||
<i class="heart icon"></i>
|
||||
<translate v-if="isApproved" translate-context="Content/Library/Card.Button.Label/Verb">Unfollow</translate>
|
||||
<translate v-else-if="isPending" translate-context="Content/Library/Card.Button.Label/Verb">Cancel follow request</translate>
|
||||
<translate v-else translate-context="Content/Library/Card.Button.Label/Verb">Follow</translate>
|
||||
<template>
|
||||
<button
|
||||
:class="['ui', 'pink', {'inverted': isApproved || isPending}, {'favorited': isApproved}, 'icon', 'labeled', 'button']"
|
||||
@click.stop="toggle"
|
||||
>
|
||||
<i class="heart icon" />
|
||||
<translate
|
||||
v-if="isApproved"
|
||||
translate-context="Content/Library/Card.Button.Label/Verb"
|
||||
>
|
||||
Unfollow
|
||||
</translate>
|
||||
<translate
|
||||
v-else-if="isPending"
|
||||
translate-context="Content/Library/Card.Button.Label/Verb"
|
||||
>
|
||||
Cancel follow request
|
||||
</translate>
|
||||
<translate
|
||||
v-else
|
||||
translate-context="Content/Library/Card.Button.Label/Verb"
|
||||
>
|
||||
Follow
|
||||
</translate>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
library: {type: Object},
|
||||
library: { type: Object, required: true }
|
||||
},
|
||||
computed: {
|
||||
isPending () {
|
||||
|
@ -34,6 +52,5 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,52 +1,121 @@
|
|||
<template>
|
||||
<span :title="title" :class="['ui', {'tiny': discrete}, {'icon': !discrete}, {'buttons': !dropdownOnly && !iconOnly}, 'play-button component-play-button']">
|
||||
<span
|
||||
:title="title"
|
||||
:class="['ui', {'tiny': discrete}, {'icon': !discrete}, {'buttons': !dropdownOnly && !iconOnly}, 'play-button component-play-button']"
|
||||
>
|
||||
<button
|
||||
v-if="!dropdownOnly"
|
||||
@click.stop.prevent="replacePlay"
|
||||
:disabled="!playable"
|
||||
:aria-label="labels.replacePlay"
|
||||
:class="buttonClasses.concat(['ui', {loading: isLoading}, {'mini': discrete}, {disabled: !playable}])">
|
||||
<i v-if="playing" class="pause icon"></i>
|
||||
<i v-else :class="[playIconClass, 'icon']"></i>
|
||||
:class="buttonClasses.concat(['ui', {loading: isLoading}, {'mini': discrete}, {disabled: !playable}])"
|
||||
@click.stop.prevent="replacePlay"
|
||||
>
|
||||
<i
|
||||
v-if="playing"
|
||||
class="pause icon"
|
||||
/>
|
||||
<i
|
||||
v-else
|
||||
:class="[playIconClass, 'icon']"
|
||||
/>
|
||||
<template v-if="!discrete && !iconOnly"> <slot><translate translate-context="*/Queue/Button.Label/Short, Verb">Play</translate></slot></template>
|
||||
</button>
|
||||
<button
|
||||
v-if="!discrete && !iconOnly"
|
||||
:class="['ui', {disabled: !playable && !filterableArtist}, 'floating', 'dropdown', {'icon': !dropdownOnly}, {'button': !dropdownOnly}]"
|
||||
@click.stop.prevent="clicked = true"
|
||||
:class="['ui', {disabled: !playable && !filterableArtist}, 'floating', 'dropdown', {'icon': !dropdownOnly}, {'button': !dropdownOnly}]">
|
||||
<i :class="dropdownIconClasses.concat(['icon'])" :title="title" ></i>
|
||||
<div class="menu" v-if="clicked">
|
||||
<button class="item basic" ref="add" data-ref="add" :disabled="!playable" @click.stop.prevent="add" :title="labels.addToQueue">
|
||||
<i class="plus icon"></i><translate translate-context="*/Queue/Dropdown/Button/Label/Short">Add to queue</translate>
|
||||
>
|
||||
<i
|
||||
:class="dropdownIconClasses.concat(['icon'])"
|
||||
:title="title"
|
||||
/>
|
||||
<div
|
||||
v-if="clicked"
|
||||
class="menu"
|
||||
>
|
||||
<button
|
||||
ref="add"
|
||||
class="item basic"
|
||||
data-ref="add"
|
||||
:disabled="!playable"
|
||||
:title="labels.addToQueue"
|
||||
@click.stop.prevent="add"
|
||||
>
|
||||
<i class="plus icon" /><translate translate-context="*/Queue/Dropdown/Button/Label/Short">Add to queue</translate>
|
||||
</button>
|
||||
<button class="item basic" ref="addNext" data-ref="addNext" :disabled="!playable" @click.stop.prevent="addNext()" :title="labels.playNext">
|
||||
<i class="step forward icon"></i>{{ labels.playNext }}
|
||||
<button
|
||||
ref="addNext"
|
||||
class="item basic"
|
||||
data-ref="addNext"
|
||||
:disabled="!playable"
|
||||
:title="labels.playNext"
|
||||
@click.stop.prevent="addNext()"
|
||||
>
|
||||
<i class="step forward icon" />{{ labels.playNext }}
|
||||
</button>
|
||||
<button class="item basic" ref="playNow" data-ref="playNow" :disabled="!playable" @click.stop.prevent="addNext(true)" :title="labels.playNow">
|
||||
<i class="play icon"></i>{{ labels.playNow }}
|
||||
<button
|
||||
ref="playNow"
|
||||
class="item basic"
|
||||
data-ref="playNow"
|
||||
:disabled="!playable"
|
||||
:title="labels.playNow"
|
||||
@click.stop.prevent="addNext(true)"
|
||||
>
|
||||
<i class="play icon" />{{ labels.playNow }}
|
||||
</button>
|
||||
<button v-if="track" class="item basic" :disabled="!playable" @click.stop.prevent="$store.dispatch('radios/start', {type: 'similar', objectId: track.id})" :title="labels.startRadio">
|
||||
<i class="feed icon"></i><translate translate-context="*/Queue/Button.Label/Short, Verb">Play radio</translate>
|
||||
<button
|
||||
v-if="track"
|
||||
class="item basic"
|
||||
:disabled="!playable"
|
||||
:title="labels.startRadio"
|
||||
@click.stop.prevent="$store.dispatch('radios/start', {type: 'similar', objectId: track.id})"
|
||||
>
|
||||
<i class="feed icon" /><translate translate-context="*/Queue/Button.Label/Short, Verb">Play radio</translate>
|
||||
</button>
|
||||
<button v-if="track" class="item basic" :disabled="!playable" @click.stop="$store.commit('playlists/chooseTrack', track)">
|
||||
<i class="list icon"></i>
|
||||
<button
|
||||
v-if="track"
|
||||
class="item basic"
|
||||
:disabled="!playable"
|
||||
@click.stop="$store.commit('playlists/chooseTrack', track)"
|
||||
>
|
||||
<i class="list icon" />
|
||||
<translate translate-context="Sidebar/Player/Icon.Tooltip/Verb">Add to playlist…</translate>
|
||||
</button>
|
||||
<button v-if="track" class="item basic" @click.stop.prevent="$router.push(`/library/tracks/${track.id}/`)">
|
||||
<i class="info icon"></i>
|
||||
<translate v-if="track.artist.content_category === 'podcast'" translate-context="*/Queue/Dropdown/Button/Label/Short">Episode details</translate>
|
||||
<translate v-else translate-context="*/Queue/Dropdown/Button/Label/Short">Track details</translate>
|
||||
<button
|
||||
v-if="track"
|
||||
class="item basic"
|
||||
@click.stop.prevent="$router.push(`/library/tracks/${track.id}/`)"
|
||||
>
|
||||
<i class="info icon" />
|
||||
<translate
|
||||
v-if="track.artist.content_category === 'podcast'"
|
||||
translate-context="*/Queue/Dropdown/Button/Label/Short"
|
||||
>Episode details</translate>
|
||||
<translate
|
||||
v-else
|
||||
translate-context="*/Queue/Dropdown/Button/Label/Short"
|
||||
>Track details</translate>
|
||||
</button>
|
||||
<div class="divider"></div>
|
||||
<button v-if="filterableArtist" ref="filterArtist" data-ref="filterArtist" class="item basic" :disabled="!filterableArtist" @click.stop.prevent="filterArtist" :title="labels.hideArtist">
|
||||
<i class="eye slash outline icon"></i><translate translate-context="*/Queue/Dropdown/Button/Label/Short">Hide content from this artist</translate>
|
||||
<div class="divider" />
|
||||
<button
|
||||
v-if="filterableArtist"
|
||||
ref="filterArtist"
|
||||
data-ref="filterArtist"
|
||||
class="item basic"
|
||||
:disabled="!filterableArtist"
|
||||
:title="labels.hideArtist"
|
||||
@click.stop.prevent="filterArtist"
|
||||
>
|
||||
<i class="eye slash outline icon" /><translate translate-context="*/Queue/Dropdown/Button/Label/Short">Hide content from this artist</translate>
|
||||
</button>
|
||||
<button
|
||||
v-for="obj in getReportableObjs({track, album, artist, playlist, account, channel})"
|
||||
:key="obj.target.type + obj.target.id"
|
||||
:ref="`report${obj.target.type}${obj.target.id}`"
|
||||
class="item basic"
|
||||
:ref="`report${obj.target.type}${obj.target.id}`" :data-ref="`report${obj.target.type}${obj.target.id}`"
|
||||
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)">
|
||||
:data-ref="`report${obj.target.type}${obj.target.id}`"
|
||||
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)"
|
||||
>
|
||||
<i class="share icon" /> {{ obj.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -55,7 +124,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import jQuery from 'jquery'
|
||||
|
||||
import ReportMixin from '@/components/mixins/Report'
|
||||
|
@ -65,23 +133,23 @@ export default {
|
|||
mixins: [ReportMixin, PlayOptionsMixin],
|
||||
props: {
|
||||
// we can either have a single or multiple tracks to play when clicked
|
||||
tracks: {type: Array, required: false},
|
||||
track: {type: Object, required: false},
|
||||
account: {type: Object, required: false},
|
||||
dropdownIconClasses: {type: Array, required: false, default: () => { return ['dropdown'] }},
|
||||
playIconClass: {type: String, required: false, default: 'play icon'},
|
||||
buttonClasses: {type: Array, required: false, default: () => { return ['button'] }},
|
||||
playlist: {type: Object, required: false},
|
||||
discrete: {type: Boolean, default: false},
|
||||
dropdownOnly: {type: Boolean, default: false},
|
||||
iconOnly: {type: Boolean, default: false},
|
||||
artist: {type: Object, required: false},
|
||||
album: {type: Object, required: false},
|
||||
library: {type: Object, required: false},
|
||||
channel: {type: Object, required: false},
|
||||
isPlayable: {type: Boolean, required: false, default: null},
|
||||
playing: {type: Boolean, required: false, default: false},
|
||||
paused: {type: Boolean, required: false, default: false}
|
||||
tracks: { type: Array, required: false, default: () => { return [] } },
|
||||
track: { type: Object, required: false, default: () => { return {} } },
|
||||
account: { type: Object, required: false, default: () => { return {} } },
|
||||
dropdownIconClasses: { type: Array, required: false, default: () => { return ['dropdown'] } },
|
||||
playIconClass: { type: String, required: false, default: 'play icon' },
|
||||
buttonClasses: { type: Array, required: false, default: () => { return ['button'] } },
|
||||
playlist: { type: Object, required: false, default: () => { return {} } },
|
||||
discrete: { type: Boolean, default: false },
|
||||
dropdownOnly: { type: Boolean, default: false },
|
||||
iconOnly: { type: Boolean, default: false },
|
||||
artist: { type: Object, required: false, default: () => { return {} } },
|
||||
album: { type: Object, required: false, default: () => { return {} } },
|
||||
library: { type: Object, required: false, default: () => { return {} } },
|
||||
channel: { type: Object, required: false, default: () => { return {} } },
|
||||
isPlayable: { type: Boolean, required: false, default: null },
|
||||
playing: { type: Boolean, required: false, default: false },
|
||||
paused: { type: Boolean, required: false, default: false }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
@ -111,7 +179,7 @@ export default {
|
|||
startRadio: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play similar songs'),
|
||||
report: this.$pgettext('*/Moderation/*/Button/Label,Verb', 'Report…'),
|
||||
addToPlaylist: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Add to playlist…'),
|
||||
replacePlay,
|
||||
replacePlay
|
||||
}
|
||||
},
|
||||
title () {
|
||||
|
@ -122,43 +190,42 @@ export default {
|
|||
return this.$pgettext('*/Queue/Button/Title', 'This track is not available in any library you have access to')
|
||||
}
|
||||
}
|
||||
},
|
||||
return null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
clicked () {
|
||||
let self = this
|
||||
const self = this
|
||||
this.$nextTick(() => {
|
||||
jQuery(this.$el).find('.ui.dropdown').dropdown({
|
||||
selectOnKeydown: false,
|
||||
action: function (text, value, $el) {
|
||||
// used to ensure focusing the dropdown and clicking via keyboard
|
||||
// works as expected
|
||||
let button = self.$refs[$el.data('ref')]
|
||||
const button = self.$refs[$el.data('ref')]
|
||||
if (Array.isArray(button)) {
|
||||
button[0].click()
|
||||
} else {
|
||||
button.click()
|
||||
}
|
||||
jQuery(self.$el).find('.ui.dropdown').dropdown('hide')
|
||||
},
|
||||
}
|
||||
})
|
||||
jQuery(this.$el).find('.ui.dropdown').dropdown('show', function () {
|
||||
// little magic to ensure the menu is always visible in the viewport
|
||||
// By default, try to diplay it on the right if there is enough room
|
||||
let menu = jQuery(self.$el).find('.ui.dropdown').find(".menu")
|
||||
let viewportOffset = menu.get(0).getBoundingClientRect();
|
||||
let left = viewportOffset.left;
|
||||
let viewportWidth = document.documentElement.clientWidth
|
||||
let rightOverflow = viewportOffset.right - viewportWidth
|
||||
let leftOverflow = -viewportOffset.left
|
||||
const menu = jQuery(self.$el).find('.ui.dropdown').find('.menu')
|
||||
const viewportOffset = menu.get(0).getBoundingClientRect()
|
||||
const viewportWidth = document.documentElement.clientWidth
|
||||
const rightOverflow = viewportOffset.right - viewportWidth
|
||||
const leftOverflow = -viewportOffset.left
|
||||
let offset = 0
|
||||
if (rightOverflow > 0) {
|
||||
offset = -rightOverflow - 5
|
||||
menu.css({cssText: `left: ${offset}px !important;`});
|
||||
}
|
||||
else if (leftOverflow > 0) {
|
||||
menu.css({ cssText: `left: ${offset}px !important;` })
|
||||
} else if (leftOverflow > 0) {
|
||||
offset = leftOverflow + 5
|
||||
menu.css({cssText: `right: -${offset}px !important;`});
|
||||
menu.css({ cssText: `right: -${offset}px !important;` })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,64 +1,144 @@
|
|||
<template>
|
||||
<section role="complementary" v-if="currentTrack" class="player-wrapper ui bottom-player component-player" aria-labelledby="player-label">
|
||||
<h1 id="player-label" class="visually-hidden">
|
||||
<translate translate-context="*/*/*">Audio player and controls</translate>
|
||||
<section
|
||||
v-if="currentTrack"
|
||||
role="complementary"
|
||||
class="player-wrapper ui bottom-player component-player"
|
||||
aria-labelledby="player-label"
|
||||
>
|
||||
<h1
|
||||
id="player-label"
|
||||
class="visually-hidden"
|
||||
>
|
||||
<translate translate-context="*/*/*">
|
||||
Audio player and controls
|
||||
</translate>
|
||||
</h1>
|
||||
<div class="ui inverted segment fixed-controls" @click.prevent.stop="toggleMobilePlayer">
|
||||
<div
|
||||
:class="['ui', 'top attached', 'small', 'inverted', {'indicating': isLoadingAudio}, 'progress']">
|
||||
<div class="buffer bar" :data-percent="bufferProgress" :style="{ 'width': bufferProgress + '%' }"></div>
|
||||
<div class="position bar" :data-percent="progress" :style="{ 'width': progress + '%' }"></div>
|
||||
class="ui inverted segment fixed-controls"
|
||||
@click.prevent.stop="toggleMobilePlayer"
|
||||
>
|
||||
<div
|
||||
:class="['ui', 'top attached', 'small', 'inverted', {'indicating': isLoadingAudio}, 'progress']"
|
||||
>
|
||||
<div
|
||||
class="buffer bar"
|
||||
:data-percent="bufferProgress"
|
||||
:style="{ 'width': bufferProgress + '%' }"
|
||||
/>
|
||||
<div
|
||||
class="position bar"
|
||||
:data-percent="progress"
|
||||
:style="{ 'width': progress + '%' }"
|
||||
/>
|
||||
</div>
|
||||
<div class="controls-row">
|
||||
|
||||
<div class="controls track-controls queue-not-focused desktop-and-up">
|
||||
<div class="ui tiny image" @click.stop.prevent="$router.push({name: 'library.tracks.detail', params: {id: currentTrack.id }})">
|
||||
<img alt="" ref="cover" v-if="currentTrack.cover && currentTrack.cover.urls.original" :src="$store.getters['instance/absoluteUrl'](currentTrack.cover.urls.medium_square_crop)">
|
||||
<img alt="" ref="cover" v-else-if="currentTrack.album && currentTrack.album.cover && currentTrack.album.cover.urls && currentTrack.album.cover.urls.original" :src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.urls.medium_square_crop)">
|
||||
<img alt="" v-else src="../../assets/audio/default-cover.png">
|
||||
<div
|
||||
class="ui tiny image"
|
||||
@click.stop.prevent="$router.push({name: 'library.tracks.detail', params: {id: currentTrack.id }})"
|
||||
>
|
||||
<img
|
||||
v-if="currentTrack.cover && currentTrack.cover.urls.original"
|
||||
ref="cover"
|
||||
alt=""
|
||||
:src="$store.getters['instance/absoluteUrl'](currentTrack.cover.urls.medium_square_crop)"
|
||||
>
|
||||
<img
|
||||
v-else-if="currentTrack.album && currentTrack.album.cover && currentTrack.album.cover.urls && currentTrack.album.cover.urls.original"
|
||||
ref="cover"
|
||||
alt=""
|
||||
:src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.urls.medium_square_crop)"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
src="../../assets/audio/default-cover.png"
|
||||
>
|
||||
</div>
|
||||
<div @click.stop.prevent="" class="middle aligned content ellipsis">
|
||||
<div
|
||||
class="middle aligned content ellipsis"
|
||||
@click.stop.prevent=""
|
||||
>
|
||||
<strong>
|
||||
<router-link @click.stop.prevent="" class="small header discrete link track" :to="{name: 'library.tracks.detail', params: {id: currentTrack.id }}">
|
||||
<router-link
|
||||
class="small header discrete link track"
|
||||
:to="{name: 'library.tracks.detail', params: {id: currentTrack.id }}"
|
||||
@click.stop.prevent=""
|
||||
>
|
||||
{{ currentTrack.title }}
|
||||
</router-link>
|
||||
</strong>
|
||||
<div class="meta">
|
||||
<router-link @click.stop.prevent="" class="discrete link" :to="{name: 'library.artists.detail', params: {id: currentTrack.artist.id }}">{{ currentTrack.artist.name }}</router-link>
|
||||
<template v-if="currentTrack.album"> /
|
||||
<router-link @click.stop.prevent="" class="discrete link" :to="{name: 'library.albums.detail', params: {id: currentTrack.album.id }}">{{ currentTrack.album.title }}</router-link>
|
||||
<router-link
|
||||
class="discrete link"
|
||||
:to="{name: 'library.artists.detail', params: {id: currentTrack.artist.id }}"
|
||||
@click.stop.prevent=""
|
||||
>
|
||||
{{ currentTrack.artist.name }}
|
||||
</router-link>
|
||||
<template v-if="currentTrack.album">
|
||||
/
|
||||
<router-link
|
||||
class="discrete link"
|
||||
:to="{name: 'library.albums.detail', params: {id: currentTrack.album.id }}"
|
||||
@click.stop.prevent=""
|
||||
>
|
||||
{{ currentTrack.album.title }}
|
||||
</router-link>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls track-controls queue-not-focused tablet-and-below">
|
||||
<div class="ui tiny image">
|
||||
<img alt="" ref="cover" v-if="currentTrack.cover && currentTrack.cover.urls.original" :src="$store.getters['instance/absoluteUrl'](currentTrack.cover.urls.medium_square_crop)">
|
||||
<img alt="" ref="cover" v-else-if="currentTrack.album && currentTrack.album.cover && currentTrack.album.cover.urls.original" :src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.urls.medium_square_crop)">
|
||||
<img alt="" v-else src="../../assets/audio/default-cover.png">
|
||||
<img
|
||||
v-if="currentTrack.cover && currentTrack.cover.urls.original"
|
||||
ref="cover"
|
||||
alt=""
|
||||
:src="$store.getters['instance/absoluteUrl'](currentTrack.cover.urls.medium_square_crop)"
|
||||
>
|
||||
<img
|
||||
v-else-if="currentTrack.album && currentTrack.album.cover && currentTrack.album.cover.urls.original"
|
||||
ref="cover"
|
||||
alt=""
|
||||
:src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.urls.medium_square_crop)"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
src="../../assets/audio/default-cover.png"
|
||||
>
|
||||
</div>
|
||||
<div class="middle aligned content ellipsis">
|
||||
<strong>
|
||||
{{ currentTrack.title }}
|
||||
</strong>
|
||||
<div class="meta">
|
||||
{{ currentTrack.artist.name }}<template v-if="currentTrack.album"> / {{ currentTrack.album.title }}</template>
|
||||
{{ currentTrack.artist.name }}<template v-if="currentTrack.album">
|
||||
/ {{ currentTrack.album.title }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls desktop-and-up fluid align-right" v-if="$store.state.auth.authenticated">
|
||||
<div
|
||||
v-if="$store.state.auth.authenticated"
|
||||
class="controls desktop-and-up fluid align-right"
|
||||
>
|
||||
<track-favorite-icon
|
||||
class="control white"
|
||||
:track="currentTrack"></track-favorite-icon>
|
||||
:track="currentTrack"
|
||||
/>
|
||||
<track-playlist-icon
|
||||
class="control white"
|
||||
:track="currentTrack"></track-playlist-icon>
|
||||
:track="currentTrack"
|
||||
/>
|
||||
<button
|
||||
@click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})"
|
||||
:class="['ui', 'really', 'basic', 'circular', 'icon', 'button', 'control']"
|
||||
:aria-label="labels.addArtistContentFilter"
|
||||
:title="labels.addArtistContentFilter">
|
||||
<i :class="['eye slash outline', 'basic', 'icon']"></i>
|
||||
:title="labels.addArtistContentFilter"
|
||||
@click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})"
|
||||
>
|
||||
<i :class="['eye slash outline', 'basic', 'icon']" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="player-controls controls queue-not-focused">
|
||||
|
@ -66,41 +146,48 @@
|
|||
:title="labels.previous"
|
||||
:aria-label="labels.previous"
|
||||
class="circular button control tablet-and-up"
|
||||
:disabled="!hasPrevious"
|
||||
@click.prevent.stop="$store.dispatch('queue/previous')"
|
||||
:disabled="!hasPrevious">
|
||||
<i :class="['ui', 'large', {'disabled': !hasPrevious}, 'backward step', 'icon']" ></i>
|
||||
>
|
||||
<i :class="['ui', 'large', {'disabled': !hasPrevious}, 'backward step', 'icon']" />
|
||||
</button>
|
||||
<button
|
||||
v-if="!playing"
|
||||
:title="labels.play"
|
||||
:aria-label="labels.play"
|
||||
class="circular button control"
|
||||
@click.prevent.stop="resumePlayback"
|
||||
class="circular button control">
|
||||
<i :class="['ui', 'big', 'play', {'disabled': !currentTrack}, 'icon']"></i>
|
||||
>
|
||||
<i :class="['ui', 'big', 'play', {'disabled': !currentTrack}, 'icon']" />
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
:title="labels.pause"
|
||||
:aria-label="labels.pause"
|
||||
class="circular button control"
|
||||
@click.prevent.stop="pausePlayback"
|
||||
class="circular button control">
|
||||
<i :class="['ui', 'big', 'pause', {'disabled': !currentTrack}, 'icon']"></i>
|
||||
>
|
||||
<i :class="['ui', 'big', 'pause', {'disabled': !currentTrack}, 'icon']" />
|
||||
</button>
|
||||
<button
|
||||
:title="labels.next"
|
||||
:aria-label="labels.next"
|
||||
class="circular button control"
|
||||
:disabled="!hasNext"
|
||||
@click.prevent.stop="$store.dispatch('queue/next')"
|
||||
:disabled="!hasNext">
|
||||
<i :class="['ui', 'large', {'disabled': !hasNext}, 'forward step', 'icon']" ></i>
|
||||
>
|
||||
<i :class="['ui', 'large', {'disabled': !hasNext}, 'forward step', 'icon']" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="controls progress-controls queue-not-focused tablet-and-up small align-left">
|
||||
<div class="timer">
|
||||
<template v-if="!isLoadingAudio">
|
||||
<span class="start" @click.stop.prevent="setCurrentTime(0)">{{currentTimeFormatted}}</span>
|
||||
| <span class="total">{{durationFormatted}}</span>
|
||||
<span
|
||||
class="start"
|
||||
@click.stop.prevent="setCurrentTime(0)"
|
||||
>{{ currentTimeFormatted }}</span>
|
||||
| <span class="total">{{ durationFormatted }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
00:00 | 00:00
|
||||
|
@ -111,35 +198,40 @@
|
|||
<div class="group">
|
||||
<volume-control class="expandable" />
|
||||
<button
|
||||
class="circular control button"
|
||||
v-if="looping === 0"
|
||||
class="circular control button"
|
||||
:title="labels.loopingDisabled"
|
||||
:aria-label="labels.loopingDisabled"
|
||||
:disabled="!currentTrack"
|
||||
@click.prevent.stop="$store.commit('player/looping', 1)"
|
||||
:disabled="!currentTrack">
|
||||
<i :class="['ui', {'disabled': !currentTrack}, 'step', 'repeat', 'icon']"></i>
|
||||
>
|
||||
<i :class="['ui', {'disabled': !currentTrack}, 'step', 'repeat', 'icon']" />
|
||||
</button>
|
||||
<button
|
||||
v-if="looping === 1"
|
||||
class="looping circular control button"
|
||||
@click.prevent.stop="$store.commit('player/looping', 2)"
|
||||
:title="labels.loopingSingle"
|
||||
:aria-label="labels.loopingSingle"
|
||||
v-if="looping === 1"
|
||||
:disabled="!currentTrack">
|
||||
:disabled="!currentTrack"
|
||||
@click.prevent.stop="$store.commit('player/looping', 2)"
|
||||
>
|
||||
<i
|
||||
class="repeat icon">
|
||||
class="repeat icon"
|
||||
>
|
||||
<span class="ui circular tiny vibrant label">1</span>
|
||||
</i>
|
||||
</button>
|
||||
<button
|
||||
v-if="looping === 2"
|
||||
class="looping circular control button"
|
||||
:title="labels.loopingWhole"
|
||||
:aria-label="labels.loopingWhole"
|
||||
v-if="looping === 2"
|
||||
:disabled="!currentTrack"
|
||||
@click.prevent.stop="$store.commit('player/looping', 0)">
|
||||
@click.prevent.stop="$store.commit('player/looping', 0)"
|
||||
>
|
||||
<i
|
||||
class="repeat icon">
|
||||
class="repeat icon"
|
||||
>
|
||||
<span class="ui circular tiny vibrant label">∞</span>
|
||||
</i>
|
||||
</button>
|
||||
|
@ -148,55 +240,80 @@
|
|||
:disabled="queue.tracks.length === 0"
|
||||
:title="labels.shuffle"
|
||||
:aria-label="labels.shuffle"
|
||||
@click.prevent.stop="shuffle()">
|
||||
<div v-if="isShuffling" class="ui inline shuffling inverted tiny active loader"></div>
|
||||
<i v-else :class="['ui', 'random', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
|
||||
@click.prevent.stop="shuffle()"
|
||||
>
|
||||
<div
|
||||
v-if="isShuffling"
|
||||
class="ui inline shuffling inverted tiny active loader"
|
||||
/>
|
||||
<i
|
||||
v-else
|
||||
:class="['ui', 'random', {'disabled': queue.tracks.length === 0}, 'icon']"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="group">
|
||||
<div class="fake-dropdown">
|
||||
<button class="position circular control button desktop-and-up" @click.stop="toggleMobilePlayer" aria-expanded="true">
|
||||
<i class="stream icon"></i>
|
||||
<translate translate-context="Sidebar/Queue/Text" :translate-params="{index: queue.currentIndex + 1, length: queue.tracks.length}">
|
||||
<button
|
||||
class="position circular control button desktop-and-up"
|
||||
aria-expanded="true"
|
||||
@click.stop="toggleMobilePlayer"
|
||||
>
|
||||
<i class="stream icon" />
|
||||
<translate
|
||||
translate-context="Sidebar/Queue/Text"
|
||||
:translate-params="{index: queue.currentIndex + 1, length: queue.tracks.length}"
|
||||
>
|
||||
%{ index } of %{ length }
|
||||
</translate>
|
||||
</button>
|
||||
<button class="position circular control button tablet-and-below" @click.stop="switchTab">
|
||||
<i class="stream icon"></i>
|
||||
<translate translate-context="Sidebar/Queue/Text" :translate-params="{index: queue.currentIndex + 1, length: queue.tracks.length}">
|
||||
<button
|
||||
class="position circular control button tablet-and-below"
|
||||
@click.stop="switchTab"
|
||||
>
|
||||
<i class="stream icon" />
|
||||
<translate
|
||||
translate-context="Sidebar/Queue/Text"
|
||||
:translate-params="{index: queue.currentIndex + 1, length: queue.tracks.length}"
|
||||
>
|
||||
%{ index } of %{ length }
|
||||
</translate>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="circular control button close-control desktop-and-up"
|
||||
v-if="$store.state.ui.queueFocused"
|
||||
@click.stop="toggleMobilePlayer">
|
||||
<i class="large down angle icon"></i>
|
||||
class="circular control button close-control desktop-and-up"
|
||||
@click.stop="toggleMobilePlayer"
|
||||
>
|
||||
<i class="large down angle icon" />
|
||||
</button>
|
||||
<button
|
||||
class="circular control button desktop-and-up"
|
||||
v-else
|
||||
@click.stop="toggleMobilePlayer">
|
||||
<i class="large up angle icon"></i>
|
||||
class="circular control button desktop-and-up"
|
||||
@click.stop="toggleMobilePlayer"
|
||||
>
|
||||
<i class="large up angle icon" />
|
||||
</button>
|
||||
<button
|
||||
class="circular control button close-control tablet-and-below"
|
||||
v-if="$store.state.ui.queueFocused === 'player'"
|
||||
@click.stop="switchTab">
|
||||
<i class="large up angle icon"></i>
|
||||
class="circular control button close-control tablet-and-below"
|
||||
@click.stop="switchTab"
|
||||
>
|
||||
<i class="large up angle icon" />
|
||||
</button>
|
||||
<button
|
||||
class="circular control button tablet-and-below"
|
||||
v-if="$store.state.ui.queueFocused === 'queue'"
|
||||
@click.stop="switchTab">
|
||||
<i class="large down angle icon"></i>
|
||||
class="circular control button tablet-and-below"
|
||||
@click.stop="switchTab"
|
||||
>
|
||||
<i class="large down angle icon" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="circular control button close-control tablet-and-below"
|
||||
@click.stop="$store.commit('ui/queueFocused', null)">
|
||||
<i class="x icon"></i>
|
||||
@click.stop="$store.commit('ui/queueFocused', null)"
|
||||
>
|
||||
<i class="x icon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -259,6 +376,124 @@ export default {
|
|||
nextTrackPreloaded: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentIndex: state => state.queue.currentIndex,
|
||||
playing: state => state.player.playing,
|
||||
isLoadingAudio: state => state.player.isLoadingAudio,
|
||||
volume: state => state.player.volume,
|
||||
looping: state => state.player.looping,
|
||||
duration: state => state.player.duration,
|
||||
bufferProgress: state => state.player.bufferProgress,
|
||||
errored: state => state.player.errored,
|
||||
currentTime: state => state.player.currentTime,
|
||||
queue: state => state.queue
|
||||
}),
|
||||
...mapGetters({
|
||||
currentTrack: 'queue/currentTrack',
|
||||
hasNext: 'queue/hasNext',
|
||||
hasPrevious: 'queue/hasPrevious',
|
||||
emptyQueue: 'queue/isEmpty',
|
||||
durationFormatted: 'player/durationFormatted',
|
||||
currentTimeFormatted: 'player/currentTimeFormatted',
|
||||
progress: 'player/progress'
|
||||
}),
|
||||
updateProgressThrottled () {
|
||||
return _.throttle(this.updateProgress, 50)
|
||||
},
|
||||
labels () {
|
||||
const audioPlayer = this.$pgettext('Sidebar/Player/Hidden text', 'Media player')
|
||||
const previous = this.$pgettext('Sidebar/Player/Icon.Tooltip', 'Previous track')
|
||||
const play = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Play')
|
||||
const pause = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Pause')
|
||||
const next = this.$pgettext('Sidebar/Player/Icon.Tooltip', 'Next track')
|
||||
const unmute = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Unmute')
|
||||
const mute = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Mute')
|
||||
const expandQueue = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Expand queue')
|
||||
const loopingDisabled = this.$pgettext('Sidebar/Player/Icon.Tooltip',
|
||||
'Looping disabled. Click to switch to single-track looping.'
|
||||
)
|
||||
const loopingSingle = this.$pgettext('Sidebar/Player/Icon.Tooltip',
|
||||
'Looping on a single track. Click to switch to whole queue looping.'
|
||||
)
|
||||
const loopingWhole = this.$pgettext('Sidebar/Player/Icon.Tooltip',
|
||||
'Looping on whole queue. Click to disable looping.'
|
||||
)
|
||||
const shuffle = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Shuffle your queue')
|
||||
const clear = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Clear your queue')
|
||||
const addArtistContentFilter = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Hide content from this artist…')
|
||||
return {
|
||||
audioPlayer,
|
||||
previous,
|
||||
play,
|
||||
pause,
|
||||
next,
|
||||
unmute,
|
||||
mute,
|
||||
loopingDisabled,
|
||||
loopingSingle,
|
||||
loopingWhole,
|
||||
shuffle,
|
||||
clear,
|
||||
expandQueue,
|
||||
addArtistContentFilter
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentTrack: {
|
||||
async handler (newValue, oldValue) {
|
||||
if (newValue === oldValue) {
|
||||
return
|
||||
}
|
||||
this.nextTrackPreloaded = false
|
||||
clearTimeout(this.playTimeout)
|
||||
if (this.currentSound) {
|
||||
this.currentSound.pause()
|
||||
}
|
||||
this.$store.commit('player/isLoadingAudio', true)
|
||||
this.playTimeout = setTimeout(async () => {
|
||||
await this.loadSound(newValue, oldValue)
|
||||
}, 100)
|
||||
this.updateMetadata()
|
||||
},
|
||||
immediate: false
|
||||
},
|
||||
volume: {
|
||||
immediate: true,
|
||||
handler (newValue) {
|
||||
this.sliderVolume = newValue
|
||||
Howler.volume(toLinearVolumeScale(newValue))
|
||||
}
|
||||
},
|
||||
sliderVolume (newValue) {
|
||||
this.$store.commit('player/volume', newValue)
|
||||
},
|
||||
playing: async function (newValue) {
|
||||
if (this.currentSound) {
|
||||
if (newValue === true) {
|
||||
this.soundId = this.currentSound.play(this.soundId)
|
||||
} else {
|
||||
this.currentSound.pause(this.soundId)
|
||||
}
|
||||
} else {
|
||||
await this.loadSound(this.currentTrack, null)
|
||||
}
|
||||
|
||||
this.observeProgress(newValue)
|
||||
},
|
||||
currentTime (newValue) {
|
||||
if (!this.isUpdatingTime) {
|
||||
this.setCurrentTime(newValue)
|
||||
}
|
||||
this.isUpdatingTime = false
|
||||
},
|
||||
emptyQueue (newValue) {
|
||||
if (newValue) {
|
||||
Howler.unload()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$store.dispatch('player/updateProgress', 0)
|
||||
this.$store.commit('player/playing', false)
|
||||
|
@ -661,124 +896,6 @@ export default {
|
|||
navigator.mediaSession.metadata = new window.MediaMetadata(metadata)
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentIndex: state => state.queue.currentIndex,
|
||||
playing: state => state.player.playing,
|
||||
isLoadingAudio: state => state.player.isLoadingAudio,
|
||||
volume: state => state.player.volume,
|
||||
looping: state => state.player.looping,
|
||||
duration: state => state.player.duration,
|
||||
bufferProgress: state => state.player.bufferProgress,
|
||||
errored: state => state.player.errored,
|
||||
currentTime: state => state.player.currentTime,
|
||||
queue: state => state.queue
|
||||
}),
|
||||
...mapGetters({
|
||||
currentTrack: 'queue/currentTrack',
|
||||
hasNext: 'queue/hasNext',
|
||||
hasPrevious: 'queue/hasPrevious',
|
||||
emptyQueue: 'queue/isEmpty',
|
||||
durationFormatted: 'player/durationFormatted',
|
||||
currentTimeFormatted: 'player/currentTimeFormatted',
|
||||
progress: 'player/progress'
|
||||
}),
|
||||
updateProgressThrottled () {
|
||||
return _.throttle(this.updateProgress, 50)
|
||||
},
|
||||
labels () {
|
||||
const audioPlayer = this.$pgettext('Sidebar/Player/Hidden text', 'Media player')
|
||||
const previous = this.$pgettext('Sidebar/Player/Icon.Tooltip', 'Previous track')
|
||||
const play = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Play')
|
||||
const pause = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Pause')
|
||||
const next = this.$pgettext('Sidebar/Player/Icon.Tooltip', 'Next track')
|
||||
const unmute = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Unmute')
|
||||
const mute = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Mute')
|
||||
const expandQueue = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Expand queue')
|
||||
const loopingDisabled = this.$pgettext('Sidebar/Player/Icon.Tooltip',
|
||||
'Looping disabled. Click to switch to single-track looping.'
|
||||
)
|
||||
const loopingSingle = this.$pgettext('Sidebar/Player/Icon.Tooltip',
|
||||
'Looping on a single track. Click to switch to whole queue looping.'
|
||||
)
|
||||
const loopingWhole = this.$pgettext('Sidebar/Player/Icon.Tooltip',
|
||||
'Looping on whole queue. Click to disable looping.'
|
||||
)
|
||||
const shuffle = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Shuffle your queue')
|
||||
const clear = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Clear your queue')
|
||||
const addArtistContentFilter = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Hide content from this artist…')
|
||||
return {
|
||||
audioPlayer,
|
||||
previous,
|
||||
play,
|
||||
pause,
|
||||
next,
|
||||
unmute,
|
||||
mute,
|
||||
loopingDisabled,
|
||||
loopingSingle,
|
||||
loopingWhole,
|
||||
shuffle,
|
||||
clear,
|
||||
expandQueue,
|
||||
addArtistContentFilter
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentTrack: {
|
||||
async handler (newValue, oldValue) {
|
||||
if (newValue === oldValue) {
|
||||
return
|
||||
}
|
||||
this.nextTrackPreloaded = false
|
||||
clearTimeout(this.playTimeout)
|
||||
if (this.currentSound) {
|
||||
this.currentSound.pause()
|
||||
}
|
||||
this.$store.commit('player/isLoadingAudio', true)
|
||||
this.playTimeout = setTimeout(async () => {
|
||||
await this.loadSound(newValue, oldValue)
|
||||
}, 100)
|
||||
this.updateMetadata()
|
||||
},
|
||||
immediate: false
|
||||
},
|
||||
volume: {
|
||||
immediate: true,
|
||||
handler (newValue) {
|
||||
this.sliderVolume = newValue
|
||||
Howler.volume(toLinearVolumeScale(newValue))
|
||||
}
|
||||
},
|
||||
sliderVolume (newValue) {
|
||||
this.$store.commit('player/volume', newValue)
|
||||
},
|
||||
playing: async function (newValue) {
|
||||
if (this.currentSound) {
|
||||
if (newValue === true) {
|
||||
this.soundId = this.currentSound.play(this.soundId)
|
||||
} else {
|
||||
this.currentSound.pause(this.soundId)
|
||||
}
|
||||
} else {
|
||||
await this.loadSound(this.currentTrack, null)
|
||||
}
|
||||
|
||||
this.observeProgress(newValue)
|
||||
},
|
||||
currentTime (newValue) {
|
||||
if (!this.isUpdatingTime) {
|
||||
this.setCurrentTime(newValue)
|
||||
}
|
||||
this.isUpdatingTime = false
|
||||
},
|
||||
emptyQueue (newValue) {
|
||||
if (newValue) {
|
||||
Howler.unload()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,29 +1,69 @@
|
|||
<template>
|
||||
<div>
|
||||
<h2><translate translate-context="Content/Search/Title">Search for some music</translate></h2>
|
||||
<h2>
|
||||
<translate translate-context="Content/Search/Title">
|
||||
Search for some music
|
||||
</translate>
|
||||
</h2>
|
||||
<div :class="['ui', {'loading': isLoading }, 'search']">
|
||||
<div class="ui icon big input">
|
||||
<i class="search icon"></i>
|
||||
<input ref="search" class="prompt" :placeholder="labels.searchPlaceholder" v-model.trim="query" type="text" />
|
||||
<i class="search icon" />
|
||||
<input
|
||||
ref="search"
|
||||
v-model.trim="query"
|
||||
class="prompt"
|
||||
:placeholder="labels.searchPlaceholder"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="query.length > 0">
|
||||
<h3 class="ui title"><translate translate-context="*/*/*/Noun">Artists</translate></h3>
|
||||
<h3 class="ui title">
|
||||
<translate translate-context="*/*/*/Noun">
|
||||
Artists
|
||||
</translate>
|
||||
</h3>
|
||||
<div v-if="results.artists.length > 0">
|
||||
<div class="ui cards">
|
||||
<artist-card :key="artist.id" v-for="artist in results.artists" :artist="artist" ></artist-card>
|
||||
<artist-card
|
||||
v-for="artist in results.artists"
|
||||
:key="artist.id"
|
||||
:artist="artist"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else><translate translate-context="Content/Search/Paragraph">No artist matched your query</translate></p>
|
||||
<p v-else>
|
||||
<translate translate-context="Content/Search/Paragraph">
|
||||
No artist matched your query
|
||||
</translate>
|
||||
</p>
|
||||
</template>
|
||||
<template v-if="query.length > 0">
|
||||
<h3 class="ui title"><translate translate-context="*/*/*">Albums</translate></h3>
|
||||
<div v-if="results.albums.length > 0" class="ui stackable three column grid">
|
||||
<div class="column" :key="album.id" v-for="album in results.albums">
|
||||
<album-card class="fluid" :album="album" ></album-card>
|
||||
<h3 class="ui title">
|
||||
<translate translate-context="*/*/*">
|
||||
Albums
|
||||
</translate>
|
||||
</h3>
|
||||
<div
|
||||
v-if="results.albums.length > 0"
|
||||
class="ui stackable three column grid"
|
||||
>
|
||||
<div
|
||||
v-for="album in results.albums"
|
||||
:key="album.id"
|
||||
class="column"
|
||||
>
|
||||
<album-card
|
||||
class="fluid"
|
||||
:album="album"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else><translate translate-context="Content/Search/Paragraph">No album matched your query</translate></p>
|
||||
<p v-else>
|
||||
<translate translate-context="Content/Search/Paragraph">
|
||||
No album matched your query
|
||||
</translate>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -41,7 +81,7 @@ export default {
|
|||
ArtistCard
|
||||
},
|
||||
props: {
|
||||
autofocus: {type: Boolean, default: false}
|
||||
autofocus: { type: Boolean, default: false }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
@ -53,12 +93,6 @@ export default {
|
|||
isLoading: false
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.autofocus) {
|
||||
this.$refs.search.focus()
|
||||
}
|
||||
this.search()
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
return {
|
||||
|
@ -66,15 +100,26 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
query () {
|
||||
this.search()
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.autofocus) {
|
||||
this.$refs.search.focus()
|
||||
}
|
||||
this.search()
|
||||
},
|
||||
methods: {
|
||||
search: _.debounce(function () {
|
||||
if (this.query.length < 1) {
|
||||
return
|
||||
}
|
||||
var self = this
|
||||
const self = this
|
||||
self.isLoading = true
|
||||
logger.default.debug('Searching track matching "' + this.query + '"')
|
||||
let params = {
|
||||
const params = {
|
||||
query: this.query
|
||||
}
|
||||
axios.get('search', {
|
||||
|
@ -90,11 +135,6 @@ export default {
|
|||
artists: results.artists
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
query () {
|
||||
this.search()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
<template>
|
||||
<div class="ui fluid category search">
|
||||
<slot></slot><div class="ui icon input">
|
||||
<input :aria-label="labels.searchContent" ref="search" type="search" class="prompt" name="search" :placeholder="labels.placeholder" @keydown.esc="$event.target.blur()">
|
||||
<i class="search icon"></i>
|
||||
<slot /><div class="ui icon input">
|
||||
<input
|
||||
ref="search"
|
||||
:aria-label="labels.searchContent"
|
||||
type="search"
|
||||
class="prompt"
|
||||
name="search"
|
||||
:placeholder="labels.placeholder"
|
||||
@keydown.esc="$event.target.blur()"
|
||||
>
|
||||
<i class="search icon" />
|
||||
</div>
|
||||
<div class="results"></div>
|
||||
<slot name="after"></slot>
|
||||
<div class="results" />
|
||||
<slot name="after" />
|
||||
<GlobalEvents
|
||||
@keydown.shift.f.prevent.exact="focusSearch"
|
||||
/>
|
||||
|
@ -16,11 +24,11 @@
|
|||
import jQuery from 'jquery'
|
||||
import router from '@/router'
|
||||
import lodash from '@/lodash'
|
||||
import GlobalEvents from "@/components/utils/global-events"
|
||||
import GlobalEvents from '@/components/utils/global-events'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlobalEvents,
|
||||
GlobalEvents
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
|
@ -31,22 +39,21 @@ export default {
|
|||
}
|
||||
},
|
||||
mounted () {
|
||||
let artistLabel = this.$pgettext('*/*/*/Noun', 'Artist')
|
||||
let albumLabel = this.$pgettext('*/*/*', 'Album')
|
||||
let trackLabel = this.$pgettext('*/*/*/Noun', 'Track')
|
||||
let tagLabel = this.$pgettext('*/*/*/Noun', 'Tag')
|
||||
let self = this
|
||||
var searchQuery;
|
||||
const artistLabel = this.$pgettext('*/*/*/Noun', 'Artist')
|
||||
const albumLabel = this.$pgettext('*/*/*', 'Album')
|
||||
const trackLabel = this.$pgettext('*/*/*/Noun', 'Track')
|
||||
const tagLabel = this.$pgettext('*/*/*/Noun', 'Tag')
|
||||
const self = this
|
||||
let searchQuery
|
||||
|
||||
jQuery(this.$el).keypress(function(e) {
|
||||
if(e.which == 13) {
|
||||
jQuery(this.$el).keypress(function (e) {
|
||||
if (e.which === 13) {
|
||||
// Cancel any API search request to backend…
|
||||
jQuery(this.$el).search('cancel query');
|
||||
jQuery(this.$el).search('cancel query')
|
||||
// Go direct to the artist page…
|
||||
router.push(`/search?q=${searchQuery}&type=artists`);
|
||||
router.push(`/search?q=${searchQuery}&type=artists`)
|
||||
}
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
jQuery(this.$el).search({
|
||||
type: 'category',
|
||||
|
@ -57,9 +64,9 @@ export default {
|
|||
noResults: this.$pgettext('Sidebar/Search/Error.Label', 'Sorry, there are no results for this search')
|
||||
},
|
||||
onSelect (result, response) {
|
||||
jQuery(self.$el).search("set value", searchQuery)
|
||||
jQuery(self.$el).search('set value', searchQuery)
|
||||
router.push(result.routerUrl)
|
||||
jQuery(self.$el).search("hide results")
|
||||
jQuery(self.$el).search('hide results')
|
||||
return false
|
||||
},
|
||||
onSearchQuery (query) {
|
||||
|
@ -78,17 +85,17 @@ export default {
|
|||
return xhrObject
|
||||
},
|
||||
onResponse: function (initialResponse) {
|
||||
let objId = self.extractObjId(searchQuery)
|
||||
let results = {}
|
||||
const objId = self.extractObjId(searchQuery)
|
||||
const results = {}
|
||||
let isEmptyResults = true
|
||||
let categories = [
|
||||
const categories = [
|
||||
{
|
||||
code: 'federation',
|
||||
name: self.$pgettext('*/*/*', 'Federation'),
|
||||
name: self.$pgettext('*/*/*', 'Federation')
|
||||
},
|
||||
{
|
||||
code: 'podcasts',
|
||||
name: self.$pgettext('*/*/*', 'Podcasts'),
|
||||
name: self.$pgettext('*/*/*', 'Podcasts')
|
||||
},
|
||||
{
|
||||
code: 'artists',
|
||||
|
@ -148,12 +155,12 @@ export default {
|
|||
},
|
||||
getId (t) {
|
||||
return t.name
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
code: 'more',
|
||||
name: '',
|
||||
},
|
||||
name: ''
|
||||
}
|
||||
]
|
||||
categories.forEach(category => {
|
||||
results[category.code] = {
|
||||
|
@ -161,29 +168,27 @@ export default {
|
|||
results: []
|
||||
}
|
||||
if (category.code === 'federation') {
|
||||
|
||||
if (objId) {
|
||||
isEmptyResults = false
|
||||
let searchMessage = self.$pgettext('Search/*/*', 'Search on the fediverse')
|
||||
results['federation'] = {
|
||||
const searchMessage = self.$pgettext('Search/*/*', 'Search on the fediverse')
|
||||
results.federation = {
|
||||
name: self.$pgettext('*/*/*', 'Federation'),
|
||||
results: [{
|
||||
title: searchMessage,
|
||||
routerUrl: {
|
||||
name: 'search',
|
||||
query: {
|
||||
id: objId,
|
||||
id: objId
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (category.code === 'podcasts') {
|
||||
} else if (category.code === 'podcasts') {
|
||||
if (objId) {
|
||||
isEmptyResults = false
|
||||
let searchMessage = self.$pgettext('Search/*/*', 'Subscribe to podcast via RSS')
|
||||
results['podcasts'] = {
|
||||
const searchMessage = self.$pgettext('Search/*/*', 'Subscribe to podcast via RSS')
|
||||
results.podcasts = {
|
||||
name: self.$pgettext('*/*/*', 'Podcasts'),
|
||||
results: [{
|
||||
title: searchMessage,
|
||||
|
@ -191,33 +196,31 @@ export default {
|
|||
name: 'search',
|
||||
query: {
|
||||
id: objId,
|
||||
type: "rss"
|
||||
type: 'rss'
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (category.code === 'more') {
|
||||
let searchMessage = self.$pgettext('Search/*/*', 'More results 🡒')
|
||||
results['more'] = {
|
||||
} else if (category.code === 'more') {
|
||||
const searchMessage = self.$pgettext('Search/*/*', 'More results 🡒')
|
||||
results.more = {
|
||||
name: '',
|
||||
results: [{
|
||||
title: searchMessage,
|
||||
routerUrl: {
|
||||
name: 'search',
|
||||
query: {
|
||||
type: "artists",
|
||||
type: 'artists',
|
||||
q: searchQuery
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
initialResponse[category.code].forEach(result => {
|
||||
isEmptyResults = false
|
||||
let id = category.getId(result)
|
||||
const id = category.getId(result)
|
||||
results[category.code].results.push({
|
||||
title: category.getTitle(result),
|
||||
id,
|
||||
|
|
|
@ -1,74 +1,87 @@
|
|||
<template>
|
||||
<button class="circular control button" :class="['component-volume-control', {'expanded': expanded}]" @click.prevent.stop="" @mouseover="handleOver" @mouseleave="handleLeave">
|
||||
<button
|
||||
class="circular control button"
|
||||
:class="['component-volume-control', {'expanded': expanded}]"
|
||||
@click.prevent.stop=""
|
||||
@mouseover="handleOver"
|
||||
@mouseleave="handleLeave"
|
||||
>
|
||||
<span
|
||||
role="button"
|
||||
v-if="sliderVolume === 0"
|
||||
role="button"
|
||||
:title="labels.unmute"
|
||||
:aria-label="labels.unmute"
|
||||
@click.prevent.stop="unmute">
|
||||
<i class="volume off icon"></i>
|
||||
@click.prevent.stop="unmute"
|
||||
>
|
||||
<i class="volume off icon" />
|
||||
</span>
|
||||
<span
|
||||
role="button"
|
||||
v-else-if="sliderVolume < 0.5"
|
||||
role="button"
|
||||
:title="labels.mute"
|
||||
:aria-label="labels.mute"
|
||||
@click.prevent.stop="mute">
|
||||
<i class="volume down icon"></i>
|
||||
@click.prevent.stop="mute"
|
||||
>
|
||||
<i class="volume down icon" />
|
||||
</span>
|
||||
<span
|
||||
role="button"
|
||||
v-else
|
||||
role="button"
|
||||
:title="labels.mute"
|
||||
:aria-label="labels.mute"
|
||||
@click.prevent.stop="mute">
|
||||
<i class="volume up icon"></i>
|
||||
@click.prevent.stop="mute"
|
||||
>
|
||||
<i class="volume up icon" />
|
||||
</span>
|
||||
<div class="popup">
|
||||
<label for="volume-slider" class="visually-hidden">{{ labels.slider }}</label>
|
||||
<label
|
||||
for="volume-slider"
|
||||
class="visually-hidden"
|
||||
>{{ labels.slider }}</label>
|
||||
<input
|
||||
id="volume-slider"
|
||||
v-model="sliderVolume"
|
||||
type="range"
|
||||
step="any"
|
||||
min="0"
|
||||
v-bind:max="volumeSteps"
|
||||
v-model="sliderVolume" />
|
||||
:max="volumeSteps"
|
||||
>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
<script>
|
||||
import { mapState, mapGetters, mapActions } from "vuex"
|
||||
import mapActions from 'vuex'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
expanded: false,
|
||||
timeout: null,
|
||||
volumeSteps: 100,
|
||||
volumeSteps: 100
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sliderVolume: {
|
||||
get () {
|
||||
return this.$store.state.player.volume * this.volumeSteps;
|
||||
return this.$store.state.player.volume * this.volumeSteps
|
||||
},
|
||||
set (v) {
|
||||
this.$store.commit("player/volume", v / this.volumeSteps)
|
||||
this.$store.commit('player/volume', v / this.volumeSteps)
|
||||
}
|
||||
},
|
||||
labels () {
|
||||
return {
|
||||
unmute: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Unmute"),
|
||||
mute: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Mute"),
|
||||
slider: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Adjust volume")
|
||||
unmute: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Unmute'),
|
||||
mute: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Mute'),
|
||||
slider: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Adjust volume')
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
mute: "player/mute",
|
||||
unmute: "player/unmute",
|
||||
toggleMute: "player/toggleMute",
|
||||
mute: 'player/mute',
|
||||
unmute: 'player/unmute',
|
||||
toggleMute: 'player/toggleMute'
|
||||
}),
|
||||
handleOver () {
|
||||
if (this.timeout) {
|
||||
|
@ -80,7 +93,7 @@ export default {
|
|||
if (this.timeout) {
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
this.timeout = setTimeout(() => {this.expanded = false}, 500)
|
||||
this.timeout = setTimeout(() => { this.expanded = false }, 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,32 @@
|
|||
<template>
|
||||
<div class="card app-card component-album-card">
|
||||
<div
|
||||
v-lazy:background-image="imageUrl"
|
||||
:class="['ui', 'head-image', 'image', {'default-cover': !album.cover || !album.cover.urls.original}]"
|
||||
@click="$router.push({name: 'library.albums.detail', params: {id: album.id}})"
|
||||
:class="['ui', 'head-image', 'image', {'default-cover': !album.cover || !album.cover.urls.original}]" v-lazy:background-image="imageUrl">
|
||||
<play-button :icon-only="true" :is-playable="album.is_playable" :button-classes="['ui', 'circular', 'large', 'vibrant', 'icon', 'button']" :album="album"></play-button>
|
||||
>
|
||||
<play-button
|
||||
:icon-only="true"
|
||||
:is-playable="album.is_playable"
|
||||
:button-classes="['ui', 'circular', 'large', 'vibrant', 'icon', 'button']"
|
||||
:album="album"
|
||||
/>
|
||||
</div>
|
||||
<div class="content">
|
||||
<strong>
|
||||
<router-link class="discrete link" :to="{name: 'library.albums.detail', params: {id: album.id}}">
|
||||
<router-link
|
||||
class="discrete link"
|
||||
:to="{name: 'library.albums.detail', params: {id: album.id}}"
|
||||
>
|
||||
{{ album.title }}
|
||||
</router-link>
|
||||
</strong>
|
||||
<div class="description">
|
||||
<span>
|
||||
<router-link class="discrete link" :to="{name: 'library.artists.detail', params: {id: album.artist.id}}">
|
||||
<router-link
|
||||
class="discrete link"
|
||||
:to="{name: 'library.artists.detail', params: {id: album.artist.id}}"
|
||||
>
|
||||
{{ album.artist.name }}
|
||||
</router-link>
|
||||
</span>
|
||||
|
@ -21,8 +34,21 @@
|
|||
</div>
|
||||
<div class="extra content">
|
||||
<span v-if="album.release_date">{{ album.release_date | moment('Y') }} · </span>
|
||||
<translate translate-context="*/*/*" :translate-params="{count: album.tracks_count}" :translate-n="album.tracks_count" translate-plural="%{ count } tracks">%{ count } track</translate>
|
||||
<play-button class="right floated basic icon" :dropdown-only="true" :is-playable="album.is_playable" :dropdown-icon-classes="['ellipsis', 'horizontal', 'large really discrete']" :album="album"></play-button>
|
||||
<translate
|
||||
translate-context="*/*/*"
|
||||
:translate-params="{count: album.tracks_count}"
|
||||
:translate-n="album.tracks_count"
|
||||
translate-plural="%{ count } tracks"
|
||||
>
|
||||
%{ count } track
|
||||
</translate>
|
||||
<play-button
|
||||
class="right floated basic icon"
|
||||
:dropdown-only="true"
|
||||
:is-playable="album.is_playable"
|
||||
:dropdown-icon-classes="['ellipsis', 'horizontal', 'large really discrete']"
|
||||
:album="album"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -31,17 +57,18 @@
|
|||
import PlayButton from '@/components/audio/PlayButton'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
album: {type: Object},
|
||||
},
|
||||
components: {
|
||||
PlayButton
|
||||
},
|
||||
props: {
|
||||
album: { type: Object, required: true }
|
||||
},
|
||||
computed: {
|
||||
imageUrl () {
|
||||
if (this.album.cover && this.album.cover.urls.original) {
|
||||
return this.$store.getters['instance/absoluteUrl'](this.album.cover.urls.medium_square_crop)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,54 @@
|
|||
<template>
|
||||
<div class="wrapper">
|
||||
<h3 v-if="!!this.$slots.title" class="ui header">
|
||||
<slot name="title"></slot>
|
||||
<span v-if="showCount" class="ui tiny circular label">{{ count }}</span>
|
||||
<h3
|
||||
v-if="!!$slots.title"
|
||||
class="ui header"
|
||||
>
|
||||
<slot name="title" />
|
||||
<span
|
||||
v-if="showCount"
|
||||
class="ui tiny circular label"
|
||||
>{{ count }}</span>
|
||||
</h3>
|
||||
<slot></slot>
|
||||
<inline-search-bar v-model="query" v-if="search" @search="albums = []; fetchData()"></inline-search-bar>
|
||||
<div class="ui hidden divider"></div>
|
||||
<slot />
|
||||
<inline-search-bar
|
||||
v-if="search"
|
||||
v-model="query"
|
||||
@search="albums = []; fetchData()"
|
||||
/>
|
||||
<div class="ui hidden divider" />
|
||||
<div class="ui app-cards cards">
|
||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||
<div class="ui loader"></div>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui inverted active dimmer"
|
||||
>
|
||||
<div class="ui loader" />
|
||||
</div>
|
||||
<album-card v-for="album in albums" :album="album" :key="album.id" />
|
||||
<album-card
|
||||
v-for="album in albums"
|
||||
:key="album.id"
|
||||
:album="album"
|
||||
/>
|
||||
</div>
|
||||
<slot v-if="!isLoading && albums.length === 0" name="empty-state">
|
||||
<empty-state @refresh="fetchData" :refresh="true"></empty-state>
|
||||
<slot
|
||||
v-if="!isLoading && albums.length === 0"
|
||||
name="empty-state"
|
||||
>
|
||||
<empty-state
|
||||
:refresh="true"
|
||||
@refresh="fetchData"
|
||||
/>
|
||||
</slot>
|
||||
<template v-if="nextPage">
|
||||
<div class="ui hidden divider"></div>
|
||||
<button v-if="nextPage" @click="fetchData(nextPage)" :class="['ui', 'basic', 'button']">
|
||||
<translate translate-context="*/*/Button,Label">Show more</translate>
|
||||
<div class="ui hidden divider" />
|
||||
<button
|
||||
v-if="nextPage"
|
||||
:class="['ui', 'basic', 'button']"
|
||||
@click="fetchData(nextPage)"
|
||||
>
|
||||
<translate translate-context="*/*/Button,Label">
|
||||
Show more
|
||||
</translate>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -30,16 +59,16 @@ import axios from 'axios'
|
|||
import AlbumCard from '@/components/audio/album/Card'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
filters: {type: Object, required: true},
|
||||
controls: {type: Boolean, default: true},
|
||||
showCount: {type: Boolean, default: false},
|
||||
search: {type: Boolean, default: false},
|
||||
limit: {type: Number, default: 12},
|
||||
},
|
||||
components: {
|
||||
AlbumCard
|
||||
},
|
||||
props: {
|
||||
filters: { type: Object, required: true },
|
||||
controls: { type: Boolean, default: true },
|
||||
showCount: { type: Boolean, default: false },
|
||||
search: { type: Boolean, default: false },
|
||||
limit: { type: Number, default: 12 }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
albums: [],
|
||||
|
@ -48,7 +77,15 @@ export default {
|
|||
errors: null,
|
||||
previousPage: null,
|
||||
nextPage: null,
|
||||
query: '',
|
||||
query: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
offset () {
|
||||
this.fetchData()
|
||||
},
|
||||
'$store.state.moderation.lastUpdate': function () {
|
||||
this.fetchData()
|
||||
}
|
||||
},
|
||||
created () {
|
||||
|
@ -58,11 +95,11 @@ export default {
|
|||
fetchData (url) {
|
||||
url = url || 'albums/'
|
||||
this.isLoading = true
|
||||
let self = this
|
||||
let params = {q: this.query, ...this.filters}
|
||||
const self = this
|
||||
const params = { q: this.query, ...this.filters }
|
||||
params.page_size = this.limit
|
||||
params.offset = this.offset
|
||||
axios.get(url, {params: params}).then((response) => {
|
||||
axios.get(url, { params: params }).then((response) => {
|
||||
self.previousPage = response.data.previous
|
||||
self.nextPage = response.data.next
|
||||
self.isLoading = false
|
||||
|
@ -79,14 +116,6 @@ export default {
|
|||
} else {
|
||||
this.offset = Math.max(this.offset - this.limit, 0)
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
offset () {
|
||||
this.fetchData()
|
||||
},
|
||||
"$store.state.moderation.lastUpdate": function () {
|
||||
this.fetchData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,49 +1,88 @@
|
|||
<template>
|
||||
<div class="app-card card">
|
||||
<div
|
||||
v-lazy:background-image="imageUrl"
|
||||
:class="['ui', 'head-image', 'circular', 'image', {'default-cover': !cover || !cover.urls.original}]"
|
||||
@click="$router.push({name: 'library.artists.detail', params: {id: artist.id}})"
|
||||
:class="['ui', 'head-image', 'circular', 'image', {'default-cover': !cover || !cover.urls.original}]" v-lazy:background-image="imageUrl">
|
||||
<play-button :icon-only="true" :is-playable="artist.is_playable" :button-classes="['ui', 'circular', 'large', 'vibrant', 'icon', 'button']" :artist="artist"></play-button>
|
||||
>
|
||||
<play-button
|
||||
:icon-only="true"
|
||||
:is-playable="artist.is_playable"
|
||||
:button-classes="['ui', 'circular', 'large', 'vibrant', 'icon', 'button']"
|
||||
:artist="artist"
|
||||
/>
|
||||
</div>
|
||||
<div class="content">
|
||||
<strong>
|
||||
<router-link class="discrete link" :to="{name: 'library.artists.detail', params: {id: artist.id}}">
|
||||
<router-link
|
||||
class="discrete link"
|
||||
:to="{name: 'library.artists.detail', params: {id: artist.id}}"
|
||||
>
|
||||
{{ artist.name|truncate(30) }}
|
||||
</router-link>
|
||||
</strong>
|
||||
|
||||
<tags-list label-classes="tiny" :truncate-size="20" :limit="2" :show-more="false" :tags="artist.tags"></tags-list>
|
||||
<tags-list
|
||||
label-classes="tiny"
|
||||
:truncate-size="20"
|
||||
:limit="2"
|
||||
:show-more="false"
|
||||
:tags="artist.tags"
|
||||
/>
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<translate v-if="artist.content_category === 'music'" translate-context="*/*/*" :translate-params="{count: artist.tracks_count}" :translate-n="artist.tracks_count" translate-plural="%{ count } tracks">%{ count } track</translate>
|
||||
<translate v-else translate-context="*/*/*" :translate-params="{count: artist.tracks_count}" :translate-n="artist.tracks_count" translate-plural="%{ count } episodes">%{ count } episode</translate>
|
||||
<play-button class="right floated basic icon" :dropdown-only="true" :is-playable="artist.is_playable" :dropdown-icon-classes="['ellipsis', 'horizontal', 'large really discrete']" :artist="artist"></play-button>
|
||||
<translate
|
||||
v-if="artist.content_category === 'music'"
|
||||
translate-context="*/*/*"
|
||||
:translate-params="{count: artist.tracks_count}"
|
||||
:translate-n="artist.tracks_count"
|
||||
translate-plural="%{ count } tracks"
|
||||
>
|
||||
%{ count } track
|
||||
</translate>
|
||||
<translate
|
||||
v-else
|
||||
translate-context="*/*/*"
|
||||
:translate-params="{count: artist.tracks_count}"
|
||||
:translate-n="artist.tracks_count"
|
||||
translate-plural="%{ count } episodes"
|
||||
>
|
||||
%{ count } episode
|
||||
</translate>
|
||||
<play-button
|
||||
class="right floated basic icon"
|
||||
:dropdown-only="true"
|
||||
:is-playable="artist.is_playable"
|
||||
:dropdown-icon-classes="['ellipsis', 'horizontal', 'large really discrete']"
|
||||
:artist="artist"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PlayButton from '@/components/audio/PlayButton'
|
||||
import TagsList from "@/components/tags/List"
|
||||
import TagsList from '@/components/tags/List'
|
||||
|
||||
export default {
|
||||
props: ['artist'],
|
||||
components: {
|
||||
PlayButton,
|
||||
TagsList
|
||||
},
|
||||
props: { artist: { type: Object, required: true } },
|
||||
data () {
|
||||
return {
|
||||
initialAlbums: 30,
|
||||
showAllAlbums: true,
|
||||
showAllAlbums: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
imageUrl () {
|
||||
let cover = this.cover
|
||||
const cover = this.cover
|
||||
if (cover && cover.urls.original) {
|
||||
return this.$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)
|
||||
}
|
||||
return null
|
||||
},
|
||||
cover () {
|
||||
if (this.artist.cover && this.artist.cover.urls.original) {
|
||||
|
@ -54,7 +93,7 @@ export default {
|
|||
}).filter((c) => {
|
||||
return c && c.urls.original
|
||||
})[0]
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,24 +1,50 @@
|
|||
<template>
|
||||
<div class="wrapper">
|
||||
<h3 v-if="header" class="ui header">
|
||||
<slot name="title"></slot>
|
||||
<h3
|
||||
v-if="header"
|
||||
class="ui header"
|
||||
>
|
||||
<slot name="title" />
|
||||
<span class="ui tiny circular label">{{ count }}</span>
|
||||
</h3>
|
||||
<inline-search-bar v-model="query" v-if="search" @search="objects = []; fetchData()"></inline-search-bar>
|
||||
<div class="ui hidden divider"></div>
|
||||
<inline-search-bar
|
||||
v-if="search"
|
||||
v-model="query"
|
||||
@search="objects = []; fetchData()"
|
||||
/>
|
||||
<div class="ui hidden divider" />
|
||||
<div class="ui five app-cards cards">
|
||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||
<div class="ui loader"></div>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui inverted active dimmer"
|
||||
>
|
||||
<div class="ui loader" />
|
||||
</div>
|
||||
<artist-card :artist="artist" v-for="artist in objects" :key="artist.id"></artist-card>
|
||||
<artist-card
|
||||
v-for="artist in objects"
|
||||
:key="artist.id"
|
||||
:artist="artist"
|
||||
/>
|
||||
</div>
|
||||
<slot v-if="!isLoading && objects.length === 0" name="empty-state">
|
||||
<empty-state @refresh="fetchData" :refresh="true"></empty-state>
|
||||
<slot
|
||||
v-if="!isLoading && objects.length === 0"
|
||||
name="empty-state"
|
||||
>
|
||||
<empty-state
|
||||
:refresh="true"
|
||||
@refresh="fetchData"
|
||||
/>
|
||||
</slot>
|
||||
<template v-if="nextPage">
|
||||
<div class="ui hidden divider"></div>
|
||||
<button v-if="nextPage" @click="fetchData(nextPage)" :class="['ui', 'basic', 'button']">
|
||||
<translate translate-context="*/*/Button,Label">Show more</translate>
|
||||
<div class="ui hidden divider" />
|
||||
<button
|
||||
v-if="nextPage"
|
||||
:class="['ui', 'basic', 'button']"
|
||||
@click="fetchData(nextPage)"
|
||||
>
|
||||
<translate translate-context="*/*/Button,Label">
|
||||
Show more
|
||||
</translate>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -26,17 +52,17 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import ArtistCard from "@/components/audio/artist/Card"
|
||||
import ArtistCard from '@/components/audio/artist/Card'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
filters: {type: Object, required: true},
|
||||
controls: {type: Boolean, default: true},
|
||||
header: {type: Boolean, default: true},
|
||||
search: {type: Boolean, default: false},
|
||||
},
|
||||
components: {
|
||||
ArtistCard,
|
||||
ArtistCard
|
||||
},
|
||||
props: {
|
||||
filters: { type: Object, required: true },
|
||||
controls: { type: Boolean, default: true },
|
||||
header: { type: Boolean, default: true },
|
||||
search: { type: Boolean, default: false }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
@ -47,7 +73,15 @@ export default {
|
|||
errors: null,
|
||||
previousPage: null,
|
||||
nextPage: null,
|
||||
query: '',
|
||||
query: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
offset () {
|
||||
this.fetchData()
|
||||
},
|
||||
'$store.state.moderation.lastUpdate': function () {
|
||||
this.fetchData()
|
||||
}
|
||||
},
|
||||
created () {
|
||||
|
@ -57,11 +91,11 @@ export default {
|
|||
fetchData (url) {
|
||||
url = url || 'artists/'
|
||||
this.isLoading = true
|
||||
let self = this
|
||||
let params = {q: this.query, ...this.filters}
|
||||
const self = this
|
||||
const params = { q: this.query, ...this.filters }
|
||||
params.page_size = this.limit
|
||||
params.offset = this.offset
|
||||
axios.get(url, {params: params}).then((response) => {
|
||||
axios.get(url, { params: params }).then((response) => {
|
||||
self.previousPage = response.data.previous
|
||||
self.nextPage = response.data.next
|
||||
self.isLoading = false
|
||||
|
@ -78,14 +112,6 @@ export default {
|
|||
} else {
|
||||
this.offset = Math.max(this.offset - this.limit, 0)
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
offset () {
|
||||
this.fetchData()
|
||||
},
|
||||
"$store.state.moderation.lastUpdate": function () {
|
||||
this.fetchData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,12 +7,10 @@
|
|||
>
|
||||
<div
|
||||
v-if="showArt"
|
||||
@click.prevent.exact="activateTrack(track, index)"
|
||||
class="image left floated column"
|
||||
@click.prevent.exact="activateTrack(track, index)"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
v-if="
|
||||
track.album && track.album.cover && track.album.cover.urls.original
|
||||
"
|
||||
|
@ -21,10 +19,10 @@
|
|||
track.album.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
>
|
||||
<img
|
||||
v-else-if="
|
||||
track.cover
|
||||
"
|
||||
|
@ -33,10 +31,10 @@
|
|||
track.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
>
|
||||
<img
|
||||
v-else-if="
|
||||
track.artist.cover
|
||||
"
|
||||
|
@ -45,19 +43,21 @@
|
|||
track.artist.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
src="../../../assets/audio/default-cover.png"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
tabindex=0
|
||||
@click="activateTrack(track, index)"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="content ellipsis left floated column"
|
||||
@click="activateTrack(track, index)"
|
||||
>
|
||||
<p
|
||||
:class="[
|
||||
|
@ -68,24 +68,33 @@
|
|||
>
|
||||
{{ track.title }}
|
||||
</p>
|
||||
<p v-if="track.artist.content_category === 'podcast'" class="track-meta mobile">
|
||||
<human-date class="really discrete" :date="track.creation_date"></human-date>
|
||||
<p
|
||||
v-if="track.artist.content_category === 'podcast'"
|
||||
class="track-meta mobile"
|
||||
>
|
||||
<human-date
|
||||
class="really discrete"
|
||||
:date="track.creation_date"
|
||||
/>
|
||||
<span>·</span>
|
||||
<human-duration
|
||||
v-if="track.uploads[0] && track.uploads[0].duration"
|
||||
:duration="track.uploads[0].duration"
|
||||
></human-duration>
|
||||
/>
|
||||
</p>
|
||||
<p v-else class="track-meta mobile">
|
||||
<p
|
||||
v-else
|
||||
class="track-meta mobile"
|
||||
>
|
||||
{{ track.artist.name }} <span>·</span>
|
||||
<human-duration
|
||||
v-if="track.uploads[0] && track.uploads[0].duration"
|
||||
:duration="track.uploads[0].duration"
|
||||
></human-duration>
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="$store.state.auth.authenticated && this.track.artist.content_category !== 'podcast'"
|
||||
v-if="$store.state.auth.authenticated && track.artist.content_category !== 'podcast'"
|
||||
:class="[
|
||||
'meta',
|
||||
'right',
|
||||
|
@ -100,12 +109,11 @@
|
|||
class="tiny"
|
||||
:border="false"
|
||||
:track="track"
|
||||
></track-favorite-icon>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
role="button"
|
||||
:aria-label="actionsButtonLabel"
|
||||
@click.prevent.exact="showTrackModal = !showTrackModal"
|
||||
:class="[
|
||||
'modal-button',
|
||||
'right',
|
||||
|
@ -114,36 +122,36 @@
|
|||
'mobile',
|
||||
{ 'with-art': showArt },
|
||||
]"
|
||||
@click.prevent.exact="showTrackModal = !showTrackModal"
|
||||
>
|
||||
<i class="ellipsis large vertical icon" />
|
||||
</div>
|
||||
<track-modal
|
||||
@update:show="showTrackModal = $event;"
|
||||
:show="showTrackModal"
|
||||
:track="track"
|
||||
:index="index"
|
||||
:is-artist="isArtist"
|
||||
:is-album="isAlbum"
|
||||
></track-modal>
|
||||
@update:show="showTrackModal = $event;"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PlayIndicator from "@/components/audio/track/PlayIndicator";
|
||||
import { mapActions, mapGetters } from "vuex";
|
||||
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon";
|
||||
import TrackModal from "@/components/audio/track/Modal";
|
||||
import PlayOptionsMixin from "@/components/mixins/PlayOptions"
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
||||
import TrackModal from '@/components/audio/track/Modal'
|
||||
import PlayOptionsMixin from '@/components/mixins/PlayOptions'
|
||||
|
||||
export default {
|
||||
mixins: [PlayOptionsMixin],
|
||||
data() {
|
||||
return {
|
||||
showTrackModal: false,
|
||||
}
|
||||
|
||||
components: {
|
||||
TrackFavoriteIcon,
|
||||
TrackModal
|
||||
},
|
||||
mixins: [PlayOptionsMixin],
|
||||
props: {
|
||||
tracks: Array,
|
||||
tracks: { type: Array, required: true },
|
||||
showAlbum: { type: Boolean, required: false, default: true },
|
||||
showArtist: { type: Boolean, required: false, default: true },
|
||||
showPosition: { type: Boolean, required: false, default: false },
|
||||
|
@ -155,41 +163,40 @@ export default {
|
|||
showDuration: { type: Boolean, required: false, default: true },
|
||||
index: { type: Number, required: true },
|
||||
track: { type: Object, required: true },
|
||||
isArtist: {type: Boolean, required: false, default: false},
|
||||
isAlbum: {type: Boolean, required: false, default: false},
|
||||
isArtist: { type: Boolean, required: false, default: false },
|
||||
isAlbum: { type: Boolean, required: false, default: false }
|
||||
},
|
||||
|
||||
components: {
|
||||
PlayIndicator,
|
||||
TrackFavoriteIcon,
|
||||
TrackModal,
|
||||
data () {
|
||||
return {
|
||||
showTrackModal: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentTrack: "queue/currentTrack",
|
||||
currentTrack: 'queue/currentTrack'
|
||||
}),
|
||||
|
||||
isPlaying() {
|
||||
return this.$store.state.player.playing;
|
||||
isPlaying () {
|
||||
return this.$store.state.player.playing
|
||||
},
|
||||
actionsButtonLabel () {
|
||||
return this.$pgettext('Content/Track/Icon.Tooltip/Verb', 'Show track actions')
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
prettyPosition(position, size) {
|
||||
var s = String(position);
|
||||
prettyPosition (position, size) {
|
||||
let s = String(position)
|
||||
while (s.length < (size || 2)) {
|
||||
s = "0" + s;
|
||||
s = '0' + s
|
||||
}
|
||||
return s;
|
||||
return s
|
||||
},
|
||||
|
||||
...mapActions({
|
||||
resumePlayback: "player/resumePlayback",
|
||||
pausePlayback: "player/pausePlayback",
|
||||
}),
|
||||
},
|
||||
};
|
||||
resumePlayback: 'player/resumePlayback',
|
||||
pausePlayback: 'player/pausePlayback'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -15,8 +15,6 @@
|
|||
@click.prevent.exact="activateTrack(track, index)"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
v-if="
|
||||
track.cover && track.cover.urls.original
|
||||
"
|
||||
|
@ -25,10 +23,10 @@
|
|||
track.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
>
|
||||
<img
|
||||
v-else-if="
|
||||
defaultCover
|
||||
"
|
||||
|
@ -37,21 +35,32 @@
|
|||
defaultCover.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
src="../../../assets/audio/default-cover.png"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
<div tabindex=0 class="content left floated column">
|
||||
<div
|
||||
tabindex="0"
|
||||
class="content left floated column"
|
||||
>
|
||||
<a
|
||||
class="podcast-episode-title ellipsis"
|
||||
@click.prevent.exact="activateTrack(track, index)">{{ track.title }}</a>
|
||||
<p class="podcast-episode-meta">{{ description.text }}</p>
|
||||
@click.prevent.exact="activateTrack(track, index)"
|
||||
>{{ track.title }}</a>
|
||||
<p class="podcast-episode-meta">
|
||||
{{ description.text }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="displayActions" class="meta right floated column">
|
||||
<div
|
||||
v-if="displayActions"
|
||||
class="meta right floated column"
|
||||
>
|
||||
<play-button
|
||||
id="playmenu"
|
||||
class="play-button basic icon"
|
||||
|
@ -63,22 +72,25 @@
|
|||
'large really discrete',
|
||||
]"
|
||||
:track="track"
|
||||
></play-button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import PlayIndicator from "@/components/audio/track/PlayIndicator";
|
||||
import { mapActions, mapGetters } from "vuex";
|
||||
import PlayButton from "@/components/audio/PlayButton";
|
||||
import PlayOptions from "@/components/mixins/PlayOptions";
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import PlayButton from '@/components/audio/PlayButton'
|
||||
import PlayOptions from '@/components/mixins/PlayOptions'
|
||||
|
||||
export default {
|
||||
|
||||
components: {
|
||||
PlayButton
|
||||
},
|
||||
mixins: [PlayOptions],
|
||||
props: {
|
||||
tracks: Array,
|
||||
tracks: { type: Array, required: true },
|
||||
showAlbum: { type: Boolean, required: false, default: true },
|
||||
showArtist: { type: Boolean, required: false, default: true },
|
||||
showPosition: { type: Boolean, required: false, default: false },
|
||||
|
@ -90,34 +102,29 @@ export default {
|
|||
showDuration: { type: Boolean, required: false, default: true },
|
||||
index: { type: Number, required: true },
|
||||
track: { type: Object, required: true },
|
||||
defaultCover: { type: Object, required: false },
|
||||
defaultCover: { type: Object, required: false, default: () => { return {} } }
|
||||
},
|
||||
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
hover: null,
|
||||
errors: null,
|
||||
description: null,
|
||||
description: null
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchData('tracks/' + this.track.id + '/' )
|
||||
},
|
||||
|
||||
components: {
|
||||
PlayIndicator,
|
||||
PlayButton,
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentTrack: "queue/currentTrack",
|
||||
currentTrack: 'queue/currentTrack'
|
||||
}),
|
||||
|
||||
isPlaying() {
|
||||
return this.$store.state.player.playing;
|
||||
isPlaying () {
|
||||
return this.$store.state.player.playing
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchData('tracks/' + this.track.id + '/')
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -126,29 +133,29 @@ export default {
|
|||
return
|
||||
}
|
||||
this.isLoading = true
|
||||
let self = this
|
||||
const self = this
|
||||
try {
|
||||
let channelsPromise = await axios.get(url)
|
||||
const channelsPromise = await axios.get(url)
|
||||
self.description = channelsPromise.data.description
|
||||
self.isLoading = false
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
self.isLoading = false
|
||||
self.errors = error.backendErrors
|
||||
self.errors = e.backendErrors
|
||||
}
|
||||
},
|
||||
|
||||
prettyPosition(position, size) {
|
||||
var s = String(position);
|
||||
prettyPosition (position, size) {
|
||||
let s = String(position)
|
||||
while (s.length < (size || 2)) {
|
||||
s = "0" + s;
|
||||
s = '0' + s
|
||||
}
|
||||
return s;
|
||||
return s
|
||||
},
|
||||
|
||||
...mapActions({
|
||||
resumePlayback: "player/resumePlayback",
|
||||
pausePlayback: "player/pausePlayback",
|
||||
}),
|
||||
},
|
||||
};
|
||||
resumePlayback: 'player/resumePlayback',
|
||||
pausePlayback: 'player/pausePlayback'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="ui hidden divider"></div>
|
||||
<div class="ui hidden divider" />
|
||||
|
||||
<!-- Add a header if needed -->
|
||||
|
||||
<slot name="header"></slot>
|
||||
<slot name="header" />
|
||||
|
||||
<div>
|
||||
<div
|
||||
|
@ -13,38 +13,44 @@
|
|||
<!-- For each item, build a row -->
|
||||
<podcast-row
|
||||
v-for="(track, index) in tracks"
|
||||
:track="track"
|
||||
:key="track.id"
|
||||
:track="track"
|
||||
:index="index"
|
||||
:tracks="tracks"
|
||||
:display-actions="displayActions"
|
||||
:show-duration="showDuration"
|
||||
:is-podcast="isPodcast"
|
||||
></podcast-row>
|
||||
/>
|
||||
</div>
|
||||
<div v-if="paginateResults" class="ui center aligned basic segment desktop-and-up">
|
||||
<div
|
||||
v-if="paginateResults"
|
||||
class="ui center aligned basic segment desktop-and-up"
|
||||
>
|
||||
<pagination
|
||||
:total="total"
|
||||
:current="page"
|
||||
:paginate-by="paginateBy"
|
||||
v-on="$listeners">
|
||||
</pagination>
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-below']"
|
||||
>
|
||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||
<div class="ui loader"></div>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui inverted active dimmer"
|
||||
>
|
||||
<div class="ui loader" />
|
||||
</div>
|
||||
|
||||
<!-- For each item, build a row -->
|
||||
|
||||
<track-mobile-row
|
||||
v-for="(track, index) in tracks"
|
||||
:track="track"
|
||||
:key="track.id"
|
||||
:track="track"
|
||||
:index="index"
|
||||
:tracks="tracks"
|
||||
:show-position="showPosition"
|
||||
|
@ -53,36 +59,37 @@
|
|||
:is-artist="isArtist"
|
||||
:is-album="isAlbum"
|
||||
:is-podcast="isPodcast"
|
||||
></track-mobile-row>
|
||||
<div v-if="paginateResults" class="ui center aligned basic segment tablet-and-below">
|
||||
/>
|
||||
<div
|
||||
v-if="paginateResults"
|
||||
class="ui center aligned basic segment tablet-and-below"
|
||||
>
|
||||
<pagination
|
||||
v-if="paginateResults"
|
||||
:total="total"
|
||||
:current="page"
|
||||
:compact="true"
|
||||
v-on="$listeners"></pagination>
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from "@/lodash";
|
||||
import TrackRow from "@/components/audio/track/Row";
|
||||
import PodcastRow from "@/components/audio/podcast/Row";
|
||||
import TrackMobileRow from "@/components/audio/track/MobileRow";
|
||||
import Pagination from "@/components/Pagination";
|
||||
import PodcastRow from '@/components/audio/podcast/Row'
|
||||
import TrackMobileRow from '@/components/audio/track/MobileRow'
|
||||
import Pagination from '@/components/Pagination'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TrackRow,
|
||||
TrackMobileRow,
|
||||
Pagination,
|
||||
PodcastRow,
|
||||
PodcastRow
|
||||
},
|
||||
|
||||
props: {
|
||||
tracks: Array,
|
||||
tracks: { type: Array, required: true },
|
||||
showAlbum: { type: Boolean, required: false, default: true },
|
||||
showArtist: { type: Boolean, required: false, default: true },
|
||||
showPosition: { type: Boolean, required: false, default: false },
|
||||
|
@ -94,33 +101,33 @@ export default {
|
|||
showDuration: { type: Boolean, required: false, default: true },
|
||||
isArtist: { type: Boolean, required: false, default: false },
|
||||
isAlbum: { type: Boolean, required: false, default: false },
|
||||
paginateResults: { type: Boolean, required: false, default: true},
|
||||
total: { type: Number, required: false},
|
||||
page: {type: Number, required: false, default: 1},
|
||||
paginateBy: {type: Number, required: false, default: 25},
|
||||
isPodcast: {type: Boolean, required: true},
|
||||
defaultCover: {type: Object, required: false},
|
||||
paginateResults: { type: Boolean, required: false, default: true },
|
||||
total: { type: Number, required: false, default: 0 },
|
||||
page: { type: Number, required: false, default: 1 },
|
||||
paginateBy: { type: Number, required: false, default: 25 },
|
||||
isPodcast: { type: Boolean, required: true },
|
||||
defaultCover: { type: Object, required: false, default: () => { return {} } }
|
||||
},
|
||||
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
isLoading: false,
|
||||
};
|
||||
isLoading: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
labels() {
|
||||
labels () {
|
||||
return {
|
||||
title: this.$pgettext("*/*/*/Noun", "Title"),
|
||||
album: this.$pgettext("*/*/*/Noun", "Album"),
|
||||
artist: this.$pgettext("*/*/*/Noun", "Artist"),
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updatePage: function(page) {
|
||||
this.$emit('page-changed', page)
|
||||
title: this.$pgettext('*/*/*/Noun', 'Title'),
|
||||
album: this.$pgettext('*/*/*/Noun', 'Album'),
|
||||
artist: this.$pgettext('*/*/*/Noun', 'Artist')
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
methods: {
|
||||
updatePage: function (page) {
|
||||
this.$emit('page-changed', page)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -7,12 +7,10 @@
|
|||
>
|
||||
<div
|
||||
v-if="showArt"
|
||||
@click.prevent.exact="activateTrack(track, index)"
|
||||
class="image left floated column"
|
||||
@click.prevent.exact="activateTrack(track, index)"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
v-if="
|
||||
track.album && track.album.cover && track.album.cover.urls.original
|
||||
"
|
||||
|
@ -21,10 +19,10 @@
|
|||
track.album.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
>
|
||||
<img
|
||||
v-else-if="
|
||||
track.cover
|
||||
"
|
||||
|
@ -33,10 +31,10 @@
|
|||
track.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
>
|
||||
<img
|
||||
v-else-if="
|
||||
track.artist.cover
|
||||
"
|
||||
|
@ -45,19 +43,21 @@
|
|||
track.artist.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
src="../../../assets/audio/default-cover.png"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
tabindex=0
|
||||
@click="activateTrack(track, index)"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="content ellipsis left floated column"
|
||||
@click="activateTrack(track, index)"
|
||||
>
|
||||
<p
|
||||
:class="[
|
||||
|
@ -73,7 +73,7 @@
|
|||
<human-duration
|
||||
v-if="track.uploads[0] && track.uploads[0].duration"
|
||||
:duration="track.uploads[0].duration"
|
||||
></human-duration>
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
|
@ -92,12 +92,11 @@
|
|||
class="tiny"
|
||||
:border="false"
|
||||
:track="track"
|
||||
></track-favorite-icon>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
role="button"
|
||||
:aria-label="actionsButtonLabel"
|
||||
@click.prevent.exact="showTrackModal = !showTrackModal"
|
||||
:class="[
|
||||
'modal-button',
|
||||
'right',
|
||||
|
@ -106,36 +105,36 @@
|
|||
'mobile',
|
||||
{ 'with-art': showArt },
|
||||
]"
|
||||
@click.prevent.exact="showTrackModal = !showTrackModal"
|
||||
>
|
||||
<i class="ellipsis large vertical icon" />
|
||||
</div>
|
||||
<track-modal
|
||||
@update:show="showTrackModal = $event;"
|
||||
:show="showTrackModal"
|
||||
:track="track"
|
||||
:index="index"
|
||||
:is-artist="isArtist"
|
||||
:is-album="isAlbum"
|
||||
></track-modal>
|
||||
@update:show="showTrackModal = $event;"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PlayIndicator from "@/components/audio/track/PlayIndicator";
|
||||
import { mapActions, mapGetters } from "vuex";
|
||||
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon";
|
||||
import TrackModal from "@/components/audio/track/Modal";
|
||||
import PlayOptionsMixin from "@/components/mixins/PlayOptions"
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
||||
import TrackModal from '@/components/audio/track/Modal'
|
||||
import PlayOptionsMixin from '@/components/mixins/PlayOptions'
|
||||
|
||||
export default {
|
||||
mixins: [PlayOptionsMixin],
|
||||
data() {
|
||||
return {
|
||||
showTrackModal: false,
|
||||
}
|
||||
|
||||
components: {
|
||||
TrackFavoriteIcon,
|
||||
TrackModal
|
||||
},
|
||||
mixins: [PlayOptionsMixin],
|
||||
props: {
|
||||
tracks: Array,
|
||||
tracks: { type: Array, required: true },
|
||||
showAlbum: { type: Boolean, required: false, default: true },
|
||||
showArtist: { type: Boolean, required: false, default: true },
|
||||
showPosition: { type: Boolean, required: false, default: false },
|
||||
|
@ -147,41 +146,40 @@ export default {
|
|||
showDuration: { type: Boolean, required: false, default: true },
|
||||
index: { type: Number, required: true },
|
||||
track: { type: Object, required: true },
|
||||
isArtist: {type: Boolean, required: false, default: false},
|
||||
isAlbum: {type: Boolean, required: false, default: false},
|
||||
isArtist: { type: Boolean, required: false, default: false },
|
||||
isAlbum: { type: Boolean, required: false, default: false }
|
||||
},
|
||||
|
||||
components: {
|
||||
PlayIndicator,
|
||||
TrackFavoriteIcon,
|
||||
TrackModal,
|
||||
data () {
|
||||
return {
|
||||
showTrackModal: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentTrack: "queue/currentTrack",
|
||||
currentTrack: 'queue/currentTrack'
|
||||
}),
|
||||
|
||||
isPlaying() {
|
||||
return this.$store.state.player.playing;
|
||||
isPlaying () {
|
||||
return this.$store.state.player.playing
|
||||
},
|
||||
actionsButtonLabel () {
|
||||
return this.$pgettext('Content/Track/Icon.Tooltip/Verb', 'Show track actions')
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
prettyPosition(position, size) {
|
||||
var s = String(position);
|
||||
prettyPosition (position, size) {
|
||||
let s = String(position)
|
||||
while (s.length < (size || 2)) {
|
||||
s = "0" + s;
|
||||
s = '0' + s
|
||||
}
|
||||
return s;
|
||||
return s
|
||||
},
|
||||
|
||||
...mapActions({
|
||||
resumePlayback: "player/resumePlayback",
|
||||
pausePlayback: "player/pausePlayback",
|
||||
}),
|
||||
},
|
||||
};
|
||||
resumePlayback: 'player/resumePlayback',
|
||||
pausePlayback: 'player/pausePlayback'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div id="audio-bars">
|
||||
<div class="audio-bar"></div>
|
||||
<div class="audio-bar"></div>
|
||||
<div class="audio-bar"></div>
|
||||
<div class="audio-bar"></div>
|
||||
<div class="audio-bar" />
|
||||
<div class="audio-bar" />
|
||||
<div class="audio-bar" />
|
||||
<div class="audio-bar" />
|
||||
</div>
|
||||
</template>
|
|
@ -21,8 +21,7 @@
|
|||
track.id === currentTrack.id &&
|
||||
!(track.id == hover)
|
||||
"
|
||||
>
|
||||
</play-indicator>
|
||||
/>
|
||||
<button
|
||||
v-else-if="
|
||||
currentTrack &&
|
||||
|
@ -51,7 +50,10 @@
|
|||
>
|
||||
<i class="play icon" />
|
||||
</button>
|
||||
<span class="track-position" v-else-if="showPosition">
|
||||
<span
|
||||
v-else-if="showPosition"
|
||||
class="track-position"
|
||||
>
|
||||
{{ prettyPosition(track.position) }}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -62,8 +64,6 @@
|
|||
@click.prevent.exact="activateTrack(track, index)"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
v-if="
|
||||
track.album && track.album.cover && track.album.cover.urls.original
|
||||
"
|
||||
|
@ -72,10 +72,10 @@
|
|||
track.album.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
>
|
||||
<img
|
||||
v-else-if="
|
||||
track.cover && track.cover.urls.original
|
||||
"
|
||||
|
@ -84,10 +84,10 @@
|
|||
track.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
>
|
||||
<img
|
||||
v-else-if="
|
||||
track.artist && track.artist.cover && track.album.cover.urls.original
|
||||
"
|
||||
|
@ -96,36 +96,49 @@
|
|||
track.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
src="../../../assets/audio/default-cover.png"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
<div tabindex=0 class="content ellipsis left floated column">
|
||||
<div
|
||||
tabindex="0"
|
||||
class="content ellipsis left floated column"
|
||||
>
|
||||
<a
|
||||
@click="activateTrack(track, index)"
|
||||
>
|
||||
{{ track.title }}
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="showAlbum" class="content ellipsis left floated column">
|
||||
<div
|
||||
v-if="showAlbum"
|
||||
class="content ellipsis left floated column"
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: 'library.albums.detail', params: { id: track.album.id } }"
|
||||
>{{ track.album.title }}</router-link
|
||||
>
|
||||
{{ track.album.title }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div v-if="showArtist" class="content ellipsis left floated column">
|
||||
<div
|
||||
v-if="showArtist"
|
||||
class="content ellipsis left floated column"
|
||||
>
|
||||
<router-link
|
||||
class="artist link"
|
||||
:to="{
|
||||
name: 'library.artists.detail',
|
||||
params: { id: track.artist.id },
|
||||
}"
|
||||
>{{ track.artist.name }}</router-link
|
||||
>
|
||||
{{ track.artist.name }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div
|
||||
v-if="$store.state.auth.authenticated"
|
||||
|
@ -135,15 +148,21 @@
|
|||
class="tiny"
|
||||
:border="false"
|
||||
:track="track"
|
||||
></track-favorite-icon>
|
||||
/>
|
||||
</div>
|
||||
<div v-if="showDuration" class="meta right floated column">
|
||||
<div
|
||||
v-if="showDuration"
|
||||
class="meta right floated column"
|
||||
>
|
||||
<human-duration
|
||||
v-if="track.uploads[0] && track.uploads[0].duration"
|
||||
:duration="track.uploads[0].duration"
|
||||
></human-duration>
|
||||
/>
|
||||
</div>
|
||||
<div v-if="displayActions" class="meta right floated column">
|
||||
<div
|
||||
v-if="displayActions"
|
||||
class="meta right floated column"
|
||||
>
|
||||
<play-button
|
||||
id="playmenu"
|
||||
class="play-button basic icon"
|
||||
|
@ -155,22 +174,28 @@
|
|||
'large really discrete',
|
||||
]"
|
||||
:track="track"
|
||||
></play-button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PlayIndicator from "@/components/audio/track/PlayIndicator";
|
||||
import { mapActions, mapGetters } from "vuex";
|
||||
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon";
|
||||
import PlayButton from "@/components/audio/PlayButton";
|
||||
import PlayOptions from "@/components/mixins/PlayOptions";
|
||||
import PlayIndicator from '@/components/audio/track/PlayIndicator'
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
||||
import PlayButton from '@/components/audio/PlayButton'
|
||||
import PlayOptions from '@/components/mixins/PlayOptions'
|
||||
|
||||
export default {
|
||||
|
||||
components: {
|
||||
PlayIndicator,
|
||||
TrackFavoriteIcon,
|
||||
PlayButton
|
||||
},
|
||||
mixins: [PlayOptions],
|
||||
props: {
|
||||
tracks: Array,
|
||||
tracks: { type: Array, required: true },
|
||||
showAlbum: { type: Boolean, required: false, default: true },
|
||||
showArtist: { type: Boolean, required: false, default: true },
|
||||
showPosition: { type: Boolean, required: false, default: false },
|
||||
|
@ -181,45 +206,39 @@ export default {
|
|||
displayActions: { type: Boolean, required: false, default: true },
|
||||
showDuration: { type: Boolean, required: false, default: true },
|
||||
index: { type: Number, required: true },
|
||||
track: { type: Object, required: true },
|
||||
track: { type: Object, required: true }
|
||||
},
|
||||
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
hover: null,
|
||||
hover: null
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
PlayIndicator,
|
||||
TrackFavoriteIcon,
|
||||
PlayButton,
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentTrack: "queue/currentTrack",
|
||||
currentTrack: 'queue/currentTrack'
|
||||
}),
|
||||
|
||||
isPlaying() {
|
||||
return this.$store.state.player.playing;
|
||||
},
|
||||
isPlaying () {
|
||||
return this.$store.state.player.playing
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
prettyPosition(position, size) {
|
||||
var s = String(position);
|
||||
prettyPosition (position, size) {
|
||||
let s = String(position)
|
||||
while (s.length < (size || 2)) {
|
||||
s = "0" + s;
|
||||
s = '0' + s
|
||||
}
|
||||
return s;
|
||||
return s
|
||||
},
|
||||
|
||||
...mapActions({
|
||||
resumePlayback: "player/resumePlayback",
|
||||
pausePlayback: "player/pausePlayback",
|
||||
}),
|
||||
},
|
||||
};
|
||||
resumePlayback: 'player/resumePlayback',
|
||||
pausePlayback: 'player/pausePlayback'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -2,65 +2,95 @@
|
|||
<div>
|
||||
<!-- Show the search bar if search is true -->
|
||||
<inline-search-bar
|
||||
v-model="query"
|
||||
v-if="search"
|
||||
v-model="query"
|
||||
@search="
|
||||
additionalTracks = [];
|
||||
fetchData();
|
||||
"
|
||||
></inline-search-bar>
|
||||
<div class="ui hidden divider"></div>
|
||||
/>
|
||||
<div class="ui hidden divider" />
|
||||
|
||||
<!-- Add a header if needed -->
|
||||
|
||||
<slot name="header"></slot>
|
||||
<slot name="header" />
|
||||
|
||||
<!-- Show a message if no tracks are available -->
|
||||
|
||||
<slot v-if="!isLoading && allTracks.length === 0" name="empty-state">
|
||||
<slot
|
||||
v-if="!isLoading && allTracks.length === 0"
|
||||
name="empty-state"
|
||||
>
|
||||
<empty-state
|
||||
@refresh="fetchData('tracks/')"
|
||||
:refresh="true"
|
||||
></empty-state>
|
||||
@refresh="fetchData('tracks/')"
|
||||
/>
|
||||
</slot>
|
||||
<div v-else>
|
||||
<div
|
||||
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-up']"
|
||||
>
|
||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||
<div class="ui loader"></div>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui inverted active dimmer"
|
||||
>
|
||||
<div class="ui loader" />
|
||||
</div>
|
||||
<div class="track-table row">
|
||||
<div v-if="showPosition" class="actions left floated column">
|
||||
<i class="hashtag icon"></i>
|
||||
<div
|
||||
v-if="showPosition"
|
||||
class="actions left floated column"
|
||||
>
|
||||
<i class="hashtag icon" />
|
||||
</div>
|
||||
<div v-else class="actions left floated column"></div>
|
||||
<div v-if="showArt" class="image left floated column"></div>
|
||||
<div
|
||||
v-else
|
||||
class="actions left floated column"
|
||||
/>
|
||||
<div
|
||||
v-if="showArt"
|
||||
class="image left floated column"
|
||||
/>
|
||||
<div class="content ellipsis left floated column">
|
||||
<b>{{ labels.title }}</b>
|
||||
</div>
|
||||
<div v-if="showAlbum" class="content ellipsisleft floated column">
|
||||
<div
|
||||
v-if="showAlbum"
|
||||
class="content ellipsisleft floated column"
|
||||
>
|
||||
<b>{{ labels.album }}</b>
|
||||
</div>
|
||||
<div v-if="showArtist" class="content ellipsis left floated column">
|
||||
<div
|
||||
v-if="showArtist"
|
||||
class="content ellipsis left floated column"
|
||||
>
|
||||
<b>{{ labels.artist }}</b>
|
||||
</div>
|
||||
<div
|
||||
v-if="$store.state.auth.authenticated"
|
||||
class="meta right floated column"
|
||||
></div>
|
||||
<div v-if="showDuration" class="meta right floated column">
|
||||
<i class="clock outline icon" style="padding: 0.5rem" />
|
||||
/>
|
||||
<div
|
||||
v-if="showDuration"
|
||||
class="meta right floated column"
|
||||
>
|
||||
<i
|
||||
class="clock outline icon"
|
||||
style="padding: 0.5rem"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="displayActions" class="meta right floated column"></div>
|
||||
<div
|
||||
v-if="displayActions"
|
||||
class="meta right floated column"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- For each item, build a row -->
|
||||
|
||||
<track-row
|
||||
v-for="(track, index) in allTracks"
|
||||
:track="track"
|
||||
:key="track.id"
|
||||
:track="track"
|
||||
:index="index"
|
||||
:tracks="allTracks"
|
||||
:show-album="showAlbum"
|
||||
|
@ -70,31 +100,37 @@
|
|||
:display-actions="displayActions"
|
||||
:show-duration="showDuration"
|
||||
:is-podcast="isPodcast"
|
||||
></track-row>
|
||||
/>
|
||||
</div>
|
||||
<div v-if="paginateResults" class="ui center aligned basic segment desktop-and-up">
|
||||
<div
|
||||
v-if="paginateResults"
|
||||
class="ui center aligned basic segment desktop-and-up"
|
||||
>
|
||||
<pagination
|
||||
:total="total"
|
||||
:current="page"
|
||||
:paginate-by="paginateBy"
|
||||
v-on="$listeners">
|
||||
</pagination>
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-below']"
|
||||
>
|
||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||
<div class="ui loader"></div>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui inverted active dimmer"
|
||||
>
|
||||
<div class="ui loader" />
|
||||
</div>
|
||||
|
||||
<!-- For each item, build a row -->
|
||||
|
||||
<track-mobile-row
|
||||
v-for="(track, index) in allTracks"
|
||||
:track="track"
|
||||
:key="track.id"
|
||||
:track="track"
|
||||
:index="index"
|
||||
:tracks="allTracks"
|
||||
:show-position="showPosition"
|
||||
|
@ -103,35 +139,39 @@
|
|||
:is-artist="isArtist"
|
||||
:is-album="isAlbum"
|
||||
:is-podcast="isPodcast"
|
||||
></track-mobile-row>
|
||||
<div v-if="paginateResults" class="ui center aligned basic segment tablet-and-below">
|
||||
/>
|
||||
<div
|
||||
v-if="paginateResults"
|
||||
class="ui center aligned basic segment tablet-and-below"
|
||||
>
|
||||
<pagination
|
||||
v-if="paginateResults"
|
||||
:total="total"
|
||||
:current="page"
|
||||
:compact="true"
|
||||
v-on="$listeners"></pagination>
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from "@/lodash";
|
||||
import axios from "axios";
|
||||
import TrackRow from "@/components/audio/track/Row";
|
||||
import TrackMobileRow from "@/components/audio/track/MobileRow";
|
||||
import Pagination from "@/components/Pagination";
|
||||
import _ from '@/lodash'
|
||||
import axios from 'axios'
|
||||
import TrackRow from '@/components/audio/track/Row'
|
||||
import TrackMobileRow from '@/components/audio/track/MobileRow'
|
||||
import Pagination from '@/components/Pagination'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TrackRow,
|
||||
TrackMobileRow,
|
||||
Pagination,
|
||||
Pagination
|
||||
},
|
||||
|
||||
props: {
|
||||
tracks: Array,
|
||||
tracks: { type: Array, default: () => { return [] } },
|
||||
showAlbum: { type: Boolean, required: false, default: true },
|
||||
showArtist: { type: Boolean, required: false, default: true },
|
||||
showPosition: { type: Boolean, required: false, default: false },
|
||||
|
@ -144,66 +184,66 @@ export default {
|
|||
isArtist: { type: Boolean, required: false, default: false },
|
||||
isAlbum: { type: Boolean, required: false, default: false },
|
||||
isPodcast: { type: Boolean, required: false, default: false },
|
||||
paginateResults: { type: Boolean, required: false, default: true},
|
||||
total: { type: Number, required: false},
|
||||
page: {type: Number, required: false, default: 1},
|
||||
paginateBy: {type: Number, required: false, default: 25}
|
||||
paginateResults: { type: Boolean, required: false, default: true },
|
||||
total: { type: Number, required: false, default: 0 },
|
||||
page: { type: Number, required: false, default: 1 },
|
||||
paginateBy: { type: Number, required: false, default: 25 }
|
||||
},
|
||||
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
fetchDataUrl: this.nextUrl,
|
||||
isLoading: false,
|
||||
additionalTracks: [],
|
||||
query: "",
|
||||
};
|
||||
query: ''
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
allTracks() {
|
||||
return (this.tracks || []).concat(this.additionalTracks);
|
||||
allTracks () {
|
||||
return (this.tracks || []).concat(this.additionalTracks)
|
||||
},
|
||||
|
||||
labels() {
|
||||
labels () {
|
||||
return {
|
||||
title: this.$pgettext("*/*/*/Noun", "Title"),
|
||||
album: this.$pgettext("*/*/*/Noun", "Album"),
|
||||
artist: this.$pgettext("*/*/*/Noun", "Artist"),
|
||||
};
|
||||
title: this.$pgettext('*/*/*/Noun', 'Title'),
|
||||
album: this.$pgettext('*/*/*/Noun', 'Album'),
|
||||
artist: this.$pgettext('*/*/*/Noun', 'Artist')
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (!this.tracks) {
|
||||
this.fetchData('tracks/')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchData(url) {
|
||||
async fetchData (url) {
|
||||
if (!url) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
this.isLoading = true;
|
||||
let self = this;
|
||||
let params = _.clone(this.filters);
|
||||
let tracksPromise = axios.get(url, { params: params })
|
||||
params.page_size = this.limit;
|
||||
params.page = this.page;
|
||||
params.include_channels = true;
|
||||
this.isLoading = true
|
||||
const self = this
|
||||
const params = _.clone(this.filters)
|
||||
const tracksPromise = axios.get(url, { params: params })
|
||||
params.page_size = this.limit
|
||||
params.page = this.page
|
||||
params.include_channels = true
|
||||
try {
|
||||
await tracksPromise
|
||||
self.nextPage = tracksPromise.data.next;
|
||||
self.objects = tracksPromise.data.results;
|
||||
self.count = tracksPromise.data.count;
|
||||
self.$emit("fetched", tracksPromise.data);
|
||||
self.isLoading = false;
|
||||
} catch(e) {
|
||||
self.isLoading = false;
|
||||
self.errors = error.backendErrors;
|
||||
self.nextPage = tracksPromise.data.next
|
||||
self.objects = tracksPromise.data.results
|
||||
self.count = tracksPromise.data.count
|
||||
self.$emit('fetched', tracksPromise.data)
|
||||
self.isLoading = false
|
||||
} catch (e) {
|
||||
self.isLoading = false
|
||||
self.errors = e.backendErrors
|
||||
}
|
||||
},
|
||||
updatePage: function(page) {
|
||||
updatePage: function (page) {
|
||||
this.$emit('page-changed', page)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (!this.tracks) {
|
||||
this.fetchData("tracks/");
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,17 +1,48 @@
|
|||
<template>
|
||||
<div class="component-track-widget">
|
||||
<h3 v-if="!!this.$slots.title">
|
||||
<slot name="title"></slot>
|
||||
<span v-if="showCount" class="ui tiny circular label">{{ count }}</span>
|
||||
<h3 v-if="!!$slots.title">
|
||||
<slot name="title" />
|
||||
<span
|
||||
v-if="showCount"
|
||||
class="ui tiny circular label"
|
||||
>{{ count }}</span>
|
||||
</h3>
|
||||
<div v-if="count > 0" class="ui divided unstackable items">
|
||||
<div :class="['item', itemClasses]" v-for="object in objects" :key="object.id">
|
||||
<div
|
||||
v-if="count > 0"
|
||||
class="ui divided unstackable items"
|
||||
>
|
||||
<div
|
||||
v-for="object in objects"
|
||||
:key="object.id"
|
||||
:class="['item', itemClasses]"
|
||||
>
|
||||
<div class="ui tiny image">
|
||||
<img alt="" v-if="object.track.album && object.track.album.cover" v-lazy="$store.getters['instance/absoluteUrl'](object.track.album.cover.urls.medium_square_crop)">
|
||||
<img alt="" v-else-if="object.track.cover" v-lazy="$store.getters['instance/absoluteUrl'](object.track.cover.urls.medium_square_crop)"/>
|
||||
<img alt="" v-else-if="object.track.artist.cover" v-lazy="$store.getters['instance/absoluteUrl'](object.track.artist.cover.urls.medium_square_crop)"/>
|
||||
<img alt="" v-else src="../../../assets/audio/default-cover.png">
|
||||
<play-button class="play-overlay" :icon-only="true" :button-classes="['ui', 'circular', 'tiny', 'vibrant', 'icon', 'button']" :track="object.track"></play-button>
|
||||
<img
|
||||
v-if="object.track.album && object.track.album.cover"
|
||||
v-lazy="$store.getters['instance/absoluteUrl'](object.track.album.cover.urls.medium_square_crop)"
|
||||
alt=""
|
||||
>
|
||||
<img
|
||||
v-else-if="object.track.cover"
|
||||
v-lazy="$store.getters['instance/absoluteUrl'](object.track.cover.urls.medium_square_crop)"
|
||||
alt=""
|
||||
>
|
||||
<img
|
||||
v-else-if="object.track.artist.cover"
|
||||
v-lazy="$store.getters['instance/absoluteUrl'](object.track.artist.cover.urls.medium_square_crop)"
|
||||
alt=""
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
src="../../../assets/audio/default-cover.png"
|
||||
>
|
||||
<play-button
|
||||
class="play-overlay"
|
||||
:icon-only="true"
|
||||
:button-classes="['ui', 'circular', 'tiny', 'vibrant', 'icon', 'button']"
|
||||
:track="object.track"
|
||||
/>
|
||||
</div>
|
||||
<div class="middle aligned content">
|
||||
<div class="ui unstackable grid">
|
||||
|
@ -23,15 +54,32 @@
|
|||
</div>
|
||||
<div class="meta ellipsis">
|
||||
<span>
|
||||
<router-link class="discrete link" :to="{name: 'library.artists.detail', params: {id: object.track.artist.id}}">
|
||||
<router-link
|
||||
class="discrete link"
|
||||
:to="{name: 'library.artists.detail', params: {id: object.track.artist.id}}"
|
||||
>
|
||||
{{ object.track.artist.name }}
|
||||
</router-link>
|
||||
</span>
|
||||
</div>
|
||||
<tags-list label-classes="tiny" :truncate-size="20" :limit="2" :show-more="false" :tags="object.track.tags"></tags-list>
|
||||
<tags-list
|
||||
label-classes="tiny"
|
||||
:truncate-size="20"
|
||||
:limit="2"
|
||||
:show-more="false"
|
||||
:tags="object.track.tags"
|
||||
/>
|
||||
|
||||
<div class="extra" v-if="isActivity">
|
||||
<router-link class="left floated" :to="{name: 'profile.overview', params: {username: object.user.username}}">@{{ object.user.username }}</router-link>
|
||||
<div
|
||||
v-if="isActivity"
|
||||
class="extra"
|
||||
>
|
||||
<router-link
|
||||
class="left floated"
|
||||
:to="{name: 'profile.overview', params: {username: object.user.username}}"
|
||||
>
|
||||
@{{ object.user.username }}
|
||||
</router-link>
|
||||
<span class="right floated"><human-date :date="object.creation_date" /></span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -41,30 +89,46 @@
|
|||
:account="object.actor"
|
||||
:dropdown-only="true"
|
||||
:dropdown-icon-classes="['ellipsis', 'vertical', 'large really discrete']"
|
||||
:track="object.track"></play-button>
|
||||
:track="object.track"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||
<div class="ui loader"></div>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui inverted active dimmer"
|
||||
>
|
||||
<div class="ui loader" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="ui placeholder segment">
|
||||
<div
|
||||
v-else
|
||||
class="ui placeholder segment"
|
||||
>
|
||||
<div class="ui icon header">
|
||||
<i class="music icon"></i>
|
||||
<i class="music icon" />
|
||||
<translate translate-context="Content/Home/Placeholder">
|
||||
Nothing found
|
||||
</translate>
|
||||
</div>
|
||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||
<div class="ui loader"></div>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui inverted active dimmer"
|
||||
>
|
||||
<div class="ui loader" />
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="nextPage">
|
||||
<div class="ui hidden divider"></div>
|
||||
<button v-if="nextPage" @click="fetchData(nextPage)" :class="['ui', 'basic', 'button']">
|
||||
<translate translate-context="*/*/Button,Label">Show more</translate>
|
||||
<div class="ui hidden divider" />
|
||||
<button
|
||||
v-if="nextPage"
|
||||
:class="['ui', 'basic', 'button']"
|
||||
@click="fetchData(nextPage)"
|
||||
>
|
||||
<translate translate-context="*/*/Button,Label">
|
||||
Show more
|
||||
</translate>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -74,21 +138,21 @@
|
|||
import _ from '@/lodash'
|
||||
import axios from 'axios'
|
||||
import PlayButton from '@/components/audio/PlayButton'
|
||||
import TagsList from "@/components/tags/List"
|
||||
import TagsList from '@/components/tags/List'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
filters: {type: Object, required: true},
|
||||
url: {type: String, required: true},
|
||||
isActivity: {type: Boolean, default: true},
|
||||
showCount: {type: Boolean, default: false},
|
||||
limit: {type: Number, default: 5},
|
||||
itemClasses: {type: String, default: ''},
|
||||
},
|
||||
components: {
|
||||
PlayButton,
|
||||
TagsList
|
||||
},
|
||||
props: {
|
||||
filters: { type: Object, required: true },
|
||||
url: { type: String, required: true },
|
||||
isActivity: { type: Boolean, default: true },
|
||||
showCount: { type: Boolean, default: false },
|
||||
limit: { type: Number, default: 5 },
|
||||
itemClasses: { type: String, default: '' }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
objects: [],
|
||||
|
@ -99,6 +163,17 @@ export default {
|
|||
nextPage: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
offset () {
|
||||
this.fetchData()
|
||||
},
|
||||
'$store.state.moderation.lastUpdate': function () {
|
||||
this.fetchData(this.url)
|
||||
},
|
||||
count (v) {
|
||||
this.$emit('count', v)
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchData(this.url)
|
||||
},
|
||||
|
@ -108,11 +183,11 @@ export default {
|
|||
return
|
||||
}
|
||||
this.isLoading = true
|
||||
let self = this
|
||||
let params = _.clone(this.filters)
|
||||
const self = this
|
||||
const params = _.clone(this.filters)
|
||||
params.page_size = this.limit
|
||||
params.offset = this.offset
|
||||
axios.get(url, {params: params}).then((response) => {
|
||||
axios.get(url, { params: params }).then((response) => {
|
||||
self.previousPage = response.data.previous
|
||||
self.nextPage = response.data.next
|
||||
self.isLoading = false
|
||||
|
@ -123,7 +198,7 @@ export default {
|
|||
newObjects = response.data.results
|
||||
} else {
|
||||
newObjects = response.data.results.map((r) => {
|
||||
return {track: r}
|
||||
return { track: r }
|
||||
})
|
||||
}
|
||||
self.objects = [...self.objects, ...newObjects]
|
||||
|
@ -139,17 +214,6 @@ export default {
|
|||
this.offset = Math.max(this.offset - this.limit, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
offset () {
|
||||
this.fetchData()
|
||||
},
|
||||
"$store.state.moderation.lastUpdate": function () {
|
||||
this.fetchData(this.url)
|
||||
},
|
||||
count (v) {
|
||||
this.$emit('count', v)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,16 +1,26 @@
|
|||
<template>
|
||||
<main class="main pusher" v-title="labels.title">
|
||||
<main
|
||||
v-title="labels.title"
|
||||
class="main pusher"
|
||||
>
|
||||
<div class="ui vertical stripe segment">
|
||||
<section class="ui text container">
|
||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||
<div class="ui loader"></div>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui inverted active dimmer"
|
||||
>
|
||||
<div class="ui loader" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<router-link :to="{name: 'settings'}">
|
||||
<translate translate-context="Content/Applications/Link">Back to settings</translate>
|
||||
<translate translate-context="Content/Applications/Link">
|
||||
Back to settings
|
||||
</translate>
|
||||
</router-link>
|
||||
<h2 class="ui header">
|
||||
<translate translate-context="Content/Applications/Title">Application details</translate>
|
||||
<translate translate-context="Content/Applications/Title">
|
||||
Application details
|
||||
</translate>
|
||||
</h2>
|
||||
<div class="ui form">
|
||||
<p>
|
||||
|
@ -20,25 +30,45 @@
|
|||
</p>
|
||||
<div class="field">
|
||||
<label for="copy-id"><translate translate-context="Content/Applications/Label">Application ID</translate></label>
|
||||
<copy-input id="copy-id" :value="application.client_id" />
|
||||
<copy-input
|
||||
id="copy-id"
|
||||
:value="application.client_id"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="copy-secret"><translate translate-context="Content/Applications/Label">Application secret</translate></label>
|
||||
<copy-input id="copy-secret" :value="application.client_secret" />
|
||||
<copy-input
|
||||
id="copy-secret"
|
||||
:value="application.client_secret"
|
||||
/>
|
||||
</div>
|
||||
<div class="field" v-if="application.token != undefined">
|
||||
<div
|
||||
v-if="application.token != undefined"
|
||||
class="field"
|
||||
>
|
||||
<label for="copy-secret"><translate translate-context="Content/Applications/Label">Access token</translate></label>
|
||||
<copy-input id="copy-secret" :value="application.token" />
|
||||
<a href="" @click.prevent="refreshToken">
|
||||
<i class="refresh icon"></i>
|
||||
<copy-input
|
||||
id="copy-secret"
|
||||
:value="application.token"
|
||||
/>
|
||||
<a
|
||||
href=""
|
||||
@click.prevent="refreshToken"
|
||||
>
|
||||
<i class="refresh icon" />
|
||||
<translate translate-context="Content/Applications/Label">Regenerate token</translate>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="ui header">
|
||||
<translate translate-context="Content/Applications/Title">Edit application</translate>
|
||||
<translate translate-context="Content/Applications/Title">
|
||||
Edit application
|
||||
</translate>
|
||||
</h2>
|
||||
<application-form @updated="application = $event" :app="application" />
|
||||
<application-form
|
||||
:app="application"
|
||||
@updated="application = $event"
|
||||
/>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
|
@ -46,19 +76,26 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios"
|
||||
import axios from 'axios'
|
||||
|
||||
import ApplicationForm from "@/components/auth/ApplicationForm"
|
||||
import ApplicationForm from '@/components/auth/ApplicationForm'
|
||||
|
||||
export default {
|
||||
props: ['id'],
|
||||
components: {
|
||||
ApplicationForm
|
||||
},
|
||||
data() {
|
||||
props: { id: { type: Number, required: true } },
|
||||
data () {
|
||||
return {
|
||||
application: null,
|
||||
isLoading: false,
|
||||
isLoading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
return {
|
||||
title: this.$pgettext('Content/Applications/Title', 'Edit application')
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
|
@ -67,7 +104,7 @@ export default {
|
|||
methods: {
|
||||
fetchApplication () {
|
||||
this.isLoading = true
|
||||
let self = this
|
||||
const self = this
|
||||
axios.get(`oauth/apps/${this.id}/`).then((response) => {
|
||||
self.isLoading = false
|
||||
self.application = response.data
|
||||
|
@ -78,17 +115,10 @@ export default {
|
|||
},
|
||||
async refreshToken () {
|
||||
self.isLoading = true
|
||||
let response = await axios.post(`oauth/apps/${this.id}/refresh-token`)
|
||||
const response = await axios.post(`oauth/apps/${this.id}/refresh-token`)
|
||||
this.application = response.data
|
||||
self.isLoading = false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels() {
|
||||
return {
|
||||
title: this.$pgettext('Content/Applications/Title', "Edit application")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,19 +1,45 @@
|
|||
<template>
|
||||
|
||||
<form class="ui form component-form" role="alert" @submit.prevent="submit()">
|
||||
<div v-if="errors.length > 0" class="ui negative message">
|
||||
<h4 class="header"><translate translate-context="Content/*/Error message.Title">We cannot save your changes</translate></h4>
|
||||
<form
|
||||
class="ui form component-form"
|
||||
role="alert"
|
||||
@submit.prevent="submit()"
|
||||
>
|
||||
<div
|
||||
v-if="errors.length > 0"
|
||||
class="ui negative message"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate translate-context="Content/*/Error message.Title">
|
||||
We cannot save your changes
|
||||
</translate>
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li v-for="error in errors">{{ error }}</li>
|
||||
<li
|
||||
v-for="(error, key) in errors"
|
||||
:key="key"
|
||||
>
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<label for="application-name"><translate translate-context="*/*/*/Noun">Name</translate></label>
|
||||
<input id="application-name" name="name" required type="text" v-model="fields.name" />
|
||||
<input
|
||||
id="application-name"
|
||||
v-model="fields.name"
|
||||
name="name"
|
||||
required
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<label for="redirect-uris"><translate translate-context="Content/Applications/Input.Label/Noun">Redirect URI</translate></label>
|
||||
<input id="redirect-uris" name="redirect_uris" type="text" v-model="fields.redirect_uris" />
|
||||
<input
|
||||
id="redirect-uris"
|
||||
v-model="fields.redirect_uris"
|
||||
name="redirect_uris"
|
||||
type="text"
|
||||
>
|
||||
<p class="help">
|
||||
<translate translate-context="Content/Applications/Help Text">
|
||||
Use "urn:ietf:wg:oauth:2.0:oob" as a redirect URI if your application is not served on the web.
|
||||
|
@ -28,13 +54,18 @@
|
|||
</translate>
|
||||
</p>
|
||||
<div class="ui stackable two column grid">
|
||||
<div v-for="parent in allScopes" class="column">
|
||||
<div
|
||||
v-for="(parent, key) in allScopes"
|
||||
:key="key"
|
||||
class="column"
|
||||
>
|
||||
<div class="ui parent checkbox">
|
||||
<input
|
||||
:id="parent.id"
|
||||
v-model="scopeArray"
|
||||
:value="parent.id"
|
||||
:id="parent.id"
|
||||
type="checkbox">
|
||||
type="checkbox"
|
||||
>
|
||||
<label :for="parent.id">
|
||||
{{ parent.label }}
|
||||
<p class="help">
|
||||
|
@ -43,13 +74,17 @@
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<div v-for="child in parent.children">
|
||||
<div
|
||||
v-for="(child, index) in parent.children"
|
||||
:key="index"
|
||||
>
|
||||
<div class="ui child checkbox">
|
||||
<input
|
||||
:id="child.id"
|
||||
v-model="scopeArray"
|
||||
:value="child.id"
|
||||
:id="child.id"
|
||||
type="checkbox">
|
||||
type="checkbox"
|
||||
>
|
||||
<label :for="child.id">
|
||||
{{ child.id }}
|
||||
<p class="help">
|
||||
|
@ -60,29 +95,43 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<button :class="['ui', {'loading': isLoading}, 'success', 'button']" type="submit">
|
||||
<translate v-if="updating" key="2" translate-context="Content/Applications/Button.Label/Verb">Update application</translate>
|
||||
<translate v-else key="3" translate-context="Content/Applications/Button.Label/Verb">Create application</translate>
|
||||
<button
|
||||
:class="['ui', {'loading': isLoading}, 'success', 'button']"
|
||||
type="submit"
|
||||
>
|
||||
<translate
|
||||
v-if="updating"
|
||||
key="2"
|
||||
translate-context="Content/Applications/Button.Label/Verb"
|
||||
>
|
||||
Update application
|
||||
</translate>
|
||||
<translate
|
||||
v-else
|
||||
key="3"
|
||||
translate-context="Content/Applications/Button.Label/Verb"
|
||||
>
|
||||
Create application
|
||||
</translate>
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from "@/lodash"
|
||||
import axios from "axios"
|
||||
import TranslationsMixin from "@/components/mixins/Translations"
|
||||
import _ from '@/lodash'
|
||||
import axios from 'axios'
|
||||
import TranslationsMixin from '@/components/mixins/Translations'
|
||||
|
||||
export default {
|
||||
mixins: [TranslationsMixin],
|
||||
props: {
|
||||
app: {type: Object, required: false},
|
||||
defaults: {type: Object, required: false}
|
||||
app: { type: Object, required: false, default: () => { return {} } },
|
||||
defaults: { type: Object, required: false, default: () => { return {} } }
|
||||
},
|
||||
data() {
|
||||
let app = this.app || {}
|
||||
let defaults = this.defaults || {}
|
||||
data () {
|
||||
const app = this.app || {}
|
||||
const defaults = this.defaults || {}
|
||||
return {
|
||||
isLoading: false,
|
||||
errors: [],
|
||||
|
@ -92,45 +141,19 @@ export default {
|
|||
scopes: app.scopes || defaults.scopes || 'read'
|
||||
},
|
||||
scopes: [
|
||||
{id: "profile", icon: 'user'},
|
||||
{id: "libraries", icon: 'book'},
|
||||
{id: "favorites", icon: 'heart'},
|
||||
{id: "listenings", icon: 'music'},
|
||||
{id: "follows", icon: 'users'},
|
||||
{id: "playlists", icon: 'list'},
|
||||
{id: "radios", icon: 'rss'},
|
||||
{id: "filters", icon: 'eye slash'},
|
||||
{id: "notifications", icon: 'bell'},
|
||||
{id: "edits", icon: 'pencil alternate'},
|
||||
{ id: 'profile', icon: 'user' },
|
||||
{ id: 'libraries', icon: 'book' },
|
||||
{ id: 'favorites', icon: 'heart' },
|
||||
{ id: 'listenings', icon: 'music' },
|
||||
{ id: 'follows', icon: 'users' },
|
||||
{ id: 'playlists', icon: 'list' },
|
||||
{ id: 'radios', icon: 'rss' },
|
||||
{ id: 'filters', icon: 'eye slash' },
|
||||
{ id: 'notifications', icon: 'bell' },
|
||||
{ id: 'edits', icon: 'pencil alternate' }
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit () {
|
||||
this.errors = []
|
||||
let self = this
|
||||
self.isLoading = true
|
||||
let payload = this.fields
|
||||
let event, promise, message
|
||||
if (this.updating) {
|
||||
event = 'updated'
|
||||
promise = axios.patch(`oauth/apps/${this.app.client_id}/`, payload)
|
||||
} else {
|
||||
event = 'created'
|
||||
promise = axios.post(`oauth/apps/`, payload)
|
||||
}
|
||||
return promise.then(
|
||||
response => {
|
||||
self.isLoading = false
|
||||
self.$emit(event, response.data)
|
||||
},
|
||||
error => {
|
||||
self.isLoading = false
|
||||
self.errors = error.backendErrors
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
updating () {
|
||||
return this.app
|
||||
|
@ -144,8 +167,8 @@ export default {
|
|||
}
|
||||
},
|
||||
allScopes () {
|
||||
let self = this
|
||||
let parents = [
|
||||
const self = this
|
||||
const parents = [
|
||||
{
|
||||
id: 'read',
|
||||
label: this.$pgettext('Content/OAuth Scopes/Label/Verb', 'Read'),
|
||||
|
@ -157,19 +180,45 @@ export default {
|
|||
label: this.$pgettext('Content/OAuth Scopes/Label/Verb', 'Write'),
|
||||
description: this.$pgettext('Content/OAuth Scopes/Help Text', 'Write-only access to user data'),
|
||||
value: this.scopeArray.indexOf('write') > -1
|
||||
},
|
||||
}
|
||||
]
|
||||
parents.forEach((p) => {
|
||||
p.children = self.scopes.map(s => {
|
||||
let id = `${p.id}:${s.id}`
|
||||
const id = `${p.id}:${s.id}`
|
||||
return {
|
||||
id,
|
||||
value: this.scopeArray.indexOf(id) > -1,
|
||||
value: this.scopeArray.indexOf(id) > -1
|
||||
}
|
||||
})
|
||||
})
|
||||
return parents
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit () {
|
||||
this.errors = []
|
||||
const self = this
|
||||
self.isLoading = true
|
||||
const payload = this.fields
|
||||
let event, promise
|
||||
if (this.updating) {
|
||||
event = 'updated'
|
||||
promise = axios.patch(`oauth/apps/${this.app.client_id}/`, payload)
|
||||
} else {
|
||||
event = 'created'
|
||||
promise = axios.post('oauth/apps/', payload)
|
||||
}
|
||||
return promise.then(
|
||||
response => {
|
||||
self.isLoading = false
|
||||
self.$emit(event, response.data)
|
||||
},
|
||||
error => {
|
||||
self.isLoading = false
|
||||
self.errors = error.backendErrors
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,46 +1,58 @@
|
|||
<template>
|
||||
<main class="main pusher" v-title="labels.title">
|
||||
<main
|
||||
v-title="labels.title"
|
||||
class="main pusher"
|
||||
>
|
||||
<div class="ui vertical stripe segment">
|
||||
<section class="ui text container">
|
||||
<router-link :to="{name: 'settings'}">
|
||||
<translate translate-context="Content/Applications/Link">Back to settings</translate>
|
||||
<translate translate-context="Content/Applications/Link">
|
||||
Back to settings
|
||||
</translate>
|
||||
</router-link>
|
||||
<h2 class="ui header">
|
||||
<translate translate-context="Content/Settings/Button.Label">Create a new application</translate>
|
||||
<translate translate-context="Content/Settings/Button.Label">
|
||||
Create a new application
|
||||
</translate>
|
||||
</h2>
|
||||
<application-form
|
||||
:defaults="defaults"
|
||||
@created="$router.push({name: 'settings.applications.edit', params: {id: $event.client_id}})" />
|
||||
@created="$router.push({name: 'settings.applications.edit', params: {id: $event.client_id}})"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ApplicationForm from "@/components/auth/ApplicationForm"
|
||||
import ApplicationForm from '@/components/auth/ApplicationForm'
|
||||
|
||||
export default {
|
||||
props: ['name', 'redirect_uris', 'scopes'],
|
||||
components: {
|
||||
ApplicationForm
|
||||
},
|
||||
data() {
|
||||
props: {
|
||||
name: { type: String, required: true },
|
||||
redirectUris: { type: String, required: true },
|
||||
scopes: { type: Array, required: true }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
application: null,
|
||||
isLoading: false,
|
||||
defaults: {
|
||||
name: this.name,
|
||||
redirect_uris: this.redirect_uris,
|
||||
scopes: this.scopes,
|
||||
redirectUris: this.redirectUris,
|
||||
scopes: this.scopes
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels() {
|
||||
labels () {
|
||||
return {
|
||||
title: this.$pgettext('Content/Settings/Button.Label', "Create a new application")
|
||||
title: this.$pgettext('Content/Settings/Button.Label', 'Create a new application')
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,34 +1,91 @@
|
|||
<template>
|
||||
<main class="main pusher" v-title="labels.title">
|
||||
<main
|
||||
v-title="labels.title"
|
||||
class="main pusher"
|
||||
>
|
||||
<section class="ui vertical stripe segment">
|
||||
<div class="ui small text container">
|
||||
<h2><i class="lock open icon"></i><translate translate-context="Content/Auth/Title/Verb">Authorize third-party app</translate></h2>
|
||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
||||
<h4 v-if="application" class="header"><translate translate-context="Popup/Moderation/Error message">Error while authorizing application</translate></h4>
|
||||
<h4 v-else class="header"><translate translate-context="Popup/Moderation/Error message">Error while fetching application data</translate></h4>
|
||||
<h2>
|
||||
<i class="lock open icon" /><translate translate-context="Content/Auth/Title/Verb">
|
||||
Authorize third-party app
|
||||
</translate>
|
||||
</h2>
|
||||
<div
|
||||
v-if="errors.length > 0"
|
||||
role="alert"
|
||||
class="ui negative message"
|
||||
>
|
||||
<h4
|
||||
v-if="application"
|
||||
class="header"
|
||||
>
|
||||
<translate translate-context="Popup/Moderation/Error message">
|
||||
Error while authorizing application
|
||||
</translate>
|
||||
</h4>
|
||||
<h4
|
||||
v-else
|
||||
class="header"
|
||||
>
|
||||
<translate translate-context="Popup/Moderation/Error message">
|
||||
Error while fetching application data
|
||||
</translate>
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li v-for="error in errors">{{ error }}</li>
|
||||
<li
|
||||
v-for="(error, key) in errors"
|
||||
:key="key"
|
||||
>
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||
<div class="ui loader"></div>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui inverted active dimmer"
|
||||
>
|
||||
<div class="ui loader" />
|
||||
</div>
|
||||
<form v-else-if="application && !code" :class="['ui', {loading: isLoading}, 'form']" @submit.prevent="submit">
|
||||
<h3><translate translate-context="Content/Auth/Title" :translate-params="{app: application.name}">%{ app } wants to access your Funkwhale account</translate></h3>
|
||||
<form
|
||||
v-else-if="application && !code"
|
||||
:class="['ui', {loading: isLoading}, 'form']"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<h3>
|
||||
<translate
|
||||
translate-context="Content/Auth/Title"
|
||||
:translate-params="{app: application.name}"
|
||||
>
|
||||
%{ app } wants to access your Funkwhale account
|
||||
</translate>
|
||||
</h3>
|
||||
|
||||
<h4 v-for="topic in topicScopes" class="ui header vertical-align">
|
||||
<span v-if="topic.write && !topic.read" :class="['ui', 'basic', 'right floated', 'tiny', 'vertically-spaced component-label label']">
|
||||
<i class="pencil icon"></i>
|
||||
<h4
|
||||
v-for="(topic, key) in topicScopes"
|
||||
:key="key"
|
||||
class="ui header vertical-align"
|
||||
>
|
||||
<span
|
||||
v-if="topic.write && !topic.read"
|
||||
:class="['ui', 'basic', 'right floated', 'tiny', 'vertically-spaced component-label label']"
|
||||
>
|
||||
<i class="pencil icon" />
|
||||
<translate translate-context="Content/Auth/Label/Noun">Write-only</translate>
|
||||
</span>
|
||||
<span v-else-if="!topic.write && topic.read" :class="['ui', 'basic', 'right floated', 'tiny', 'vertically-spaced component-label label']">
|
||||
<span
|
||||
v-else-if="!topic.write && topic.read"
|
||||
:class="['ui', 'basic', 'right floated', 'tiny', 'vertically-spaced component-label label']"
|
||||
>
|
||||
<translate translate-context="Content/Auth/Label/Noun">Read-only</translate>
|
||||
</span>
|
||||
<span v-else-if="topic.write && topic.read" :class="['ui', 'basic', 'right floated', 'tiny', 'vertically-spaced component-label label']">
|
||||
<i class="pencil icon"></i>
|
||||
<span
|
||||
v-else-if="topic.write && topic.read"
|
||||
:class="['ui', 'basic', 'right floated', 'tiny', 'vertically-spaced component-label label']"
|
||||
>
|
||||
<i class="pencil icon" />
|
||||
<translate translate-context="Content/Auth/Label/Noun">Full access</translate>
|
||||
</span>
|
||||
<i :class="[topic.icon, 'icon']"></i>
|
||||
<i :class="[topic.icon, 'icon']" />
|
||||
<div class="content">
|
||||
{{ topic.label }}
|
||||
<div class="sub header">
|
||||
|
@ -38,23 +95,46 @@
|
|||
</h4>
|
||||
<div v-if="unknownRequestedScopes.length > 0">
|
||||
<p><strong><translate translate-context="Content/Auth/Paragraph">The application is also requesting the following unknown permissions:</translate></strong></p>
|
||||
<ul v-for="scope in unknownRequestedScopes">
|
||||
<li>{{ scope }}</li>
|
||||
<ul
|
||||
v-for="(unknownscope, key) in unknownRequestedScopes"
|
||||
:key="key"
|
||||
>
|
||||
<li>{{ unknownscope }}</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
<button class="ui success labeled icon button" type="submit">
|
||||
<i class="lock open icon"></i>
|
||||
<translate translate-context="Content/Signup/Button.Label/Verb" :translate-params="{app: application.name}">Authorize %{ app }</translate>
|
||||
<button
|
||||
class="ui success labeled icon button"
|
||||
type="submit"
|
||||
>
|
||||
<i class="lock open icon" />
|
||||
<translate
|
||||
translate-context="Content/Signup/Button.Label/Verb"
|
||||
:translate-params="{app: application.name}"
|
||||
>
|
||||
Authorize %{ app }
|
||||
</translate>
|
||||
</button>
|
||||
<p v-if="redirectUri === 'urn:ietf:wg:oauth:2.0:oob'" key="1" v-translate translate-context="Content/Auth/Paragraph">
|
||||
You will be shown a code to copy-paste in the application.</p>
|
||||
<p v-else key="2" v-translate="{url: redirectUri}" translate-context="Content/Auth/Paragraph" :translate-params="{url: redirectUri}">You will be redirected to <strong>%{ url }</strong></p>
|
||||
|
||||
<p
|
||||
v-if="redirectUri === 'urn:ietf:wg:oauth:2.0:oob'"
|
||||
key="1"
|
||||
v-translate
|
||||
translate-context="Content/Auth/Paragraph"
|
||||
>
|
||||
You will be shown a code to copy-paste in the application.
|
||||
</p>
|
||||
<p
|
||||
v-else
|
||||
key="2"
|
||||
v-translate="{url: redirectUri}"
|
||||
translate-context="Content/Auth/Paragraph"
|
||||
:translate-params="{url: redirectUri}"
|
||||
>
|
||||
You will be redirected to <strong>%{ url }</strong>
|
||||
</p>
|
||||
</form>
|
||||
<div v-else-if="code">
|
||||
<p><strong><translate translate-context="Content/Auth/Paragraph">Copy-paste the following code in the application:</translate></strong></p>
|
||||
<copy-input :value="code"></copy-input>
|
||||
<copy-input :value="code" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -62,60 +142,54 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import TranslationsMixin from "@/components/mixins/Translations"
|
||||
import TranslationsMixin from '@/components/mixins/Translations'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
import {checkRedirectToLogin} from '@/utils'
|
||||
import { checkRedirectToLogin } from '@/utils'
|
||||
export default {
|
||||
mixins: [TranslationsMixin],
|
||||
props: [
|
||||
'clientId',
|
||||
'redirectUri',
|
||||
'scope',
|
||||
'responseType',
|
||||
'nonce',
|
||||
'state',
|
||||
],
|
||||
data() {
|
||||
props: {
|
||||
clientId: { type: String, required: true },
|
||||
redirectUri: { type: String, required: true },
|
||||
scope: { type: String, required: true },
|
||||
responseType: { type: String, required: true },
|
||||
nonce: { type: String, required: true },
|
||||
state: { type: String, required: true }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
application: null,
|
||||
isLoading: false,
|
||||
errors: [],
|
||||
code: null,
|
||||
knownScopes: [
|
||||
{id: "profile", icon: 'user'},
|
||||
{id: "libraries", icon: 'book'},
|
||||
{id: "favorites", icon: 'heart'},
|
||||
{id: "listenings", icon: 'music'},
|
||||
{id: "follows", icon: 'users'},
|
||||
{id: "playlists", icon: 'list'},
|
||||
{id: "radios", icon: 'rss'},
|
||||
{id: "filters", icon: 'eye slash'},
|
||||
{id: "notifications", icon: 'bell'},
|
||||
{id: "edits", icon: 'pencil alternate'},
|
||||
{id: "security", icon: 'lock'},
|
||||
{id: "reports", icon: 'warning sign'},
|
||||
{ id: 'profile', icon: 'user' },
|
||||
{ id: 'libraries', icon: 'book' },
|
||||
{ id: 'favorites', icon: 'heart' },
|
||||
{ id: 'listenings', icon: 'music' },
|
||||
{ id: 'follows', icon: 'users' },
|
||||
{ id: 'playlists', icon: 'list' },
|
||||
{ id: 'radios', icon: 'rss' },
|
||||
{ id: 'filters', icon: 'eye slash' },
|
||||
{ id: 'notifications', icon: 'bell' },
|
||||
{ id: 'edits', icon: 'pencil alternate' },
|
||||
{ id: 'security', icon: 'lock' },
|
||||
{ id: 'reports', icon: 'warning sign' }
|
||||
]
|
||||
}
|
||||
},
|
||||
created () {
|
||||
checkRedirectToLogin(this.$store, this.$router)
|
||||
if (this.clientId) {
|
||||
this.fetchApplication()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
return {
|
||||
title: this.$pgettext('Head/Authorize/Title', "Allow application")
|
||||
title: this.$pgettext('Head/Authorize/Title', 'Allow application')
|
||||
}
|
||||
},
|
||||
requestedScopes () {
|
||||
return (this.scope || '').split(' ')
|
||||
},
|
||||
supportedScopes () {
|
||||
let supported = ['read', 'write']
|
||||
const supported = ['read', 'write']
|
||||
this.knownScopes.forEach(s => {
|
||||
supported.push(`read:${s.id}`)
|
||||
supported.push(`write:${s.id}`)
|
||||
|
@ -123,14 +197,14 @@ export default {
|
|||
return supported
|
||||
},
|
||||
unknownRequestedScopes () {
|
||||
let self = this
|
||||
const self = this
|
||||
return this.requestedScopes.filter(s => {
|
||||
return self.supportedScopes.indexOf(s) < 0
|
||||
})
|
||||
},
|
||||
topicScopes () {
|
||||
let self = this
|
||||
let requested = this.requestedScopes
|
||||
const self = this
|
||||
const requested = this.requestedScopes
|
||||
let write = false
|
||||
let read = false
|
||||
if (requested.indexOf('read') > -1) {
|
||||
|
@ -141,24 +215,30 @@ export default {
|
|||
}
|
||||
|
||||
return this.knownScopes.map(s => {
|
||||
let id = s.id
|
||||
const id = s.id
|
||||
return {
|
||||
id: id,
|
||||
icon: s.icon,
|
||||
label: self.sharedLabels.scopes[s.id].label,
|
||||
description: self.sharedLabels.scopes[s.id].description,
|
||||
read: read || requested.indexOf(`read:${id}`) > -1,
|
||||
write: write || requested.indexOf(`write:${id}`) > -1,
|
||||
write: write || requested.indexOf(`write:${id}`) > -1
|
||||
}
|
||||
}).filter(c => {
|
||||
return c.read || c.write
|
||||
})
|
||||
}
|
||||
},
|
||||
created () {
|
||||
checkRedirectToLogin(this.$store, this.$router)
|
||||
if (this.clientId) {
|
||||
this.fetchApplication()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchApplication () {
|
||||
this.isLoading = true
|
||||
let self = this
|
||||
const self = this
|
||||
axios.get(`oauth/apps/${this.clientId}/`).then((response) => {
|
||||
self.isLoading = false
|
||||
self.application = response.data
|
||||
|
@ -169,8 +249,8 @@ export default {
|
|||
},
|
||||
submit () {
|
||||
this.isLoading = true
|
||||
let self = this
|
||||
let data = new FormData();
|
||||
const self = this
|
||||
const data = new FormData()
|
||||
data.set('redirect_uri', this.redirectUri)
|
||||
data.set('scope', this.scope)
|
||||
data.set('allow', true)
|
||||
|
@ -178,7 +258,7 @@ export default {
|
|||
data.set('response_type', this.responseType)
|
||||
data.set('state', this.state)
|
||||
data.set('nonce', this.nonce)
|
||||
axios.post(`oauth/authorize/`, data, {headers: {'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest'}}).then((response) => {
|
||||
axios.post('oauth/authorize/', data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest' } }).then((response) => {
|
||||
if (self.redirectUri === 'urn:ietf:wg:oauth:2.0:oob') {
|
||||
self.isLoading = false
|
||||
self.code = response.data.code
|
||||
|
|
|
@ -1,18 +1,35 @@
|
|||
<template>
|
||||
<form class="ui form" @submit.prevent="submit()">
|
||||
<div v-if="error" role="alert" class="ui negative message">
|
||||
<h4 class="header"><translate translate-context="Content/Login/Error message.Title">We cannot log you in</translate></h4>
|
||||
<form
|
||||
class="ui form"
|
||||
@submit.prevent="submit()"
|
||||
>
|
||||
<div
|
||||
v-if="error"
|
||||
role="alert"
|
||||
class="ui negative message"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate translate-context="Content/Login/Error message.Title">
|
||||
We cannot log you in
|
||||
</translate>
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li v-if="error == 'invalid_credentials' && $store.state.instance.settings.moderation.signup_approval_enabled.value">
|
||||
<translate translate-context="Content/Login/Error message.List item/Call to action">If you signed-up recently, you may need to wait before our moderation team review your account, or verify your e-mail address.</translate>
|
||||
<translate translate-context="Content/Login/Error message.List item/Call to action">
|
||||
If you signed-up recently, you may need to wait before our moderation team review your account, or verify your e-mail address.
|
||||
</translate>
|
||||
</li>
|
||||
<li v-else-if="error == 'invalid_credentials'">
|
||||
<translate translate-context="Content/Login/Error message.List item/Call to action">Please double-check that your username and password combination is correct and make sure you verified your e-mail address.</translate>
|
||||
<translate translate-context="Content/Login/Error message.List item/Call to action">
|
||||
Please double-check that your username and password combination is correct and make sure you verified your e-mail address.
|
||||
</translate>
|
||||
</li>
|
||||
<li v-else>
|
||||
{{ error }}
|
||||
</li>
|
||||
<li v-else>{{ error }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<template v-if="$store.getters['instance/appDomain'] === $store.getters['instance/domain']" >
|
||||
<template v-if="$store.getters['instance/appDomain'] === $store.getters['instance/domain']">
|
||||
<div class="field">
|
||||
<label for="username-field">
|
||||
<translate translate-context="Content/Login/Input.Label/Noun">Username or e-mail address</translate>
|
||||
|
@ -24,14 +41,14 @@
|
|||
</template>
|
||||
</label>
|
||||
<input
|
||||
id="username-field"
|
||||
ref="username"
|
||||
v-model="credentials.username"
|
||||
required
|
||||
name="username"
|
||||
type="text"
|
||||
id="username-field"
|
||||
autofocus
|
||||
:placeholder="labels.usernamePlaceholder"
|
||||
v-model="credentials.username"
|
||||
>
|
||||
</div>
|
||||
<div class="field">
|
||||
|
@ -41,65 +58,78 @@
|
|||
<translate translate-context="*/Login/*/Verb">Reset your password</translate>
|
||||
</router-link>
|
||||
</label>
|
||||
<password-input field-id="password-field" required v-model="credentials.password" />
|
||||
|
||||
<password-input
|
||||
v-model="credentials.password"
|
||||
field-id="password-field"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>
|
||||
<translate translate-context="Contant/Auth/Paragraph" :translate-params="{domain: $store.getters['instance/domain']}">You will be redirected to %{ domain } to authenticate.</translate>
|
||||
<translate
|
||||
translate-context="Contant/Auth/Paragraph"
|
||||
:translate-params="{domain: $store.getters['instance/domain']}"
|
||||
>
|
||||
You will be redirected to %{ domain } to authenticate.
|
||||
</translate>
|
||||
</p>
|
||||
</template>
|
||||
<button :class="['ui', {'loading': isLoading}, 'right', 'floated', buttonClasses, 'button']" type="submit">
|
||||
<translate translate-context="*/Login/*/Verb">Login</translate>
|
||||
<button
|
||||
:class="['ui', {'loading': isLoading}, 'right', 'floated', buttonClasses, 'button']"
|
||||
type="submit"
|
||||
>
|
||||
<translate translate-context="*/Login/*/Verb">
|
||||
Login
|
||||
</translate>
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PasswordInput from "@/components/forms/PasswordInput"
|
||||
import PasswordInput from '@/components/forms/PasswordInput'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
next: { type: String, default: "/library" },
|
||||
buttonClasses: { type: String, default: "success" },
|
||||
showSignup: { type: Boolean, default: true},
|
||||
},
|
||||
components: {
|
||||
PasswordInput
|
||||
},
|
||||
data() {
|
||||
props: {
|
||||
next: { type: String, default: '/library' },
|
||||
buttonClasses: { type: String, default: 'success' },
|
||||
showSignup: { type: Boolean, default: true }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
// We need to initialize the component with any
|
||||
// properties that will be used in it
|
||||
credentials: {
|
||||
username: "",
|
||||
password: ""
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
error: "",
|
||||
error: '',
|
||||
isLoading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
const usernamePlaceholder = this.$pgettext('Content/Login/Input.Placeholder', 'Enter your username or e-mail address')
|
||||
return {
|
||||
usernamePlaceholder
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (this.$store.state.auth.authenticated) {
|
||||
this.$router.push(this.next)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
mounted () {
|
||||
if (this.$refs.username) {
|
||||
this.$refs.username.focus()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels() {
|
||||
let usernamePlaceholder = this.$pgettext('Content/Login/Input.Placeholder', "Enter your username or e-mail address")
|
||||
return {
|
||||
usernamePlaceholder,
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async submit() {
|
||||
async submit () {
|
||||
if (this.$store.getters['instance/appDomain'] === this.$store.getters['instance/domain']) {
|
||||
return await this.submitSession()
|
||||
} else {
|
||||
|
@ -107,21 +137,21 @@ export default {
|
|||
await this.$store.dispatch('auth/oauthLogin', this.next)
|
||||
}
|
||||
},
|
||||
async submitSession() {
|
||||
var self = this
|
||||
async submitSession () {
|
||||
const self = this
|
||||
self.isLoading = true
|
||||
this.error = ""
|
||||
var credentials = {
|
||||
this.error = ''
|
||||
const credentials = {
|
||||
username: this.credentials.username,
|
||||
password: this.credentials.password
|
||||
}
|
||||
this.$store
|
||||
.dispatch("auth/login", {
|
||||
.dispatch('auth/login', {
|
||||
credentials,
|
||||
next: this.next,
|
||||
onError: error => {
|
||||
if (error.response.status === 400) {
|
||||
self.error = "invalid_credentials"
|
||||
self.error = 'invalid_credentials'
|
||||
} else {
|
||||
self.error = error.backendErrors[0]
|
||||
}
|
||||
|
|
|
@ -1,18 +1,50 @@
|
|||
<template>
|
||||
<main class="main pusher" v-title="labels.title">
|
||||
<main
|
||||
v-title="labels.title"
|
||||
class="main pusher"
|
||||
>
|
||||
<section class="ui vertical stripe segment">
|
||||
<div v-if="$store.state.auth.authenticated" class="ui small text container">
|
||||
<div
|
||||
v-if="$store.state.auth.authenticated"
|
||||
class="ui small text container"
|
||||
>
|
||||
<h2>
|
||||
<translate translate-context="Content/Login/Title">Are you sure you want to log out?</translate>
|
||||
<translate translate-context="Content/Login/Title">
|
||||
Are you sure you want to log out?
|
||||
</translate>
|
||||
</h2>
|
||||
<p v-translate="{username: $store.state.auth.username}" translate-context="Content/Login/Paragraph">You are currently logged in as %{ username }</p>
|
||||
<button class="ui button" @click="$store.dispatch('auth/logout')"><translate translate-context="Content/Login/Button.Label">Yes, log me out!</translate></button>
|
||||
<p
|
||||
v-translate="{username: $store.state.auth.username}"
|
||||
translate-context="Content/Login/Paragraph"
|
||||
>
|
||||
You are currently logged in as %{ username }
|
||||
</p>
|
||||
<button
|
||||
class="ui button"
|
||||
@click="$store.dispatch('auth/logout')"
|
||||
>
|
||||
<translate translate-context="Content/Login/Button.Label">
|
||||
Yes, log me out!
|
||||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="ui small text container">
|
||||
<div
|
||||
v-else
|
||||
class="ui small text container"
|
||||
>
|
||||
<h2>
|
||||
<translate translate-context="Content/Login/Title">You aren't currently logged in</translate>
|
||||
<translate translate-context="Content/Login/Title">
|
||||
You aren't currently logged in
|
||||
</translate>
|
||||
</h2>
|
||||
<router-link to='/login' class="ui button"><translate translate-context="Content/Login/Button.Label">Log in!</translate></router-link>
|
||||
<router-link
|
||||
to="/login"
|
||||
class="ui button"
|
||||
>
|
||||
<translate translate-context="Content/Login/Button.Label">
|
||||
Log in!
|
||||
</translate>
|
||||
</router-link>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
@ -21,9 +53,9 @@
|
|||
<script>
|
||||
export default {
|
||||
computed: {
|
||||
labels() {
|
||||
labels () {
|
||||
return {
|
||||
title: this.$pgettext('Head/Login/Title', "Log Out")
|
||||
title: this.$pgettext('Head/Login/Title', 'Log Out')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,96 +1,193 @@
|
|||
<template>
|
||||
<form :class="['ui segment form', {loading: isLoading}]" @submit.prevent="submit">
|
||||
<form
|
||||
:class="['ui segment form', {loading: isLoading}]"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<h3>{{ plugin.label }}</h3>
|
||||
<div v-if="plugin.description" v-html="markdown.makeHtml(plugin.description)"></div>
|
||||
<template v-if="plugin.homepage" >
|
||||
<div class="ui small hidden divider"></div>
|
||||
<a :href="plugin.homepage" target="_blank">
|
||||
<i class="external icon"></i>
|
||||
<div
|
||||
v-if="plugin.description"
|
||||
v-html="markdown.makeHtml(plugin.description)"
|
||||
/>
|
||||
<template v-if="plugin.homepage">
|
||||
<div class="ui small hidden divider" />
|
||||
<a
|
||||
:href="plugin.homepage"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="external icon" />
|
||||
<translate translate-context="Footer/*/List item.Link/Short, Noun">Documentation</translate>
|
||||
</a>
|
||||
</template>
|
||||
<div class="ui clearing hidden divider"></div>
|
||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
||||
<h4 class="header"><translate translate-context="Content/*/Error message.Title">Error while saving plugin</translate></h4>
|
||||
<div class="ui clearing hidden divider" />
|
||||
<div
|
||||
v-if="errors.length > 0"
|
||||
role="alert"
|
||||
class="ui negative message"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate translate-context="Content/*/Error message.Title">
|
||||
Error while saving plugin
|
||||
</translate>
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li v-for="error in errors">{{ error }}</li>
|
||||
<li
|
||||
v-for="(error, key) in errors"
|
||||
:key="key"
|
||||
>
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui toggle checkbox">
|
||||
<input :id="`${plugin.name}-enabled`" type="checkbox" v-model="enabled" />
|
||||
<input
|
||||
:id="`${plugin.name}-enabled`"
|
||||
v-model="enabled"
|
||||
type="checkbox"
|
||||
>
|
||||
<label :for="`${plugin.name}-enabled`"><translate translate-context="*/*/*">Enabled</translate></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui clearing hidden divider"></div>
|
||||
<div v-if="plugin.source" class="field">
|
||||
<div class="ui clearing hidden divider" />
|
||||
<div
|
||||
v-if="plugin.source"
|
||||
class="field"
|
||||
>
|
||||
<label for="plugin-library"><translate translate-context="*/*/*/Noun">Library</translate></label>
|
||||
<select id="plugin-library" v-model="values['library']">
|
||||
<option :value="l.uuid" v-for="l in libraries" :key="l.uuid">{{ l.name }}</option>
|
||||
<select
|
||||
id="plugin-library"
|
||||
v-model="values['library']"
|
||||
>
|
||||
<option
|
||||
v-for="l in libraries"
|
||||
:key="l.uuid"
|
||||
:value="l.uuid"
|
||||
>
|
||||
{{ l.name }}
|
||||
</option>
|
||||
</select>
|
||||
<div>
|
||||
<translate translate-context="*/*/Paragraph/Noun">Library where files should be imported.</translate>
|
||||
<translate translate-context="*/*/Paragraph/Noun">
|
||||
Library where files should be imported.
|
||||
</translate>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="plugin.conf && plugin.conf.length > 0" v-for="field in plugin.conf">
|
||||
<div v-if="field.type === 'text'" class="field">
|
||||
<template
|
||||
v-for="(field, key) in plugin.conf"
|
||||
v-if="plugin.conf && plugin.conf.length > 0"
|
||||
>
|
||||
<div
|
||||
v-if="field.type === 'text'"
|
||||
:key="key"
|
||||
class="field"
|
||||
>
|
||||
<label :for="`plugin-${field.name}`">{{ field.label || field.name }}</label>
|
||||
<input :id="`plugin-${field.name}`" type="text" v-model="values[field.name]">
|
||||
<div v-if="field.help" v-html="markdown.makeHtml(field.help)"></div>
|
||||
<input
|
||||
:id="`plugin-${field.name}`"
|
||||
v-model="values[field.name]"
|
||||
type="text"
|
||||
>
|
||||
<div
|
||||
v-if="field.help"
|
||||
v-html="markdown.makeHtml(field.help)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="field.type === 'long_text'" class="field">
|
||||
<div
|
||||
v-if="field.type === 'long_text'"
|
||||
:key="key"
|
||||
class="field"
|
||||
>
|
||||
<label :for="`plugin-${field.name}`">{{ field.label || field.name }}</label>
|
||||
<textarea :id="`plugin-${field.name}`" type="text" v-model="values[field.name]" rows="5" />
|
||||
<div v-if="field.help" v-html="markdown.makeHtml(field.help)"></div>
|
||||
<textarea
|
||||
:id="`plugin-${field.name}`"
|
||||
v-model="values[field.name]"
|
||||
type="text"
|
||||
rows="5"
|
||||
/>
|
||||
<div
|
||||
v-if="field.help"
|
||||
v-html="markdown.makeHtml(field.help)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="field.type === 'url'" class="field">
|
||||
<div
|
||||
v-if="field.type === 'url'"
|
||||
:key="key"
|
||||
class="field"
|
||||
>
|
||||
<label :for="`plugin-${field.name}`">{{ field.label || field.name }}</label>
|
||||
<input :id="`plugin-${field.name}`" type="url" v-model="values[field.name]">
|
||||
<div v-if="field.help" v-html="markdown.makeHtml(field.help)"></div>
|
||||
<input
|
||||
:id="`plugin-${field.name}`"
|
||||
v-model="values[field.name]"
|
||||
type="url"
|
||||
>
|
||||
<div
|
||||
v-if="field.help"
|
||||
v-html="markdown.makeHtml(field.help)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="field.type === 'password'" class="field">
|
||||
<div
|
||||
v-if="field.type === 'password'"
|
||||
:key="key"
|
||||
class="field"
|
||||
>
|
||||
<label :for="`plugin-${field.name}`">{{ field.label || field.name }}</label>
|
||||
<input :id="`plugin-${field.name}`" type="password" v-model="values[field.name]">
|
||||
<div v-if="field.help" v-html="markdown.makeHtml(field.help)"></div>
|
||||
<input
|
||||
:id="`plugin-${field.name}`"
|
||||
v-model="values[field.name]"
|
||||
type="password"
|
||||
>
|
||||
<div
|
||||
v-if="field.help"
|
||||
v-html="markdown.makeHtml(field.help)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<button
|
||||
type="submit"
|
||||
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']">
|
||||
<translate translate-context="Content/*/Button.Label/Verb">Save</translate>
|
||||
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']"
|
||||
>
|
||||
<translate translate-context="Content/*/Button.Label/Verb">
|
||||
Save
|
||||
</translate>
|
||||
</button>
|
||||
<button
|
||||
type="scan"
|
||||
v-if="plugin.source"
|
||||
type="scan"
|
||||
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']"
|
||||
@click.prevent="submitAndScan"
|
||||
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']">
|
||||
<translate translate-context="Content/*/Button.Label/Verb">Scan</translate>
|
||||
>
|
||||
<translate translate-context="Content/*/Button.Label/Verb">
|
||||
Scan
|
||||
</translate>
|
||||
</button>
|
||||
<div class="ui clearing hidden divider"></div>
|
||||
<div class="ui clearing hidden divider" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios"
|
||||
import axios from 'axios'
|
||||
import lodash from '@/lodash'
|
||||
import showdown from 'showdown'
|
||||
export default {
|
||||
props: ['plugin', "libraries"],
|
||||
props: {
|
||||
plugin: { type: Object, required: true },
|
||||
libraries: { type: Array, required: true }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
markdown: new showdown.Converter(),
|
||||
isLoading: false,
|
||||
enabled: this.plugin.enabled,
|
||||
values: lodash.clone(this.plugin.values || {}),
|
||||
errors: [],
|
||||
errors: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async submit () {
|
||||
this.isLoading = true
|
||||
this.errors = []
|
||||
let url = `plugins/${this.plugin.name}`
|
||||
let enableUrl = this.enabled ? `${url}/enable` : `${url}/disable`
|
||||
const url = `plugins/${this.plugin.name}`
|
||||
const enableUrl = this.enabled ? `${url}/enable` : `${url}/disable`
|
||||
await axios.post(enableUrl)
|
||||
try {
|
||||
await axios.post(url, this.values)
|
||||
|
@ -102,7 +199,7 @@ export default {
|
|||
async scan () {
|
||||
this.isLoading = true
|
||||
this.errors = []
|
||||
let url = `plugins/${this.plugin.name}/scan`
|
||||
const url = `plugins/${this.plugin.name}/scan`
|
||||
try {
|
||||
await axios.post(url, this.values)
|
||||
} catch (e) {
|
||||
|
@ -114,6 +211,6 @@ export default {
|
|||
await this.submit()
|
||||
await this.scan()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -2,140 +2,197 @@
|
|||
<div v-if="submitted">
|
||||
<div class="ui success message">
|
||||
<p v-if="signupRequiresApproval">
|
||||
<translate translate-context="Content/Signup/Form/Paragraph">Your account request was successfully submitted. You will be notified by e-mail when our moderation team has reviewed your request.</translate>
|
||||
<translate translate-context="Content/Signup/Form/Paragraph">
|
||||
Your account request was successfully submitted. You will be notified by e-mail when our moderation team has reviewed your request.
|
||||
</translate>
|
||||
</p>
|
||||
<p v-else>
|
||||
<translate translate-context="Content/Signup/Form/Paragraph">Your account was successfully created. Please verify your e-mail address before trying to login.</translate>
|
||||
<translate translate-context="Content/Signup/Form/Paragraph">
|
||||
Your account was successfully created. Please verify your e-mail address before trying to login.
|
||||
</translate>
|
||||
</p>
|
||||
</div>
|
||||
<h2><translate translate-context="Content/Login/Title/Verb">Log in to your Funkwhale account</translate></h2>
|
||||
<login-form button-classes="basic success" :show-signup="false"></login-form>
|
||||
<h2>
|
||||
<translate translate-context="Content/Login/Title/Verb">
|
||||
Log in to your Funkwhale account
|
||||
</translate>
|
||||
</h2>
|
||||
<login-form
|
||||
button-classes="basic success"
|
||||
:show-signup="false"
|
||||
/>
|
||||
</div>
|
||||
<form
|
||||
v-else
|
||||
:class="['ui', {'loading': isLoadingInstanceSetting}, 'form']"
|
||||
@submit.prevent="submit()">
|
||||
<p class="ui message" v-if="!$store.state.instance.settings.users.registration_enabled.value">
|
||||
<translate translate-context="Content/Signup/Form/Paragraph">Public registrations are not possible on this instance. You will need an invitation code to sign up.</translate>
|
||||
@submit.prevent="submit()"
|
||||
>
|
||||
<p
|
||||
v-if="!$store.state.instance.settings.users.registration_enabled.value"
|
||||
class="ui message"
|
||||
>
|
||||
<translate translate-context="Content/Signup/Form/Paragraph">
|
||||
Public registrations are not possible on this instance. You will need an invitation code to sign up.
|
||||
</translate>
|
||||
</p>
|
||||
<p class="ui message" v-else-if="signupRequiresApproval">
|
||||
<translate translate-context="Content/Signup/Form/Paragraph">Registrations on this pod are open, but reviewed by moderators before approval.</translate>
|
||||
<p
|
||||
v-else-if="signupRequiresApproval"
|
||||
class="ui message"
|
||||
>
|
||||
<translate translate-context="Content/Signup/Form/Paragraph">
|
||||
Registrations on this pod are open, but reviewed by moderators before approval.
|
||||
</translate>
|
||||
</p>
|
||||
<template v-if="formCustomization && formCustomization.help_text">
|
||||
<rendered-description :content="formCustomization.help_text" :fetch-html="fetchDescriptionHtml" :permissive="true"></rendered-description>
|
||||
<div class="ui hidden divider"></div>
|
||||
<rendered-description
|
||||
:content="formCustomization.help_text"
|
||||
:fetch-html="fetchDescriptionHtml"
|
||||
:permissive="true"
|
||||
/>
|
||||
<div class="ui hidden divider" />
|
||||
</template>
|
||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
||||
<h4 class="header"><translate translate-context="Content/Signup/Form/Paragraph">Your account cannot be created.</translate></h4>
|
||||
<div
|
||||
v-if="errors.length > 0"
|
||||
role="alert"
|
||||
class="ui negative message"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate translate-context="Content/Signup/Form/Paragraph">
|
||||
Your account cannot be created.
|
||||
</translate>
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li v-for="error in errors">{{ error }}</li>
|
||||
<li
|
||||
v-for="(error, key) in errors"
|
||||
:key="key"
|
||||
>
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="required field">
|
||||
<label for="username-field"><translate translate-context="Content/*/*">Username</translate></label>
|
||||
<input
|
||||
id="username-field"
|
||||
ref="username"
|
||||
v-model="username"
|
||||
name="username"
|
||||
required
|
||||
id="username-field"
|
||||
type="text"
|
||||
autofocus
|
||||
:placeholder="labels.usernamePlaceholder"
|
||||
v-model="username">
|
||||
>
|
||||
</div>
|
||||
<div class="required field">
|
||||
<label for="email-field"><translate translate-context="Content/*/*/Noun">E-mail address</translate></label>
|
||||
<input
|
||||
id="email-field"
|
||||
ref="email"
|
||||
v-model="email"
|
||||
name="email"
|
||||
required
|
||||
type="email"
|
||||
:placeholder="labels.emailPlaceholder"
|
||||
v-model="email">
|
||||
>
|
||||
</div>
|
||||
<div class="required field">
|
||||
<label for="password-field"><translate translate-context="*/*/*">Password</translate></label>
|
||||
<password-input field-id="password-field" v-model="password" />
|
||||
<password-input
|
||||
v-model="password"
|
||||
field-id="password-field"
|
||||
/>
|
||||
</div>
|
||||
<div class="required field" v-if="!$store.state.instance.settings.users.registration_enabled.value">
|
||||
<div
|
||||
v-if="!$store.state.instance.settings.users.registration_enabled.value"
|
||||
class="required field"
|
||||
>
|
||||
<label for="invitation-code"><translate translate-context="Content/*/Input.Label">Invitation code</translate></label>
|
||||
<input
|
||||
id="invitation-code"
|
||||
v-model="invitation"
|
||||
required
|
||||
type="text"
|
||||
name="invitation"
|
||||
:placeholder="labels.placeholder"
|
||||
v-model="invitation">
|
||||
>
|
||||
</div>
|
||||
<template v-if="signupRequiresApproval && formCustomization && formCustomization.fields && formCustomization.fields.length > 0">
|
||||
<div :class="[{required: field.required}, 'field']" v-for="(field, idx) in formCustomization.fields" :key="idx">
|
||||
<div
|
||||
v-for="(field, idx) in formCustomization.fields"
|
||||
:key="idx"
|
||||
:class="[{required: field.required}, 'field']"
|
||||
>
|
||||
<label :for="`custom-field-${idx}`">{{ field.label }}</label>
|
||||
<textarea
|
||||
v-if="field.input_type === 'long_text'"
|
||||
:id="`custom-field-${idx}`"
|
||||
:value="customFields[field.label]"
|
||||
:required="field.required"
|
||||
@input="$set(customFields, field.label, $event.target.value)" rows="5"></textarea>
|
||||
<input v-else :id="`custom-field-${idx}`" type="text" :value="customFields[field.label]" :required="field.required" @input="$set(customFields, field.label, $event.target.value)">
|
||||
rows="5"
|
||||
@input="$set(customFields, field.label, $event.target.value)"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
:id="`custom-field-${idx}`"
|
||||
type="text"
|
||||
:value="customFields[field.label]"
|
||||
:required="field.required"
|
||||
@input="$set(customFields, field.label, $event.target.value)"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<button :class="['ui', buttonClasses, {'loading': isLoading}, ' right floated button']" type="submit">
|
||||
<translate translate-context="Content/Signup/Button.Label">Create my account</translate>
|
||||
<button
|
||||
:class="['ui', buttonClasses, {'loading': isLoading}, ' right floated button']"
|
||||
type="submit"
|
||||
>
|
||||
<translate translate-context="Content/Signup/Button.Label">
|
||||
Create my account
|
||||
</translate>
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios"
|
||||
import logger from "@/logging"
|
||||
import axios from 'axios'
|
||||
import logger from '@/logging'
|
||||
|
||||
import LoginForm from "@/components/auth/LoginForm"
|
||||
import PasswordInput from "@/components/forms/PasswordInput"
|
||||
import LoginForm from '@/components/auth/LoginForm'
|
||||
import PasswordInput from '@/components/forms/PasswordInput'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
defaultInvitation: { type: String, required: false, default: null },
|
||||
next: { type: String, default: "/" },
|
||||
buttonClasses: { type: String, default: "success" },
|
||||
customization: { type: Object, default: null},
|
||||
fetchDescriptionHtml: { type: Boolean, default: false},
|
||||
fetchDescriptionHtml: { type: Boolean, default: false},
|
||||
signupApprovalEnabled: {type: Boolean, default: null, required: false},
|
||||
},
|
||||
components: {
|
||||
LoginForm,
|
||||
PasswordInput,
|
||||
PasswordInput
|
||||
},
|
||||
data() {
|
||||
props: {
|
||||
defaultInvitation: { type: String, required: false, default: null },
|
||||
next: { type: String, default: '/' },
|
||||
buttonClasses: { type: String, default: 'success' },
|
||||
customization: { type: Object, default: null },
|
||||
fetchDescriptionHtml: { type: Boolean, default: false },
|
||||
signupApprovalEnabled: { type: Boolean, default: null, required: false }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
isLoadingInstanceSetting: true,
|
||||
errors: [],
|
||||
isLoading: false,
|
||||
invitation: this.defaultInvitation,
|
||||
customFields: {},
|
||||
submitted: false,
|
||||
submitted: false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
let self = this
|
||||
this.$store.dispatch("instance/fetchSettings", {
|
||||
callback: function() {
|
||||
self.isLoadingInstanceSetting = false
|
||||
}
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
labels() {
|
||||
let placeholder = this.$pgettext(
|
||||
"Content/Signup/Form/Placeholder",
|
||||
"Enter your invitation code (case insensitive)"
|
||||
labels () {
|
||||
const placeholder = this.$pgettext(
|
||||
'Content/Signup/Form/Placeholder',
|
||||
'Enter your invitation code (case insensitive)'
|
||||
)
|
||||
let usernamePlaceholder = this.$pgettext("Content/Signup/Form/Placeholder", "Enter your username")
|
||||
let emailPlaceholder = this.$pgettext("Content/Signup/Form/Placeholder", "Enter your e-mail address")
|
||||
const usernamePlaceholder = this.$pgettext('Content/Signup/Form/Placeholder', 'Enter your username')
|
||||
const emailPlaceholder = this.$pgettext('Content/Signup/Form/Placeholder', 'Enter your e-mail address')
|
||||
return {
|
||||
usernamePlaceholder,
|
||||
emailPlaceholder,
|
||||
|
@ -152,22 +209,30 @@ export default {
|
|||
return this.signupApprovalEnabled
|
||||
}
|
||||
},
|
||||
created () {
|
||||
const self = this
|
||||
this.$store.dispatch('instance/fetchSettings', {
|
||||
callback: function () {
|
||||
self.isLoadingInstanceSetting = false
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
var self = this
|
||||
submit () {
|
||||
const self = this
|
||||
self.isLoading = true
|
||||
this.errors = []
|
||||
var payload = {
|
||||
const payload = {
|
||||
username: this.username,
|
||||
password1: this.password,
|
||||
password2: this.password,
|
||||
email: this.email,
|
||||
invitation: this.invitation,
|
||||
request_fields: this.customFields,
|
||||
request_fields: this.customFields
|
||||
}
|
||||
return axios.post("auth/registration/", payload).then(
|
||||
return axios.post('auth/registration/', payload).then(
|
||||
response => {
|
||||
logger.default.info("Successfully created account")
|
||||
logger.default.info('Successfully created account')
|
||||
self.submitted = true
|
||||
self.isLoading = false
|
||||
},
|
||||
|
|
|
@ -1,60 +1,143 @@
|
|||
<template>
|
||||
<form class="ui form" @submit.prevent="requestNewToken()">
|
||||
<h2><translate translate-context="Content/Settings/Title">Subsonic API password</translate></h2>
|
||||
<p class="ui message" v-if="!subsonicEnabled">
|
||||
<translate translate-context="Content/Settings/Paragraph">The Subsonic API is not available on this Funkwhale instance.</translate>
|
||||
<form
|
||||
class="ui form"
|
||||
@submit.prevent="requestNewToken()"
|
||||
>
|
||||
<h2>
|
||||
<translate translate-context="Content/Settings/Title">
|
||||
Subsonic API password
|
||||
</translate>
|
||||
</h2>
|
||||
<p
|
||||
v-if="!subsonicEnabled"
|
||||
class="ui message"
|
||||
>
|
||||
<translate translate-context="Content/Settings/Paragraph">
|
||||
The Subsonic API is not available on this Funkwhale instance.
|
||||
</translate>
|
||||
</p>
|
||||
<p>
|
||||
<translate translate-context="Content/Settings/Paragraph'">Funkwhale is compatible with other music players that support the Subsonic API.</translate> <translate translate-context="Content/Settings/Paragraph">You can use those to enjoy your playlist and music in offline mode, on your smartphone or tablet, for instance.</translate>
|
||||
<translate translate-context="Content/Settings/Paragraph'">
|
||||
Funkwhale is compatible with other music players that support the Subsonic API.
|
||||
</translate> <translate translate-context="Content/Settings/Paragraph">
|
||||
You can use those to enjoy your playlist and music in offline mode, on your smartphone or tablet, for instance.
|
||||
</translate>
|
||||
</p>
|
||||
<p>
|
||||
<translate translate-context="Content/Settings/Paragraph">However, accessing Funkwhale from those clients requires a separate password you can set below.</translate>
|
||||
<translate translate-context="Content/Settings/Paragraph">
|
||||
However, accessing Funkwhale from those clients requires a separate password you can set below.
|
||||
</translate>
|
||||
</p>
|
||||
<p><a href="https://docs.funkwhale.audio/users/apps.html#subsonic-compatible-clients" target="_blank">
|
||||
<p>
|
||||
<a
|
||||
href="https://docs.funkwhale.audio/users/apps.html#subsonic-compatible-clients"
|
||||
target="_blank"
|
||||
>
|
||||
<translate translate-context="Content/Settings/Link">Discover how to use Funkwhale from other apps</translate>
|
||||
</a></p>
|
||||
<div v-if="success" class="ui positive message">
|
||||
<h4 class="header">{{ successMessage }}</h4>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
v-if="success"
|
||||
class="ui positive message"
|
||||
>
|
||||
<h4 class="header">
|
||||
{{ successMessage }}
|
||||
</h4>
|
||||
</div>
|
||||
<div v-if="subsonicEnabled && errors.length > 0" role="alert" class="ui negative message">
|
||||
<h4 class="header"><translate translate-context="Content/*/Error message.Title">Error</translate></h4>
|
||||
<div
|
||||
v-if="subsonicEnabled && errors.length > 0"
|
||||
role="alert"
|
||||
class="ui negative message"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate translate-context="Content/*/Error message.Title">
|
||||
Error
|
||||
</translate>
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li v-for="error in errors">{{ error }}</li>
|
||||
<li
|
||||
v-for="(error, key) in errors"
|
||||
:key="key"
|
||||
>
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<template v-if="subsonicEnabled">
|
||||
<div v-if="token" class="field">
|
||||
<label for="subsonic-password" class="visually-hidden">{{ labels.subsonicField }}</label>
|
||||
<div
|
||||
v-if="token"
|
||||
class="field"
|
||||
>
|
||||
<label
|
||||
for="subsonic-password"
|
||||
class="visually-hidden"
|
||||
>{{ labels.subsonicField }}</label>
|
||||
<password-input
|
||||
field-id="subsonic-password"
|
||||
ref="passwordInput"
|
||||
v-model="token"
|
||||
:key="token"
|
||||
v-model="token"
|
||||
field-id="subsonic-password"
|
||||
:copy-button="true"
|
||||
:default-show="showToken"/>
|
||||
:default-show="showToken"
|
||||
/>
|
||||
</div>
|
||||
<dangerous-button
|
||||
v-if="token"
|
||||
:class="['ui', {'loading': isLoading}, 'button']"
|
||||
:action="requestNewToken">
|
||||
<translate translate-context="*/Settings/Button.Label/Verb">Request a new password</translate>
|
||||
<p slot="modal-header"><translate translate-context="Popup/Settings/Title">Request a new Subsonic API password?</translate></p>
|
||||
<p slot="modal-content"><translate translate-context="Popup/Settings/Paragraph">This will log you out from existing devices that use the current password.</translate></p>
|
||||
<div slot="modal-confirm"><translate translate-context="*/Settings/Button.Label/Verb">Request a new password</translate></div>
|
||||
:action="requestNewToken"
|
||||
>
|
||||
<translate translate-context="*/Settings/Button.Label/Verb">
|
||||
Request a new password
|
||||
</translate>
|
||||
<p slot="modal-header">
|
||||
<translate translate-context="Popup/Settings/Title">
|
||||
Request a new Subsonic API password?
|
||||
</translate>
|
||||
</p>
|
||||
<p slot="modal-content">
|
||||
<translate translate-context="Popup/Settings/Paragraph">
|
||||
This will log you out from existing devices that use the current password.
|
||||
</translate>
|
||||
</p>
|
||||
<div slot="modal-confirm">
|
||||
<translate translate-context="*/Settings/Button.Label/Verb">
|
||||
Request a new password
|
||||
</translate>
|
||||
</div>
|
||||
</dangerous-button>
|
||||
<button
|
||||
v-else
|
||||
color=""
|
||||
:class="['ui', {'loading': isLoading}, 'button']"
|
||||
@click="requestNewToken"><translate translate-context="Content/Settings/Button.Label/Verb">Request a password</translate></button>
|
||||
@click="requestNewToken"
|
||||
>
|
||||
<translate translate-context="Content/Settings/Button.Label/Verb">
|
||||
Request a password
|
||||
</translate>
|
||||
</button>
|
||||
<dangerous-button
|
||||
v-if="token"
|
||||
:class="['ui', {'loading': isLoading}, 'warning', 'button']"
|
||||
:action="disable">
|
||||
<translate translate-context="Content/Settings/Button.Label/Verb">Disable Subsonic access</translate>
|
||||
<p slot="modal-header"><translate translate-context="Popup/Settings/Title">Disable Subsonic API access?</translate></p>
|
||||
<p slot="modal-content"><translate translate-context="Popup/Settings/Paragraph">This will completely disable access to the Subsonic API using from account.</translate></p>
|
||||
<div slot="modal-confirm"><translate translate-context="Popup/Settings/Button.Label">Disable access</translate></div>
|
||||
:action="disable"
|
||||
>
|
||||
<translate translate-context="Content/Settings/Button.Label/Verb">
|
||||
Disable Subsonic access
|
||||
</translate>
|
||||
<p slot="modal-header">
|
||||
<translate translate-context="Popup/Settings/Title">
|
||||
Disable Subsonic API access?
|
||||
</translate>
|
||||
</p>
|
||||
<p slot="modal-content">
|
||||
<translate translate-context="Popup/Settings/Paragraph">
|
||||
This will completely disable access to the Subsonic API using from account.
|
||||
</translate>
|
||||
</p>
|
||||
<div slot="modal-confirm">
|
||||
<translate translate-context="Popup/Settings/Button.Label">
|
||||
Disable access
|
||||
</translate>
|
||||
</div>
|
||||
</dangerous-button>
|
||||
</template>
|
||||
</form>
|
||||
|
@ -78,6 +161,16 @@ export default {
|
|||
showToken: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
subsonicEnabled () {
|
||||
return this.$store.state.instance.settings.subsonic.enabled.value
|
||||
},
|
||||
labels () {
|
||||
return {
|
||||
subsonicField: this.$pgettext('Content/Password/Input.label', 'Your subsonic API password')
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchToken()
|
||||
},
|
||||
|
@ -86,10 +179,10 @@ export default {
|
|||
this.success = false
|
||||
this.errors = []
|
||||
this.isLoading = true
|
||||
let self = this
|
||||
let url = `users/${this.$store.state.auth.username}/subsonic-token/`
|
||||
const self = this
|
||||
const url = `users/${this.$store.state.auth.username}/subsonic-token/`
|
||||
return axios.get(url).then(response => {
|
||||
self.token = response.data['subsonic_api_token']
|
||||
self.token = response.data.subsonic_api_token
|
||||
self.isLoading = false
|
||||
}, error => {
|
||||
self.isLoading = false
|
||||
|
@ -101,11 +194,11 @@ export default {
|
|||
this.success = false
|
||||
this.errors = []
|
||||
this.isLoading = true
|
||||
let self = this
|
||||
let url = `users/${this.$store.state.auth.username}/subsonic-token/`
|
||||
const self = this
|
||||
const url = `users/${this.$store.state.auth.username}/subsonic-token/`
|
||||
return axios.post(url, {}).then(response => {
|
||||
self.showToken = true
|
||||
self.token = response.data['subsonic_api_token']
|
||||
self.token = response.data.subsonic_api_token
|
||||
self.isLoading = false
|
||||
self.success = true
|
||||
}, error => {
|
||||
|
@ -118,8 +211,8 @@ export default {
|
|||
this.success = false
|
||||
this.errors = []
|
||||
this.isLoading = true
|
||||
let self = this
|
||||
let url = `users/${this.$store.state.auth.username}/subsonic-token/`
|
||||
const self = this
|
||||
const url = `users/${this.$store.state.auth.username}/subsonic-token/`
|
||||
return axios.delete(url).then(response => {
|
||||
self.isLoading = false
|
||||
self.token = null
|
||||
|
@ -129,16 +222,6 @@ export default {
|
|||
self.errors = error.backendErrors
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
subsonicEnabled () {
|
||||
return this.$store.state.instance.settings.subsonic.enabled.value
|
||||
},
|
||||
labels () {
|
||||
return {
|
||||
subsonicField: this.$pgettext("Content/Password/Input.label", "Your subsonic API password")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,16 +1,35 @@
|
|||
<template>
|
||||
<form @submit.stop.prevent :class="['ui', {loading: isLoading}, 'form']">
|
||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
||||
<h4 class="header"><translate translate-context="Content/*/Error message.Title">Error while creating</translate></h4>
|
||||
<form
|
||||
:class="['ui', {loading: isLoading}, 'form']"
|
||||
@submit.stop.prevent
|
||||
>
|
||||
<div
|
||||
v-if="errors.length > 0"
|
||||
role="alert"
|
||||
class="ui negative message"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate translate-context="Content/*/Error message.Title">
|
||||
Error while creating
|
||||
</translate>
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li v-for="error in errors">{{ error }}</li>
|
||||
<li
|
||||
v-for="(error, key) in errors"
|
||||
:key="key"
|
||||
>
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ui required field">
|
||||
<label for="album-title">
|
||||
<translate translate-context="*/*/*/Noun">Title</translate>
|
||||
</label>
|
||||
<input type="text" v-model="values.title">
|
||||
<input
|
||||
v-model="values.title"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
@ -18,17 +37,17 @@
|
|||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
channel: {type: Object, required: true},
|
||||
},
|
||||
components: {},
|
||||
props: {
|
||||
channel: { type: Object, required: true }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
errors: [],
|
||||
isLoading: false,
|
||||
values: {
|
||||
title: "",
|
||||
},
|
||||
title: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -36,20 +55,28 @@ export default {
|
|||
return this.values.title.length > 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
submittable (v) {
|
||||
this.$emit('submittable', v)
|
||||
},
|
||||
isLoading (v) {
|
||||
this.$emit('loading', v)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
submit () {
|
||||
let self = this
|
||||
const self = this
|
||||
self.isLoading = true
|
||||
self.errors = []
|
||||
let payload = {
|
||||
const payload = {
|
||||
...this.values,
|
||||
artist: this.channel.artist.id,
|
||||
artist: this.channel.artist.id
|
||||
}
|
||||
return axios.post('albums/', payload).then(
|
||||
response => {
|
||||
self.isLoading = false
|
||||
self.$emit("created")
|
||||
self.$emit('created')
|
||||
},
|
||||
error => {
|
||||
self.errors = error.backendErrors
|
||||
|
@ -57,14 +84,6 @@ export default {
|
|||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
submittable (v) {
|
||||
this.$emit("submittable", v)
|
||||
},
|
||||
isLoading (v) {
|
||||
this.$emit("loading", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,21 +1,47 @@
|
|||
<template>
|
||||
<modal class="small" :show.sync="show">
|
||||
<modal
|
||||
class="small"
|
||||
:show.sync="show"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate key="1" v-if="channel.content_category === 'podcasts'" translate-context="Popup/Channels/Title/Verb">New series</translate>
|
||||
<translate key="2" v-else translate-context="Popup/Channels/Title">New album</translate>
|
||||
<translate
|
||||
v-if="channel.content_category === 'podcasts'"
|
||||
key="1"
|
||||
translate-context="Popup/Channels/Title/Verb"
|
||||
>
|
||||
New series
|
||||
</translate>
|
||||
<translate
|
||||
v-else
|
||||
key="2"
|
||||
translate-context="Popup/Channels/Title"
|
||||
>
|
||||
New album
|
||||
</translate>
|
||||
</h4>
|
||||
<div class="scrolling content">
|
||||
<channel-album-form
|
||||
ref="albumForm"
|
||||
:channel="channel"
|
||||
@loading="isLoading = $event"
|
||||
@submittable="submittable = $event"
|
||||
@created="$emit('created', $event)"
|
||||
:channel="channel"></channel-album-form>
|
||||
/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="ui basic cancel button"><translate translate-context="*/*/Button.Label/Verb">Cancel</translate></button>
|
||||
<button :class="['ui', 'primary', {loading: isLoading}, 'button']" :disabled="!submittable" @click.stop.prevent="$refs.albumForm.submit()">
|
||||
<translate translate-context="*/*/Button.Label">Create</translate>
|
||||
<button class="ui basic cancel button">
|
||||
<translate translate-context="*/*/Button.Label/Verb">
|
||||
Cancel
|
||||
</translate>
|
||||
</button>
|
||||
<button
|
||||
:class="['ui', 'primary', {loading: isLoading}, 'button']"
|
||||
:disabled="!submittable"
|
||||
@click.stop.prevent="$refs.albumForm.submit()"
|
||||
>
|
||||
<translate translate-context="*/*/Button.Label">
|
||||
Create
|
||||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
|
@ -26,16 +52,16 @@ import Modal from '@/components/semantic/Modal'
|
|||
import ChannelAlbumForm from '@/components/channels/AlbumForm'
|
||||
|
||||
export default {
|
||||
props: ['channel'],
|
||||
components: {
|
||||
Modal,
|
||||
ChannelAlbumForm
|
||||
},
|
||||
props: { channel: { type: Object, required: true } },
|
||||
data () {
|
||||
return {
|
||||
isLoading: false,
|
||||
submittable: false,
|
||||
show: false,
|
||||
show: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
|
|
@ -1,15 +1,41 @@
|
|||
<template>
|
||||
<div>
|
||||
<label for="album-dropdown">
|
||||
<translate v-if="channel && channel.artist.content_category === 'podcast'" key="1" translate-context="*/*/*">Series</translate>
|
||||
<translate v-else key="2" translate-context="*/*/*">Album</translate>
|
||||
<translate
|
||||
v-if="channel && channel.artist.content_category === 'podcast'"
|
||||
key="1"
|
||||
translate-context="*/*/*"
|
||||
>Series</translate>
|
||||
<translate
|
||||
v-else
|
||||
key="2"
|
||||
translate-context="*/*/*"
|
||||
>Album</translate>
|
||||
</label>
|
||||
<select id="album-dropdown" :value="value" @input="$emit('input', $event.target.value)" class="ui search normal dropdown">
|
||||
<select
|
||||
id="album-dropdown"
|
||||
:value="value"
|
||||
class="ui search normal dropdown"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
>
|
||||
<option value="">
|
||||
<translate translate-context="*/*/*">None</translate>
|
||||
<translate translate-context="*/*/*">
|
||||
None
|
||||
</translate>
|
||||
</option>
|
||||
<option v-for="album in albums" :key="album.id" :value="album.id">
|
||||
{{ album.title }} (<translate translate-context="*/*/*" :translate-params="{count: album.tracks_count}" :translate-n="album.tracks_count" translate-plural="%{ count } tracks">%{ count } track</translate>)
|
||||
<option
|
||||
v-for="album in albums"
|
||||
:key="album.id"
|
||||
:value="album.id"
|
||||
>
|
||||
{{ album.title }} (<translate
|
||||
translate-context="*/*/*"
|
||||
:translate-params="{count: album.tracks_count}"
|
||||
:translate-n="album.tracks_count"
|
||||
translate-plural="%{ count } tracks"
|
||||
>
|
||||
%{ count } track
|
||||
</translate>)
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
@ -18,11 +44,19 @@
|
|||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
props: ['value', 'channel'],
|
||||
props: {
|
||||
value: { type: String, required: true },
|
||||
channel: { type: Object, required: true }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
albums: [],
|
||||
isLoading: false,
|
||||
isLoading: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
async channel () {
|
||||
await this.fetchData()
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
|
@ -35,14 +69,9 @@ export default {
|
|||
return
|
||||
}
|
||||
this.isLoading = true
|
||||
let response = await axios.get('albums/', {params: {artist: this.channel.artist.id, include_channels: 'true'}})
|
||||
const response = await axios.get('albums/', { params: { artist: this.channel.artist.id, include_channels: 'true' } })
|
||||
this.albums = response.data.results
|
||||
this.isLoading = false
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
async channel () {
|
||||
await this.fetchData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,15 +3,36 @@
|
|||
<label for="license-dropdown">
|
||||
<translate translate-context="Content/*/*/Noun">License</translate>
|
||||
</label>
|
||||
<select id="license-dropdown" :value="value" @input="$emit('input', $event.target.value)" class="ui search normal dropdown">
|
||||
<select
|
||||
id="license-dropdown"
|
||||
:value="value"
|
||||
class="ui search normal dropdown"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
>
|
||||
<option value="">
|
||||
<translate translate-context="*/*/*">None</translate>
|
||||
<translate translate-context="*/*/*">
|
||||
None
|
||||
</translate>
|
||||
</option>
|
||||
<option
|
||||
v-for="l in featuredLicenses"
|
||||
:key="l.code"
|
||||
:value="l.code"
|
||||
>
|
||||
{{ l.name }}
|
||||
</option>
|
||||
<option v-for="l in featuredLicenses" :key="l.code" :value="l.code">{{ l.name }}</option>
|
||||
</select>
|
||||
<p class="help" v-if="value">
|
||||
<div class="ui very small hidden divider"></div>
|
||||
<a :href="currentLicense.url" v-if="value" target="_blank" rel="noreferrer noopener">
|
||||
<div class="ui very small hidden divider" />
|
||||
<p
|
||||
v-if="value"
|
||||
class="help"
|
||||
>
|
||||
<a
|
||||
v-if="value"
|
||||
:href="currentLicense.url"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<translate translate-context="Content/*/*">About this license</translate>
|
||||
</a>
|
||||
</p>
|
||||
|
@ -21,7 +42,7 @@
|
|||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
props: ['value'],
|
||||
props: { value: { type: String, required: true } },
|
||||
data () {
|
||||
return {
|
||||
availableLicenses: [],
|
||||
|
@ -32,38 +53,38 @@ export default {
|
|||
'cc-by-nc-4.0',
|
||||
'cc-by-nc-sa-4.0',
|
||||
'cc-by-nc-nd-4.0',
|
||||
'cc-by-nd-4.0',
|
||||
'cc-by-nd-4.0'
|
||||
],
|
||||
isLoading: false,
|
||||
isLoading: false
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
await this.fetchLicenses()
|
||||
},
|
||||
computed: {
|
||||
featuredLicenses () {
|
||||
let self = this
|
||||
const self = this
|
||||
return this.availableLicenses.filter((l) => {
|
||||
return self.featuredLicensesIds.indexOf(l.code) > -1
|
||||
})
|
||||
},
|
||||
currentLicense () {
|
||||
let self = this
|
||||
const self = this
|
||||
if (this.value) {
|
||||
return this.availableLicenses.filter((l) => {
|
||||
return l.code === self.value
|
||||
})[0]
|
||||
|
||||
}
|
||||
return null
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
await this.fetchLicenses()
|
||||
},
|
||||
methods: {
|
||||
async fetchLicenses () {
|
||||
this.isLoading = true
|
||||
let response = await axios.get('licenses/')
|
||||
const response = await axios.get('licenses/')
|
||||
this.availableLicenses = response.data.results
|
||||
this.isLoading = false
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,20 +1,40 @@
|
|||
<template>
|
||||
<button v-if="$store.state.auth.authenticated" @click.stop="toggle" :class="['ui', 'pink', {'inverted': isSubscribed}, {'favorited': isSubscribed}, 'icon', 'labeled', 'button']">
|
||||
<i class="heart icon"></i>
|
||||
<translate v-if="isSubscribed" translate-context="Content/Track/Button.Message">Unsubscribe</translate>
|
||||
<translate v-else translate-context="Content/Track/*/Verb">Subscribe</translate>
|
||||
<template>
|
||||
<button
|
||||
v-if="$store.state.auth.authenticated"
|
||||
:class="['ui', 'pink', {'inverted': isSubscribed}, {'favorited': isSubscribed}, 'icon', 'labeled', 'button']"
|
||||
@click.stop="toggle"
|
||||
>
|
||||
<i class="heart icon" />
|
||||
<translate
|
||||
v-if="isSubscribed"
|
||||
translate-context="Content/Track/Button.Message"
|
||||
>
|
||||
Unsubscribe
|
||||
</translate>
|
||||
<translate
|
||||
v-else
|
||||
translate-context="Content/Track/*/Verb"
|
||||
>
|
||||
Subscribe
|
||||
</translate>
|
||||
</button>
|
||||
<button @click="$refs.loginModal.show = true" v-else :class="['ui', 'pink', 'icon', 'labeled', 'button']">
|
||||
<i class="heart icon"></i>
|
||||
<translate translate-context="Content/Track/*/Verb">Subscribe</translate>
|
||||
<button
|
||||
v-else
|
||||
:class="['ui', 'pink', 'icon', 'labeled', 'button']"
|
||||
@click="$refs.loginModal.show = true"
|
||||
>
|
||||
<i class="heart icon" />
|
||||
<translate translate-context="Content/Track/*/Verb">
|
||||
Subscribe
|
||||
</translate>
|
||||
<login-modal
|
||||
ref="loginModal"
|
||||
class="small"
|
||||
:nextRoute='this.$route.fullPath'
|
||||
:message='this.message.authMessage'
|
||||
:cover='this.channel.artist.cover'
|
||||
@created="$refs.loginModal.show = false;">
|
||||
</login-modal>
|
||||
:next-route="$route.fullPath"
|
||||
:message="message.authMessage"
|
||||
:cover="channel.artist.cover"
|
||||
@created="$refs.loginModal.show = false;"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
@ -22,12 +42,12 @@
|
|||
import LoginModal from '@/components/common/LoginModal'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
channel: {type: Object},
|
||||
},
|
||||
components: {
|
||||
LoginModal
|
||||
},
|
||||
props: {
|
||||
channel: { type: Object, required: true }
|
||||
},
|
||||
computed: {
|
||||
title () {
|
||||
if (this.isSubscribed) {
|
||||
|
@ -43,7 +63,7 @@ export default {
|
|||
return {
|
||||
authMessage: this.$pgettext('Popup/Message/Paragraph', 'You need to be logged in to subscribe to this channel')
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggle () {
|
||||
|
@ -56,6 +76,5 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,70 +1,132 @@
|
|||
<template>
|
||||
<form @submit.stop.prevent :class="['ui', {loading: isLoadingStep1}, 'form component-file-upload']">
|
||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
||||
<h4 class="header"><translate translate-context="Content/*/Error message.Title">Error while publishing</translate></h4>
|
||||
<form
|
||||
:class="['ui', {loading: isLoadingStep1}, 'form component-file-upload']"
|
||||
@submit.stop.prevent
|
||||
>
|
||||
<div
|
||||
v-if="errors.length > 0"
|
||||
role="alert"
|
||||
class="ui negative message"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate translate-context="Content/*/Error message.Title">
|
||||
Error while publishing
|
||||
</translate>
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li v-for="error in errors">{{ error }}</li>
|
||||
<li
|
||||
v-for="(error, key) in errors"
|
||||
:key="key"
|
||||
>
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div :class="['ui', 'required', {hidden: step > 1}, 'field']">
|
||||
<label for="channel-dropdown">
|
||||
<translate translate-context="*/*/*">Channel</translate>
|
||||
</label>
|
||||
<div id="channel-dropdown" class="ui search normal selection dropdown">
|
||||
<div class="text"></div>
|
||||
<i class="dropdown icon"></i>
|
||||
<div
|
||||
id="channel-dropdown"
|
||||
class="ui search normal selection dropdown"
|
||||
>
|
||||
<div class="text" />
|
||||
<i class="dropdown icon" />
|
||||
</div>
|
||||
</div>
|
||||
<album-select v-model.number="values.album" :channel="selectedChannel" :class="['ui', {hidden: step > 1}, 'field']"></album-select>
|
||||
<license-select v-model="values.license" :class="['ui', {hidden: step > 1}, 'field']"></license-select>
|
||||
<album-select
|
||||
v-model.number="values.album"
|
||||
:channel="selectedChannel"
|
||||
:class="['ui', {hidden: step > 1}, 'field']"
|
||||
/>
|
||||
<license-select
|
||||
v-model="values.license"
|
||||
:class="['ui', {hidden: step > 1}, 'field']"
|
||||
/>
|
||||
<div :class="['ui', {hidden: step > 1}, 'message']">
|
||||
<div class="content">
|
||||
<p>
|
||||
<i class="copyright icon"></i>
|
||||
<translate translate-context="Content/Channels/Popup.Paragraph">Add a license to your upload to ensure some freedoms to your public.</translate>
|
||||
<i class="copyright icon" />
|
||||
<translate translate-context="Content/Channels/Popup.Paragraph">
|
||||
Add a license to your upload to ensure some freedoms to your public.
|
||||
</translate>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="step >= 2 && step < 4">
|
||||
<div role="alert" class="ui warning message" v-if="remainingSpace === 0">
|
||||
<div
|
||||
v-if="remainingSpace === 0"
|
||||
role="alert"
|
||||
class="ui warning message"
|
||||
>
|
||||
<div class="content">
|
||||
<p>
|
||||
<i class="warning icon"></i>
|
||||
<translate translate-context="Content/Library/Paragraph">You don't have any space left to upload your files. Please contact the moderators.</translate>
|
||||
<i class="warning icon" />
|
||||
<translate translate-context="Content/Library/Paragraph">
|
||||
You don't have any space left to upload your files. Please contact the moderators.
|
||||
</translate>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="ui visible info message" v-if="step === 2 && draftUploads && draftUploads.length > 0 && includeDraftUploads === null">
|
||||
<div
|
||||
v-if="step === 2 && draftUploads && draftUploads.length > 0 && includeDraftUploads === null"
|
||||
class="ui visible info message"
|
||||
>
|
||||
<p>
|
||||
<i class="redo icon"></i>
|
||||
<translate translate-context="Popup/Channels/Paragraph">You have some draft uploads pending publication.</translate>
|
||||
<i class="redo icon" />
|
||||
<translate translate-context="Popup/Channels/Paragraph">
|
||||
You have some draft uploads pending publication.
|
||||
</translate>
|
||||
</p>
|
||||
<button @click.stop.prevent="includeDraftUploads = false" class="ui basic button">
|
||||
<translate translate-context="*/*/*">Ignore</translate>
|
||||
<button
|
||||
class="ui basic button"
|
||||
@click.stop.prevent="includeDraftUploads = false"
|
||||
>
|
||||
<translate translate-context="*/*/*">
|
||||
Ignore
|
||||
</translate>
|
||||
</button>
|
||||
<button @click.stop.prevent="includeDraftUploads = true" class="ui basic button">
|
||||
<translate translate-context="*/*/*">Resume</translate>
|
||||
<button
|
||||
class="ui basic button"
|
||||
@click.stop.prevent="includeDraftUploads = true"
|
||||
>
|
||||
<translate translate-context="*/*/*">
|
||||
Resume
|
||||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="uploadedFiles.length > 0" :class="[{hidden: step === 3}]">
|
||||
<div class="channel-file" v-for="(file, idx) in uploadedFiles">
|
||||
<div
|
||||
v-if="uploadedFiles.length > 0"
|
||||
:class="[{hidden: step === 3}]"
|
||||
>
|
||||
<div
|
||||
v-for="(file, idx) in uploadedFiles"
|
||||
:key="idx"
|
||||
class="channel-file"
|
||||
>
|
||||
<div class="content">
|
||||
<div role="button"
|
||||
<div
|
||||
v-if="file.response.uuid"
|
||||
@click.stop.prevent="selectedUploadId = file.response.uuid"
|
||||
role="button"
|
||||
class="ui basic icon button"
|
||||
:title="labels.editTitle">
|
||||
<i class="pencil icon"></i>
|
||||
:title="labels.editTitle"
|
||||
@click.stop.prevent="selectedUploadId = file.response.uuid"
|
||||
>
|
||||
<i class="pencil icon" />
|
||||
</div>
|
||||
<div
|
||||
v-if="file.error"
|
||||
@click.stop.prevent="selectedUploadId = file.response.uuid"
|
||||
class="ui basic danger icon label"
|
||||
:title="file.error">
|
||||
<i class="warning sign icon"></i>
|
||||
:title="file.error"
|
||||
@click.stop.prevent="selectedUploadId = file.response.uuid"
|
||||
>
|
||||
<i class="warning sign icon" />
|
||||
</div>
|
||||
<div v-else-if="file.active" class="ui active slow inline loader"></div>
|
||||
<div
|
||||
v-else-if="file.active"
|
||||
class="ui active slow inline loader"
|
||||
/>
|
||||
</div>
|
||||
<h4 class="ui header">
|
||||
<template v-if="file.metadata.title">
|
||||
|
@ -77,20 +139,39 @@
|
|||
<template v-if="file.response.uuid">
|
||||
{{ file.size | humanSize }}
|
||||
<template v-if="file.response.duration">
|
||||
· <human-duration :duration="file.response.duration"></human-duration>
|
||||
· <human-duration :duration="file.response.duration" />
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<translate key="1" v-if="file.active" translate-context="Channels/*/*">Uploading</translate>
|
||||
<translate key="2" v-else-if="file.error" translate-context="Channels/*/*">Errored</translate>
|
||||
<translate key="3" v-else translate-context="Channels/*/*">Pending</translate>
|
||||
<translate
|
||||
v-if="file.active"
|
||||
key="1"
|
||||
translate-context="Channels/*/*"
|
||||
>
|
||||
Uploading
|
||||
</translate>
|
||||
<translate
|
||||
v-else-if="file.error"
|
||||
key="2"
|
||||
translate-context="Channels/*/*"
|
||||
>
|
||||
Errored
|
||||
</translate>
|
||||
<translate
|
||||
v-else
|
||||
key="3"
|
||||
translate-context="Channels/*/*"
|
||||
>
|
||||
Pending
|
||||
</translate>
|
||||
· {{ file.size | humanSize }}
|
||||
· {{ parseInt(file.progress) }}%
|
||||
</template>
|
||||
· <a @click.stop.prevent="remove(file)">
|
||||
<translate translate-context="Content/Radio/Button.Label/Verb">Remove</translate>
|
||||
</a>
|
||||
<template v-if="file.error"> ·
|
||||
<template v-if="file.error">
|
||||
·
|
||||
<a @click.stop.prevent="retry(file)">
|
||||
<translate translate-context="*/*/*">Retry</translate>
|
||||
</a>
|
||||
|
@ -100,20 +181,30 @@
|
|||
</div>
|
||||
</div>
|
||||
<upload-metadata-form
|
||||
:key="selectedUploadId"
|
||||
v-if="selectedUpload"
|
||||
:key="selectedUploadId"
|
||||
:upload="selectedUpload"
|
||||
:values="uploadImportData[selectedUploadId]"
|
||||
@values="setDynamic('uploadImportData', selectedUploadId, $event)"></upload-metadata-form>
|
||||
<div class="ui message" v-if="step === 2">
|
||||
@values="setDynamic('uploadImportData', selectedUploadId, $event)"
|
||||
/>
|
||||
<div
|
||||
v-if="step === 2"
|
||||
class="ui message"
|
||||
>
|
||||
<div class="content">
|
||||
<p>
|
||||
<i class="info icon"></i>
|
||||
<translate translate-context="Content/Library/Paragraph" :translate-params="{extensions: $store.state.ui.supportedExtensions.join(', ')}">Supported extensions: %{ extensions }</translate>
|
||||
<i class="info icon" />
|
||||
<translate
|
||||
translate-context="Content/Library/Paragraph"
|
||||
:translate-params="{extensions: $store.state.ui.supportedExtensions.join(', ')}"
|
||||
>
|
||||
Supported extensions: %{ extensions }
|
||||
</translate>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<file-upload-widget
|
||||
ref="upload"
|
||||
:class="['ui', 'icon', 'basic', 'button', 'channels', {hidden: step === 3}]"
|
||||
:post-action="uploadUrl"
|
||||
:multiple="true"
|
||||
|
@ -121,21 +212,25 @@
|
|||
:drop="true"
|
||||
:extensions="$store.state.ui.supportedExtensions"
|
||||
:value="files"
|
||||
@input="updateFiles"
|
||||
name="audio_file"
|
||||
:thread="1"
|
||||
@input="updateFiles"
|
||||
@input-file="inputFile"
|
||||
ref="upload">
|
||||
>
|
||||
<div>
|
||||
<i class="upload icon"></i>
|
||||
<translate translate-context="Content/Channels/Paragraph">Drag and drop your files here or open the browser to upload your files</translate>
|
||||
<i class="upload icon" />
|
||||
<translate translate-context="Content/Channels/Paragraph">
|
||||
Drag and drop your files here or open the browser to upload your files
|
||||
</translate>
|
||||
</div>
|
||||
<div class="ui very small divider"></div>
|
||||
<div class="ui very small divider" />
|
||||
<div>
|
||||
<translate translate-context="*/*/*">Browse…</translate>
|
||||
<translate translate-context="*/*/*">
|
||||
Browse…
|
||||
</translate>
|
||||
</div>
|
||||
</file-upload-widget>
|
||||
<div class="ui hidden divider"></div>
|
||||
<div class="ui hidden divider" />
|
||||
</template>
|
||||
</template>
|
||||
</form>
|
||||
|
@ -146,31 +241,31 @@ import $ from 'jquery'
|
|||
|
||||
import LicenseSelect from '@/components/channels/LicenseSelect'
|
||||
import AlbumSelect from '@/components/channels/AlbumSelect'
|
||||
import FileUploadWidget from "@/components/library/FileUploadWidget";
|
||||
import FileUploadWidget from '@/components/library/FileUploadWidget'
|
||||
import UploadMetadataForm from '@/components/channels/UploadMetadataForm'
|
||||
|
||||
function setIfEmpty (obj, k, v) {
|
||||
if (obj[k] != undefined) {
|
||||
if (obj[k] !== undefined) {
|
||||
return
|
||||
}
|
||||
obj[k] = v
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
channel: {type: Object, default: null, required: false},
|
||||
},
|
||||
components: {
|
||||
AlbumSelect,
|
||||
LicenseSelect,
|
||||
FileUploadWidget,
|
||||
UploadMetadataForm,
|
||||
UploadMetadataForm
|
||||
},
|
||||
props: {
|
||||
channel: { type: Object, default: null, required: false }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
availableChannels: {
|
||||
results: [],
|
||||
count: 0,
|
||||
count: 0
|
||||
},
|
||||
audioMetadata: {},
|
||||
uploadData: {},
|
||||
|
@ -180,29 +275,22 @@ export default {
|
|||
errors: [],
|
||||
removed: [],
|
||||
includeDraftUploads: null,
|
||||
uploadUrl: this.$store.getters['instance/absoluteUrl']("/api/v1/uploads/"),
|
||||
uploadUrl: this.$store.getters['instance/absoluteUrl']('/api/v1/uploads/'),
|
||||
quotaStatus: null,
|
||||
isLoadingStep1: true,
|
||||
step: 1,
|
||||
values: {
|
||||
channel: (this.channel || {}).uuid,
|
||||
license: null,
|
||||
album: null,
|
||||
album: null
|
||||
},
|
||||
selectedUploadId: null,
|
||||
selectedUploadId: null
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
this.isLoadingStep1 = true
|
||||
let p1 = this.fetchChannels()
|
||||
await p1
|
||||
this.isLoadingStep1 = false
|
||||
this.fetchQuota()
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
return {
|
||||
editTitle: this.$pgettext('Content/*/Button.Label/Verb', 'Edit'),
|
||||
editTitle: this.$pgettext('Content/*/Button.Label/Verb', 'Edit')
|
||||
|
||||
}
|
||||
},
|
||||
|
@ -210,7 +298,7 @@ export default {
|
|||
return {
|
||||
channel: this.values.channel,
|
||||
import_status: 'draft',
|
||||
import_metadata: {license: this.values.license, album: this.values.album || null}
|
||||
import_metadata: { license: this.values.license, album: this.values.album || null }
|
||||
}
|
||||
},
|
||||
remainingSpace () {
|
||||
|
@ -220,18 +308,18 @@ export default {
|
|||
return Math.max(0, this.quotaStatus.remaining - (this.uploadedSize / (1000 * 1000)))
|
||||
},
|
||||
selectedChannel () {
|
||||
let self = this
|
||||
const self = this
|
||||
return this.availableChannels.results.filter((c) => {
|
||||
return c.uuid === self.values.channel
|
||||
})[0]
|
||||
},
|
||||
selectedUpload () {
|
||||
let self = this
|
||||
const self = this
|
||||
if (!this.selectedUploadId) {
|
||||
return null
|
||||
}
|
||||
let selected = this.uploadedFiles.filter((f) => {
|
||||
return f.response && f.response.uuid == self.selectedUploadId
|
||||
const selected = this.uploadedFiles.filter((f) => {
|
||||
return f.response && f.response.uuid === self.selectedUploadId
|
||||
})[0]
|
||||
return {
|
||||
...selected.response,
|
||||
|
@ -239,27 +327,24 @@ export default {
|
|||
}
|
||||
},
|
||||
uploadedFilesById () {
|
||||
let data = {}
|
||||
const data = {}
|
||||
this.uploadedFiles.forEach((u) => {
|
||||
data[u.response.uuid] = u
|
||||
})
|
||||
return data
|
||||
},
|
||||
uploadedFiles () {
|
||||
let self = this
|
||||
self.uploadData
|
||||
self.audioMetadata
|
||||
let files = this.files.map((f) => {
|
||||
let data = {
|
||||
const self = this
|
||||
const files = this.files.map((f) => {
|
||||
const data = {
|
||||
...f,
|
||||
_fileObj: f,
|
||||
metadata: {}
|
||||
}
|
||||
let metadata = {}
|
||||
if (f.response && f.response.uuid) {
|
||||
let uploadImportMetadata = self.uploadImportData[f.response.uuid] || self.uploadData[f.response.uuid].import_metadata
|
||||
const uploadImportMetadata = self.uploadImportData[f.response.uuid] || self.uploadData[f.response.uuid].import_metadata
|
||||
data.metadata = {
|
||||
...uploadImportMetadata,
|
||||
...uploadImportMetadata
|
||||
}
|
||||
data.removed = self.removed.indexOf(f.response.uuid) >= 0
|
||||
}
|
||||
|
@ -308,7 +393,7 @@ export default {
|
|||
canSubmit: !this.activeFile && this.uploadedFiles.length > 0,
|
||||
speed,
|
||||
remaining,
|
||||
quotaStatus: this.quotaStatus,
|
||||
quotaStatus: this.quotaStatus
|
||||
}
|
||||
},
|
||||
totalSize () {
|
||||
|
@ -335,44 +420,92 @@ export default {
|
|||
})[0]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'availableChannels.results' () {
|
||||
this.setupChannelsDropdown()
|
||||
},
|
||||
'values.channel': {
|
||||
async handler (v) {
|
||||
this.files = []
|
||||
if (v) {
|
||||
await this.fetchDraftUploads(v)
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
step: {
|
||||
handler (value) {
|
||||
this.$emit('step', value)
|
||||
if (value === 2) {
|
||||
this.selectedUploadId = null
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
async selectedUploadId (v, o) {
|
||||
if (v) {
|
||||
this.step = 3
|
||||
} else {
|
||||
this.step = 2
|
||||
}
|
||||
if (o) {
|
||||
await this.patchUpload(o, { import_metadata: this.uploadImportData[o] })
|
||||
}
|
||||
},
|
||||
summaryData: {
|
||||
handler (v) {
|
||||
this.$emit('status', v)
|
||||
},
|
||||
immediate: true
|
||||
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
this.isLoadingStep1 = true
|
||||
const p1 = this.fetchChannels()
|
||||
await p1
|
||||
this.isLoadingStep1 = false
|
||||
this.fetchQuota()
|
||||
},
|
||||
methods: {
|
||||
async fetchChannels () {
|
||||
let response = await axios.get('channels/', {params: {scope: 'me'}})
|
||||
const response = await axios.get('channels/', { params: { scope: 'me' } })
|
||||
this.availableChannels = response.data
|
||||
},
|
||||
async patchUpload (id, data) {
|
||||
let response = await axios.patch(`uploads/${id}/`, data)
|
||||
const response = await axios.patch(`uploads/${id}/`, data)
|
||||
this.uploadData[id] = response.data
|
||||
this.uploadImportData[id] = response.data.import_metadata
|
||||
},
|
||||
fetchQuota () {
|
||||
let self = this
|
||||
const self = this
|
||||
axios.get('users/me/').then((response) => {
|
||||
self.quotaStatus = response.data.quota_status
|
||||
})
|
||||
},
|
||||
publish () {
|
||||
let self = this
|
||||
const self = this
|
||||
self.isLoading = true
|
||||
self.errors = []
|
||||
let ids = this.uploadedFiles.map((f) => {
|
||||
const ids = this.uploadedFiles.map((f) => {
|
||||
return f.response.uuid
|
||||
})
|
||||
let payload = {
|
||||
const payload = {
|
||||
action: 'publish',
|
||||
objects: ids,
|
||||
objects: ids
|
||||
}
|
||||
return axios.post('uploads/action/', payload).then(
|
||||
response => {
|
||||
self.isLoading = false
|
||||
self.$emit("published", {
|
||||
self.$emit('published', {
|
||||
uploads: self.uploadedFiles.map((u) => {
|
||||
return {
|
||||
...u.response,
|
||||
import_status: 'pending',
|
||||
import_status: 'pending'
|
||||
}
|
||||
}),
|
||||
channel: self.selectedChannel})
|
||||
channel: self.selectedChannel
|
||||
})
|
||||
},
|
||||
error => {
|
||||
self.errors = error.backendErrors
|
||||
|
@ -380,32 +513,31 @@ export default {
|
|||
)
|
||||
},
|
||||
setupChannelsDropdown () {
|
||||
let self = this
|
||||
const self = this
|
||||
$(this.$el).find('#channel-dropdown').dropdown({
|
||||
onChange (value, text, $choice) {
|
||||
self.values.channel = value
|
||||
},
|
||||
values: this.availableChannels.results.map((c) => {
|
||||
let d = {
|
||||
const d = {
|
||||
name: c.artist.name,
|
||||
value: c.uuid,
|
||||
selected: self.channel && self.channel.uuid === c.uuid,
|
||||
selected: self.channel && self.channel.uuid === c.uuid
|
||||
}
|
||||
if (c.artist.cover && c.artist.cover.urls.medium_square_crop) {
|
||||
let coverUrl = self.$store.getters['instance/absoluteUrl'](c.artist.cover.urls.medium_square_crop)
|
||||
const coverUrl = self.$store.getters['instance/absoluteUrl'](c.artist.cover.urls.medium_square_crop)
|
||||
d.image = coverUrl
|
||||
if (c.artist.content_category === 'podcast') {
|
||||
d.imageClass = 'ui image'
|
||||
} else {
|
||||
d.imageClass = "ui avatar image"
|
||||
d.imageClass = 'ui avatar image'
|
||||
}
|
||||
} else {
|
||||
d.icon = "user"
|
||||
d.icon = 'user'
|
||||
if (c.artist.content_category === 'podcast') {
|
||||
d.iconClass = "bordered icon"
|
||||
d.iconClass = 'bordered icon'
|
||||
} else {
|
||||
d.iconClass = "circular icon"
|
||||
|
||||
d.iconClass = 'circular icon'
|
||||
}
|
||||
}
|
||||
return d
|
||||
|
@ -413,23 +545,23 @@ export default {
|
|||
})
|
||||
$(this.$el).find('#channel-dropdown').dropdown('hide')
|
||||
},
|
||||
inputFile(newFile, oldFile) {
|
||||
inputFile (newFile, oldFile) {
|
||||
if (!newFile) {
|
||||
return
|
||||
}
|
||||
if (this.remainingSpace < newFile.size / (1000 * 1000)) {
|
||||
newFile.error = 'denied'
|
||||
} else {
|
||||
this.$refs.upload.active = true;
|
||||
this.$refs.upload.active = true
|
||||
}
|
||||
},
|
||||
fetchAudioMetadata (uuid) {
|
||||
let self = this
|
||||
const self = this
|
||||
self.audioMetadata[uuid] = null
|
||||
axios.get(`uploads/${uuid}/audio-file-metadata/`).then((response) => {
|
||||
self.setDynamic('audioMetadata', uuid, response.data)
|
||||
let uploadedFile = self.uploadedFilesById[uuid]
|
||||
if (uploadedFile._fileObj && uploadedFile.response.import_metadata.title === uploadedFile._fileObj.name.replace(/\.[^/.]+$/, "") && response.data.title) {
|
||||
const uploadedFile = self.uploadedFilesById[uuid]
|
||||
if (uploadedFile._fileObj && uploadedFile.response.import_metadata.title === uploadedFile._fileObj.name.replace(/\.[^/.]+$/, '') && response.data.title) {
|
||||
// replace existing title deduced from file by the one in audio file metadat, if any
|
||||
self.uploadImportData[uuid].title = response.data.title
|
||||
} else {
|
||||
|
@ -439,17 +571,17 @@ export default {
|
|||
setIfEmpty(self.uploadImportData[uuid], 'position', response.data.position)
|
||||
setIfEmpty(self.uploadImportData[uuid], 'tags', response.data.tags)
|
||||
setIfEmpty(self.uploadImportData[uuid], 'description', (response.data.description || {}).text)
|
||||
self.patchUpload(uuid, {import_metadata: self.uploadImportData[uuid]})
|
||||
self.patchUpload(uuid, { import_metadata: self.uploadImportData[uuid] })
|
||||
})
|
||||
},
|
||||
setDynamic (objName, key, data) {
|
||||
// cf https://vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
|
||||
let newData = {}
|
||||
const newData = {}
|
||||
newData[key] = data
|
||||
this[objName] = Object.assign({}, this[objName], newData)
|
||||
},
|
||||
updateFiles (value) {
|
||||
let self = this
|
||||
const self = this
|
||||
this.files = value
|
||||
this.files.forEach((f) => {
|
||||
if (f.response && f.response.uuid && self.audioMetadata[f.response.uuid] === undefined) {
|
||||
|
@ -462,9 +594,9 @@ export default {
|
|||
})
|
||||
},
|
||||
async fetchDraftUploads (channel) {
|
||||
let self = this
|
||||
const self = this
|
||||
this.draftUploads = null
|
||||
let response = await axios.get('uploads', {params: {import_status: 'draft', channel: channel}})
|
||||
const response = await axios.get('uploads', { params: { import_status: 'draft', channel: channel } })
|
||||
this.draftUploads = response.data.results
|
||||
this.draftUploads.forEach((u) => {
|
||||
self.uploadImportData[u.uuid] = u.import_metadata
|
||||
|
@ -479,49 +611,8 @@ export default {
|
|||
}
|
||||
},
|
||||
retry (file) {
|
||||
this.$refs.upload.update(file, {error: '', progress: '0.00'})
|
||||
this.$refs.upload.active = true;
|
||||
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"availableChannels.results" () {
|
||||
this.setupChannelsDropdown()
|
||||
},
|
||||
"values.channel": {
|
||||
async handler (v) {
|
||||
this.files = []
|
||||
if (v) {
|
||||
await this.fetchDraftUploads(v)
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
step: {
|
||||
handler (value) {
|
||||
this.$emit('step', value)
|
||||
if (value === 2) {
|
||||
this.selectedUploadId = null
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
async selectedUploadId (v, o) {
|
||||
if (v) {
|
||||
this.step = 3
|
||||
} else {
|
||||
this.step = 2
|
||||
}
|
||||
if (o) {
|
||||
await this.patchUpload(o, {import_metadata: this.uploadImportData[o]})
|
||||
}
|
||||
},
|
||||
summaryData: {
|
||||
handler (v) {
|
||||
this.$emit('status', v)
|
||||
},
|
||||
immediate: true,
|
||||
|
||||
this.$refs.upload.update(file, { error: '', progress: '0.00' })
|
||||
this.$refs.upload.active = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,55 +4,75 @@
|
|||
<label for="upload-title">
|
||||
<translate translate-context="*/*/*/Noun">Title</translate>
|
||||
</label>
|
||||
<input type="text" v-model="newValues.title">
|
||||
<input
|
||||
v-model="newValues.title"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
<attachment-input
|
||||
v-model="newValues.cover"
|
||||
:required="false"
|
||||
@delete="newValues.cover = null">
|
||||
<translate translate-context="Content/Channel/*" slot="label">Track Picture</translate>
|
||||
@delete="newValues.cover = null"
|
||||
>
|
||||
<translate
|
||||
slot="label"
|
||||
translate-context="Content/Channel/*"
|
||||
>
|
||||
Track Picture
|
||||
</translate>
|
||||
</attachment-input>
|
||||
<div class="ui small hidden divider"></div>
|
||||
<div class="ui small hidden divider" />
|
||||
<div class="ui two fields">
|
||||
<div class="ui field">
|
||||
<label for="upload-tags">
|
||||
<translate translate-context="*/*/*/Noun">Tags</translate>
|
||||
</label>
|
||||
<tags-selector
|
||||
v-model="newValues.tags"
|
||||
id="upload-tags"
|
||||
:required="false"></tags-selector>
|
||||
v-model="newValues.tags"
|
||||
:required="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<label for="upload-position">
|
||||
<translate translate-context="*/*/*/Short, Noun">Position</translate>
|
||||
</label>
|
||||
<input type="number" min="1" step="1" v-model="newValues.position">
|
||||
<input
|
||||
v-model="newValues.position"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<label for="upload-description">
|
||||
<translate translate-context="*/*/*">Description</translate>
|
||||
</label>
|
||||
<content-form v-model="newValues.description" field-id="upload-description"></content-form>
|
||||
<content-form
|
||||
v-model="newValues.description"
|
||||
field-id="upload-description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import TagsSelector from '@/components/library/TagsSelector'
|
||||
import AttachmentInput from '@/components/common/AttachmentInput'
|
||||
|
||||
export default {
|
||||
props: ['upload', 'values'],
|
||||
components: {
|
||||
TagsSelector,
|
||||
AttachmentInput
|
||||
},
|
||||
props: {
|
||||
upload: { type: Object, required: true },
|
||||
values: { type: Object, required: true }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
newValues: {...this.values} || this.upload.import_metadata
|
||||
newValues: { ...this.values } || this.upload.import_metadata
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -66,7 +86,7 @@ export default {
|
|||
this.$emit('values', v)
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,60 +1,142 @@
|
|||
<template>
|
||||
<modal class="small" @update:show="update" :show="$store.state.channels.showUploadModal">
|
||||
<modal
|
||||
class="small"
|
||||
:show="$store.state.channels.showUploadModal"
|
||||
@update:show="update"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate key="1" v-if="step === 1" translate-context="Popup/Channels/Title/Verb">Publish audio</translate>
|
||||
<translate key="2" v-else-if="step === 2" translate-context="Popup/Channels/Title">Files to upload</translate>
|
||||
<translate key="3" v-else-if="step === 3" translate-context="Popup/Channels/Title">Upload details</translate>
|
||||
<translate key="4" v-else-if="step === 4" translate-context="Popup/Channels/Title">Processing uploads</translate>
|
||||
<translate
|
||||
v-if="step === 1"
|
||||
key="1"
|
||||
translate-context="Popup/Channels/Title/Verb"
|
||||
>
|
||||
Publish audio
|
||||
</translate>
|
||||
<translate
|
||||
v-else-if="step === 2"
|
||||
key="2"
|
||||
translate-context="Popup/Channels/Title"
|
||||
>
|
||||
Files to upload
|
||||
</translate>
|
||||
<translate
|
||||
v-else-if="step === 3"
|
||||
key="3"
|
||||
translate-context="Popup/Channels/Title"
|
||||
>
|
||||
Upload details
|
||||
</translate>
|
||||
<translate
|
||||
v-else-if="step === 4"
|
||||
key="4"
|
||||
translate-context="Popup/Channels/Title"
|
||||
>
|
||||
Processing uploads
|
||||
</translate>
|
||||
</h4>
|
||||
<div class="scrolling content">
|
||||
<channel-upload-form
|
||||
ref="uploadForm"
|
||||
:channel="$store.state.channels.uploadModalConfig.channel"
|
||||
@step="step = $event"
|
||||
@loading="isLoading = $event"
|
||||
@published="$store.commit('channels/publish', $event)"
|
||||
@status="statusData = $event"
|
||||
@submittable="submittable = $event"
|
||||
:channel="$store.state.channels.uploadModalConfig.channel"></channel-upload-form>
|
||||
/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="left floated text left align">
|
||||
<template v-if="statusData && step >= 2">
|
||||
{{ statusInfo.join(' · ') }}
|
||||
</template>
|
||||
<div class="ui very small hidden divider"></div>
|
||||
<div class="ui very small hidden divider" />
|
||||
<template v-if="statusData && statusData.quotaStatus">
|
||||
<translate translate-context="Content/Library/Paragraph">Remaining storage space:</translate>
|
||||
<translate translate-context="Content/Library/Paragraph">
|
||||
Remaining storage space:
|
||||
</translate>
|
||||
{{ (statusData.quotaStatus.remaining * 1000 * 1000) - statusData.uploadedSize | humanSize }}
|
||||
</template>
|
||||
</div>
|
||||
<div class="ui hidden clearing divider mobile-only"></div>
|
||||
<button class="ui basic cancel button" v-if="step === 1"><translate translate-context="*/*/Button.Label/Verb">Cancel</translate></button>
|
||||
<button class="ui basic button" v-else-if="step < 3" @click.stop.prevent="$refs.uploadForm.step -= 1"><translate translate-context="*/*/Button.Label/Verb">Previous step</translate></button>
|
||||
<button class="ui basic button" v-else-if="step === 3" @click.stop.prevent="$refs.uploadForm.step -= 1"><translate translate-context="*/*/Button.Label/Verb">Update</translate></button>
|
||||
<button v-if="step === 1" class="ui primary button" @click.stop.prevent="$refs.uploadForm.step += 1">
|
||||
<translate translate-context="*/*/Button.Label">Next step</translate>
|
||||
<div class="ui hidden clearing divider mobile-only" />
|
||||
<button
|
||||
v-if="step === 1"
|
||||
class="ui basic cancel button"
|
||||
>
|
||||
<translate translate-context="*/*/Button.Label/Verb">
|
||||
Cancel
|
||||
</translate>
|
||||
</button>
|
||||
<div class="ui primary buttons" v-if="step === 2">
|
||||
<button
|
||||
v-else-if="step < 3"
|
||||
class="ui basic button"
|
||||
@click.stop.prevent="$refs.uploadForm.step -= 1"
|
||||
>
|
||||
<translate translate-context="*/*/Button.Label/Verb">
|
||||
Previous step
|
||||
</translate>
|
||||
</button>
|
||||
<button
|
||||
v-else-if="step === 3"
|
||||
class="ui basic button"
|
||||
@click.stop.prevent="$refs.uploadForm.step -= 1"
|
||||
>
|
||||
<translate translate-context="*/*/Button.Label/Verb">
|
||||
Update
|
||||
</translate>
|
||||
</button>
|
||||
<button
|
||||
v-if="step === 1"
|
||||
class="ui primary button"
|
||||
@click.stop.prevent="$refs.uploadForm.step += 1"
|
||||
>
|
||||
<translate translate-context="*/*/Button.Label">
|
||||
Next step
|
||||
</translate>
|
||||
</button>
|
||||
<div
|
||||
v-if="step === 2"
|
||||
class="ui primary buttons"
|
||||
>
|
||||
<button
|
||||
:class="['ui', 'primary button', {loading: isLoading}]"
|
||||
type="submit"
|
||||
:disabled="!statusData || !statusData.canSubmit"
|
||||
@click.prevent.stop="$refs.uploadForm.publish">
|
||||
<translate translate-context="*/Channels/Button.Label">Publish</translate>
|
||||
@click.prevent.stop="$refs.uploadForm.publish"
|
||||
>
|
||||
<translate translate-context="*/Channels/Button.Label">
|
||||
Publish
|
||||
</translate>
|
||||
</button>
|
||||
<button class="ui floating dropdown icon button" ref="dropdown" v-dropdown :disabled="!statusData || !statusData.canSubmit">
|
||||
<i class="dropdown icon"></i>
|
||||
<button
|
||||
ref="dropdown"
|
||||
v-dropdown
|
||||
class="ui floating dropdown icon button"
|
||||
:disabled="!statusData || !statusData.canSubmit"
|
||||
>
|
||||
<i class="dropdown icon" />
|
||||
<div class="menu">
|
||||
<div
|
||||
role="button"
|
||||
class="basic item"
|
||||
@click="update(false)"
|
||||
class="basic item">
|
||||
<translate translate-context="Content/*/Button.Label/Verb">Finish later</translate>
|
||||
>
|
||||
<translate translate-context="Content/*/Button.Label/Verb">
|
||||
Finish later
|
||||
</translate>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<button class="ui basic cancel button" @click="update(false)" v-if="step === 4"><translate translate-context="*/*/Button.Label/Verb">Close</translate></button>
|
||||
<button
|
||||
v-if="step === 4"
|
||||
class="ui basic cancel button"
|
||||
@click="update(false)"
|
||||
>
|
||||
<translate translate-context="*/*/Button.Label/Verb">
|
||||
Close
|
||||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
@ -62,7 +144,7 @@
|
|||
<script>
|
||||
import Modal from '@/components/semantic/Modal'
|
||||
import ChannelUploadForm from '@/components/channels/UploadForm'
|
||||
import {humanSize} from '@/filters'
|
||||
import { humanSize } from '@/filters'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -74,14 +156,9 @@ export default {
|
|||
step: 1,
|
||||
isLoading: false,
|
||||
submittable: true,
|
||||
statusData: null,
|
||||
statusData: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update (v) {
|
||||
this.$store.commit('channels/showUploadModal', {show: v})
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
return {}
|
||||
|
@ -90,14 +167,14 @@ export default {
|
|||
if (!this.statusData) {
|
||||
return []
|
||||
}
|
||||
let info = []
|
||||
const info = []
|
||||
if (this.statusData.totalSize) {
|
||||
info.push(humanSize(this.statusData.totalSize))
|
||||
}
|
||||
if (this.statusData.totalFiles) {
|
||||
let msg = this.$npgettext('*/*/*', '%{ count } file', '%{ count } files', this.statusData.totalFiles)
|
||||
const msg = this.$npgettext('*/*/*', '%{ count } file', '%{ count } files', this.statusData.totalFiles)
|
||||
info.push(
|
||||
this.$gettextInterpolate(msg, {count: this.statusData.totalFiles}),
|
||||
this.$gettextInterpolate(msg, { count: this.statusData.totalFiles })
|
||||
)
|
||||
}
|
||||
if (this.statusData.progress) {
|
||||
|
@ -107,13 +184,17 @@ export default {
|
|||
info.push(`${humanSize(this.statusData.speed)}/s`)
|
||||
}
|
||||
return info
|
||||
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$store.state.route.path' () {
|
||||
this.$store.commit('channels/showUploadModal', {show: false})
|
||||
this.$store.commit('channels/showUploadModal', { show: false })
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update (v) {
|
||||
this.$store.commit('channels/showUploadModal', { show: v })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,32 +1,35 @@
|
|||
<template>
|
||||
<span class="feedback" v-if="isLoading || isDone">
|
||||
<span v-if="isLoading" :class="['ui', 'active', size, 'inline', 'loader']"></span>
|
||||
<i v-if="isDone" :class="['success', size, 'check', 'icon']"></i>
|
||||
<span
|
||||
v-if="isLoading || isDone"
|
||||
class="feedback"
|
||||
>
|
||||
<span
|
||||
v-if="isLoading"
|
||||
:class="['ui', 'active', size, 'inline', 'loader']"
|
||||
/>
|
||||
<i
|
||||
v-if="isDone"
|
||||
:class="['success', size, 'check', 'icon']"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {hashCode, intToRGB} from '@/utils/color'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
isLoading: {type: Boolean, required: true},
|
||||
size: {type: String, default: 'small'},
|
||||
isLoading: { type: Boolean, required: true },
|
||||
size: { type: String, default: 'small' }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
timer: null,
|
||||
isDone: false,
|
||||
}
|
||||
},
|
||||
destroyed () {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer)
|
||||
isDone: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isLoading (v) {
|
||||
let self = this
|
||||
const self = this
|
||||
if (v && this.timer) {
|
||||
clearTimeout(this.timer)
|
||||
}
|
||||
|
@ -36,10 +39,14 @@ export default {
|
|||
this.isDone = true
|
||||
this.timer = setTimeout(() => {
|
||||
self.isDone = false
|
||||
}, (2000));
|
||||
|
||||
}, (2000))
|
||||
}
|
||||
}
|
||||
},
|
||||
destroyed () {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -4,111 +4,188 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th colspan="1000">
|
||||
<div v-if="refreshable" class="right floated">
|
||||
<div
|
||||
v-if="refreshable"
|
||||
class="right floated"
|
||||
>
|
||||
<span v-if="needsRefresh">
|
||||
<translate translate-context="Content/*/Button.Help text.Paragraph">Content has been updated, click refresh to see up-to-date content</translate>
|
||||
</span>
|
||||
<button
|
||||
@click="$emit('refresh')"
|
||||
class="ui basic icon button"
|
||||
:title="labels.refresh"
|
||||
:aria-label="labels.refresh">
|
||||
<i class="refresh icon"></i>
|
||||
:aria-label="labels.refresh"
|
||||
@click="$emit('refresh')"
|
||||
>
|
||||
<i class="refresh icon" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="ui small left floated form" v-if="actionUrl && actions.length > 0">
|
||||
<div
|
||||
v-if="actionUrl && actions.length > 0"
|
||||
class="ui small left floated form"
|
||||
>
|
||||
<div class="ui inline fields">
|
||||
<div class="field">
|
||||
<label for="actions-select"><translate translate-context="Content/*/*/Noun">Actions</translate></label>
|
||||
<select id="actions-select" class="ui dropdown" v-model="currentActionName">
|
||||
<option v-for="action in actions" :value="action.name">
|
||||
<select
|
||||
id="actions-select"
|
||||
v-model="currentActionName"
|
||||
class="ui dropdown"
|
||||
>
|
||||
<option
|
||||
v-for="(action, key) in actions"
|
||||
:key="key"
|
||||
:value="action.name"
|
||||
>
|
||||
{{ action.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<dangerous-button
|
||||
v-if="selectAll || currentAction.isDangerous" :class="['ui', {disabled: checked.length === 0}, {'loading': actionLoading}, 'button']"
|
||||
v-if="selectAll || currentAction.isDangerous"
|
||||
:class="['ui', {disabled: checked.length === 0}, {'loading': actionLoading}, 'button']"
|
||||
:confirm-color="currentAction.confirmColor || 'success'"
|
||||
@confirm="launchAction" :aria-label="labels.performAction">
|
||||
<translate translate-context="Content/*/Button.Label/Short, Verb">Go</translate>
|
||||
:aria-label="labels.performAction"
|
||||
@confirm="launchAction"
|
||||
>
|
||||
<translate translate-context="Content/*/Button.Label/Short, Verb">
|
||||
Go
|
||||
</translate>
|
||||
<p slot="modal-header">
|
||||
<translate translate-context="Modal/*/Title"
|
||||
<translate
|
||||
key="1"
|
||||
translate-context="Modal/*/Title"
|
||||
:translate-n="affectedObjectsCount"
|
||||
:translate-params="{count: affectedObjectsCount, action: currentActionName}"
|
||||
translate-plural="Do you want to launch %{ action } on %{ count } elements?">
|
||||
translate-plural="Do you want to launch %{ action } on %{ count } elements?"
|
||||
>
|
||||
Do you want to launch %{ action } on %{ count } element?
|
||||
</translate>
|
||||
</p>
|
||||
<p slot="modal-content">
|
||||
<template v-if="currentAction.confirmationMessage">{{ currentAction.confirmationMessage }}</template>
|
||||
<translate v-else translate-context="Modal/*/Paragraph">This may affect a lot of elements or have irreversible consequences, please double check this is really what you want.</translate>
|
||||
<template v-if="currentAction.confirmationMessage">
|
||||
{{ currentAction.confirmationMessage }}
|
||||
</template>
|
||||
<translate
|
||||
v-else
|
||||
translate-context="Modal/*/Paragraph"
|
||||
>
|
||||
This may affect a lot of elements or have irreversible consequences, please double check this is really what you want.
|
||||
</translate>
|
||||
</p>
|
||||
<div :aria-label="labels.performAction" slot="modal-confirm"><translate translate-context="Modal/*/Button.Label/Short, Verb">Launch</translate></div>
|
||||
<div
|
||||
slot="modal-confirm"
|
||||
:aria-label="labels.performAction"
|
||||
>
|
||||
<translate translate-context="Modal/*/Button.Label/Short, Verb">
|
||||
Launch
|
||||
</translate>
|
||||
</div>
|
||||
</dangerous-button>
|
||||
<button
|
||||
v-else
|
||||
@click="launchAction"
|
||||
:disabled="checked.length === 0"
|
||||
:aria-label="labels.performAction"
|
||||
:class="['ui', {disabled: checked.length === 0}, {'loading': actionLoading}, 'button']">
|
||||
<translate translate-context="Content/*/Button.Label/Short, Verb">Go</translate></button>
|
||||
:class="['ui', {disabled: checked.length === 0}, {'loading': actionLoading}, 'button']"
|
||||
@click="launchAction"
|
||||
>
|
||||
<translate translate-context="Content/*/Button.Label/Short, Verb">
|
||||
Go
|
||||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
<div class="count field">
|
||||
<translate translate-context="Content/*/Paragraph"
|
||||
tag="span"
|
||||
<translate
|
||||
v-if="selectAll"
|
||||
key="1"
|
||||
translate-context="Content/*/Paragraph"
|
||||
tag="span"
|
||||
:translate-n="objectsData.count"
|
||||
:translate-params="{count: objectsData.count, total: objectsData.count}"
|
||||
translate-plural="All %{ count } elements selected">
|
||||
translate-plural="All %{ count } elements selected"
|
||||
>
|
||||
All %{ count } element selected
|
||||
</translate>
|
||||
<translate translate-context="Content/*/Paragraph"
|
||||
tag="span"
|
||||
<translate
|
||||
v-else
|
||||
key="2"
|
||||
translate-context="Content/*/Paragraph"
|
||||
tag="span"
|
||||
:translate-n="checked.length"
|
||||
:translate-params="{count: checked.length, total: objectsData.count}"
|
||||
translate-plural="%{ count } on %{ total } selected">
|
||||
translate-plural="%{ count } on %{ total } selected"
|
||||
>
|
||||
%{ count } on %{ total } selected
|
||||
</translate>
|
||||
<template v-if="currentAction.allowAll && checkable.length > 0 && checkable.length === checked.length">
|
||||
<a @click.prevent="selectAll = true" v-if="!selectAll" href="">
|
||||
<translate translate-context="Content/*/Link/Verb"
|
||||
<a
|
||||
v-if="!selectAll"
|
||||
href=""
|
||||
@click.prevent="selectAll = true"
|
||||
>
|
||||
<translate
|
||||
key="3"
|
||||
translate-context="Content/*/Link/Verb"
|
||||
:translate-n="objectsData.count"
|
||||
:translate-params="{total: objectsData.count}"
|
||||
translate-plural="Select all %{ total } elements">
|
||||
translate-plural="Select all %{ total } elements"
|
||||
>
|
||||
Select one element
|
||||
</translate>
|
||||
</a>
|
||||
<a @click.prevent="selectAll = false" v-else href="">
|
||||
<translate translate-context="Content/*/Link/Verb" key="4">Select only current page</translate>
|
||||
<a
|
||||
v-else
|
||||
href=""
|
||||
@click.prevent="selectAll = false"
|
||||
>
|
||||
<translate
|
||||
key="4"
|
||||
translate-context="Content/*/Link/Verb"
|
||||
>Select only current page</translate>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="actionErrors.length > 0" role="alert" class="ui negative message">
|
||||
<h4 class="header"><translate translate-context="Content/*/Error message/Header">Error while applying action</translate></h4>
|
||||
<div
|
||||
v-if="actionErrors.length > 0"
|
||||
role="alert"
|
||||
class="ui negative message"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate translate-context="Content/*/Error message/Header">
|
||||
Error while applying action
|
||||
</translate>
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li v-for="error in actionErrors">{{ error }}</li>
|
||||
<li
|
||||
v-for="(error, key) in actionErrors"
|
||||
:key="key"
|
||||
>
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="actionResult" class="ui positive message">
|
||||
<div
|
||||
v-if="actionResult"
|
||||
class="ui positive message"
|
||||
>
|
||||
<p>
|
||||
<translate translate-context="Content/*/Paragraph"
|
||||
<translate
|
||||
translate-context="Content/*/Paragraph"
|
||||
:translate-n="actionResult.updated"
|
||||
:translate-params="{count: actionResult.updated, action: actionResult.action}"
|
||||
translate-plural="Action %{ action } was launched successfully on %{ count } elements">
|
||||
translate-plural="Action %{ action } was launched successfully on %{ count } elements"
|
||||
>
|
||||
Action %{ action } was launched successfully on %{ count } element
|
||||
</translate>
|
||||
</p>
|
||||
|
||||
<slot name="action-success-footer" :result="actionResult">
|
||||
</slot>
|
||||
<slot
|
||||
name="action-success-footer"
|
||||
:result="actionResult"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
|
@ -118,26 +195,37 @@
|
|||
<div class="ui checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
@change="toggleCheckAll"
|
||||
:aria-label="labels.selectAllItems"
|
||||
:disabled="checkable.length === 0"
|
||||
:checked="checkable.length > 0 && checked.length === checkable.length">
|
||||
:checked="checkable.length > 0 && checked.length === checkable.length"
|
||||
@change="toggleCheckAll"
|
||||
>
|
||||
</div>
|
||||
</th>
|
||||
<slot name="header-cells"></slot>
|
||||
<slot name="header-cells" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-if="objectsData.count > 0">
|
||||
<tr v-for="(obj, index) in objects">
|
||||
<td v-if="actions.length > 0" class="collapsing">
|
||||
<tr
|
||||
v-for="(obj, index) in objects"
|
||||
:key="index"
|
||||
>
|
||||
<td
|
||||
v-if="actions.length > 0"
|
||||
class="collapsing"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:aria-label="labels.selectItem"
|
||||
:disabled="checkable.indexOf(getId(obj)) === -1"
|
||||
:checked="checked.indexOf(getId(obj)) > -1"
|
||||
@click="toggleCheck($event, getId(obj), index)"
|
||||
:checked="checked.indexOf(getId(obj)) > -1">
|
||||
>
|
||||
</td>
|
||||
<slot name="row-cells" :obj="obj"></slot>
|
||||
<slot
|
||||
name="row-cells"
|
||||
:obj="obj"
|
||||
/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -147,19 +235,19 @@
|
|||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
actionUrl: {type: String, required: false, default: null},
|
||||
idField: {type: String, required: false, default: 'id'},
|
||||
refreshable: {type: Boolean, required: false, default: false},
|
||||
needsRefresh: {type: Boolean, required: false, default: false},
|
||||
objectsData: {type: Object, required: true},
|
||||
actions: {type: Array, required: true, default: () => { return [] }},
|
||||
filters: {type: Object, required: false, default: () => { return {} }},
|
||||
customObjects: {type: Array, required: false, default: () => { return [] }},
|
||||
},
|
||||
components: {},
|
||||
props: {
|
||||
actionUrl: { type: String, required: false, default: null },
|
||||
idField: { type: String, required: false, default: 'id' },
|
||||
refreshable: { type: Boolean, required: false, default: false },
|
||||
needsRefresh: { type: Boolean, required: false, default: false },
|
||||
objectsData: { type: Object, required: true },
|
||||
actions: { type: Array, required: true, default: () => { return [] } },
|
||||
filters: { type: Object, required: false, default: () => { return {} } },
|
||||
customObjects: { type: Array, required: false, default: () => { return [] } }
|
||||
},
|
||||
data () {
|
||||
let d = {
|
||||
const d = {
|
||||
checked: [],
|
||||
actionLoading: false,
|
||||
actionResult: null,
|
||||
|
@ -173,86 +261,20 @@ export default {
|
|||
}
|
||||
return d
|
||||
},
|
||||
methods: {
|
||||
toggleCheckAll () {
|
||||
this.lastCheckedIndex = -1
|
||||
if (this.checked.length === this.checkable.length) {
|
||||
// we uncheck
|
||||
this.checked = []
|
||||
} else {
|
||||
this.checked = this.checkable.map(i => { return i })
|
||||
}
|
||||
},
|
||||
toggleCheck (event, id, index) {
|
||||
let self = this
|
||||
let affectedIds = [id]
|
||||
let newValue = null
|
||||
if (this.checked.indexOf(id) > -1) {
|
||||
// we uncheck
|
||||
this.selectAll = false
|
||||
newValue = false
|
||||
} else {
|
||||
newValue = true
|
||||
}
|
||||
if (event.shiftKey && this.lastCheckedIndex > -1) {
|
||||
// we also add inbetween ids to the list of affected ids
|
||||
let idxs = [index, this.lastCheckedIndex]
|
||||
idxs.sort((a, b) => a - b)
|
||||
let objs = this.objectsData.results.slice(idxs[0], idxs[1] + 1)
|
||||
affectedIds = affectedIds.concat(objs.map((o) => { return o.id }))
|
||||
}
|
||||
affectedIds.forEach((i) => {
|
||||
let checked = self.checked.indexOf(i) > -1
|
||||
if (newValue && !checked && self.checkable.indexOf(i) > -1) {
|
||||
return self.checked.push(i)
|
||||
}
|
||||
if (!newValue && checked) {
|
||||
self.checked.splice(self.checked.indexOf(i), 1)
|
||||
}
|
||||
})
|
||||
this.lastCheckedIndex = index
|
||||
},
|
||||
launchAction () {
|
||||
let self = this
|
||||
self.actionLoading = true
|
||||
self.result = null
|
||||
self.actionErrors = []
|
||||
let payload = {
|
||||
action: this.currentActionName,
|
||||
filters: this.filters
|
||||
}
|
||||
if (this.selectAll) {
|
||||
payload.objects = 'all'
|
||||
} else {
|
||||
payload.objects = this.checked
|
||||
}
|
||||
axios.post(this.actionUrl, payload).then((response) => {
|
||||
self.actionResult = response.data
|
||||
self.actionLoading = false
|
||||
self.$emit('action-launched', response.data)
|
||||
}, error => {
|
||||
self.actionLoading = false
|
||||
self.actionErrors = error.backendErrors
|
||||
})
|
||||
},
|
||||
getId (obj) {
|
||||
return obj[this.idField]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentAction () {
|
||||
let self = this
|
||||
const self = this
|
||||
return this.actions.filter((a) => {
|
||||
return a.name === self.currentActionName
|
||||
})[0]
|
||||
},
|
||||
checkable () {
|
||||
let self = this
|
||||
const self = this
|
||||
if (!this.currentAction) {
|
||||
return []
|
||||
}
|
||||
let objs = this.objectsData.results
|
||||
let filter = this.currentAction.filterCheckable
|
||||
const filter = this.currentAction.filterCheckable
|
||||
if (filter) {
|
||||
objs = objs.filter((o) => {
|
||||
return filter(o)
|
||||
|
@ -261,9 +283,9 @@ export default {
|
|||
return objs.map((o) => { return self.getId(o) })
|
||||
},
|
||||
objects () {
|
||||
let self = this
|
||||
const self = this
|
||||
return this.objectsData.results.map((o) => {
|
||||
let custom = self.customObjects.filter((co) => {
|
||||
const custom = self.customObjects.filter((co) => {
|
||||
return self.getId(co) === self.getId(o)
|
||||
})[0]
|
||||
if (custom) {
|
||||
|
@ -298,11 +320,77 @@ export default {
|
|||
currentActionName () {
|
||||
// we update checked status as some actions have specific filters
|
||||
// on what is checkable or not
|
||||
let self = this
|
||||
const self = this
|
||||
this.checked = this.checked.filter(r => {
|
||||
return self.checkable.indexOf(r) > -1
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleCheckAll () {
|
||||
this.lastCheckedIndex = -1
|
||||
if (this.checked.length === this.checkable.length) {
|
||||
// we uncheck
|
||||
this.checked = []
|
||||
} else {
|
||||
this.checked = this.checkable.map(i => { return i })
|
||||
}
|
||||
},
|
||||
toggleCheck (event, id, index) {
|
||||
const self = this
|
||||
let affectedIds = [id]
|
||||
let newValue = null
|
||||
if (this.checked.indexOf(id) > -1) {
|
||||
// we uncheck
|
||||
this.selectAll = false
|
||||
newValue = false
|
||||
} else {
|
||||
newValue = true
|
||||
}
|
||||
if (event.shiftKey && this.lastCheckedIndex > -1) {
|
||||
// we also add inbetween ids to the list of affected ids
|
||||
const idxs = [index, this.lastCheckedIndex]
|
||||
idxs.sort((a, b) => a - b)
|
||||
const objs = this.objectsData.results.slice(idxs[0], idxs[1] + 1)
|
||||
affectedIds = affectedIds.concat(objs.map((o) => { return o.id }))
|
||||
}
|
||||
affectedIds.forEach((i) => {
|
||||
const checked = self.checked.indexOf(i) > -1
|
||||
if (newValue && !checked && self.checkable.indexOf(i) > -1) {
|
||||
return self.checked.push(i)
|
||||
}
|
||||
if (!newValue && checked) {
|
||||
self.checked.splice(self.checked.indexOf(i), 1)
|
||||
}
|
||||
})
|
||||
this.lastCheckedIndex = index
|
||||
},
|
||||
launchAction () {
|
||||
const self = this
|
||||
self.actionLoading = true
|
||||
self.result = null
|
||||
self.actionErrors = []
|
||||
const payload = {
|
||||
action: this.currentActionName,
|
||||
filters: this.filters
|
||||
}
|
||||
if (this.selectAll) {
|
||||
payload.objects = 'all'
|
||||
} else {
|
||||
payload.objects = this.checked
|
||||
}
|
||||
axios.post(this.actionUrl, payload).then((response) => {
|
||||
self.actionResult = response.data
|
||||
self.actionLoading = false
|
||||
self.$emit('action-launched', response.data)
|
||||
}, error => {
|
||||
self.actionLoading = false
|
||||
self.actionErrors = error.backendErrors
|
||||
})
|
||||
},
|
||||
getId (obj) {
|
||||
return obj[this.idField]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,13 +1,22 @@
|
|||
<template>
|
||||
<img alt="" v-if="actor.icon && actor.icon.urls.original" :src="actor.icon.urls.medium_square_crop" class="ui avatar circular image" />
|
||||
<span v-else :style="defaultAvatarStyle" class="ui avatar circular label">{{ actor.preferred_username[0]}}</span>
|
||||
<img
|
||||
v-if="actor.icon && actor.icon.urls.original"
|
||||
alt=""
|
||||
:src="actor.icon.urls.medium_square_crop"
|
||||
class="ui avatar circular image"
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
:style="defaultAvatarStyle"
|
||||
class="ui avatar circular label"
|
||||
>{{ actor.preferred_username[0] }}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {hashCode, intToRGB} from '@/utils/color'
|
||||
import { hashCode, intToRGB } from '@/utils/color'
|
||||
|
||||
export default {
|
||||
props: ['actor'],
|
||||
props: { actor: { type: Object, required: true } },
|
||||
computed: {
|
||||
actorColor () {
|
||||
return intToRGB(hashCode(this.actor.full_username))
|
||||
|
|
|
@ -1,29 +1,33 @@
|
|||
<template>
|
||||
<router-link :to="url" :title="actor.full_username">
|
||||
<template v-if="avatar"><actor-avatar :actor="actor" /><span> </span></template><slot>{{ repr | truncate(truncateLength) }}</slot>
|
||||
<router-link
|
||||
:to="url"
|
||||
:title="actor.full_username"
|
||||
>
|
||||
<template v-if="avatar">
|
||||
<actor-avatar :actor="actor" /><span> </span>
|
||||
</template><slot>{{ repr | truncate(truncateLength) }}</slot>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {hashCode, intToRGB} from '@/utils/color'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
actor: {type: Object},
|
||||
avatar: {type: Boolean, default: true},
|
||||
admin: {type: Boolean, default: false},
|
||||
displayName: {type: Boolean, default: false},
|
||||
truncateLength: {type: Number, default: 30},
|
||||
actor: { type: Object, required: true },
|
||||
avatar: { type: Boolean, default: true },
|
||||
admin: { type: Boolean, default: false },
|
||||
displayName: { type: Boolean, default: false },
|
||||
truncateLength: { type: Number, default: 30 }
|
||||
},
|
||||
computed: {
|
||||
url () {
|
||||
if (this.admin) {
|
||||
return {name: 'manage.moderation.accounts.detail', params: {id: this.actor.full_username}}
|
||||
return { name: 'manage.moderation.accounts.detail', params: { id: this.actor.full_username } }
|
||||
}
|
||||
if (this.actor.is_local) {
|
||||
return {name: 'profile.overview', params: {username: this.actor.preferred_username}}
|
||||
return { name: 'profile.overview', params: { username: this.actor.preferred_username } }
|
||||
} else {
|
||||
return {name: 'profile.full.overview', params: {username: this.actor.preferred_username, domain: this.actor.domain}}
|
||||
return { name: 'profile.full.overview', params: { username: this.actor.preferred_username, domain: this.actor.domain } }
|
||||
}
|
||||
},
|
||||
repr () {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
<template>
|
||||
<button @click="ajaxCall" :class="['ui', {loading: isLoading}, 'button']">
|
||||
<slot></slot>
|
||||
<button
|
||||
:class="['ui', {loading: isLoading}, 'button']"
|
||||
@click="ajaxCall"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
<script>
|
||||
|
@ -8,17 +11,17 @@ import axios from 'axios'
|
|||
|
||||
export default {
|
||||
props: {
|
||||
url: {type: String, required: true},
|
||||
method: {type: String, required: true},
|
||||
url: { type: String, required: true },
|
||||
method: { type: String, required: true }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isLoading: false,
|
||||
isLoading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
ajaxCall () {
|
||||
var self = this
|
||||
const self = this
|
||||
this.isLoading = true
|
||||
axios[this.method](this.url).then(response => {
|
||||
self.$emit('action-done', response.data)
|
||||
|
|
|
@ -1,36 +1,84 @@
|
|||
<template>
|
||||
<div class="ui form">
|
||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
||||
<h4 class="header"><translate translate-context="Content/*/Error message.Title">Your attachment cannot be saved</translate></h4>
|
||||
<div
|
||||
v-if="errors.length > 0"
|
||||
role="alert"
|
||||
class="ui negative message"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate translate-context="Content/*/Error message.Title">
|
||||
Your attachment cannot be saved
|
||||
</translate>
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li v-for="error in errors">{{ error }}</li>
|
||||
<li
|
||||
v-for="(error, key) in errors"
|
||||
:key="key"
|
||||
>
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<span id="avatarLabel">
|
||||
<slot name="label"></slot>
|
||||
<slot name="label" />
|
||||
</span>
|
||||
<div class="ui stackable grid row">
|
||||
<div class="three wide column">
|
||||
<img alt="" :class="['ui', imageClass, 'image']" v-if="value && value === initialValue" :src="$store.getters['instance/absoluteUrl'](`api/v1/attachments/${value}/proxy?next=medium_square_crop`)" />
|
||||
<img alt="" :class="['ui', imageClass, 'image']" v-else-if="attachment" :src="$store.getters['instance/absoluteUrl'](`api/v1/attachments/${attachment.uuid}/proxy?next=medium_square_crop`)" />
|
||||
<div :class="['ui', imageClass, 'static', 'large placeholder image']" v-else></div>
|
||||
<img
|
||||
v-if="value && value === initialValue"
|
||||
alt=""
|
||||
:class="['ui', imageClass, 'image']"
|
||||
:src="$store.getters['instance/absoluteUrl'](`api/v1/attachments/${value}/proxy?next=medium_square_crop`)"
|
||||
>
|
||||
<img
|
||||
v-else-if="attachment"
|
||||
alt=""
|
||||
:class="['ui', imageClass, 'image']"
|
||||
:src="$store.getters['instance/absoluteUrl'](`api/v1/attachments/${attachment.uuid}/proxy?next=medium_square_crop`)"
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
:class="['ui', imageClass, 'static', 'large placeholder image']"
|
||||
/>
|
||||
</div>
|
||||
<div class="eleven wide column">
|
||||
<div class="file-input">
|
||||
<label :for="attachmentId">
|
||||
<translate translate-context="*/*/*">Upload New Picture…</translate>
|
||||
</label>
|
||||
<input class="ui input" ref="attachment" type="file" :id="attachmentId" accept="image/x-png,image/jpeg" @change="submit" />
|
||||
<input
|
||||
:id="attachmentId"
|
||||
ref="attachment"
|
||||
class="ui input"
|
||||
type="file"
|
||||
accept="image/x-png,image/jpeg"
|
||||
@change="submit"
|
||||
>
|
||||
</div>
|
||||
<div class="ui very small hidden divider"></div>
|
||||
<p><translate translate-context="Content/*/Paragraph">PNG or JPG. Dimensions should be between 1400x1400px and 3000x3000px. Maximum file size allowed is 5MB.</translate></p>
|
||||
<button class="ui basic tiny button" v-if="value" @click.stop.prevent="remove(value)">
|
||||
<translate translate-context="Content/Radio/Button.Label/Verb">Remove</translate>
|
||||
<div class="ui very small hidden divider" />
|
||||
<p>
|
||||
<translate translate-context="Content/*/Paragraph">
|
||||
PNG or JPG. Dimensions should be between 1400x1400px and 3000x3000px. Maximum file size allowed is 5MB.
|
||||
</translate>
|
||||
</p>
|
||||
<button
|
||||
v-if="value"
|
||||
class="ui basic tiny button"
|
||||
@click.stop.prevent="remove(value)"
|
||||
>
|
||||
<translate translate-context="Content/Radio/Button.Label/Verb">
|
||||
Remove
|
||||
</translate>
|
||||
</button>
|
||||
<div v-if="isLoading" class="ui active inverted dimmer">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui active inverted dimmer"
|
||||
>
|
||||
<div class="ui indeterminate text loader">
|
||||
<translate translate-context="Content/*/*/Noun">Uploading file…</translate>
|
||||
<translate translate-context="Content/*/*/Noun">
|
||||
Uploading file…
|
||||
</translate>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -43,8 +91,8 @@ import axios from 'axios'
|
|||
|
||||
export default {
|
||||
props: {
|
||||
value: {},
|
||||
imageClass: {default: '', required: false}
|
||||
value: { type: String, required: true },
|
||||
imageClass: { type: String, default: '', required: false }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
@ -52,21 +100,29 @@ export default {
|
|||
isLoading: false,
|
||||
errors: [],
|
||||
initialValue: this.value,
|
||||
attachmentId: Math.random().toString(36).substring(7),
|
||||
attachmentId: Math.random().toString(36).substring(7)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value (v) {
|
||||
if (this.attachment && v === this.initialValue) {
|
||||
// we had a reset to initial value
|
||||
this.remove(this.attachment.uuid)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
submit () {
|
||||
this.isLoading = true
|
||||
this.errors = []
|
||||
let self = this
|
||||
const self = this
|
||||
this.file = this.$refs.attachment.files[0]
|
||||
let formData = new FormData()
|
||||
formData.append("file", this.file)
|
||||
const formData = new FormData()
|
||||
formData.append('file', this.file)
|
||||
axios
|
||||
.post(`attachments/`, formData, {
|
||||
.post('attachments/', formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
.then(
|
||||
|
@ -81,10 +137,10 @@ export default {
|
|||
}
|
||||
)
|
||||
},
|
||||
remove(uuid) {
|
||||
remove (uuid) {
|
||||
this.isLoading = true
|
||||
this.errors = []
|
||||
let self = this
|
||||
const self = this
|
||||
axios.delete(`attachments/${uuid}/`)
|
||||
.then(
|
||||
response => {
|
||||
|
@ -97,14 +153,6 @@ export default {
|
|||
self.errors = error.backendErrors
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value (v) {
|
||||
if (this.attachment && v === this.initialValue) {
|
||||
// we had a reset to initial value
|
||||
this.remove(this.attachment.uuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,27 @@
|
|||
<template>
|
||||
<a role="button" class="collapse link" @click.prevent="$emit('input', !value)">
|
||||
<translate v-if="isCollapsed" key="1" translate-context="*/*/Button,Label">Expand</translate>
|
||||
<translate v-else key="2" translate-context="*/*/Button,Label">Collapse</translate>
|
||||
<i :class="[{down: !isCollapsed}, {right: isCollapsed}, 'angle', 'icon']"></i>
|
||||
<a
|
||||
role="button"
|
||||
class="collapse link"
|
||||
@click.prevent="$emit('input', !value)"
|
||||
>
|
||||
<translate
|
||||
v-if="isCollapsed"
|
||||
key="1"
|
||||
translate-context="*/*/Button,Label"
|
||||
>Expand</translate>
|
||||
<translate
|
||||
v-else
|
||||
key="2"
|
||||
translate-context="*/*/Button,Label"
|
||||
>Collapse</translate>
|
||||
<i :class="[{down: !isCollapsed}, {right: isCollapsed}, 'angle', 'icon']" />
|
||||
</a>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {type: Boolean, required: true},
|
||||
value: { type: Boolean, required: true }
|
||||
},
|
||||
computed: {
|
||||
isCollapsed () {
|
||||
|
|
|
@ -2,48 +2,71 @@
|
|||
<div class="content-form ui segments">
|
||||
<div class="ui segment">
|
||||
<div class="ui tiny secondary pointing menu">
|
||||
<button @click.prevent="isPreviewing = false" :class="[{active: !isPreviewing}, 'item']">
|
||||
<translate translate-context="*/Form/Menu.item">Write</translate>
|
||||
<button
|
||||
:class="[{active: !isPreviewing}, 'item']"
|
||||
@click.prevent="isPreviewing = false"
|
||||
>
|
||||
<translate translate-context="*/Form/Menu.item">
|
||||
Write
|
||||
</translate>
|
||||
</button>
|
||||
<button @click.prevent="isPreviewing = true" :class="[{active: isPreviewing}, 'item']">
|
||||
<translate translate-context="*/Form/Menu.item">Preview</translate>
|
||||
<button
|
||||
:class="[{active: isPreviewing}, 'item']"
|
||||
@click.prevent="isPreviewing = true"
|
||||
>
|
||||
<translate translate-context="*/Form/Menu.item">
|
||||
Preview
|
||||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="isPreviewing" >
|
||||
|
||||
<div class="ui placeholder" v-if="isLoadingPreview">
|
||||
<template v-if="isPreviewing">
|
||||
<div
|
||||
v-if="isLoadingPreview"
|
||||
class="ui placeholder"
|
||||
>
|
||||
<div class="paragraph">
|
||||
<div class="line"></div>
|
||||
<div class="line"></div>
|
||||
<div class="line"></div>
|
||||
<div class="line"></div>
|
||||
<div class="line" />
|
||||
<div class="line" />
|
||||
<div class="line" />
|
||||
<div class="line" />
|
||||
</div>
|
||||
</div>
|
||||
<p v-else-if="preview === null">
|
||||
<translate translate-context="*/Form/Paragraph">Nothing to preview.</translate>
|
||||
<translate translate-context="*/Form/Paragraph">
|
||||
Nothing to preview.
|
||||
</translate>
|
||||
</p>
|
||||
<div v-html="preview" v-else></div>
|
||||
<div
|
||||
v-else
|
||||
v-html="preview"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="ui transparent input">
|
||||
<textarea
|
||||
ref="textarea"
|
||||
:name="fieldId"
|
||||
:id="fieldId"
|
||||
:rows="rows"
|
||||
ref="textarea"
|
||||
v-model="newValue"
|
||||
:name="fieldId"
|
||||
:rows="rows"
|
||||
:required="required"
|
||||
:placeholder="placeholder || labels.placeholder"></textarea>
|
||||
:placeholder="placeholder || labels.placeholder"
|
||||
/>
|
||||
</div>
|
||||
<div class="ui very small hidden divider"></div>
|
||||
<div class="ui very small hidden divider" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="ui bottom attached segment">
|
||||
<span :class="['right', 'floated', {'ui danger text': remainingChars < 0}]" v-if="charLimit">
|
||||
<span
|
||||
v-if="charLimit"
|
||||
:class="['right', 'floated', {'ui danger text': remainingChars < 0}]"
|
||||
>
|
||||
{{ remainingChars }}
|
||||
</span>
|
||||
<p>
|
||||
<translate translate-context="*/Form/Paragraph">Markdown syntax is supported.</translate>
|
||||
<translate translate-context="*/Form/Paragraph">
|
||||
Markdown syntax is supported.
|
||||
</translate>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -54,50 +77,31 @@ import axios from 'axios'
|
|||
|
||||
export default {
|
||||
props: {
|
||||
value: {type: String, default: ""},
|
||||
fieldId: {type: String, default: "change-content"},
|
||||
placeholder: {type: String, default: null},
|
||||
autofocus: {type: Boolean, default: false},
|
||||
charLimit: {type: Number, default: 5000, required: false},
|
||||
rows: {type: Number, default: 5, required: false},
|
||||
permissive: {type: Boolean, default: false},
|
||||
required: {type: Boolean, default: false},
|
||||
value: { type: String, default: '' },
|
||||
fieldId: { type: String, default: 'change-content' },
|
||||
placeholder: { type: String, default: null },
|
||||
autofocus: { type: Boolean, default: false },
|
||||
charLimit: { type: Number, default: 5000, required: false },
|
||||
rows: { type: Number, default: 5, required: false },
|
||||
permissive: { type: Boolean, default: false },
|
||||
required: { type: Boolean, default: false }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isPreviewing: false,
|
||||
preview: null,
|
||||
newValue: this.value,
|
||||
isLoadingPreview: false,
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.autofocus) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.textarea.focus()
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadPreview () {
|
||||
this.isLoadingPreview = true
|
||||
try {
|
||||
let response = await axios.post('text-preview/', {text: this.newValue, permissive: this.permissive})
|
||||
this.preview = response.data.rendered
|
||||
} catch {
|
||||
|
||||
}
|
||||
this.isLoadingPreview = false
|
||||
isLoadingPreview: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
return {
|
||||
placeholder: this.$pgettext("*/Form/Placeholder", "Write a few words here…")
|
||||
placeholder: this.$pgettext('*/Form/Placeholder', 'Write a few words here…')
|
||||
}
|
||||
},
|
||||
remainingChars () {
|
||||
return this.charLimit - (this.value || "").length
|
||||
return this.charLimit - (this.value || '').length
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -113,7 +117,7 @@ export default {
|
|||
await this.loadPreview()
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
immediate: true
|
||||
},
|
||||
async isPreviewing (v) {
|
||||
if (v && !!this.value && this.preview === null && !this.isLoadingPreview) {
|
||||
|
@ -125,6 +129,25 @@ export default {
|
|||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.autofocus) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.textarea.focus()
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadPreview () {
|
||||
this.isLoadingPreview = true
|
||||
try {
|
||||
const response = await axios.post('text-preview/', { text: this.newValue, permissive: this.permissive })
|
||||
this.preview = response.data.rendered
|
||||
} catch {
|
||||
|
||||
}
|
||||
this.isLoadingPreview = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,21 +1,38 @@
|
|||
<template>
|
||||
<div class="ui fluid action input component-copy-input">
|
||||
<p class="message" v-if="copied">
|
||||
<translate translate-context="Content/*/Paragraph">Text copied to clipboard!</translate>
|
||||
<p
|
||||
v-if="copied"
|
||||
class="message"
|
||||
>
|
||||
<translate translate-context="Content/*/Paragraph">
|
||||
Text copied to clipboard!
|
||||
</translate>
|
||||
</p>
|
||||
<input :id="id" :name="id" ref="input" :value="value" type="text" readonly>
|
||||
<button @click="copy" :class="['ui', buttonClasses, 'right', 'labeled', 'icon', 'button']">
|
||||
<i class="copy icon"></i>
|
||||
<translate translate-context="*/*/Button.Label/Short, Verb">Copy</translate>
|
||||
<input
|
||||
:id="id"
|
||||
ref="input"
|
||||
:name="id"
|
||||
:value="value"
|
||||
type="text"
|
||||
readonly
|
||||
>
|
||||
<button
|
||||
:class="['ui', buttonClasses, 'right', 'labeled', 'icon', 'button']"
|
||||
@click="copy"
|
||||
>
|
||||
<i class="copy icon" />
|
||||
<translate translate-context="*/*/Button.Label/Short, Verb">
|
||||
Copy
|
||||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {type: String},
|
||||
buttonClasses: {type: String, default: 'accent'},
|
||||
id: {type: String, default: 'copy-input'},
|
||||
value: { type: String, required: true },
|
||||
buttonClasses: { type: String, default: 'accent' },
|
||||
id: { type: String, default: 'copy-input' }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
@ -29,8 +46,8 @@ export default {
|
|||
clearTimeout(this.timeout)
|
||||
}
|
||||
this.$refs.input.select()
|
||||
document.execCommand("Copy")
|
||||
let self = this
|
||||
document.execCommand('Copy')
|
||||
const self = this
|
||||
self.copied = true
|
||||
this.timeout = setTimeout(() => {
|
||||
self.copied = false
|
||||
|
|
|
@ -1,44 +1,59 @@
|
|||
<template>
|
||||
<button @click="showModal = true" :class="[{disabled: disabled}]" :disabled="disabled">
|
||||
<slot></slot>
|
||||
<button
|
||||
:class="[{disabled: disabled}]"
|
||||
:disabled="disabled"
|
||||
@click="showModal = true"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<modal class="small" :show.sync="showModal">
|
||||
<modal
|
||||
class="small"
|
||||
:show.sync="showModal"
|
||||
>
|
||||
<h4 class="header">
|
||||
<slot name="modal-header">
|
||||
<translate translate-context="Modal/*/Title">Do you want to confirm this action?</translate>
|
||||
<translate translate-context="Modal/*/Title">
|
||||
Do you want to confirm this action?
|
||||
</translate>
|
||||
</slot>
|
||||
</h4>
|
||||
<div class="scrolling content">
|
||||
<div class="description">
|
||||
<slot name="modal-content"></slot>
|
||||
<slot name="modal-content" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="ui basic cancel button">
|
||||
<translate translate-context="*/*/Button.Label/Verb">Cancel</translate>
|
||||
<translate translate-context="*/*/Button.Label/Verb">
|
||||
Cancel
|
||||
</translate>
|
||||
</button>
|
||||
<button :class="['ui', 'confirm', confirmButtonColor, 'button']" @click="confirm">
|
||||
<button
|
||||
:class="['ui', 'confirm', confirmButtonColor, 'button']"
|
||||
@click="confirm"
|
||||
>
|
||||
<slot name="modal-confirm">
|
||||
<translate translate-context="Modal/*/Button.Label/Short, Verb">Confirm</translate>
|
||||
<translate translate-context="Modal/*/Button.Label/Short, Verb">
|
||||
Confirm
|
||||
</translate>
|
||||
</slot>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</button>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import Modal from '@/components/semantic/Modal'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
action: {type: Function, required: false},
|
||||
disabled: {type: Boolean, default: false},
|
||||
confirmColor: {type: String, default: "danger", required: false}
|
||||
},
|
||||
components: {
|
||||
Modal
|
||||
},
|
||||
props: {
|
||||
action: { type: Function, required: false, default: () => {} },
|
||||
disabled: { type: Boolean, default: false },
|
||||
confirmColor: { type: String, default: 'danger', required: false }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showModal: false
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
<template>
|
||||
<span>
|
||||
<translate translate-context="Content/*/Paragraph"
|
||||
<translate
|
||||
v-if="durationData.hours > 0"
|
||||
:translate-params="{minutes: durationData.minutes, hours: durationData.hours}">%{ hours } h %{ minutes } min</translate>
|
||||
<translate translate-context="Content/*/Paragraph"
|
||||
translate-context="Content/*/Paragraph"
|
||||
:translate-params="{minutes: durationData.minutes, hours: durationData.hours}"
|
||||
>%{ hours } h %{ minutes } min</translate>
|
||||
<translate
|
||||
v-else
|
||||
:translate-params="{minutes: durationData.minutes}">%{ minutes } min</translate>
|
||||
translate-context="Content/*/Paragraph"
|
||||
:translate-params="{minutes: durationData.minutes}"
|
||||
>%{ minutes } min</translate>
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
import {secondsToObject} from '@/filters'
|
||||
import { secondsToObject } from '@/filters'
|
||||
|
||||
export default {
|
||||
props: ['seconds'],
|
||||
props: { seconds: { type: Number, required: true } },
|
||||
computed: {
|
||||
durationData () {
|
||||
return secondsToObject(this.seconds)
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
<h4 class="ui header">
|
||||
<div class="content">
|
||||
<slot name="title">
|
||||
|
||||
<i class="search icon"></i>
|
||||
<i class="search icon" />
|
||||
<translate translate-context="Content/*/Paragraph">
|
||||
No results were found.
|
||||
</translate>
|
||||
|
@ -12,8 +11,12 @@
|
|||
</div>
|
||||
</h4>
|
||||
<div class="inline center aligned text">
|
||||
<slot></slot>
|
||||
<button v-if="refresh" class="ui button" @click="$emit('refresh')">
|
||||
<slot />
|
||||
<button
|
||||
v-if="refresh"
|
||||
class="ui button"
|
||||
@click="$emit('refresh')"
|
||||
>
|
||||
<translate translate-context="Content/*/Button.Label/Short, Verb">
|
||||
Refresh
|
||||
</translate>
|
||||
|
@ -24,7 +27,7 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
refresh: {type: Boolean, default: false}
|
||||
refresh: { type: Boolean, default: false }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -3,10 +3,22 @@
|
|||
<div :class="['expandable-content', {expandable: truncated.length < content.length}, {expanded: isExpanded}]">
|
||||
<slot>{{ content }}</slot>
|
||||
</div>
|
||||
<a v-if="truncated.length < content.length" role="button" @click.prevent="isExpanded = !isExpanded">
|
||||
<a
|
||||
v-if="truncated.length < content.length"
|
||||
role="button"
|
||||
@click.prevent="isExpanded = !isExpanded"
|
||||
>
|
||||
<br>
|
||||
<translate v-if="isExpanded" key="1" translate-context="*/*/Button,Label">Show less</translate>
|
||||
<translate v-else key="2" translate-context="*/*/Button,Label">Show more</translate>
|
||||
<translate
|
||||
v-if="isExpanded"
|
||||
key="1"
|
||||
translate-context="*/*/Button,Label"
|
||||
>Show less</translate>
|
||||
<translate
|
||||
v-else
|
||||
key="2"
|
||||
translate-context="*/*/Button,Label"
|
||||
>Show more</translate>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -15,12 +27,12 @@
|
|||
|
||||
export default {
|
||||
props: {
|
||||
content: {type: String, required: true},
|
||||
length: {type: Number, default: 150, required: false},
|
||||
content: { type: String, required: true },
|
||||
length: { type: Number, default: 150, required: false }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isExpanded: false,
|
||||
isExpanded: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
<template>
|
||||
<time :datetime="date" :title="date | moment">
|
||||
<i v-if="icon" class="outline clock icon"></i>
|
||||
<time
|
||||
:datetime="date"
|
||||
:title="date | moment"
|
||||
>
|
||||
<i
|
||||
v-if="icon"
|
||||
class="outline clock icon"
|
||||
/>
|
||||
{{ realDate | ago($store.state.ui.momentLocale) }}
|
||||
</time>
|
||||
</template>
|
||||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
import { mapState } from 'vuex'
|
||||
export default {
|
||||
props: {
|
||||
date: {required: true},
|
||||
icon: {type: Boolean, required: false, default: false},
|
||||
date: { type: String, required: true },
|
||||
icon: { type: Boolean, required: false, default: false }
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
<template>
|
||||
<time :datetime="`${duration}s`">
|
||||
{{ duration | duration}}
|
||||
{{ duration | duration }}
|
||||
</time>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
duration: {required: true},
|
||||
},
|
||||
duration: { type: Object, required: true }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,13 +1,34 @@
|
|||
<template>
|
||||
<form class="ui inline form" @submit.stop.prevent="$emit('search', value)">
|
||||
<form
|
||||
class="ui inline form"
|
||||
@submit.stop.prevent="$emit('search', value)"
|
||||
>
|
||||
<div :class="['ui', 'action', {icon: isClearable}, 'input']">
|
||||
<label for="search-query" class="hidden">
|
||||
<label
|
||||
for="search-query"
|
||||
class="hidden"
|
||||
>
|
||||
<translate translate-context="Content/Search/Input.Label/Noun">Search</translate>
|
||||
</label>
|
||||
<input id="search-query" name="search-query" type="text" :placeholder="placeholder || labels.searchPlaceholder" :value="value" @input="$emit('input', $event.target.value)">
|
||||
<i v-if="isClearable" class="x link icon" :title="labels.clear" @click.stop.prevent="$emit('input', ''); $emit('search', value)"></i>
|
||||
<button type="submit" class="ui icon basic button">
|
||||
<i class="search icon"></i>
|
||||
<input
|
||||
id="search-query"
|
||||
name="search-query"
|
||||
type="text"
|
||||
:placeholder="placeholder || labels.searchPlaceholder"
|
||||
:value="value"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
>
|
||||
<i
|
||||
v-if="isClearable"
|
||||
class="x link icon"
|
||||
:title="labels.clear"
|
||||
@click.stop.prevent="$emit('input', ''); $emit('search', value)"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
class="ui icon basic button"
|
||||
>
|
||||
<i class="search icon" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -15,14 +36,14 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {type: String, required: true},
|
||||
placeholder: {type: String, required: false},
|
||||
value: { type: String, required: true },
|
||||
placeholder: { type: String, required: false, default: '' }
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
return {
|
||||
searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search…'),
|
||||
clear: this.$pgettext("Content/Library/Button.Label", 'Clear'),
|
||||
clear: this.$pgettext('Content/Library/Button.Label', 'Clear')
|
||||
}
|
||||
},
|
||||
isClearable () {
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
<template>
|
||||
<modal :show.sync="show">
|
||||
<h4 class="header">{{ labels.header }}</h4>
|
||||
<div v-if="cover" class="image content">
|
||||
<h4 class="header">
|
||||
{{ labels.header }}
|
||||
</h4>
|
||||
<div
|
||||
v-if="cover"
|
||||
class="image content"
|
||||
>
|
||||
<div class="ui medium image">
|
||||
<img :src="cover.urls.medium_square_crop">
|
||||
</div>
|
||||
|
@ -14,7 +19,10 @@
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="content">
|
||||
<div
|
||||
v-else
|
||||
class="content"
|
||||
>
|
||||
<div class="ui centered header">
|
||||
{{ labels.description }}
|
||||
</div>
|
||||
|
@ -23,10 +31,19 @@
|
|||
</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<router-link :to="{path: '/login', query: { next: nextRoute }}" class="ui labeled icon button"><i class="key icon"></i>
|
||||
<router-link
|
||||
:to="{path: '/login', query: { next: nextRoute }}"
|
||||
class="ui labeled icon button"
|
||||
>
|
||||
<i class="key icon" />
|
||||
{{ labels.login }}
|
||||
</router-link>
|
||||
<router-link v-if="$store.state.instance.settings.users.registration_enabled.value" :to="{path: '/signup'}" class="ui labeled icon button"><i class="user icon"></i>
|
||||
<router-link
|
||||
v-if="$store.state.instance.settings.users.registration_enabled.value"
|
||||
:to="{path: '/signup'}"
|
||||
class="ui labeled icon button"
|
||||
>
|
||||
<i class="user icon" />
|
||||
{{ labels.signup }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
@ -37,28 +54,28 @@
|
|||
import Modal from '@/components/semantic/Modal'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
nextRoute: {type: String},
|
||||
message: {type: String},
|
||||
cover: {type: Object},
|
||||
},
|
||||
components: {
|
||||
Modal,
|
||||
Modal
|
||||
},
|
||||
data() {
|
||||
props: {
|
||||
nextRoute: { type: String, required: true },
|
||||
message: { type: String, required: true },
|
||||
cover: { type: Object, required: true }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
show: false,
|
||||
show: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels() {
|
||||
labels () {
|
||||
return {
|
||||
header: this.$pgettext('Popup/Title/Noun', "Unauthenticated"),
|
||||
login: this.$pgettext('*/*/Button.Label/Verb', "Log in"),
|
||||
signup: this.$pgettext('*/*/Button.Label/Verb', "Sign up"),
|
||||
description: this.$pgettext('Popup/*/Paragraph', "You don't have access!"),
|
||||
header: this.$pgettext('Popup/Title/Noun', 'Unauthenticated'),
|
||||
login: this.$pgettext('*/*/Button.Label/Verb', 'Log in'),
|
||||
signup: this.$pgettext('*/*/Button.Label/Verb', 'Sign up'),
|
||||
description: this.$pgettext('Popup/*/Paragraph', "You don't have access!")
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
<template>
|
||||
<div></div>
|
||||
<div />
|
||||
</template>
|
||||
<script>
|
||||
import $ from 'jquery'
|
||||
|
||||
export default {
|
||||
props: ['message'],
|
||||
props: { message: { type: Object, required: true } },
|
||||
mounted () {
|
||||
let self = this
|
||||
let params = {
|
||||
context: "#app",
|
||||
const self = this
|
||||
const params = {
|
||||
context: '#app',
|
||||
message: this.message.content,
|
||||
showProgress: 'top',
|
||||
position: "bottom right",
|
||||
position: 'bottom right',
|
||||
progressUp: true,
|
||||
onRemove () {
|
||||
self.$store.commit("ui/removeMessage", self.message.key)
|
||||
self.$store.commit('ui/removeMessage', self.message.key)
|
||||
},
|
||||
...this.message,
|
||||
...this.message
|
||||
}
|
||||
$("body").toast(params)
|
||||
$('body').toast(params)
|
||||
|
||||
$(".ui.toast.visible").last().attr('role', 'alert')
|
||||
$('.ui.toast.visible').last().attr('role', 'alert')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,79 +1,113 @@
|
|||
<template>
|
||||
<div>
|
||||
<template v-if="content && !isUpdating">
|
||||
<div v-html="html"></div>
|
||||
<div v-html="html" />
|
||||
<template v-if="isTruncated">
|
||||
<div class="ui small hidden divider"></div>
|
||||
<a href="" @click.stop.prevent="showMore = true" v-if="showMore === false">
|
||||
<div class="ui small hidden divider" />
|
||||
<a
|
||||
v-if="showMore === false"
|
||||
href=""
|
||||
@click.stop.prevent="showMore = true"
|
||||
>
|
||||
<translate translate-context="*/*/Button,Label">Show more</translate>
|
||||
</a>
|
||||
<a href="" @click.stop.prevent="showMore = false" v-else="showMore === true">
|
||||
<a
|
||||
v-else
|
||||
href=""
|
||||
@click.stop.prevent="showMore = false"
|
||||
>
|
||||
<translate translate-context="*/*/Button,Label">Show less</translate>
|
||||
</a>
|
||||
|
||||
</template>
|
||||
</template>
|
||||
<p v-else-if="!isUpdating">
|
||||
<translate translate-context="*/*/Placeholder">No description available</translate>
|
||||
<translate translate-context="*/*/Placeholder">
|
||||
No description available
|
||||
</translate>
|
||||
</p>
|
||||
<template v-if="!isUpdating && canUpdate && updateUrl">
|
||||
<div class="ui hidden divider"></div>
|
||||
<span role="button" @click="isUpdating = true">
|
||||
<i class="pencil icon"></i>
|
||||
<div class="ui hidden divider" />
|
||||
<span
|
||||
role="button"
|
||||
@click="isUpdating = true"
|
||||
>
|
||||
<i class="pencil icon" />
|
||||
<translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
|
||||
</span>
|
||||
</template>
|
||||
<form v-if="isUpdating" class="ui form" @submit.prevent="submit()">
|
||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
||||
<h4 class="header"><translate translate-context="Content/Channels/Error message.Title">Error while updating description</translate></h4>
|
||||
<form
|
||||
v-if="isUpdating"
|
||||
class="ui form"
|
||||
@submit.prevent="submit()"
|
||||
>
|
||||
<div
|
||||
v-if="errors.length > 0"
|
||||
role="alert"
|
||||
class="ui negative message"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate translate-context="Content/Channels/Error message.Title">
|
||||
Error while updating description
|
||||
</translate>
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li v-for="error in errors">{{ error }}</li>
|
||||
<li
|
||||
v-for="(error, key) in errors"
|
||||
:key="key"
|
||||
>
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<content-form v-model="newText" :autofocus="true"></content-form>
|
||||
<a @click.prevent="isUpdating = false" class="left floated">
|
||||
<content-form
|
||||
v-model="newText"
|
||||
:autofocus="true"
|
||||
/>
|
||||
<a
|
||||
class="left floated"
|
||||
@click.prevent="isUpdating = false"
|
||||
>
|
||||
<translate translate-context="*/*/Button.Label/Verb">Cancel</translate>
|
||||
</a>
|
||||
<button :class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']" type="submit" :disabled="isLoading">
|
||||
<translate translate-context="Content/Channels/Button.Label/Verb">Update description</translate>
|
||||
<button
|
||||
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']"
|
||||
type="submit"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
<translate translate-context="Content/Channels/Button.Label/Verb">
|
||||
Update description
|
||||
</translate>
|
||||
</button>
|
||||
<div class="ui clearing hidden divider"></div>
|
||||
<div class="ui clearing hidden divider" />
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {secondsToObject} from '@/filters'
|
||||
import axios from 'axios'
|
||||
import clip from 'text-clipper'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
content: {required: true},
|
||||
fieldName: {required: false, default: 'description'},
|
||||
updateUrl: {required: false, type: String},
|
||||
canUpdate: {required: false, default: true, type: Boolean},
|
||||
fetchHtml: {required: false, default: false, type: Boolean},
|
||||
permissive: {required: false, default: false, type: Boolean},
|
||||
truncateLength: {required: false, default: 500, type: Number},
|
||||
content: { type: String, required: true },
|
||||
fieldName: { type: String, required: false, default: 'description' },
|
||||
updateUrl: { required: false, type: String, default: '' },
|
||||
canUpdate: { required: false, default: true, type: Boolean },
|
||||
fetchHtml: { required: false, default: false, type: Boolean },
|
||||
permissive: { required: false, default: false, type: Boolean },
|
||||
truncateLength: { required: false, default: 500, type: Number }
|
||||
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isUpdating: false,
|
||||
showMore: false,
|
||||
newText: (this.content || {text: ''}).text,
|
||||
errors: null,
|
||||
newText: (this.content || { text: '' }).text,
|
||||
isLoading: false,
|
||||
errors: [],
|
||||
preview: null
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
if (this.fetchHtml) {
|
||||
await this.fetchPreview()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
html () {
|
||||
if (this.fetchHtml) {
|
||||
|
@ -91,21 +125,26 @@ export default {
|
|||
return this.truncateLength > 0 && this.truncatedHtml.length < this.content.html.length
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
if (this.fetchHtml) {
|
||||
await this.fetchPreview()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchPreview () {
|
||||
let response = await axios.post('text-preview/', {text: this.content.text, permissive: this.permissive})
|
||||
const response = await axios.post('text-preview/', { text: this.content.text, permissive: this.permissive })
|
||||
this.preview = response.data.rendered
|
||||
},
|
||||
submit () {
|
||||
let self = this
|
||||
const self = this
|
||||
this.isLoading = true
|
||||
this.errors = []
|
||||
let payload = {}
|
||||
const payload = {}
|
||||
payload[this.fieldName] = null
|
||||
if (this.newText) {
|
||||
payload[this.fieldName] = {
|
||||
content_type: "text/markdown",
|
||||
text: this.newText,
|
||||
content_type: 'text/markdown',
|
||||
text: this.newText
|
||||
}
|
||||
}
|
||||
axios.patch(this.updateUrl, payload).then((response) => {
|
||||
|
@ -116,7 +155,7 @@ export default {
|
|||
self.errors = error.backendErrors
|
||||
self.isLoading = false
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
<template>
|
||||
<span class="tooltip" :data-tooltip="content"><i class="question circle icon"></i></span>
|
||||
<span
|
||||
class="tooltip"
|
||||
:data-tooltip="content"
|
||||
><i class="question circle icon" /></span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
content: {type: String, required: true},
|
||||
content: { type: String, required: true }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -2,11 +2,16 @@
|
|||
<span class="component-user-link">
|
||||
<template v-if="avatar">
|
||||
<img
|
||||
v-if="user.avatar && user.avatar.urls.medium_square_crop"
|
||||
v-lazy="$store.getters['instance/absoluteUrl'](user.avatar.urls.medium_square_crop)"
|
||||
class="ui tiny circular avatar"
|
||||
alt=""
|
||||
v-if="user.avatar && user.avatar.urls.medium_square_crop"
|
||||
v-lazy="$store.getters['instance/absoluteUrl'](user.avatar.urls.medium_square_crop)" />
|
||||
<span v-else :style="defaultAvatarStyle" class="ui circular label">{{ user.username[0]}}</span>
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
:style="defaultAvatarStyle"
|
||||
class="ui circular label"
|
||||
>{{ user.username[0] }}</span>
|
||||
|
||||
</template>
|
||||
@{{ user.username }}
|
||||
|
@ -14,12 +19,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import {hashCode, intToRGB} from '@/utils/color'
|
||||
import { hashCode, intToRGB } from '@/utils/color'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
user: {required: true},
|
||||
avatar: {type: Boolean, default: true}
|
||||
user: { type: String, required: true },
|
||||
avatar: { type: Boolean, default: true }
|
||||
},
|
||||
computed: {
|
||||
userColor () {
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['username']
|
||||
props: { username: { type: String, required: true } }
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,98 +1,158 @@
|
|||
<template>
|
||||
<main class="main pusher" v-title="labels.title">
|
||||
<main
|
||||
v-title="labels.title"
|
||||
class="main pusher"
|
||||
>
|
||||
<section class="ui vertical center aligned stripe segment">
|
||||
<div :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']">
|
||||
<div class="ui text loader">
|
||||
<translate translate-context="Content/Favorites/Message">Loading your favorites…</translate>
|
||||
<translate translate-context="Content/Favorites/Message">
|
||||
Loading your favorites…
|
||||
</translate>
|
||||
</div>
|
||||
</div>
|
||||
<h2 v-if="results" class="ui center aligned icon header">
|
||||
<i class="circular inverted heart pink icon"></i>
|
||||
<h2
|
||||
v-if="results"
|
||||
class="ui center aligned icon header"
|
||||
>
|
||||
<i class="circular inverted heart pink icon" />
|
||||
<translate
|
||||
translate-plural="%{ count } favorites"
|
||||
:translate-n="$store.state.favorites.count"
|
||||
:translate-params="{count: results.count}"
|
||||
translate-context="Content/Favorites/Title">
|
||||
translate-context="Content/Favorites/Title"
|
||||
>
|
||||
%{ count } favorite
|
||||
</translate>
|
||||
</h2>
|
||||
<radio-button v-if="hasFavorites" type="favorites"></radio-button>
|
||||
<radio-button
|
||||
v-if="hasFavorites"
|
||||
type="favorites"
|
||||
/>
|
||||
</section>
|
||||
<section v-if="hasFavorites" class="ui vertical stripe segment">
|
||||
<section
|
||||
v-if="hasFavorites"
|
||||
class="ui vertical stripe segment"
|
||||
>
|
||||
<div :class="['ui', {'loading': isLoading}, 'form']">
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<label for="favorites-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label>
|
||||
<select id="favorites-ordering" class="ui dropdown" v-model="ordering">
|
||||
<option v-for="option in orderingOptions" :value="option[0]" :key="option[0]">
|
||||
<select
|
||||
id="favorites-ordering"
|
||||
v-model="ordering"
|
||||
class="ui dropdown"
|
||||
>
|
||||
<option
|
||||
v-for="option in orderingOptions"
|
||||
:key="option[0]"
|
||||
:value="option[0]"
|
||||
>
|
||||
{{ sharedLabels.filters[option[1]] }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="favorites-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Order</translate></label>
|
||||
<select id="favorites-ordering-direction" class="ui dropdown" v-model="orderingDirection">
|
||||
<option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option>
|
||||
<option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option>
|
||||
<select
|
||||
id="favorites-ordering-direction"
|
||||
v-model="orderingDirection"
|
||||
class="ui dropdown"
|
||||
>
|
||||
<option value="+">
|
||||
<translate translate-context="Content/Search/Dropdown">
|
||||
Ascending
|
||||
</translate>
|
||||
</option>
|
||||
<option value="-">
|
||||
<translate translate-context="Content/Search/Dropdown">
|
||||
Descending
|
||||
</translate>
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="favorites-results"><translate translate-context="Content/Search/Dropdown.Label/Noun">Results per page</translate></label>
|
||||
<select id="favorites-results" class="ui dropdown" v-model="paginateBy">
|
||||
<option :value="parseInt(12)">12</option>
|
||||
<option :value="parseInt(25)">25</option>
|
||||
<option :value="parseInt(50)">50</option>
|
||||
<select
|
||||
id="favorites-results"
|
||||
v-model="paginateBy"
|
||||
class="ui dropdown"
|
||||
>
|
||||
<option :value="parseInt(12)">
|
||||
12
|
||||
</option>
|
||||
<option :value="parseInt(25)">
|
||||
25
|
||||
</option>
|
||||
<option :value="parseInt(50)">
|
||||
50
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<track-table :show-artist="true" :show-album="true" v-if="results" :tracks="results.results"></track-table>
|
||||
<track-table
|
||||
v-if="results"
|
||||
:show-artist="true"
|
||||
:show-album="true"
|
||||
:tracks="results.results"
|
||||
/>
|
||||
<div class="ui center aligned basic segment">
|
||||
<pagination
|
||||
v-if="results && results.count > paginateBy"
|
||||
@page-changed="selectPage"
|
||||
:current="page"
|
||||
:paginate-by="paginateBy"
|
||||
:total="results.count"
|
||||
></pagination>
|
||||
@page-changed="selectPage"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<div v-else class="ui placeholder segment">
|
||||
<div
|
||||
v-else
|
||||
class="ui placeholder segment"
|
||||
>
|
||||
<div class="ui icon header">
|
||||
<i class="broken heart icon"></i>
|
||||
<i class="broken heart icon" />
|
||||
<translate
|
||||
translate-context="Content/Home/Placeholder"
|
||||
>No tracks have been added to your favorites yet</translate>
|
||||
>
|
||||
No tracks have been added to your favorites yet
|
||||
</translate>
|
||||
</div>
|
||||
<router-link :to="'/library'" class="ui success labeled icon button">
|
||||
<i class="headphones icon"></i>
|
||||
<translate translate-context="Content/*/Verb">Browse the library</translate>
|
||||
<router-link
|
||||
:to="'/library'"
|
||||
class="ui success labeled icon button"
|
||||
>
|
||||
<i class="headphones icon" />
|
||||
<translate translate-context="Content/*/Verb">
|
||||
Browse the library
|
||||
</translate>
|
||||
</router-link>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios"
|
||||
import $ from "jquery"
|
||||
import logger from "@/logging"
|
||||
import RadioButton from "@/components/radios/Button"
|
||||
import Pagination from "@/components/Pagination"
|
||||
import OrderingMixin from "@/components/mixins/Ordering"
|
||||
import PaginationMixin from "@/components/mixins/Pagination"
|
||||
import TranslationsMixin from "@/components/mixins/Translations"
|
||||
import {checkRedirectToLogin} from '@/utils'
|
||||
import axios from 'axios'
|
||||
import $ from 'jquery'
|
||||
import logger from '@/logging'
|
||||
import RadioButton from '@/components/radios/Button'
|
||||
import Pagination from '@/components/Pagination'
|
||||
import OrderingMixin from '@/components/mixins/Ordering'
|
||||
import PaginationMixin from '@/components/mixins/Pagination'
|
||||
import TranslationsMixin from '@/components/mixins/Translations'
|
||||
import { checkRedirectToLogin } from '@/utils'
|
||||
import TrackTable from '@/components/audio/track/Table'
|
||||
const FAVORITES_URL = "tracks/"
|
||||
const FAVORITES_URL = 'tracks/'
|
||||
|
||||
export default {
|
||||
mixins: [OrderingMixin, PaginationMixin, TranslationsMixin],
|
||||
components: {
|
||||
RadioButton,
|
||||
Pagination,
|
||||
TrackTable
|
||||
},
|
||||
data() {
|
||||
mixins: [OrderingMixin, PaginationMixin, TranslationsMixin],
|
||||
data () {
|
||||
return {
|
||||
results: null,
|
||||
isLoading: false,
|
||||
|
@ -100,33 +160,46 @@ export default {
|
|||
previousLink: null,
|
||||
page: parseInt(this.defaultPage),
|
||||
orderingOptions: [
|
||||
["creation_date", "creation_date"],
|
||||
["title", "track_title"],
|
||||
["album__title", "album_title"],
|
||||
["artist__name", "artist_name"]
|
||||
['creation_date', 'creation_date'],
|
||||
['title', 'track_title'],
|
||||
['album__title', 'album_title'],
|
||||
['artist__name', 'artist_name']
|
||||
]
|
||||
}
|
||||
},
|
||||
created() {
|
||||
checkRedirectToLogin(this.$store, this.$router)
|
||||
this.fetchFavorites(FAVORITES_URL)
|
||||
|
||||
},
|
||||
mounted() {
|
||||
$(".ui.dropdown").dropdown()
|
||||
},
|
||||
computed: {
|
||||
labels() {
|
||||
labels () {
|
||||
return {
|
||||
title: this.$pgettext('Head/Favorites/Title', 'Your Favorites')
|
||||
}
|
||||
},
|
||||
hasFavorites () {
|
||||
return this.$store.state.favorites.count > 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
page: function () {
|
||||
this.updateQueryString()
|
||||
},
|
||||
paginateBy: function () {
|
||||
this.updateQueryString()
|
||||
},
|
||||
orderingDirection: function () {
|
||||
this.updateQueryString()
|
||||
},
|
||||
ordering: function () {
|
||||
this.updateQueryString()
|
||||
}
|
||||
},
|
||||
created () {
|
||||
checkRedirectToLogin(this.$store, this.$router)
|
||||
this.fetchFavorites(FAVORITES_URL)
|
||||
},
|
||||
mounted () {
|
||||
$('.ui.dropdown').dropdown()
|
||||
},
|
||||
methods: {
|
||||
updateQueryString: function() {
|
||||
updateQueryString: function () {
|
||||
this.$router.replace({
|
||||
query: {
|
||||
page: this.page,
|
||||
|
@ -136,44 +209,30 @@ export default {
|
|||
})
|
||||
this.fetchFavorites(FAVORITES_URL)
|
||||
},
|
||||
fetchFavorites(url) {
|
||||
var self = this
|
||||
fetchFavorites (url) {
|
||||
const self = this
|
||||
this.isLoading = true
|
||||
let params = {
|
||||
favorites: "true",
|
||||
const params = {
|
||||
favorites: 'true',
|
||||
page: this.page,
|
||||
page_size: this.paginateBy,
|
||||
ordering: this.getOrderingAsString()
|
||||
}
|
||||
logger.default.time("Loading user favorites")
|
||||
logger.default.time('Loading user favorites')
|
||||
axios.get(url, { params: params }).then(response => {
|
||||
self.results = response.data
|
||||
self.nextLink = response.data.next
|
||||
self.previousLink = response.data.previous
|
||||
self.results.results.forEach(track => {
|
||||
self.$store.commit("favorites/track", { id: track.id, value: true })
|
||||
self.$store.commit('favorites/track', { id: track.id, value: true })
|
||||
})
|
||||
logger.default.timeEnd("Loading user favorites")
|
||||
logger.default.timeEnd('Loading user favorites')
|
||||
self.isLoading = false
|
||||
})
|
||||
},
|
||||
selectPage: function(page) {
|
||||
selectPage: function (page) {
|
||||
this.page = page
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
page: function() {
|
||||
this.updateQueryString()
|
||||
},
|
||||
paginateBy: function() {
|
||||
this.updateQueryString()
|
||||
},
|
||||
orderingDirection: function() {
|
||||
this.updateQueryString()
|
||||
},
|
||||
ordering: function() {
|
||||
this.updateQueryString()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,25 +1,40 @@
|
|||
<template>
|
||||
<button @click.stop="$store.dispatch('favorites/toggle', track.id)" v-if="button" :class="['ui', 'pink', {'inverted': isFavorite}, {'favorited': isFavorite}, 'icon', 'labeled', 'button']">
|
||||
<i class="heart icon"></i>
|
||||
<translate v-if="isFavorite" translate-context="Content/Track/Button.Message">In favorites</translate>
|
||||
<translate v-else translate-context="Content/Track/*/Verb">Add to favorites</translate>
|
||||
<template>
|
||||
<button
|
||||
v-if="button"
|
||||
:class="['ui', 'pink', {'inverted': isFavorite}, {'favorited': isFavorite}, 'icon', 'labeled', 'button']"
|
||||
@click.stop="$store.dispatch('favorites/toggle', track.id)"
|
||||
>
|
||||
<i class="heart icon" />
|
||||
<translate
|
||||
v-if="isFavorite"
|
||||
translate-context="Content/Track/Button.Message"
|
||||
>
|
||||
In favorites
|
||||
</translate>
|
||||
<translate
|
||||
v-else
|
||||
translate-context="Content/Track/*/Verb"
|
||||
>
|
||||
Add to favorites
|
||||
</translate>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
@click.stop="$store.dispatch('favorites/toggle', track.id)"
|
||||
:class="['ui', 'favorite-icon', {'pink': isFavorite}, {'favorited': isFavorite}, 'basic', 'circular', 'icon', {'really': !border}, 'button']"
|
||||
:aria-label="title"
|
||||
:title="title">
|
||||
<i :class="['heart', {'pink': isFavorite}, 'basic', 'icon']"></i>
|
||||
:title="title"
|
||||
@click.stop="$store.dispatch('favorites/toggle', track.id)"
|
||||
>
|
||||
<i :class="['heart', {'pink': isFavorite}, 'basic', 'icon']" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
track: {type: Object},
|
||||
button: {type: Boolean, default: false},
|
||||
border: {type: Boolean, default: false},
|
||||
track: { type: Object, default: () => { return {} } },
|
||||
button: { type: Boolean, default: false },
|
||||
border: { type: Boolean, default: false }
|
||||
},
|
||||
computed: {
|
||||
title () {
|
||||
|
|
|
@ -1,30 +1,73 @@
|
|||
<template>
|
||||
<div @click="createFetch" role="button">
|
||||
<div
|
||||
role="button"
|
||||
@click="createFetch"
|
||||
>
|
||||
<div>
|
||||
<slot></slot>
|
||||
<slot />
|
||||
</div>
|
||||
<modal class="small" :show.sync="showModal">
|
||||
<modal
|
||||
class="small"
|
||||
:show.sync="showModal"
|
||||
>
|
||||
<h3 class="header">
|
||||
<translate translate-context="Popup/*/Title">Refreshing object from remote server…</translate>
|
||||
<translate translate-context="Popup/*/Title">
|
||||
Refreshing object from remote server…
|
||||
</translate>
|
||||
</h3>
|
||||
<div class="scrolling content">
|
||||
<template v-if="fetch && fetch.status != 'pending'">
|
||||
<div v-if="fetch.status === 'skipped'" class="ui message">
|
||||
<h4 class="header"><translate translate-context="Popup/*/Message.Title">Refresh was skipped</translate></h4>
|
||||
<p><translate translate-context="Popup/*/Message.Content">The remote server answered, but returned data was unsupported by Funkwhale.</translate></p>
|
||||
<div
|
||||
v-if="fetch.status === 'skipped'"
|
||||
class="ui message"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate translate-context="Popup/*/Message.Title">
|
||||
Refresh was skipped
|
||||
</translate>
|
||||
</h4>
|
||||
<p>
|
||||
<translate translate-context="Popup/*/Message.Content">
|
||||
The remote server answered, but returned data was unsupported by Funkwhale.
|
||||
</translate>
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="fetch.status === 'finished'" class="ui success message">
|
||||
<h4 class="header"><translate translate-context="Popup/*/Message.Title">Refresh successful</translate></h4>
|
||||
<p><translate translate-context="Popup/*/Message.Content">Data was refreshed successfully from remote server.</translate></p>
|
||||
<div
|
||||
v-else-if="fetch.status === 'finished'"
|
||||
class="ui success message"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate translate-context="Popup/*/Message.Title">
|
||||
Refresh successful
|
||||
</translate>
|
||||
</h4>
|
||||
<p>
|
||||
<translate translate-context="Popup/*/Message.Content">
|
||||
Data was refreshed successfully from remote server.
|
||||
</translate>
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="fetch.status === 'errored'" class="ui error message">
|
||||
<h4 class="header"><translate translate-context="Popup/*/Message.Title">Refresh error</translate></h4>
|
||||
<p><translate translate-context="Popup/*/Message.Content">An error occurred while trying to refresh data:</translate></p>
|
||||
<div
|
||||
v-else-if="fetch.status === 'errored'"
|
||||
class="ui error message"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate translate-context="Popup/*/Message.Title">
|
||||
Refresh error
|
||||
</translate>
|
||||
</h4>
|
||||
<p>
|
||||
<translate translate-context="Popup/*/Message.Content">
|
||||
An error occurred while trying to refresh data:
|
||||
</translate>
|
||||
</p>
|
||||
<table class="ui very basic collapsing celled table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<translate translate-context="Popup/Import/Table.Label/Noun">Error type</translate>
|
||||
<translate translate-context="Popup/Import/Table.Label/Noun">
|
||||
Error type
|
||||
</translate>
|
||||
</td>
|
||||
<td>
|
||||
{{ fetch.detail.error_code }}
|
||||
|
@ -32,61 +75,136 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<translate translate-context="Popup/Import/Table.Label/Noun">Error detail</translate>
|
||||
<translate translate-context="Popup/Import/Table.Label/Noun">
|
||||
Error detail
|
||||
</translate>
|
||||
</td>
|
||||
<td>
|
||||
<translate
|
||||
v-if="fetch.detail.error_code === 'http' && fetch.detail.status_code"
|
||||
:translate-params="{status: fetch.detail.status_code}"
|
||||
translate-context="*/*/Error">The remote server answered with HTTP %{ status }</translate>
|
||||
translate-context="*/*/Error"
|
||||
>
|
||||
The remote server answered with HTTP %{ status }
|
||||
</translate>
|
||||
<translate
|
||||
v-else-if="['http', 'request'].indexOf(fetch.detail.error_code) > -1"
|
||||
translate-context="*/*/Error">An HTTP error occurred while contacting the remote server</translate>
|
||||
translate-context="*/*/Error"
|
||||
>
|
||||
An HTTP error occurred while contacting the remote server
|
||||
</translate>
|
||||
<translate
|
||||
v-else-if="fetch.detail.error_code === 'timeout'"
|
||||
translate-context="*/*/Error">The remote server didn't respond quickly enough</translate>
|
||||
translate-context="*/*/Error"
|
||||
>
|
||||
The remote server didn't respond quickly enough
|
||||
</translate>
|
||||
<translate
|
||||
v-else-if="fetch.detail.error_code === 'connection'"
|
||||
translate-context="*/*/Error">Impossible to connect to the remote server</translate>
|
||||
translate-context="*/*/Error"
|
||||
>
|
||||
Impossible to connect to the remote server
|
||||
</translate>
|
||||
<translate
|
||||
v-else-if="['invalid_json', 'invalid_jsonld', 'missing_jsonld_type'].indexOf(fetch.detail.error_code) > -1"
|
||||
translate-context="*/*/Error">The remote server returned invalid JSON or JSON-LD data</translate>
|
||||
<translate v-else-if="fetch.detail.error_code === 'validation'" translate-context="*/*/Error">Data returned by the remote server had invalid or missing attributes</translate>
|
||||
<translate v-else-if="fetch.detail.error_code === 'unhandled'" translate-context="*/*/Error">Unknown error</translate>
|
||||
<translate v-else translate-context="*/*/Error">Unknown error</translate>
|
||||
translate-context="*/*/Error"
|
||||
>
|
||||
The remote server returned invalid JSON or JSON-LD data
|
||||
</translate>
|
||||
<translate
|
||||
v-else-if="fetch.detail.error_code === 'validation'"
|
||||
translate-context="*/*/Error"
|
||||
>
|
||||
Data returned by the remote server had invalid or missing attributes
|
||||
</translate>
|
||||
<translate
|
||||
v-else-if="fetch.detail.error_code === 'unhandled'"
|
||||
translate-context="*/*/Error"
|
||||
>
|
||||
Unknown error
|
||||
</translate>
|
||||
<translate
|
||||
v-else
|
||||
translate-context="*/*/Error"
|
||||
>
|
||||
Unknown error
|
||||
</translate>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="isCreatingFetch" class="ui active inverted dimmer">
|
||||
<div
|
||||
v-else-if="isCreatingFetch"
|
||||
class="ui active inverted dimmer"
|
||||
>
|
||||
<div class="ui text loader">
|
||||
<translate translate-context="Popup/*/Loading.Title">Requesting a fetch…</translate>
|
||||
<translate translate-context="Popup/*/Loading.Title">
|
||||
Requesting a fetch…
|
||||
</translate>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="isWaitingFetch" class="ui active inverted dimmer">
|
||||
<div
|
||||
v-else-if="isWaitingFetch"
|
||||
class="ui active inverted dimmer"
|
||||
>
|
||||
<div class="ui text loader">
|
||||
<translate translate-context="Popup/*/Loading.Title">Waiting for result…</translate>
|
||||
<translate translate-context="Popup/*/Loading.Title">
|
||||
Waiting for result…
|
||||
</translate>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
||||
<h4 class="header"><translate translate-context="Content/*/Error message.Title">Error while saving settings</translate></h4>
|
||||
<div
|
||||
v-if="errors.length > 0"
|
||||
role="alert"
|
||||
class="ui negative message"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate translate-context="Content/*/Error message.Title">
|
||||
Error while saving settings
|
||||
</translate>
|
||||
</h4>
|
||||
<ul class="list">
|
||||
<li v-for="error in errors">{{ error }}</li>
|
||||
<li
|
||||
v-for="(error, key) in errors"
|
||||
:key="key"
|
||||
>
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else-if="fetch && fetch.status === 'pending' && pollsCount >= maxPolls" role="alert" class="ui warning message">
|
||||
<h4 class="header"><translate translate-context="Popup/*/Message.Title">Refresh pending</translate></h4>
|
||||
<p><translate translate-context="Popup/*/Message.Content">The refresh request hasn't been processed in time by our server. It will be processed later.</translate></p>
|
||||
<div
|
||||
v-else-if="fetch && fetch.status === 'pending' && pollsCount >= maxPolls"
|
||||
role="alert"
|
||||
class="ui warning message"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate translate-context="Popup/*/Message.Title">
|
||||
Refresh pending
|
||||
</translate>
|
||||
</h4>
|
||||
<p>
|
||||
<translate translate-context="Popup/*/Message.Content">
|
||||
The refresh request hasn't been processed in time by our server. It will be processed later.
|
||||
</translate>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="ui basic cancel button">
|
||||
<translate translate-context="*/*/Button.Label/Verb">Close</translate>
|
||||
<translate translate-context="*/*/Button.Label/Verb">
|
||||
Close
|
||||
</translate>
|
||||
</button>
|
||||
<button @click.prevent="showModal = false; $emit('refresh')" class="ui confirm success button" v-if="fetch && fetch.status === 'finished'">
|
||||
<translate translate-context="*/*/Button.Label/Verb">Close and reload page</translate>
|
||||
<button
|
||||
v-if="fetch && fetch.status === 'finished'"
|
||||
class="ui confirm success button"
|
||||
@click.prevent="showModal = false; $emit('refresh')"
|
||||
>
|
||||
<translate translate-context="*/*/Button.Label/Verb">
|
||||
Close and reload page
|
||||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
|
@ -94,14 +212,14 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios"
|
||||
import axios from 'axios'
|
||||
import Modal from '@/components/semantic/Modal'
|
||||
|
||||
export default {
|
||||
props: ['url'],
|
||||
components: {
|
||||
Modal
|
||||
},
|
||||
props: { url: { type: String, required: true } },
|
||||
data () {
|
||||
return {
|
||||
fetch: null,
|
||||
|
@ -110,12 +228,12 @@ export default {
|
|||
showModal: false,
|
||||
isWaitingFetch: false,
|
||||
maxPolls: 15,
|
||||
pollsCount: 0,
|
||||
pollsCount: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createFetch () {
|
||||
let self = this
|
||||
const self = this
|
||||
this.fetch = null
|
||||
this.pollsCount = 0
|
||||
this.errors = []
|
||||
|
@ -134,8 +252,8 @@ export default {
|
|||
pollFetch () {
|
||||
this.isWaitingFetch = true
|
||||
this.pollsCount += 1
|
||||
let url = `federation/fetches/${this.fetch.id}/`
|
||||
let self = this
|
||||
const url = `federation/fetches/${this.fetch.id}/`
|
||||
const self = this
|
||||
self.showModal = true
|
||||
axios.get(url).then((response) => {
|
||||
self.isCreatingFetch = false
|
||||
|
|
|
@ -1,27 +1,52 @@
|
|||
<template>
|
||||
<div class="wrapper">
|
||||
<h3 v-if="!!this.$slots.title" class="ui header">
|
||||
<slot name="title"></slot>
|
||||
<h3
|
||||
v-if="!!$slots.title"
|
||||
class="ui header"
|
||||
>
|
||||
<slot name="title" />
|
||||
</h3>
|
||||
<p v-if="!isLoading && libraries.length > 0" class="ui subtitle"><slot name="subtitle"></slot></p>
|
||||
<p v-if="!isLoading && libraries.length === 0" class="ui subtitle"><translate translate-context="Content/Federation/Paragraph">No matching library.</translate></p>
|
||||
<div class="ui hidden divider"></div>
|
||||
<p
|
||||
v-if="!isLoading && libraries.length > 0"
|
||||
class="ui subtitle"
|
||||
>
|
||||
<slot name="subtitle" />
|
||||
</p>
|
||||
<p
|
||||
v-if="!isLoading && libraries.length === 0"
|
||||
class="ui subtitle"
|
||||
>
|
||||
<translate translate-context="Content/Federation/Paragraph">
|
||||
No matching library.
|
||||
</translate>
|
||||
</p>
|
||||
<div class="ui hidden divider" />
|
||||
<div class="ui cards">
|
||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||
<div class="ui loader"></div>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui inverted active dimmer"
|
||||
>
|
||||
<div class="ui loader" />
|
||||
</div>
|
||||
<library-card
|
||||
v-for="library in libraries"
|
||||
:key="library.uuid"
|
||||
:display-scan="false"
|
||||
:display-follow="$store.state.auth.authenticated && library.actor.full_username != $store.state.auth.fullUsername"
|
||||
:library="library"
|
||||
:display-copy-fid="true"
|
||||
v-for="library in libraries"
|
||||
:key="library.uuid"></library-card>
|
||||
/>
|
||||
</div>
|
||||
<template v-if="nextPage">
|
||||
<div class="ui hidden divider"></div>
|
||||
<button v-if="nextPage" @click="fetchData(nextPage)" :class="['ui', 'basic', 'button']">
|
||||
<translate translate-context="*/*/Button,Label">Show more</translate>
|
||||
<div class="ui hidden divider" />
|
||||
<button
|
||||
v-if="nextPage"
|
||||
:class="['ui', 'basic', 'button']"
|
||||
@click="fetchData(nextPage)"
|
||||
>
|
||||
<translate translate-context="*/*/Button,Label">
|
||||
Show more
|
||||
</translate>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -33,12 +58,12 @@ import axios from 'axios'
|
|||
import LibraryCard from '@/views/content/remote/Card'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
url: {type: String, required: true}
|
||||
},
|
||||
components: {
|
||||
LibraryCard
|
||||
},
|
||||
props: {
|
||||
url: { type: String, required: true }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
libraries: [],
|
||||
|
@ -49,17 +74,22 @@ export default {
|
|||
nextPage: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
offset () {
|
||||
this.fetchData()
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchData(this.url)
|
||||
},
|
||||
methods: {
|
||||
fetchData (url) {
|
||||
this.isLoading = true
|
||||
let self = this
|
||||
let params = _.clone({})
|
||||
const self = this
|
||||
const params = _.clone({})
|
||||
params.page_size = this.limit
|
||||
params.offset = this.offset
|
||||
axios.get(url, {params: params}).then((response) => {
|
||||
axios.get(url, { params: params }).then((response) => {
|
||||
self.previousPage = response.data.previous
|
||||
self.nextPage = response.data.next
|
||||
self.isLoading = false
|
||||
|
@ -77,11 +107,6 @@ export default {
|
|||
this.offset = Math.max(this.offset - this.limit, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
offset () {
|
||||
this.fetchData()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,55 +1,60 @@
|
|||
<template>
|
||||
<div class="ui fluid action input">
|
||||
<input
|
||||
:id="fieldId"
|
||||
required
|
||||
name="password"
|
||||
:type="passwordInputType"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
:id="fieldId"
|
||||
:value="value"
|
||||
/>
|
||||
@input="$emit('input', $event.target.value)"
|
||||
>
|
||||
<button
|
||||
@click.prevent="showPassword = !showPassword"
|
||||
type="button"
|
||||
:title="labels.title"
|
||||
class="ui icon button"
|
||||
@click.prevent="showPassword = !showPassword"
|
||||
>
|
||||
<i class="eye icon"></i>
|
||||
<i class="eye icon" />
|
||||
</button>
|
||||
<button
|
||||
v-if="copyButton"
|
||||
@click.prevent="copyPassword"
|
||||
type="button"
|
||||
class="ui icon button"
|
||||
:title="labels.copy"
|
||||
@click.prevent="copyPassword"
|
||||
>
|
||||
<i class="copy icon"></i>
|
||||
<i class="copy icon" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ["value", "defaultShow", "copyButton", "fieldId"],
|
||||
data() {
|
||||
props: {
|
||||
value: { type: String, required: true },
|
||||
defaultShow: { type: Boolean, default: false },
|
||||
copyButton: { type: Boolean, default: false },
|
||||
fieldId: { type: Number, default: 0 }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showPassword: this.defaultShow || false,
|
||||
};
|
||||
showPassword: this.defaultShow || false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
return {
|
||||
title: this.$pgettext(
|
||||
"Content/Settings/Button.Tooltip/Verb",
|
||||
"Show/hide password"
|
||||
'Content/Settings/Button.Tooltip/Verb',
|
||||
'Show/hide password'
|
||||
),
|
||||
copy: this.$pgettext("*/*/Button.Label/Short, Verb", "Copy"),
|
||||
copy: this.$pgettext('*/*/Button.Label/Short, Verb', 'Copy')
|
||||
}
|
||||
},
|
||||
passwordInputType() {
|
||||
passwordInputType () {
|
||||
if (this.showPassword) {
|
||||
return "text";
|
||||
return 'text'
|
||||
}
|
||||
return "password";
|
||||
return 'password'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import Vue from 'vue'
|
||||
|
||||
Vue.component('human-date', () => import(/* webpackChunkName: "common" */ "@/components/common/HumanDate"))
|
||||
Vue.component('human-duration', () => import(/* webpackChunkName: "common" */ "@/components/common/HumanDuration"))
|
||||
Vue.component('username', () => import(/* webpackChunkName: "common" */ "@/components/common/Username"))
|
||||
Vue.component('user-link', () => import(/* webpackChunkName: "common" */ "@/components/common/UserLink"))
|
||||
Vue.component('actor-link', () => import(/* webpackChunkName: "common" */ "@/components/common/ActorLink"))
|
||||
Vue.component('actor-avatar', () => import(/* webpackChunkName: "common" */ "@/components/common/ActorAvatar"))
|
||||
Vue.component('duration', () => import(/* webpackChunkName: "common" */ "@/components/common/Duration"))
|
||||
Vue.component('dangerous-button', () => import(/* webpackChunkName: "common" */ "@/components/common/DangerousButton"))
|
||||
Vue.component('message', () => import(/* webpackChunkName: "common" */ "@/components/common/Message"))
|
||||
Vue.component('copy-input', () => import(/* webpackChunkName: "common" */ "@/components/common/CopyInput"))
|
||||
Vue.component('ajax-button', () => import(/* webpackChunkName: "common" */ "@/components/common/AjaxButton"))
|
||||
Vue.component('tooltip', () => import(/* webpackChunkName: "common" */ "@/components/common/Tooltip"))
|
||||
Vue.component('empty-state', () => import(/* webpackChunkName: "common" */ "@/components/common/EmptyState"))
|
||||
Vue.component('expandable-div', () => import(/* webpackChunkName: "common" */ "@/components/common/ExpandableDiv"))
|
||||
Vue.component('collapse-link', () => import(/* webpackChunkName: "common" */ "@/components/common/CollapseLink"))
|
||||
Vue.component('action-feedback', () => import(/* webpackChunkName: "common" */ "@/components/common/ActionFeedback"))
|
||||
Vue.component('rendered-description', () => import(/* webpackChunkName: "common" */ "@/components/common/RenderedDescription"))
|
||||
Vue.component('content-form', () => import(/* webpackChunkName: "common" */ "@/components/common/ContentForm"))
|
||||
Vue.component('inline-search-bar', () => import(/* webpackChunkName: "common" */ "@/components/common/InlineSearchBar"))
|
||||
Vue.component('HumanDate', () => import(/* webpackChunkName: "common" */ '@/components/common/HumanDate'))
|
||||
Vue.component('HumanDuration', () => import(/* webpackChunkName: "common" */ '@/components/common/HumanDuration'))
|
||||
Vue.component('Username', () => import(/* webpackChunkName: "common" */ '@/components/common/Username'))
|
||||
Vue.component('UserLink', () => import(/* webpackChunkName: "common" */ '@/components/common/UserLink'))
|
||||
Vue.component('ActorLink', () => import(/* webpackChunkName: "common" */ '@/components/common/ActorLink'))
|
||||
Vue.component('ActorAvatar', () => import(/* webpackChunkName: "common" */ '@/components/common/ActorAvatar'))
|
||||
Vue.component('Duration', () => import(/* webpackChunkName: "common" */ '@/components/common/Duration'))
|
||||
Vue.component('DangerousButton', () => import(/* webpackChunkName: "common" */ '@/components/common/DangerousButton'))
|
||||
Vue.component('Message', () => import(/* webpackChunkName: "common" */ '@/components/common/Message'))
|
||||
Vue.component('CopyInput', () => import(/* webpackChunkName: "common" */ '@/components/common/CopyInput'))
|
||||
Vue.component('AjaxButton', () => import(/* webpackChunkName: "common" */ '@/components/common/AjaxButton'))
|
||||
Vue.component('Tooltip', () => import(/* webpackChunkName: "common" */ '@/components/common/Tooltip'))
|
||||
Vue.component('EmptyState', () => import(/* webpackChunkName: "common" */ '@/components/common/EmptyState'))
|
||||
Vue.component('ExpandableDiv', () => import(/* webpackChunkName: "common" */ '@/components/common/ExpandableDiv'))
|
||||
Vue.component('CollapseLink', () => import(/* webpackChunkName: "common" */ '@/components/common/CollapseLink'))
|
||||
Vue.component('ActionFeedback', () => import(/* webpackChunkName: "common" */ '@/components/common/ActionFeedback'))
|
||||
Vue.component('RenderedDescription', () => import(/* webpackChunkName: "common" */ '@/components/common/RenderedDescription'))
|
||||
Vue.component('ContentForm', () => import(/* webpackChunkName: "common" */ '@/components/common/ContentForm'))
|
||||
Vue.component('InlineSearchBar', () => import(/* webpackChunkName: "common" */ '@/components/common/InlineSearchBar'))
|
||||
|
||||
export default {}
|
||||
|
|
|
@ -1,82 +1,90 @@
|
|||
<template>
|
||||
<main>
|
||||
<div v-if="isLoading" class="ui vertical segment" v-title="labels.title">
|
||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
v-title="labels.title"
|
||||
class="ui vertical segment"
|
||||
>
|
||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']" />
|
||||
</div>
|
||||
<template v-if="object">
|
||||
<section class="ui vertical stripe segment channel-serie">
|
||||
<div class="ui stackable grid container">
|
||||
<div class="ui seven wide column">
|
||||
<div v-if="isSerie" class="padded basic segment">
|
||||
<div class="ui two column grid" v-if="isSerie">
|
||||
<div
|
||||
v-if="isSerie"
|
||||
class="padded basic segment"
|
||||
>
|
||||
<div
|
||||
v-if="isSerie"
|
||||
class="ui two column grid"
|
||||
>
|
||||
<div class="column">
|
||||
<div class="large two-images">
|
||||
<img alt="" class="channel-image" v-if="object.cover && object.cover.urls.original" v-lazy="$store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)">
|
||||
<img alt="" class="channel-image" v-else src="../../assets/audio/default-cover.png">
|
||||
<img alt="" class="channel-image" v-if="object.cover && object.cover.urls.original" v-lazy="$store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)">
|
||||
<img alt="" class="channel-image" v-else src="../../assets/audio/default-cover.png">
|
||||
<img
|
||||
v-if="object.cover && object.cover.urls.original"
|
||||
v-lazy="$store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)"
|
||||
alt=""
|
||||
class="channel-image"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
class="channel-image"
|
||||
src="../../assets/audio/default-cover.png"
|
||||
>
|
||||
<img
|
||||
v-if="object.cover && object.cover.urls.original"
|
||||
v-lazy="$store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)"
|
||||
alt=""
|
||||
class="channel-image"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
class="channel-image"
|
||||
src="../../assets/audio/default-cover.png"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui column right aligned">
|
||||
<tags-list v-if="object.tags && object.tags.length > 0" :tags="object.tags"></tags-list>
|
||||
<div class="ui small hidden divider"></div>
|
||||
<human-duration v-if="totalDuration > 0" :duration="totalDuration"></human-duration>
|
||||
<tags-list
|
||||
v-if="object.tags && object.tags.length > 0"
|
||||
:tags="object.tags"
|
||||
/>
|
||||
<div class="ui small hidden divider" />
|
||||
<human-duration
|
||||
v-if="totalDuration > 0"
|
||||
:duration="totalDuration"
|
||||
/>
|
||||
<template v-if="totalTracks > 0">
|
||||
<div class="ui hidden very small divider"></div>
|
||||
<translate key="1" v-if="isSerie" translate-context="Content/Channel/Paragraph"
|
||||
<div class="ui hidden very small divider" />
|
||||
<translate
|
||||
v-if="isSerie"
|
||||
key="1"
|
||||
translate-context="Content/Channel/Paragraph"
|
||||
translate-plural="%{ count } episodes"
|
||||
:translate-n="totalTracks"
|
||||
:translate-params="{count: totalTracks}">
|
||||
:translate-params="{count: totalTracks}"
|
||||
>
|
||||
%{ count } episode
|
||||
</translate>
|
||||
<translate v-else translate-context="*/*/*" :translate-params="{count: totalTracks}" :translate-n="totalTracks" translate-plural="%{ count } tracks">%{ count } track</translate>
|
||||
</template>
|
||||
<div class="ui small hidden divider"></div>
|
||||
<play-button class="vibrant" :tracks="object.tracks"></play-button>
|
||||
<div class="ui hidden horizontal divider"></div>
|
||||
<album-dropdown
|
||||
:object="object"
|
||||
:public-libraries="publicLibraries"
|
||||
:is-loading="isLoading"
|
||||
:is-album="isAlbum"
|
||||
:is-serie="isSerie"
|
||||
:is-channel="isChannel"
|
||||
:artist="artist"></album-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui small hidden divider"></div>
|
||||
<header>
|
||||
<h2 class="ui header" :title="object.title">
|
||||
{{ object.title }}
|
||||
</h2>
|
||||
<artist-label :artist="artist"></artist-label>
|
||||
</header>
|
||||
</div>
|
||||
<div v-else class="ui center aligned text padded basic segment">
|
||||
<img alt="" class="channel-image" v-if="object.cover && object.cover.urls.original" v-lazy="$store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)">
|
||||
<img alt="" class="channel-image" v-else src="../../assets/audio/default-cover.png">
|
||||
<div class="ui hidden divider"></div>
|
||||
<header>
|
||||
<h2 class="ui header" :title="object.title">
|
||||
{{ object.title }}
|
||||
</h2>
|
||||
<artist-label class="rounded" :artist="artist"></artist-label>
|
||||
</header>
|
||||
<div v-if="object.release_date || (totalTracks > 0)" class="ui small hidden divider"></div>
|
||||
<span v-if="object.release_date">{{ object.release_date | moment('Y') }} · </span>
|
||||
<template v-if="totalTracks > 0">
|
||||
<translate key="1" v-if="isSerie" translate-context="Content/Channel/Paragraph"
|
||||
translate-plural="%{ count } episodes"
|
||||
<translate
|
||||
v-else
|
||||
translate-context="*/*/*"
|
||||
:translate-params="{count: totalTracks}"
|
||||
:translate-n="totalTracks"
|
||||
:translate-params="{count: totalTracks}">
|
||||
%{ count } episode
|
||||
translate-plural="%{ count } tracks"
|
||||
>
|
||||
%{ count } track
|
||||
</translate>
|
||||
<translate v-else translate-context="*/*/*" :translate-params="{count: totalTracks}" :translate-n="totalTracks" translate-plural="%{ count } tracks">%{ count } track</translate> ·
|
||||
</template>
|
||||
<human-duration v-if="totalDuration > 0" :duration="totalDuration"></human-duration>
|
||||
<div class="ui small hidden divider"></div>
|
||||
<play-button class="vibrant" :album="object"></play-button>
|
||||
<div class="ui horizontal hidden divider"></div>
|
||||
<div class="ui small hidden divider" />
|
||||
<play-button
|
||||
class="vibrant"
|
||||
:tracks="object.tracks"
|
||||
/>
|
||||
<div class="ui hidden horizontal divider" />
|
||||
<album-dropdown
|
||||
:object="object"
|
||||
:public-libraries="publicLibraries"
|
||||
|
@ -85,40 +93,151 @@
|
|||
:is-serie="isSerie"
|
||||
:is-channel="isChannel"
|
||||
:artist="artist"
|
||||
></album-dropdown>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui small hidden divider" />
|
||||
<header>
|
||||
<h2
|
||||
class="ui header"
|
||||
:title="object.title"
|
||||
>
|
||||
{{ object.title }}
|
||||
</h2>
|
||||
<artist-label :artist="artist" />
|
||||
</header>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="ui center aligned text padded basic segment"
|
||||
>
|
||||
<img
|
||||
v-if="object.cover && object.cover.urls.original"
|
||||
v-lazy="$store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)"
|
||||
alt=""
|
||||
class="channel-image"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
class="channel-image"
|
||||
src="../../assets/audio/default-cover.png"
|
||||
>
|
||||
<div class="ui hidden divider" />
|
||||
<header>
|
||||
<h2
|
||||
class="ui header"
|
||||
:title="object.title"
|
||||
>
|
||||
{{ object.title }}
|
||||
</h2>
|
||||
<artist-label
|
||||
class="rounded"
|
||||
:artist="artist"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
v-if="object.release_date || (totalTracks > 0)"
|
||||
class="ui small hidden divider"
|
||||
/>
|
||||
<span v-if="object.release_date">{{ object.release_date | moment('Y') }} · </span>
|
||||
<template v-if="totalTracks > 0">
|
||||
<translate
|
||||
v-if="isSerie"
|
||||
key="1"
|
||||
translate-context="Content/Channel/Paragraph"
|
||||
translate-plural="%{ count } episodes"
|
||||
:translate-n="totalTracks"
|
||||
:translate-params="{count: totalTracks}"
|
||||
>
|
||||
%{ count } episode
|
||||
</translate>
|
||||
<translate
|
||||
v-else
|
||||
translate-context="*/*/*"
|
||||
:translate-params="{count: totalTracks}"
|
||||
:translate-n="totalTracks"
|
||||
translate-plural="%{ count } tracks"
|
||||
>
|
||||
%{ count } track
|
||||
</translate> ·
|
||||
</template>
|
||||
<human-duration
|
||||
v-if="totalDuration > 0"
|
||||
:duration="totalDuration"
|
||||
/>
|
||||
<div class="ui small hidden divider" />
|
||||
<play-button
|
||||
class="vibrant"
|
||||
:album="object"
|
||||
/>
|
||||
<div class="ui horizontal hidden divider" />
|
||||
<album-dropdown
|
||||
:object="object"
|
||||
:public-libraries="publicLibraries"
|
||||
:is-loading="isLoading"
|
||||
:is-album="isAlbum"
|
||||
:is-serie="isSerie"
|
||||
:is-channel="isChannel"
|
||||
:artist="artist"
|
||||
/>
|
||||
<div v-if="(object.tags && object.tags.length > 0) || object.description || $store.state.auth.authenticated && object.is_local">
|
||||
<div class="ui small hidden divider"></div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui small hidden divider"></div>
|
||||
<template v-if="object.tags && object.tags.length > 0" >
|
||||
<tags-list :tags="object.tags"></tags-list>
|
||||
<div class="ui small hidden divider"></div>
|
||||
<div class="ui small hidden divider" />
|
||||
<div class="ui divider" />
|
||||
<div class="ui small hidden divider" />
|
||||
<template v-if="object.tags && object.tags.length > 0">
|
||||
<tags-list :tags="object.tags" />
|
||||
<div class="ui small hidden divider" />
|
||||
</template>
|
||||
<rendered-description
|
||||
v-if="object.description"
|
||||
:content="object.description"
|
||||
:can-update="false"></rendered-description>
|
||||
<router-link v-else-if="$store.state.auth.authenticated && object.is_local" :to="{name: 'library.albums.edit', params: {id: object.id }}">
|
||||
<i class="pencil icon"></i>
|
||||
<translate translate-context="Content/*/Button.Label/Verb">Add a description…</translate>
|
||||
:can-update="false"
|
||||
/>
|
||||
<router-link
|
||||
v-else-if="$store.state.auth.authenticated && object.is_local"
|
||||
:to="{name: 'library.albums.edit', params: {id: object.id }}"
|
||||
>
|
||||
<i class="pencil icon" />
|
||||
<translate translate-context="Content/*/Button.Label/Verb">
|
||||
Add a description…
|
||||
</translate>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="isSerie">
|
||||
<div class="ui hidden divider"></div>
|
||||
<div class="ui hidden divider" />
|
||||
<rendered-description
|
||||
v-if="object.description"
|
||||
:content="object.description"
|
||||
:can-update="false"></rendered-description>
|
||||
<router-link v-else-if="$store.state.auth.authenticated && object.is_local" :to="{name: 'library.albums.edit', params: {id: object.id }}">
|
||||
<i class="pencil icon"></i>
|
||||
<translate translate-context="Content/*/Button.Label/Verb">Add a description…</translate>
|
||||
:can-update="false"
|
||||
/>
|
||||
<router-link
|
||||
v-else-if="$store.state.auth.authenticated && object.is_local"
|
||||
:to="{name: 'library.albums.edit', params: {id: object.id }}"
|
||||
>
|
||||
<i class="pencil icon" />
|
||||
<translate translate-context="Content/*/Button.Label/Verb">
|
||||
Add a description…
|
||||
</translate>
|
||||
</router-link>
|
||||
|
||||
</template>
|
||||
</div>
|
||||
<div class="nine wide column">
|
||||
<router-view v-if="object" :paginate-by="paginateBy" :page="page" :total-tracks="totalTracks" :is-serie="isSerie" :artist="artist" :discs="discs" @libraries-loaded="libraries = $event" :object="object" object-type="album" :key="$route.fullPath" @page-changed="page = $event"></router-view>
|
||||
<router-view
|
||||
v-if="object"
|
||||
:key="$route.fullPath"
|
||||
:paginate-by="paginateBy"
|
||||
:page="page"
|
||||
:total-tracks="totalTracks"
|
||||
:is-serie="isSerie"
|
||||
:artist="artist"
|
||||
:discs="discs"
|
||||
:object="object"
|
||||
object-type="album"
|
||||
@libraries-loaded="libraries = $event"
|
||||
@page-changed="page = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -127,17 +246,17 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios"
|
||||
import lodash from "@/lodash"
|
||||
import PlayButton from "@/components/audio/PlayButton"
|
||||
import TagsList from "@/components/tags/List"
|
||||
import axios from 'axios'
|
||||
import lodash from '@/lodash'
|
||||
import PlayButton from '@/components/audio/PlayButton'
|
||||
import TagsList from '@/components/tags/List'
|
||||
import ArtistLabel from '@/components/audio/ArtistLabel'
|
||||
import AlbumDropdown from './AlbumDropdown'
|
||||
|
||||
function groupByDisc(initial) {
|
||||
function inner(acc, track) {
|
||||
var dn = track.disc_number - initial
|
||||
if (acc[dn] == undefined) {
|
||||
function groupByDisc (initial) {
|
||||
function inner (acc, track) {
|
||||
const dn = track.disc_number - initial
|
||||
if (acc[dn] === undefined) {
|
||||
acc.push([track])
|
||||
} else {
|
||||
acc[dn].push(track)
|
||||
|
@ -148,14 +267,14 @@ function groupByDisc(initial) {
|
|||
}
|
||||
|
||||
export default {
|
||||
props: ["id"],
|
||||
components: {
|
||||
PlayButton,
|
||||
TagsList,
|
||||
ArtistLabel,
|
||||
AlbumDropdown
|
||||
},
|
||||
data() {
|
||||
props: { id: { type: Number, required: true } },
|
||||
data () {
|
||||
return {
|
||||
isLoading: true,
|
||||
object: null,
|
||||
|
@ -163,39 +282,7 @@ export default {
|
|||
discs: [],
|
||||
libraries: [],
|
||||
page: 1,
|
||||
paginateBy: 50,
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
await this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
async fetchData() {
|
||||
this.isLoading = true
|
||||
let tracksResponse = axios.get(`tracks/`, {params: {ordering: 'disc_number,position', album: this.id, page_size: this.paginateBy, page:this.page, include_channels: 'true', playable: 'true'}})
|
||||
let albumResponse = await axios.get(`albums/${this.id}/`, {params: {refresh: 'true'}})
|
||||
let artistResponse = await axios.get(`artists/${albumResponse.data.artist.id}/`)
|
||||
this.artist = artistResponse.data
|
||||
if (this.artist.channel) {
|
||||
this.artist.channel.artist = this.artist
|
||||
}
|
||||
tracksResponse = await tracksResponse
|
||||
this.object = albumResponse.data
|
||||
this.object.tracks = tracksResponse.data.results
|
||||
this.discs = this.object.tracks.reduce(groupByDisc(this.object.tracks[0].disc_number), [])
|
||||
this.isLoading = false
|
||||
},
|
||||
remove () {
|
||||
let self = this
|
||||
self.isLoading = true
|
||||
axios.delete(`albums/${this.object.id}`).then((response) => {
|
||||
self.isLoading = false
|
||||
self.$emit('deleted')
|
||||
self.$router.push({name: 'library.artists.detail', params: {id: this.artist.id}})
|
||||
}, error => {
|
||||
self.isLoading = false
|
||||
self.errors = error.backendErrors
|
||||
})
|
||||
paginateBy: 50
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -212,7 +299,7 @@ export default {
|
|||
return this.object.artist.content_category === 'music'
|
||||
},
|
||||
totalDuration () {
|
||||
let durations = [0]
|
||||
const durations = [0]
|
||||
this.object.tracks.forEach((t) => {
|
||||
if (t.uploads[0] && t.uploads[0].duration) {
|
||||
durations.push(t.uploads[0].duration)
|
||||
|
@ -220,24 +307,56 @@ export default {
|
|||
})
|
||||
return lodash.sum(durations)
|
||||
},
|
||||
labels() {
|
||||
labels () {
|
||||
return {
|
||||
title: this.$pgettext('*/*/*', 'Album'),
|
||||
title: this.$pgettext('*/*/*', 'Album')
|
||||
}
|
||||
},
|
||||
publicLibraries () {
|
||||
return this.libraries.filter(l => {
|
||||
return l.privacy_level === 'everyone'
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
id() {
|
||||
id () {
|
||||
this.fetchData()
|
||||
},
|
||||
page() {
|
||||
page () {
|
||||
this.fetchData()
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
await this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
async fetchData () {
|
||||
this.isLoading = true
|
||||
let tracksResponse = axios.get('tracks/', { params: { ordering: 'disc_number,position', album: this.id, page_size: this.paginateBy, page: this.page, include_channels: 'true', playable: 'true' } })
|
||||
const albumResponse = await axios.get(`albums/${this.id}/`, { params: { refresh: 'true' } })
|
||||
const artistResponse = await axios.get(`artists/${albumResponse.data.artist.id}/`)
|
||||
this.artist = artistResponse.data
|
||||
if (this.artist.channel) {
|
||||
this.artist.channel.artist = this.artist
|
||||
}
|
||||
tracksResponse = await tracksResponse
|
||||
this.object = albumResponse.data
|
||||
this.object.tracks = tracksResponse.data.results
|
||||
this.discs = this.object.tracks.reduce(groupByDisc(this.object.tracks[0].disc_number), [])
|
||||
this.isLoading = false
|
||||
},
|
||||
remove () {
|
||||
const self = this
|
||||
self.isLoading = true
|
||||
axios.delete(`albums/${this.object.id}`).then((response) => {
|
||||
self.isLoading = false
|
||||
self.$emit('deleted')
|
||||
self.$router.push({ name: 'library.artists.detail', params: { id: this.artist.id } })
|
||||
}, error => {
|
||||
self.isLoading = false
|
||||
self.errors = error.backendErrors
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,20 +1,44 @@
|
|||
<template>
|
||||
<div v-if="object">
|
||||
<h2 class="ui header">
|
||||
<translate key="1" v-if="isSerie" translate-context="Content/Channels/*">Episodes</translate>
|
||||
<translate key="2" v-else translate-context="*/*/*">Tracks</translate>
|
||||
<translate
|
||||
v-if="isSerie"
|
||||
key="1"
|
||||
translate-context="Content/Channels/*"
|
||||
>
|
||||
Episodes
|
||||
</translate>
|
||||
<translate
|
||||
v-else
|
||||
key="2"
|
||||
translate-context="*/*/*"
|
||||
>
|
||||
Tracks
|
||||
</translate>
|
||||
</h2>
|
||||
<channel-entries v-if="artist.channel && isSerie" :is-podcast="isSerie" :limit="50" :filters="{channel: artist.channel.uuid, album: object.id, ordering: '-creation_date'}">
|
||||
</channel-entries>
|
||||
<channel-entries
|
||||
v-if="artist.channel && isSerie"
|
||||
:is-podcast="isSerie"
|
||||
:limit="50"
|
||||
:filters="{channel: artist.channel.uuid, album: object.id, ordering: '-creation_date'}"
|
||||
/>
|
||||
<template v-else-if="discs && discs.length > 1">
|
||||
<div v-for="tracks in discs" :key="tracks.disc_number">
|
||||
<div class="ui hidden divider"></div>
|
||||
<play-button class="right floated mini inverted vibrant" :tracks="tracks"></play-button>
|
||||
<div
|
||||
v-for="tracks in discs"
|
||||
:key="tracks.disc_number"
|
||||
>
|
||||
<div class="ui hidden divider" />
|
||||
<play-button
|
||||
class="right floated mini inverted vibrant"
|
||||
:tracks="tracks"
|
||||
/>
|
||||
<translate
|
||||
tag="h3"
|
||||
:translate-params="{number: tracks[0].disc_number}"
|
||||
translate-context="Content/Album/"
|
||||
>Volume %{ number }</translate>
|
||||
>
|
||||
Volume %{ number }
|
||||
</translate>
|
||||
<track-table
|
||||
:is-album="true"
|
||||
:tracks="object.tracks"
|
||||
|
@ -26,8 +50,8 @@
|
|||
:total="totalTracks"
|
||||
:paginate-by="paginateBy"
|
||||
:page="page"
|
||||
@page-changed="updatePage">
|
||||
</track-table>
|
||||
@page-changed="updatePage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
|
@ -42,15 +66,25 @@
|
|||
:total="totalTracks"
|
||||
:paginate-by="paginateBy"
|
||||
:page="page"
|
||||
@page-changed="updatePage">
|
||||
</track-table>
|
||||
@page-changed="updatePage"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="!artist.channel && !isSerie">
|
||||
<h2>
|
||||
<translate translate-context="Content/*/Title/Noun">User libraries</translate>
|
||||
<translate translate-context="Content/*/Title/Noun">
|
||||
User libraries
|
||||
</translate>
|
||||
</h2>
|
||||
<library-widget @loaded="$emit('libraries-loaded', $event)" :url="'albums/' + object.id + '/libraries/'">
|
||||
<translate slot="subtitle" translate-context="Content/Album/Paragraph">This album is present in the following libraries:</translate>
|
||||
<library-widget
|
||||
:url="'albums/' + object.id + '/libraries/'"
|
||||
@loaded="$emit('libraries-loaded', $event)"
|
||||
>
|
||||
<translate
|
||||
slot="subtitle"
|
||||
translate-context="Content/Album/Paragraph"
|
||||
>
|
||||
This album is present in the following libraries:
|
||||
</translate>
|
||||
</library-widget>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -58,30 +92,38 @@
|
|||
|
||||
<script>
|
||||
|
||||
import time from "@/utils/time"
|
||||
import LibraryWidget from "@/components/federation/LibraryWidget"
|
||||
import time from '@/utils/time'
|
||||
import LibraryWidget from '@/components/federation/LibraryWidget'
|
||||
import ChannelEntries from '@/components/audio/ChannelEntries'
|
||||
import TrackTable from '@/components/audio/track/Table'
|
||||
import PlayButton from "@/components/audio/PlayButton"
|
||||
import PlayButton from '@/components/audio/PlayButton'
|
||||
|
||||
export default {
|
||||
props: ["object", "libraries", "discs", "isSerie", "artist", "page", "paginateBy", "totalTracks"],
|
||||
components: {
|
||||
LibraryWidget,
|
||||
TrackTable,
|
||||
ChannelEntries,
|
||||
PlayButton
|
||||
},
|
||||
data() {
|
||||
props: {
|
||||
object: { type: Object, required: true },
|
||||
discs: { type: Array, required: true },
|
||||
isSerie: { type: Boolean, required: true },
|
||||
artist: { type: Object, required: true },
|
||||
page: { type: Number, required: true },
|
||||
paginateBy: { type: Number, required: true },
|
||||
totalTracks: { type: Number, required: true }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
time,
|
||||
id: this.object.id,
|
||||
id: this.object.id
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updatePage: function(page) {
|
||||
updatePage: function (page) {
|
||||
this.$emit('page-changed', page)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
<template>
|
||||
<span>
|
||||
|
||||
<modal v-if="isEmbedable" :show.sync="showEmbedModal">
|
||||
<modal
|
||||
v-if="isEmbedable"
|
||||
:show.sync="showEmbedModal"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate translate-context="Popup/Album/Title/Verb">Embed this album on your website</translate>
|
||||
</h4>
|
||||
<div class="scrolling content">
|
||||
<div class="description">
|
||||
<embed-wizard type="album" :id="object.id" />
|
||||
<embed-wizard
|
||||
:id="object.id"
|
||||
type="album"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@ -17,46 +23,69 @@
|
|||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
<button class="ui floating dropdown circular icon basic button" :title="labels.more" v-dropdown="{direction: 'downward'}">
|
||||
<i class="ellipsis vertical icon"></i>
|
||||
<button
|
||||
v-dropdown="{direction: 'downward'}"
|
||||
class="ui floating dropdown circular icon basic button"
|
||||
:title="labels.more"
|
||||
>
|
||||
<i class="ellipsis vertical icon" />
|
||||
<div class="menu">
|
||||
<a
|
||||
:href="object.fid"
|
||||
v-if="domain != $store.getters['instance/domain']"
|
||||
:href="object.fid"
|
||||
target="_blank"
|
||||
class="basic item">
|
||||
<i class="external icon"></i>
|
||||
<translate :translate-params="{domain: domain}" translate-context="Content/*/Button.Label/Verb">View on %{ domain }</translate>
|
||||
class="basic item"
|
||||
>
|
||||
<i class="external icon" />
|
||||
<translate
|
||||
:translate-params="{domain: domain}"
|
||||
translate-context="Content/*/Button.Label/Verb"
|
||||
>View on %{ domain }</translate>
|
||||
</a>
|
||||
|
||||
<div
|
||||
role="button"
|
||||
v-if="isEmbedable"
|
||||
role="button"
|
||||
class="basic item"
|
||||
@click="showEmbedModal = !showEmbedModal"
|
||||
class="basic item">
|
||||
<i class="code icon"></i>
|
||||
>
|
||||
<i class="code icon" />
|
||||
<translate translate-context="Content/*/Button.Label/Verb">Embed</translate>
|
||||
</div>
|
||||
<a v-if="isAlbum && musicbrainzUrl" :href="musicbrainzUrl" target="_blank" rel="noreferrer noopener" class="basic item">
|
||||
<i class="external icon"></i>
|
||||
<a
|
||||
v-if="isAlbum && musicbrainzUrl"
|
||||
:href="musicbrainzUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
class="basic item"
|
||||
>
|
||||
<i class="external icon" />
|
||||
<translate translate-context="Content/*/*/Clickable, Verb">View on MusicBrainz</translate>
|
||||
</a>
|
||||
<a v-if="!isChannel && isAlbum" :href="discogsUrl" target="_blank" rel="noreferrer noopener" class="basic item">
|
||||
<i class="external icon"></i>
|
||||
<a
|
||||
v-if="!isChannel && isAlbum"
|
||||
:href="discogsUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
class="basic item"
|
||||
>
|
||||
<i class="external icon" />
|
||||
<translate translate-context="Content/*/Button.Label/Verb">Search on Discogs</translate>
|
||||
</a>
|
||||
<router-link
|
||||
v-if="object.is_local"
|
||||
:to="{name: 'library.albums.edit', params: {id: object.id }}"
|
||||
class="basic item">
|
||||
<i class="edit icon"></i>
|
||||
class="basic item"
|
||||
>
|
||||
<i class="edit icon" />
|
||||
<translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
|
||||
</router-link>
|
||||
<dangerous-button
|
||||
:class="['ui', {loading: isLoading}, 'item']"
|
||||
v-if="artist && $store.state.auth.authenticated && artist.channel && artist.attributed_to.full_username === $store.state.auth.fullUsername"
|
||||
@confirm="remove()">
|
||||
<i class="ui trash icon"></i>
|
||||
:class="['ui', {loading: isLoading}, 'item']"
|
||||
@confirm="remove()"
|
||||
>
|
||||
<i class="ui trash icon" />
|
||||
<translate translate-context="*/*/*/Verb">Delete…</translate>
|
||||
<p slot="modal-header"><translate translate-context="Popup/Channel/Title">Delete this album?</translate></p>
|
||||
<div slot="modal-content">
|
||||
|
@ -64,26 +93,33 @@
|
|||
</div>
|
||||
<p slot="modal-confirm"><translate translate-context="*/*/*/Verb">Delete</translate></p>
|
||||
</dangerous-button>
|
||||
<div class="divider"></div>
|
||||
<div class="divider" />
|
||||
<div
|
||||
role="button"
|
||||
class="basic item"
|
||||
v-for="obj in getReportableObjs({album: object, channel: artist.channel})"
|
||||
:key="obj.target.type + obj.target.id"
|
||||
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)">
|
||||
role="button"
|
||||
class="basic item"
|
||||
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)"
|
||||
>
|
||||
<i class="share icon" /> {{ obj.label }}
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<router-link class="basic item" v-if="$store.state.auth.availablePermissions['library']" :to="{name: 'manage.library.albums.detail', params: {id: object.id}}">
|
||||
<i class="wrench icon"></i>
|
||||
<div class="divider" />
|
||||
<router-link
|
||||
v-if="$store.state.auth.availablePermissions['library']"
|
||||
class="basic item"
|
||||
:to="{name: 'manage.library.albums.detail', params: {id: object.id}}"
|
||||
>
|
||||
<i class="wrench icon" />
|
||||
<translate translate-context="Content/Moderation/Link">Open in moderation interface</translate>
|
||||
</router-link>
|
||||
<a
|
||||
v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser"
|
||||
class="basic item"
|
||||
:href="$store.getters['instance/absoluteUrl'](`/api/admin/music/album/${object.id}`)"
|
||||
target="_blank" rel="noopener noreferrer">
|
||||
<i class="wrench icon"></i>
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i class="wrench icon" />
|
||||
<translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -91,30 +127,30 @@
|
|||
</span>
|
||||
</template>
|
||||
<script>
|
||||
import EmbedWizard from "@/components/audio/EmbedWizard"
|
||||
import EmbedWizard from '@/components/audio/EmbedWizard'
|
||||
import Modal from '@/components/semantic/Modal'
|
||||
import ReportMixin from '@/components/mixins/Report'
|
||||
|
||||
import {getDomain} from '@/utils'
|
||||
import { getDomain } from '@/utils'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmbedWizard,
|
||||
Modal
|
||||
},
|
||||
mixins: [ReportMixin],
|
||||
props: {
|
||||
isLoading: Boolean,
|
||||
artist: Object,
|
||||
object: Object,
|
||||
publicLibraries: Array,
|
||||
artist: { type: Object, required: true },
|
||||
object: { type: Object, required: true },
|
||||
publicLibraries: { type: Array, required: true },
|
||||
isAlbum: Boolean,
|
||||
isChannel: Boolean,
|
||||
isSerie: Boolean,
|
||||
},
|
||||
components: {
|
||||
EmbedWizard,
|
||||
Modal,
|
||||
isSerie: Boolean
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showEmbedModal: false,
|
||||
showEmbedModal: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -122,28 +158,30 @@ export default {
|
|||
if (this.object) {
|
||||
return getDomain(this.object.fid)
|
||||
}
|
||||
return null
|
||||
},
|
||||
labels() {
|
||||
labels () {
|
||||
return {
|
||||
more: this.$pgettext('*/*/Button.Label/Noun', "More…"),
|
||||
more: this.$pgettext('*/*/Button.Label/Noun', 'More…')
|
||||
}
|
||||
},
|
||||
isEmbedable () {
|
||||
return (this.isChannel && this.artist.channel.actor) || this.publicLibraries.length > 0
|
||||
},
|
||||
|
||||
musicbrainzUrl() {
|
||||
musicbrainzUrl () {
|
||||
if (this.object.mbid) {
|
||||
return "https://musicbrainz.org/release/" + this.object.mbid
|
||||
return 'https://musicbrainz.org/release/' + this.object.mbid
|
||||
}
|
||||
return null
|
||||
},
|
||||
discogsUrl() {
|
||||
discogsUrl () {
|
||||
return (
|
||||
"https://discogs.com/search/?type=release&title=" +
|
||||
encodeURI(this.object.title) + "&artist=" +
|
||||
'https://discogs.com/search/?type=release&title=' +
|
||||
encodeURI(this.object.title) + '&artist=' +
|
||||
encodeURI(this.object.artist.name)
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,37 +1,56 @@
|
|||
<template>
|
||||
|
||||
<section class="ui vertical stripe segment">
|
||||
<div class="ui text container">
|
||||
<h2>
|
||||
<translate v-if="canEdit" key="1" translate-context="Content/*/Title">Edit this album</translate>
|
||||
<translate v-else key="2" translate-context="Content/*/Title">Suggest an edit on this album</translate>
|
||||
<translate
|
||||
v-if="canEdit"
|
||||
key="1"
|
||||
translate-context="Content/*/Title"
|
||||
>
|
||||
Edit this album
|
||||
</translate>
|
||||
<translate
|
||||
v-else
|
||||
key="2"
|
||||
translate-context="Content/*/Title"
|
||||
>
|
||||
Suggest an edit on this album
|
||||
</translate>
|
||||
</h2>
|
||||
<div class="ui message" v-if="!object.is_local">
|
||||
<translate translate-context="Content/*/Message">This object is managed by another server, you cannot edit it.</translate>
|
||||
<div
|
||||
v-if="!object.is_local"
|
||||
class="ui message"
|
||||
>
|
||||
<translate translate-context="Content/*/Message">
|
||||
This object is managed by another server, you cannot edit it.
|
||||
</translate>
|
||||
</div>
|
||||
<edit-form
|
||||
v-else
|
||||
:object-type="objectType"
|
||||
:object="object"
|
||||
:can-edit="canEdit"></edit-form>
|
||||
:can-edit="canEdit"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios"
|
||||
|
||||
import EditForm from '@/components/library/EditForm'
|
||||
export default {
|
||||
props: ["objectType", "object", "libraries"],
|
||||
data() {
|
||||
return {
|
||||
id: this.object.id,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
EditForm
|
||||
},
|
||||
props: {
|
||||
objectType: { type: String, required: true },
|
||||
object: { type: Object, required: true },
|
||||
libraries: { type: Array, required: true }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
id: this.object.id
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
canEdit () {
|
||||
return true
|
||||
|
|
|
@ -2,69 +2,121 @@
|
|||
<main v-title="labels.title">
|
||||
<section class="ui vertical stripe segment">
|
||||
<h2 class="ui header">
|
||||
<translate translate-context="Content/Album/Title">Browsing albums</translate>
|
||||
<translate translate-context="Content/Album/Title">
|
||||
Browsing albums
|
||||
</translate>
|
||||
</h2>
|
||||
<form :class="['ui', {'loading': isLoading}, 'form']" @submit.prevent="updatePage();updateQueryString();fetchData()">
|
||||
<form
|
||||
:class="['ui', {'loading': isLoading}, 'form']"
|
||||
@submit.prevent="updatePage();updateQueryString();fetchData()"
|
||||
>
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<label for="albums-search">
|
||||
<translate translate-context="Content/Search/Input.Label/Noun">Search</translate>
|
||||
</label>
|
||||
<div class="ui action input">
|
||||
<input id="albums-search" type="text" name="search" v-model="query" :placeholder="labels.searchPlaceholder"/>
|
||||
<button class="ui icon button" type="submit" :aria-label="$pgettext('Content/Search/Input.Label/Noun', 'Search')">
|
||||
<i class="search icon"></i>
|
||||
<input
|
||||
id="albums-search"
|
||||
v-model="query"
|
||||
type="text"
|
||||
name="search"
|
||||
:placeholder="labels.searchPlaceholder"
|
||||
>
|
||||
<button
|
||||
class="ui icon button"
|
||||
type="submit"
|
||||
:aria-label="$pgettext('Content/Search/Input.Label/Noun', 'Search')"
|
||||
>
|
||||
<i class="search icon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="tags-search"><translate translate-context="*/*/*/Noun">Tags</translate></label>
|
||||
<tags-selector v-model="tags"></tags-selector>
|
||||
<tags-selector v-model="tags" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="album-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label>
|
||||
<select id="album-ordering" class="ui dropdown" v-model="ordering">
|
||||
<option v-for="option in orderingOptions" :value="option[0]">
|
||||
<select
|
||||
id="album-ordering"
|
||||
v-model="ordering"
|
||||
class="ui dropdown"
|
||||
>
|
||||
<option
|
||||
v-for="(option, key) in orderingOptions"
|
||||
:key="key"
|
||||
:value="option[0]"
|
||||
>
|
||||
{{ sharedLabels.filters[option[1]] }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="album-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label>
|
||||
<select id="album-ordering-direction" class="ui dropdown" v-model="orderingDirection">
|
||||
<option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option>
|
||||
<option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option>
|
||||
<select
|
||||
id="album-ordering-direction"
|
||||
v-model="orderingDirection"
|
||||
class="ui dropdown"
|
||||
>
|
||||
<option value="+">
|
||||
<translate translate-context="Content/Search/Dropdown">
|
||||
Ascending
|
||||
</translate>
|
||||
</option>
|
||||
<option value="-">
|
||||
<translate translate-context="Content/Search/Dropdown">
|
||||
Descending
|
||||
</translate>
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="album-results"><translate translate-context="Content/Search/Dropdown.Label/Noun">Results per page</translate></label>
|
||||
<select id="album-results" class="ui dropdown" v-model="paginateBy">
|
||||
<option :value="parseInt(12)">12</option>
|
||||
<option :value="parseInt(25)">25</option>
|
||||
<option :value="parseInt(50)">50</option>
|
||||
<select
|
||||
id="album-results"
|
||||
v-model="paginateBy"
|
||||
class="ui dropdown"
|
||||
>
|
||||
<option :value="parseInt(12)">
|
||||
12
|
||||
</option>
|
||||
<option :value="parseInt(25)">
|
||||
25
|
||||
</option>
|
||||
<option :value="parseInt(50)">
|
||||
50
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="ui hidden divider"></div>
|
||||
<div class="ui hidden divider" />
|
||||
<div
|
||||
v-if="result"
|
||||
transition-duration="0"
|
||||
item-selector=".column"
|
||||
percent-position="true"
|
||||
stagger="0"
|
||||
class="">
|
||||
class=""
|
||||
>
|
||||
<div
|
||||
v-if="result.results.length > 0"
|
||||
class="ui app-cards cards">
|
||||
class="ui app-cards cards"
|
||||
>
|
||||
<album-card
|
||||
v-for="album in result.results"
|
||||
:key="album.id"
|
||||
:album="album"></album-card>
|
||||
:album="album"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="ui placeholder segment sixteen wide column" style="text-align: center; display: flex; align-items: center">
|
||||
<div
|
||||
v-else
|
||||
class="ui placeholder segment sixteen wide column"
|
||||
style="text-align: center; display: flex; align-items: center"
|
||||
>
|
||||
<div class="ui icon header">
|
||||
<i class="compact disc icon"></i>
|
||||
<i class="compact disc icon" />
|
||||
<translate translate-context="Content/Albums/Placeholder">
|
||||
No results matching your query
|
||||
</translate>
|
||||
|
@ -72,8 +124,9 @@
|
|||
<router-link
|
||||
v-if="$store.state.auth.authenticated"
|
||||
:to="{name: 'content.index'}"
|
||||
class="ui success button labeled icon">
|
||||
<i class="upload icon"></i>
|
||||
class="ui success button labeled icon"
|
||||
>
|
||||
<i class="upload icon" />
|
||||
<translate translate-context="Content/*/Verb">
|
||||
Add some music
|
||||
</translate>
|
||||
|
@ -83,11 +136,11 @@
|
|||
<div class="ui center aligned basic segment">
|
||||
<pagination
|
||||
v-if="result && result.count > paginateBy"
|
||||
@page-changed="selectPage"
|
||||
:current="page"
|
||||
:paginate-by="paginateBy"
|
||||
:total="result.count"
|
||||
></pagination>
|
||||
@page-changed="selectPage"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
@ -95,61 +148,69 @@
|
|||
|
||||
<script>
|
||||
import qs from 'qs'
|
||||
import axios from "axios"
|
||||
import _ from "@/lodash"
|
||||
import $ from "jquery"
|
||||
import axios from 'axios'
|
||||
import $ from 'jquery'
|
||||
|
||||
import logger from "@/logging"
|
||||
import logger from '@/logging'
|
||||
|
||||
import OrderingMixin from "@/components/mixins/Ordering"
|
||||
import PaginationMixin from "@/components/mixins/Pagination"
|
||||
import TranslationsMixin from "@/components/mixins/Translations"
|
||||
import AlbumCard from "@/components/audio/album/Card"
|
||||
import Pagination from "@/components/Pagination"
|
||||
import OrderingMixin from '@/components/mixins/Ordering'
|
||||
import PaginationMixin from '@/components/mixins/Pagination'
|
||||
import TranslationsMixin from '@/components/mixins/Translations'
|
||||
import AlbumCard from '@/components/audio/album/Card'
|
||||
import Pagination from '@/components/Pagination'
|
||||
import TagsSelector from '@/components/library/TagsSelector'
|
||||
|
||||
const FETCH_URL = "albums/"
|
||||
const FETCH_URL = 'albums/'
|
||||
|
||||
export default {
|
||||
mixins: [OrderingMixin, PaginationMixin, TranslationsMixin],
|
||||
props: {
|
||||
defaultQuery: { type: String, required: false, default: "" },
|
||||
defaultTags: { type: Array, required: false, default: () => { return [] } },
|
||||
scope: { type: String, required: false, default: "all" },
|
||||
},
|
||||
components: {
|
||||
AlbumCard,
|
||||
Pagination,
|
||||
TagsSelector,
|
||||
TagsSelector
|
||||
},
|
||||
data() {
|
||||
mixins: [OrderingMixin, PaginationMixin, TranslationsMixin],
|
||||
props: {
|
||||
defaultQuery: { type: String, required: false, default: '' },
|
||||
defaultTags: { type: Array, required: false, default: () => { return [] } },
|
||||
scope: { type: String, required: false, default: 'all' }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isLoading: true,
|
||||
result: null,
|
||||
page: parseInt(this.defaultPage),
|
||||
query: this.defaultQuery,
|
||||
tags: (this.defaultTags || []).filter((t) => { return t.length > 0 }),
|
||||
orderingOptions: [["creation_date", "creation_date"], ["title", "album_title"],["release_date","release_date"]]
|
||||
orderingOptions: [['creation_date', 'creation_date'], ['title', 'album_title'], ['release_date', 'release_date']]
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchData()
|
||||
},
|
||||
mounted() {
|
||||
$(".ui.dropdown").dropdown()
|
||||
},
|
||||
computed: {
|
||||
labels() {
|
||||
let searchPlaceholder = this.$pgettext('Content/Search/Input.Placeholder', "Enter album title…")
|
||||
let title = this.$pgettext('*/*/*', "Albums")
|
||||
labels () {
|
||||
const searchPlaceholder = this.$pgettext('Content/Search/Input.Placeholder', 'Enter album title…')
|
||||
const title = this.$pgettext('*/*/*', 'Albums')
|
||||
return {
|
||||
searchPlaceholder,
|
||||
title
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
page () {
|
||||
this.updateQueryString()
|
||||
this.fetchData()
|
||||
},
|
||||
'$store.state.moderation.lastUpdate': function () {
|
||||
this.fetchData()
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchData()
|
||||
},
|
||||
mounted () {
|
||||
$('.ui.dropdown').dropdown()
|
||||
},
|
||||
methods: {
|
||||
updateQueryString: function() {
|
||||
updateQueryString: function () {
|
||||
history.pushState(
|
||||
{},
|
||||
null,
|
||||
|
@ -163,53 +224,44 @@ export default {
|
|||
}).toString()
|
||||
)
|
||||
},
|
||||
fetchData: function() {
|
||||
var self = this
|
||||
fetchData: function () {
|
||||
const self = this
|
||||
this.isLoading = true
|
||||
let url = FETCH_URL
|
||||
let params = {
|
||||
const url = FETCH_URL
|
||||
const params = {
|
||||
scope: this.scope,
|
||||
page: this.page,
|
||||
page_size: this.paginateBy,
|
||||
q: this.query,
|
||||
ordering: this.getOrderingAsString(),
|
||||
playable: "true",
|
||||
playable: 'true',
|
||||
tag: this.tags,
|
||||
include_channels: "true",
|
||||
content_category: "music"
|
||||
include_channels: 'true',
|
||||
content_category: 'music'
|
||||
}
|
||||
logger.default.debug("Fetching albums")
|
||||
logger.default.debug('Fetching albums')
|
||||
axios.get(
|
||||
url,
|
||||
{
|
||||
params: params,
|
||||
paramsSerializer: function(params) {
|
||||
paramsSerializer: function (params) {
|
||||
return qs.stringify(params, { indices: false })
|
||||
}
|
||||
}
|
||||
).then(response => {
|
||||
self.result = response.data
|
||||
self.isLoading = false
|
||||
}, error => {
|
||||
}, () => {
|
||||
self.result = null
|
||||
self.isLoading = false
|
||||
})
|
||||
},
|
||||
selectPage: function(page) {
|
||||
selectPage: function (page) {
|
||||
this.page = page
|
||||
},
|
||||
updatePage() {
|
||||
updatePage () {
|
||||
this.page = this.defaultPage
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
page() {
|
||||
this.updateQueryString()
|
||||
this.fetchData()
|
||||
},
|
||||
"$store.state.moderation.lastUpdate": function () {
|
||||
this.fetchData()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,119 +1,195 @@
|
|||
<template>
|
||||
<main v-title="labels.title">
|
||||
<div v-if="isLoading" class="ui vertical segment">
|
||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui vertical segment"
|
||||
>
|
||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']" />
|
||||
</div>
|
||||
<template v-if="object && !isLoading">
|
||||
<section :class="['ui', 'head', {'with-background': cover}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" :style="headerStyle" v-title="object.name">
|
||||
<section
|
||||
v-title="object.name"
|
||||
:class="['ui', 'head', {'with-background': cover}, 'vertical', 'center', 'aligned', 'stripe', 'segment']"
|
||||
:style="headerStyle"
|
||||
>
|
||||
<div class="segment-content">
|
||||
<h2 class="ui center aligned icon header">
|
||||
<i class="circular inverted users violet icon"></i>
|
||||
<i class="circular inverted users violet icon" />
|
||||
<div class="content">
|
||||
{{ object.name }}
|
||||
<div class="sub header" v-if="albums">
|
||||
<translate translate-context="Content/Artist/Paragraph"
|
||||
<div
|
||||
v-if="albums"
|
||||
class="sub header"
|
||||
>
|
||||
<translate
|
||||
translate-context="Content/Artist/Paragraph"
|
||||
tag="div"
|
||||
translate-plural="%{ count } tracks in %{ albumsCount } albums"
|
||||
:translate-n="totalTracks"
|
||||
:translate-params="{count: totalTracks, albumsCount: totalAlbums}">
|
||||
:translate-params="{count: totalTracks, albumsCount: totalAlbums}"
|
||||
>
|
||||
%{ count } track in %{ albumsCount } albums
|
||||
</translate>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
<tags-list v-if="object.tags && object.tags.length > 0" :tags="object.tags"></tags-list>
|
||||
<div class="ui hidden divider"></div>
|
||||
<tags-list
|
||||
v-if="object.tags && object.tags.length > 0"
|
||||
:tags="object.tags"
|
||||
/>
|
||||
<div class="ui hidden divider" />
|
||||
<div class="header-buttons">
|
||||
<div class="ui buttons">
|
||||
<radio-button type="artist" :object-id="object.id"></radio-button>
|
||||
|
||||
<radio-button
|
||||
type="artist"
|
||||
:object-id="object.id"
|
||||
/>
|
||||
</div>
|
||||
<div class="ui buttons">
|
||||
<play-button :is-playable="isPlayable" class="vibrant" :artist="object">
|
||||
<translate translate-context="Content/Artist/Button.Label/Verb">Play all albums</translate>
|
||||
<play-button
|
||||
:is-playable="isPlayable"
|
||||
class="vibrant"
|
||||
:artist="object"
|
||||
>
|
||||
<translate translate-context="Content/Artist/Button.Label/Verb">
|
||||
Play all albums
|
||||
</translate>
|
||||
</play-button>
|
||||
</div>
|
||||
|
||||
<modal :show.sync="showEmbedModal" v-if="publicLibraries.length > 0">
|
||||
<modal
|
||||
v-if="publicLibraries.length > 0"
|
||||
:show.sync="showEmbedModal"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate translate-context="Popup/Artist/Title/Verb">Embed this artist work on your website</translate>
|
||||
<translate translate-context="Popup/Artist/Title/Verb">
|
||||
Embed this artist work on your website
|
||||
</translate>
|
||||
</h4>
|
||||
<div class="scrolling content">
|
||||
<div class="description">
|
||||
<embed-wizard type="artist" :id="object.id" />
|
||||
|
||||
<embed-wizard
|
||||
:id="object.id"
|
||||
type="artist"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="ui deny button">
|
||||
<translate translate-context="*/*/Button.Label/Verb">Cancel</translate>
|
||||
<translate translate-context="*/*/Button.Label/Verb">
|
||||
Cancel
|
||||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
<div class="ui buttons">
|
||||
<button class="ui button" @click="$refs.dropdown.click()">
|
||||
<translate translate-context="*/*/Button.Label/Noun">More…</translate>
|
||||
<button
|
||||
class="ui button"
|
||||
@click="$refs.dropdown.click()"
|
||||
>
|
||||
<translate translate-context="*/*/Button.Label/Noun">
|
||||
More…
|
||||
</translate>
|
||||
</button>
|
||||
<button class="ui floating dropdown icon button" ref="dropdown" v-dropdown>
|
||||
<i class="dropdown icon"></i>
|
||||
<button
|
||||
ref="dropdown"
|
||||
v-dropdown
|
||||
class="ui floating dropdown icon button"
|
||||
>
|
||||
<i class="dropdown icon" />
|
||||
<div class="menu">
|
||||
<a
|
||||
:href="object.fid"
|
||||
v-if="domain != $store.getters['instance/domain']"
|
||||
:href="object.fid"
|
||||
target="_blank"
|
||||
class="basic item">
|
||||
<i class="external icon"></i>
|
||||
<translate :translate-params="{domain: domain}" translate-context="Content/*/Button.Label/Verb">View on %{ domain }</translate>
|
||||
class="basic item"
|
||||
>
|
||||
<i class="external icon" />
|
||||
<translate
|
||||
:translate-params="{domain: domain}"
|
||||
translate-context="Content/*/Button.Label/Verb"
|
||||
>View on %{ domain }</translate>
|
||||
</a>
|
||||
|
||||
<button
|
||||
role="button"
|
||||
v-if="publicLibraries.length > 0"
|
||||
role="button"
|
||||
class="basic item"
|
||||
@click.prevent="showEmbedModal = !showEmbedModal"
|
||||
class="basic item">
|
||||
<i class="code icon"></i>
|
||||
<translate translate-context="Content/*/Button.Label/Verb">Embed</translate>
|
||||
>
|
||||
<i class="code icon" />
|
||||
<translate translate-context="Content/*/Button.Label/Verb">
|
||||
Embed
|
||||
</translate>
|
||||
</button>
|
||||
<a :href="wikipediaUrl" target="_blank" rel="noreferrer noopener" class="basic item">
|
||||
<i class="wikipedia w icon"></i>
|
||||
<a
|
||||
:href="wikipediaUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
class="basic item"
|
||||
>
|
||||
<i class="wikipedia w icon" />
|
||||
<translate translate-context="Content/*/Button.Label/Verb">Search on Wikipedia</translate>
|
||||
</a>
|
||||
<a v-if="musicbrainzUrl" :href="musicbrainzUrl" target="_blank" rel="noreferrer noopener" class="basic item">
|
||||
<i class="external icon"></i>
|
||||
<a
|
||||
v-if="musicbrainzUrl"
|
||||
:href="musicbrainzUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
class="basic item"
|
||||
>
|
||||
<i class="external icon" />
|
||||
<translate translate-context="Content/*/*/Clickable, Verb">View on MusicBrainz</translate>
|
||||
</a>
|
||||
<a :href="discogsUrl" target="_blank" rel="noreferrer noopener" class="basic item">
|
||||
<i class="external icon"></i>
|
||||
<a
|
||||
:href="discogsUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
class="basic item"
|
||||
>
|
||||
<i class="external icon" />
|
||||
<translate translate-context="Content/*/Button.Label/Verb">Search on Discogs</translate>
|
||||
</a>
|
||||
<router-link
|
||||
v-if="object.is_local"
|
||||
:to="{name: 'library.artists.edit', params: {id: object.id }}"
|
||||
class="basic item">
|
||||
<i class="edit icon"></i>
|
||||
<translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
|
||||
</router-link>
|
||||
<div class="divider"></div>
|
||||
<div
|
||||
role="button"
|
||||
class="basic item"
|
||||
>
|
||||
<i class="edit icon" />
|
||||
<translate translate-context="Content/*/Button.Label/Verb">
|
||||
Edit
|
||||
</translate>
|
||||
</router-link>
|
||||
<div class="divider" />
|
||||
<div
|
||||
v-for="obj in getReportableObjs({artist: object})"
|
||||
:key="obj.target.type + obj.target.id"
|
||||
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)">
|
||||
role="button"
|
||||
class="basic item"
|
||||
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)"
|
||||
>
|
||||
<i class="share icon" /> {{ obj.label }}
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
<router-link class="basic item" v-if="$store.state.auth.availablePermissions['library']" :to="{name: 'manage.library.artists.detail', params: {id: object.id}}">
|
||||
<i class="wrench icon"></i>
|
||||
<translate translate-context="Content/Moderation/Link">Open in moderation interface</translate>
|
||||
<div class="divider" />
|
||||
<router-link
|
||||
v-if="$store.state.auth.availablePermissions['library']"
|
||||
class="basic item"
|
||||
:to="{name: 'manage.library.artists.detail', params: {id: object.id}}"
|
||||
>
|
||||
<i class="wrench icon" />
|
||||
<translate translate-context="Content/Moderation/Link">
|
||||
Open in moderation interface
|
||||
</translate>
|
||||
</router-link>
|
||||
<a
|
||||
v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser"
|
||||
class="basic item"
|
||||
:href="$store.getters['instance/absoluteUrl'](`/api/admin/music/artist/${object.id}`)"
|
||||
target="_blank" rel="noopener noreferrer">
|
||||
<i class="wrench icon"></i>
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i class="wrench icon" />
|
||||
<translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -123,44 +199,43 @@
|
|||
</div>
|
||||
</section>
|
||||
<router-view
|
||||
:key="$route.fullPath"
|
||||
:tracks="tracks"
|
||||
:next-tracks-url="nextTracksUrl"
|
||||
:next-albums-url="nextAlbumsUrl"
|
||||
:albums="albums"
|
||||
:is-loading-albums="isLoadingAlbums"
|
||||
:object="object"
|
||||
object-type="artist"
|
||||
@libraries-loaded="libraries = $event"
|
||||
:object="object" object-type="artist"
|
||||
:key="$route.fullPath"></router-view>
|
||||
/>
|
||||
</template>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios"
|
||||
import logger from "@/logging"
|
||||
import backend from "@/audio/backend"
|
||||
import PlayButton from "@/components/audio/PlayButton"
|
||||
import EmbedWizard from "@/components/audio/EmbedWizard"
|
||||
import axios from 'axios'
|
||||
import logger from '@/logging'
|
||||
import PlayButton from '@/components/audio/PlayButton'
|
||||
import EmbedWizard from '@/components/audio/EmbedWizard'
|
||||
import Modal from '@/components/semantic/Modal'
|
||||
import RadioButton from "@/components/radios/Button"
|
||||
import TagsList from "@/components/tags/List"
|
||||
import RadioButton from '@/components/radios/Button'
|
||||
import TagsList from '@/components/tags/List'
|
||||
import ReportMixin from '@/components/mixins/Report'
|
||||
|
||||
import {getDomain} from '@/utils'
|
||||
|
||||
const FETCH_URL = "albums/"
|
||||
import { getDomain } from '@/utils'
|
||||
|
||||
export default {
|
||||
mixins: [ReportMixin],
|
||||
props: ["id"],
|
||||
components: {
|
||||
PlayButton,
|
||||
EmbedWizard,
|
||||
Modal,
|
||||
RadioButton,
|
||||
TagsList,
|
||||
TagsList
|
||||
},
|
||||
data() {
|
||||
mixins: [ReportMixin],
|
||||
props: { id: { type: Number, required: true } },
|
||||
data () {
|
||||
return {
|
||||
isLoading: true,
|
||||
isLoadingAlbums: true,
|
||||
|
@ -172,47 +247,7 @@ export default {
|
|||
nextAlbumsUrl: null,
|
||||
nextTracksUrl: null,
|
||||
totalAlbums: null,
|
||||
totalTracks: null,
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
await this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
async fetchData() {
|
||||
var self = this
|
||||
this.isLoading = true
|
||||
logger.default.debug('Fetching artist "' + this.id + '"')
|
||||
|
||||
let artistPromise = axios.get("artists/" + this.id + "/", {params: {refresh: 'true'}}).then(response => {
|
||||
if (response.data.channel) {
|
||||
self.$router.replace({name: 'channels.detail', params: {id: response.data.channel.uuid}})
|
||||
} else {
|
||||
self.object = response.data
|
||||
}
|
||||
})
|
||||
await artistPromise
|
||||
if (!self.object) {
|
||||
return
|
||||
}
|
||||
let trackPromise = axios.get("tracks/", { params: { artist: this.id, hidden: '', ordering: "-creation_date" } }).then(response => {
|
||||
self.tracks = response.data.results
|
||||
self.nextTracksUrl = response.data.next
|
||||
self.totalTracks = response.data.count
|
||||
})
|
||||
let albumPromise = axios.get("albums/", {
|
||||
params: { artist: self.id, ordering: "-release_date", hidden: '' }
|
||||
}).then(response => {
|
||||
self.nextAlbumsUrl = response.data.next
|
||||
self.totalAlbums = response.data.count
|
||||
let parsed = JSON.parse(JSON.stringify(response.data.results))
|
||||
self.albums = parsed
|
||||
|
||||
})
|
||||
await trackPromise
|
||||
await albumPromise
|
||||
self.isLoadingAlbums = false
|
||||
self.isLoading = false
|
||||
totalTracks: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -220,37 +255,39 @@ export default {
|
|||
if (this.object) {
|
||||
return getDomain(this.object.fid)
|
||||
}
|
||||
return null
|
||||
},
|
||||
isPlayable() {
|
||||
isPlayable () {
|
||||
return (
|
||||
this.object.albums.filter(a => {
|
||||
return a.is_playable
|
||||
}).length > 0
|
||||
)
|
||||
},
|
||||
labels() {
|
||||
labels () {
|
||||
return {
|
||||
title: this.$pgettext('*/*/*', 'Album')
|
||||
}
|
||||
},
|
||||
wikipediaUrl() {
|
||||
wikipediaUrl () {
|
||||
return (
|
||||
"https://en.wikipedia.org/w/index.php?search=" +
|
||||
'https://en.wikipedia.org/w/index.php?search=' +
|
||||
encodeURI(this.object.name)
|
||||
)
|
||||
},
|
||||
musicbrainzUrl() {
|
||||
musicbrainzUrl () {
|
||||
if (this.object.mbid) {
|
||||
return "https://musicbrainz.org/artist/" + this.object.mbid
|
||||
return 'https://musicbrainz.org/artist/' + this.object.mbid
|
||||
}
|
||||
return null
|
||||
},
|
||||
discogsUrl() {
|
||||
discogsUrl () {
|
||||
return (
|
||||
"https://discogs.com/search/?type=artist&title=" +
|
||||
'https://discogs.com/search/?type=artist&title=' +
|
||||
encodeURI(this.object.name)
|
||||
)
|
||||
},
|
||||
cover() {
|
||||
cover () {
|
||||
if (this.object.cover && this.object.cover.urls.original) {
|
||||
return this.object.cover
|
||||
}
|
||||
|
@ -268,27 +305,65 @@ export default {
|
|||
return l.privacy_level === 'everyone'
|
||||
})
|
||||
},
|
||||
headerStyle() {
|
||||
headerStyle () {
|
||||
if (!this.cover || !this.cover.urls.original) {
|
||||
return ""
|
||||
return ''
|
||||
}
|
||||
return (
|
||||
"background-image: url(" +
|
||||
this.$store.getters["instance/absoluteUrl"](this.cover.urls.original) +
|
||||
")"
|
||||
'background-image: url(' +
|
||||
this.$store.getters['instance/absoluteUrl'](this.cover.urls.original) +
|
||||
')'
|
||||
)
|
||||
},
|
||||
contentFilter () {
|
||||
let self = this
|
||||
return this.$store.getters['moderation/artistFilters']().filter((e) => {
|
||||
return e.target.id === this.object.id
|
||||
})[0]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
id() {
|
||||
id () {
|
||||
this.fetchData()
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
await this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
async fetchData () {
|
||||
const self = this
|
||||
this.isLoading = true
|
||||
logger.default.debug('Fetching artist "' + this.id + '"')
|
||||
|
||||
const artistPromise = axios.get('artists/' + this.id + '/', { params: { refresh: 'true' } }).then(response => {
|
||||
if (response.data.channel) {
|
||||
self.$router.replace({ name: 'channels.detail', params: { id: response.data.channel.uuid } })
|
||||
} else {
|
||||
self.object = response.data
|
||||
}
|
||||
})
|
||||
await artistPromise
|
||||
if (!self.object) {
|
||||
return
|
||||
}
|
||||
const trackPromise = axios.get('tracks/', { params: { artist: this.id, hidden: '', ordering: '-creation_date' } }).then(response => {
|
||||
self.tracks = response.data.results
|
||||
self.nextTracksUrl = response.data.next
|
||||
self.totalTracks = response.data.count
|
||||
})
|
||||
const albumPromise = axios.get('albums/', {
|
||||
params: { artist: self.id, ordering: '-release_date', hidden: '' }
|
||||
}).then(response => {
|
||||
self.nextAlbumsUrl = response.data.next
|
||||
self.totalAlbums = response.data.count
|
||||
const parsed = JSON.parse(JSON.stringify(response.data.results))
|
||||
self.albums = parsed
|
||||
})
|
||||
await trackPromise
|
||||
await albumPromise
|
||||
self.isLoadingAlbums = false
|
||||
self.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,69 +1,127 @@
|
|||
<template>
|
||||
<div v-if="object">
|
||||
<div class="ui small text container" v-if="contentFilter">
|
||||
<div class="ui hidden divider"></div>
|
||||
<div
|
||||
v-if="contentFilter"
|
||||
class="ui small text container"
|
||||
>
|
||||
<div class="ui hidden divider" />
|
||||
<div class="ui message">
|
||||
<p>
|
||||
<translate translate-context="Content/Artist/Paragraph">You are currently hiding content related to this artist.</translate>
|
||||
<translate translate-context="Content/Artist/Paragraph">
|
||||
You are currently hiding content related to this artist.
|
||||
</translate>
|
||||
</p>
|
||||
<router-link class="right floated" :to="{name: 'settings'}">
|
||||
<translate translate-context="Content/Moderation/Link">Review my filters</translate>
|
||||
<router-link
|
||||
class="right floated"
|
||||
:to="{name: 'settings'}"
|
||||
>
|
||||
<translate translate-context="Content/Moderation/Link">
|
||||
Review my filters
|
||||
</translate>
|
||||
</router-link>
|
||||
<button @click="$store.dispatch('moderation/deleteContentFilter', contentFilter.uuid)" class="ui basic tiny button">
|
||||
<translate translate-context="Content/Moderation/Button.Label">Remove filter</translate>
|
||||
<button
|
||||
class="ui basic tiny button"
|
||||
@click="$store.dispatch('moderation/deleteContentFilter', contentFilter.uuid)"
|
||||
>
|
||||
<translate translate-context="Content/Moderation/Button.Label">
|
||||
Remove filter
|
||||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<section v-if="tracks.length > 0" class="ui vertical stripe segment">
|
||||
<track-table :is-artist="true" :show-position="false" :track-only="true" :tracks="tracks.slice(0,5)">
|
||||
<section
|
||||
v-if="tracks.length > 0"
|
||||
class="ui vertical stripe segment"
|
||||
>
|
||||
<track-table
|
||||
:is-artist="true"
|
||||
:show-position="false"
|
||||
:track-only="true"
|
||||
:tracks="tracks.slice(0,5)"
|
||||
>
|
||||
<template slot="header">
|
||||
<h2>
|
||||
<translate translate-context="Content/Artist/Title">New tracks by this artist</translate>
|
||||
<translate translate-context="Content/Artist/Title">
|
||||
New tracks by this artist
|
||||
</translate>
|
||||
</h2>
|
||||
<div class="ui hidden divider"></div>
|
||||
<div class="ui hidden divider" />
|
||||
</template>
|
||||
</track-table>
|
||||
</section>
|
||||
<section v-if="isLoadingAlbums" class="ui vertical stripe segment">
|
||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
||||
<section
|
||||
v-if="isLoadingAlbums"
|
||||
class="ui vertical stripe segment"
|
||||
>
|
||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']" />
|
||||
</section>
|
||||
<section v-else-if="albums && albums.length > 0" class="ui vertical stripe segment">
|
||||
<section
|
||||
v-else-if="albums && albums.length > 0"
|
||||
class="ui vertical stripe segment"
|
||||
>
|
||||
<h2>
|
||||
<translate translate-context="Content/Artist/Title">Albums by this artist</translate>
|
||||
<translate translate-context="Content/Artist/Title">
|
||||
Albums by this artist
|
||||
</translate>
|
||||
</h2>
|
||||
<div class="ui cards app-cards">
|
||||
<album-card :album="album" :key="album.id" v-for="album in allAlbums"></album-card>
|
||||
<album-card
|
||||
v-for="album in allAlbums"
|
||||
:key="album.id"
|
||||
:album="album"
|
||||
/>
|
||||
</div>
|
||||
<div class="ui hidden divider"></div>
|
||||
<button :class="['ui', {loading: isLoadingMoreAlbums}, 'button']" v-if="nextAlbumsUrl && loadMoreAlbumsUrl" @click="loadMoreAlbums(loadMoreAlbumsUrl)">
|
||||
<translate translate-context="Content/*/Button.Label">Load more…</translate>
|
||||
<div class="ui hidden divider" />
|
||||
<button
|
||||
v-if="nextAlbumsUrl && loadMoreAlbumsUrl"
|
||||
:class="['ui', {loading: isLoadingMoreAlbums}, 'button']"
|
||||
@click="loadMoreAlbums(loadMoreAlbumsUrl)"
|
||||
>
|
||||
<translate translate-context="Content/*/Button.Label">
|
||||
Load more…
|
||||
</translate>
|
||||
</button>
|
||||
</section>
|
||||
<section class="ui vertical stripe segment">
|
||||
<h2>
|
||||
<translate translate-context="Content/*/Title/Noun">User libraries</translate>
|
||||
<translate translate-context="Content/*/Title/Noun">
|
||||
User libraries
|
||||
</translate>
|
||||
</h2>
|
||||
<library-widget @loaded="$emit('libraries-loaded', $event)" :url="'artists/' + object.id + '/libraries/'">
|
||||
<translate translate-context="Content/Artist/Paragraph" slot="subtitle">This artist is present in the following libraries:</translate>
|
||||
<library-widget
|
||||
:url="'artists/' + object.id + '/libraries/'"
|
||||
@loaded="$emit('libraries-loaded', $event)"
|
||||
>
|
||||
<translate
|
||||
slot="subtitle"
|
||||
translate-context="Content/Artist/Paragraph"
|
||||
>
|
||||
This artist is present in the following libraries:
|
||||
</translate>
|
||||
</library-widget>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from "@/lodash"
|
||||
import axios from "axios"
|
||||
import logger from "@/logging"
|
||||
import AlbumCard from "@/components/audio/album/Card"
|
||||
import TrackTable from "@/components/audio/track/Table"
|
||||
import LibraryWidget from "@/components/federation/LibraryWidget"
|
||||
import axios from 'axios'
|
||||
import AlbumCard from '@/components/audio/album/Card'
|
||||
import TrackTable from '@/components/audio/track/Table'
|
||||
import LibraryWidget from '@/components/federation/LibraryWidget'
|
||||
|
||||
export default {
|
||||
props: ["object", "tracks", "albums", "isLoadingAlbums", "nextTracksUrl", "nextAlbumsUrl"],
|
||||
components: {
|
||||
AlbumCard,
|
||||
TrackTable,
|
||||
LibraryWidget,
|
||||
LibraryWidget
|
||||
},
|
||||
props: {
|
||||
object: { type: Object, required: true },
|
||||
tracks: { type: Array, required: true },
|
||||
albums: { type: Array, required: true },
|
||||
isLoadingAlbums: { type: Boolean, required: true },
|
||||
nextTracksUrl: { type: String, required: true },
|
||||
nextAlbumsUrl: { type: String, required: true }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
@ -74,7 +132,6 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
contentFilter () {
|
||||
let self = this
|
||||
return this.$store.getters['moderation/artistFilters']().filter((e) => {
|
||||
return e.target.id === this.object.id
|
||||
})[0]
|
||||
|
@ -85,15 +142,14 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
loadMoreAlbums (url) {
|
||||
let self = this
|
||||
const self = this
|
||||
self.isLoadingMoreAlbums = true
|
||||
axios.get(url).then((response) => {
|
||||
self.additionalAlbums = self.additionalAlbums.concat(response.data.results)
|
||||
self.loadMoreAlbumsUrl = response.data.next
|
||||
self.isLoadingMoreAlbums = false
|
||||
}, (error) => {
|
||||
}, () => {
|
||||
self.isLoadingMoreAlbums = false
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,37 +1,56 @@
|
|||
<template>
|
||||
|
||||
<section class="ui vertical stripe segment">
|
||||
<div class="ui text container">
|
||||
<h2>
|
||||
<translate v-if="canEdit" key="1" translate-context="Content/*/Title">Edit this artist</translate>
|
||||
<translate v-else key="2" translate-context="Content/*/Title">Suggest an edit on this artist</translate>
|
||||
<translate
|
||||
v-if="canEdit"
|
||||
key="1"
|
||||
translate-context="Content/*/Title"
|
||||
>
|
||||
Edit this artist
|
||||
</translate>
|
||||
<translate
|
||||
v-else
|
||||
key="2"
|
||||
translate-context="Content/*/Title"
|
||||
>
|
||||
Suggest an edit on this artist
|
||||
</translate>
|
||||
</h2>
|
||||
<div class="ui message" v-if="!object.is_local">
|
||||
<translate translate-context="Content/*/Message">This object is managed by another server, you cannot edit it.</translate>
|
||||
<div
|
||||
v-if="!object.is_local"
|
||||
class="ui message"
|
||||
>
|
||||
<translate translate-context="Content/*/Message">
|
||||
This object is managed by another server, you cannot edit it.
|
||||
</translate>
|
||||
</div>
|
||||
<edit-form
|
||||
v-else
|
||||
:object-type="objectType"
|
||||
:object="object"
|
||||
:can-edit="canEdit"></edit-form>
|
||||
:can-edit="canEdit"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios"
|
||||
|
||||
import EditForm from '@/components/library/EditForm'
|
||||
export default {
|
||||
props: ["objectType", "object", "libraries"],
|
||||
data() {
|
||||
return {
|
||||
id: this.object.id,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
EditForm
|
||||
},
|
||||
props: {
|
||||
objectType: { type: String, required: true },
|
||||
object: { type: Object, required: true },
|
||||
libraries: { type: Array, required: true }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
id: this.object.id
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
canEdit () {
|
||||
return true
|
||||
|
|
|
@ -2,67 +2,138 @@
|
|||
<main v-title="labels.title">
|
||||
<section class="ui vertical stripe segment">
|
||||
<h2 class="ui header">
|
||||
<translate translate-context="Content/Artist/Title">Browsing artists</translate>
|
||||
<translate translate-context="Content/Artist/Title">
|
||||
Browsing artists
|
||||
</translate>
|
||||
</h2>
|
||||
<form :class="['ui', {'loading': isLoading}, 'form']" @submit.prevent="updatePage();updateQueryString();fetchData()">
|
||||
<form
|
||||
:class="['ui', {'loading': isLoading}, 'form']"
|
||||
@submit.prevent="updatePage();updateQueryString();fetchData()"
|
||||
>
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<label for="artist-search">
|
||||
<translate translate-context="Content/Search/Input.Label/Noun">Artist name</translate>
|
||||
</label>
|
||||
<div class="ui action input">
|
||||
<input id="artist-search" type="text" name="search" v-model="query" :placeholder="labels.searchPlaceholder"/>
|
||||
<button class="ui icon button" type="submit" :aria-label="$pgettext('Content/Search/Input.Label/Noun', 'Search')">
|
||||
<i class="search icon"></i>
|
||||
<input
|
||||
id="artist-search"
|
||||
v-model="query"
|
||||
type="text"
|
||||
name="search"
|
||||
:placeholder="labels.searchPlaceholder"
|
||||
>
|
||||
<button
|
||||
class="ui icon button"
|
||||
type="submit"
|
||||
:aria-label="$pgettext('Content/Search/Input.Label/Noun', 'Search')"
|
||||
>
|
||||
<i class="search icon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="tags-search"><translate translate-context="*/*/*/Noun">Tags</translate></label>
|
||||
<tags-selector v-model="tags"></tags-selector>
|
||||
<tags-selector v-model="tags" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="artist-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label>
|
||||
<select id="artist-ordering" class="ui dropdown" v-model="ordering">
|
||||
<option v-for="option in orderingOptions" :value="option[0]">
|
||||
<select
|
||||
id="artist-ordering"
|
||||
v-model="ordering"
|
||||
class="ui dropdown"
|
||||
>
|
||||
<option
|
||||
v-for="(option, key) in orderingOptions"
|
||||
:key="key"
|
||||
:value="option[0]"
|
||||
>
|
||||
{{ sharedLabels.filters[option[1]] }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="artist-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label>
|
||||
<select id="artist-ordering-direction" class="ui dropdown" v-model="orderingDirection">
|
||||
<option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option>
|
||||
<option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option>
|
||||
<select
|
||||
id="artist-ordering-direction"
|
||||
v-model="orderingDirection"
|
||||
class="ui dropdown"
|
||||
>
|
||||
<option value="+">
|
||||
<translate translate-context="Content/Search/Dropdown">
|
||||
Ascending
|
||||
</translate>
|
||||
</option>
|
||||
<option value="-">
|
||||
<translate translate-context="Content/Search/Dropdown">
|
||||
Descending
|
||||
</translate>
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="artist-results"><translate translate-context="Content/Search/Dropdown.Label/Noun">Results per page</translate></label>
|
||||
<select id="artist-results" class="ui dropdown" v-model="paginateBy">
|
||||
<option :value="parseInt(12)">12</option>
|
||||
<option :value="parseInt(30)">30</option>
|
||||
<option :value="parseInt(50)">50</option>
|
||||
<select
|
||||
id="artist-results"
|
||||
v-model="paginateBy"
|
||||
class="ui dropdown"
|
||||
>
|
||||
<option :value="parseInt(12)">
|
||||
12
|
||||
</option>
|
||||
<option :value="parseInt(30)">
|
||||
30
|
||||
</option>
|
||||
<option :value="parseInt(50)">
|
||||
50
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span id="excludeHeader">Exclude Compilation Artists</span>
|
||||
<div id="excludeCompilation" class="ui toggle checkbox">
|
||||
<input id="exclude-compilation" v-model="excludeCompilation" true-value="true" false-value="null" type="checkbox">
|
||||
<label for="exclude-compilation" class="visually-hidden"><translate translate-context="Content/Search/Checkbox/Noun">Exclude Compilation Artists</translate></label>
|
||||
<div
|
||||
id="excludeCompilation"
|
||||
class="ui toggle checkbox"
|
||||
>
|
||||
<input
|
||||
id="exclude-compilation"
|
||||
v-model="excludeCompilation"
|
||||
true-value="true"
|
||||
false-value="null"
|
||||
type="checkbox"
|
||||
>
|
||||
<label
|
||||
for="exclude-compilation"
|
||||
class="visually-hidden"
|
||||
><translate translate-context="Content/Search/Checkbox/Noun">Exclude Compilation Artists</translate></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="ui hidden divider"></div>
|
||||
<div v-if="result && result.results.length > 0" class="ui five app-cards cards">
|
||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||
<div class="ui loader"></div>
|
||||
<div class="ui hidden divider" />
|
||||
<div
|
||||
v-if="result && result.results.length > 0"
|
||||
class="ui five app-cards cards"
|
||||
>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui inverted active dimmer"
|
||||
>
|
||||
<div class="ui loader" />
|
||||
</div>
|
||||
<artist-card :artist="artist" v-for="artist in result.results" :key="artist.id"></artist-card>
|
||||
<artist-card
|
||||
v-for="artist in result.results"
|
||||
:key="artist.id"
|
||||
:artist="artist"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="!isLoading" class="ui placeholder segment sixteen wide column" style="text-align: center; display: flex; align-items: center">
|
||||
<div
|
||||
v-else-if="!isLoading"
|
||||
class="ui placeholder segment sixteen wide column"
|
||||
style="text-align: center; display: flex; align-items: center"
|
||||
>
|
||||
<div class="ui icon header">
|
||||
<i class="compact disc icon"></i>
|
||||
<i class="compact disc icon" />
|
||||
<translate translate-context="Content/Artists/Placeholder">
|
||||
No results matching your query
|
||||
</translate>
|
||||
|
@ -70,8 +141,9 @@
|
|||
<router-link
|
||||
v-if="$store.state.auth.authenticated"
|
||||
:to="{name: 'content.index'}"
|
||||
class="ui success button labeled icon">
|
||||
<i class="upload icon"></i>
|
||||
class="ui success button labeled icon"
|
||||
>
|
||||
<i class="upload icon" />
|
||||
<translate translate-context="Content/*/Verb">
|
||||
Add some music
|
||||
</translate>
|
||||
|
@ -80,11 +152,11 @@
|
|||
<div class="ui center aligned basic segment">
|
||||
<pagination
|
||||
v-if="result && result.count > paginateBy"
|
||||
@page-changed="selectPage"
|
||||
:current="page"
|
||||
:paginate-by="paginateBy"
|
||||
:total="result.count"
|
||||
></pagination>
|
||||
@page-changed="selectPage"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
@ -92,34 +164,33 @@
|
|||
|
||||
<script>
|
||||
import qs from 'qs'
|
||||
import axios from "axios"
|
||||
import _ from "@/lodash"
|
||||
import $ from "jquery"
|
||||
import axios from 'axios'
|
||||
import $ from 'jquery'
|
||||
|
||||
import logger from "@/logging"
|
||||
import logger from '@/logging'
|
||||
|
||||
import OrderingMixin from "@/components/mixins/Ordering"
|
||||
import PaginationMixin from "@/components/mixins/Pagination"
|
||||
import TranslationsMixin from "@/components/mixins/Translations"
|
||||
import ArtistCard from "@/components/audio/artist/Card"
|
||||
import Pagination from "@/components/Pagination"
|
||||
import OrderingMixin from '@/components/mixins/Ordering'
|
||||
import PaginationMixin from '@/components/mixins/Pagination'
|
||||
import TranslationsMixin from '@/components/mixins/Translations'
|
||||
import ArtistCard from '@/components/audio/artist/Card'
|
||||
import Pagination from '@/components/Pagination'
|
||||
import TagsSelector from '@/components/library/TagsSelector'
|
||||
|
||||
const FETCH_URL = "artists/"
|
||||
const FETCH_URL = 'artists/'
|
||||
|
||||
export default {
|
||||
mixins: [OrderingMixin, PaginationMixin, TranslationsMixin],
|
||||
props: {
|
||||
defaultQuery: { type: String, required: false, default: "" },
|
||||
defaultTags: { type: Array, required: false, default: () => { return [] } },
|
||||
scope: { type: String, required: false, default: "all" },
|
||||
},
|
||||
components: {
|
||||
ArtistCard,
|
||||
Pagination,
|
||||
TagsSelector,
|
||||
TagsSelector
|
||||
},
|
||||
data() {
|
||||
mixins: [OrderingMixin, PaginationMixin, TranslationsMixin],
|
||||
props: {
|
||||
defaultQuery: { type: String, required: false, default: '' },
|
||||
defaultTags: { type: Array, required: false, default: () => { return [] } },
|
||||
scope: { type: String, required: false, default: 'all' }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isLoading: true,
|
||||
result: null,
|
||||
|
@ -127,27 +198,39 @@ export default {
|
|||
page: parseInt(this.defaultPage),
|
||||
query: this.defaultQuery,
|
||||
tags: (this.defaultTags || []).filter((t) => { return t.length > 0 }),
|
||||
orderingOptions: [["creation_date", "creation_date"], ["name", "name"]]
|
||||
orderingOptions: [['creation_date', 'creation_date'], ['name', 'name']]
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchData()
|
||||
},
|
||||
mounted() {
|
||||
$(".ui.dropdown").dropdown()
|
||||
},
|
||||
computed: {
|
||||
labels() {
|
||||
let searchPlaceholder = this.$pgettext('Content/Search/Input.Placeholder', "Search…")
|
||||
let title = this.$pgettext('*/*/*/Noun', "Artists")
|
||||
labels () {
|
||||
const searchPlaceholder = this.$pgettext('Content/Search/Input.Placeholder', 'Search…')
|
||||
const title = this.$pgettext('*/*/*/Noun', 'Artists')
|
||||
return {
|
||||
searchPlaceholder,
|
||||
title
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
page () {
|
||||
this.updateQueryString()
|
||||
this.fetchData()
|
||||
},
|
||||
'$store.state.moderation.lastUpdate': function () {
|
||||
this.fetchData()
|
||||
},
|
||||
excludeCompilation () {
|
||||
this.fetchData()
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchData()
|
||||
},
|
||||
mounted () {
|
||||
$('.ui.dropdown').dropdown()
|
||||
},
|
||||
methods: {
|
||||
updateQueryString: function() {
|
||||
updateQueryString: function () {
|
||||
history.pushState(
|
||||
{},
|
||||
null,
|
||||
|
@ -159,61 +242,49 @@ export default {
|
|||
paginateBy: this.paginateBy,
|
||||
ordering: this.getOrderingAsString(),
|
||||
content_category: 'music',
|
||||
include_channels: true,
|
||||
include_channels: true
|
||||
}).toString()
|
||||
)
|
||||
},
|
||||
fetchData: function() {
|
||||
var self = this
|
||||
fetchData: function () {
|
||||
const self = this
|
||||
this.isLoading = true
|
||||
let url = FETCH_URL
|
||||
let params = {
|
||||
const url = FETCH_URL
|
||||
const params = {
|
||||
scope: this.scope,
|
||||
page: this.page,
|
||||
page_size: this.paginateBy,
|
||||
has_albums: this.excludeCompilation,
|
||||
q: this.query,
|
||||
ordering: this.getOrderingAsString(),
|
||||
playable: "true",
|
||||
playable: 'true',
|
||||
tag: this.tags,
|
||||
include_channels: "true",
|
||||
content_category: 'music',
|
||||
include_channels: 'true',
|
||||
content_category: 'music'
|
||||
}
|
||||
logger.default.debug("Fetching artists")
|
||||
logger.default.debug('Fetching artists')
|
||||
axios.get(
|
||||
url,
|
||||
{
|
||||
params: params,
|
||||
paramsSerializer: function(params) {
|
||||
paramsSerializer: function (params) {
|
||||
return qs.stringify(params, { indices: false })
|
||||
}
|
||||
}
|
||||
).then(response => {
|
||||
self.result = response.data
|
||||
self.isLoading = false
|
||||
}, error => {
|
||||
}, () => {
|
||||
self.result = null
|
||||
self.isLoading = false
|
||||
})
|
||||
},
|
||||
selectPage: function(page) {
|
||||
selectPage: function (page) {
|
||||
this.page = page
|
||||
},
|
||||
updatePage() {
|
||||
updatePage () {
|
||||
this.page = this.defaultPage
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
page() {
|
||||
this.updateQueryString()
|
||||
this.fetchData()
|
||||
},
|
||||
"$store.state.moderation.lastUpdate": function () {
|
||||
this.fetchData()
|
||||
},
|
||||
excludeCompilation() {
|
||||
this.fetchData()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue