Lint the frontend code
This commit is contained in:
parent
869fc20536
commit
8ee9a536e1
|
@ -136,10 +136,7 @@ eslint:
|
||||||
- cd front
|
- cd front
|
||||||
- yarn install
|
- yarn install
|
||||||
script:
|
script:
|
||||||
# We search for all files ending with .vue or .js in src which changed in relation to develop
|
- yarn lint
|
||||||
# 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' ' ')
|
|
||||||
cache:
|
cache:
|
||||||
key: "$CI_PROJECT_ID__eslint_npm_cache"
|
key: "$CI_PROJECT_ID__eslint_npm_cache"
|
||||||
paths:
|
paths:
|
||||||
|
|
|
@ -20,5 +20,7 @@ module.exports = {
|
||||||
'vue'
|
'vue'
|
||||||
],
|
],
|
||||||
rules: {
|
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}",
|
"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",
|
"build": "scripts/i18n-compile.sh && vue-cli-service build",
|
||||||
"test:unit": "vue-cli-service test:unit --reporter mocha-junit-reporter",
|
"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-compile": "scripts/i18n-compile.sh",
|
||||||
"i18n-extract": "scripts/i18n-extract.sh",
|
"i18n-extract": "scripts/i18n-extract.sh",
|
||||||
"fix-fomantic-css": "scripts/fix-fomantic-css.sh",
|
"fix-fomantic-css": "scripts/fix-fomantic-css.sh",
|
||||||
|
|
|
@ -2,57 +2,110 @@
|
||||||
<template>
|
<template>
|
||||||
<main :class="[theme]">
|
<main :class="[theme]">
|
||||||
<!-- SVG from https://cdn.plyr.io/3.4.7/plyr.svg -->
|
<!-- 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">
|
<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>
|
aria-hidden="true"
|
||||||
<symbol id="plyr-enter-fullscreen"><path d="M10 3h3.6l-4 4L11 8.4l4-4V8h2V1h-7zM7 9.6l-4 4V10H1v7h7v-2H4.4l4-4z"/></symbol>
|
style="display: none"
|
||||||
<symbol id="plyr-exit-fullscreen"><path d="M1 12h3.6l-4 4L2 17.4l4-4V17h2v-7H1zM16 .6l-4 4V1h-2v7h7V6h-3.6l4-4z"/></symbol>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<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-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-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-enter-fullscreen"><path d="M10 3h3.6l-4 4L11 8.4l4-4V8h2V1h-7zM7 9.6l-4 4V10H1v7h7v-2H4.4l4-4z" /></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-exit-fullscreen"><path d="M1 12h3.6l-4 4L2 17.4l4-4V17h2v-7H1zM16 .6l-4 4V1h-2v7h7V6h-3.6l4-4z" /></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-fast-forward"><path d="M7.875 7.171L0 1v16l7.875-6.171V17L18 9 7.875 1z" /></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-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-rewind"><path d="M10.125 1L0 9l10.125 8v-6.171L18 17V1l-7.875 6.171z"/></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-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-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-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>
|
<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 -->
|
<!-- 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-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-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>
|
</svg>
|
||||||
<article>
|
<article>
|
||||||
<aside class="cover main" v-if="currentTrack">
|
<aside
|
||||||
<img height="120" v-if="currentTrack.cover" :src="currentTrack.cover" alt="Cover" />
|
v-if="currentTrack"
|
||||||
<img height="120" v-else src="./assets/embed/default-cover.jpeg" alt="Cover" />
|
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>
|
</aside>
|
||||||
<div class="content" aria-label="Track information">
|
<div
|
||||||
|
class="content"
|
||||||
|
aria-label="Track information"
|
||||||
|
>
|
||||||
<header v-if="currentTrack">
|
<header v-if="currentTrack">
|
||||||
<h3><a :href="fullUrl('/library/tracks/' + currentTrack.id)" target="_blank" rel="noopener noreferrer">{{ currentTrack.title }}</a></h3>
|
<h3>
|
||||||
<a :href="fullUrl('/library/artists/' + currentTrack.artist.id)" target="_blank" rel="noopener noreferrer">{{ currentTrack.artist.name }}</a>
|
<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>
|
</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">
|
<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">
|
<div class="plyr__controls">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
|
class="plyr__control"
|
||||||
|
aria-label="Play previous track"
|
||||||
@focus="setControlFocus($event, true)"
|
@focus="setControlFocus($event, true)"
|
||||||
@blur="setControlFocus($event, false)"
|
@blur="setControlFocus($event, false)"
|
||||||
@click="previous()"
|
@click="previous()"
|
||||||
type="button"
|
>
|
||||||
class="plyr__control"
|
<svg
|
||||||
aria-label="Play previous track">
|
class="icon--not-pressed"
|
||||||
<svg class="icon--not-pressed" role="presentation" focusable="false" viewBox="0 0 1100 1650" width="80" height="80">
|
role="presentation"
|
||||||
<use xlink:href="#plyr-step-backward"></use>
|
focusable="false"
|
||||||
|
viewBox="0 0 1100 1650"
|
||||||
|
width="80"
|
||||||
|
height="80"
|
||||||
|
>
|
||||||
|
<use xlink:href="#plyr-step-backward" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
|
class="plyr__control"
|
||||||
|
aria-label="Play next track"
|
||||||
@click="next()"
|
@click="next()"
|
||||||
@focus="setControlFocus($event, true)"
|
@focus="setControlFocus($event, true)"
|
||||||
@blur="setControlFocus($event, false)"
|
@blur="setControlFocus($event, false)"
|
||||||
type="button"
|
>
|
||||||
class="plyr__control"
|
<svg
|
||||||
aria-label="Play next track">
|
class="icon--not-pressed"
|
||||||
<svg class="icon--not-pressed" role="presentation" focusable="false" viewBox="0 0 1100 1650" width="80" height="80">
|
role="presentation"
|
||||||
<use xlink:href="#plyr-step-forward"></use>
|
focusable="false"
|
||||||
|
viewBox="0 0 1100 1650"
|
||||||
|
width="80"
|
||||||
|
height="80"
|
||||||
|
>
|
||||||
|
<use xlink:href="#plyr-step-forward" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,51 +115,122 @@
|
||||||
:key="currentIndex"
|
:key="currentIndex"
|
||||||
ref="player"
|
ref="player"
|
||||||
class="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">
|
<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>
|
</audio>
|
||||||
</vue-plyr>
|
</vue-plyr>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="player">
|
<div
|
||||||
<span v-if="error === 'invalid_type'" class="error">Widget improperly configured (bad resource type {{ type }}).</span>
|
v-else
|
||||||
<span v-else-if="error === 'invalid_id'" class="error">Widget improperly configured (missing resource id).</span>
|
class="player"
|
||||||
<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
|
||||||
<span v-else-if="error === 'server_error'" class="error">A server error occurred.</span>
|
v-if="error === 'invalid_type'"
|
||||||
<span v-else-if="error === 'server_error'" class="error">An unknown error occurred while loading track data from server.</span>
|
class="error"
|
||||||
<span v-else-if="currentTrack && currentTrack.sources.length === 0" class="error">This track is unavailable.</span>
|
>Widget improperly configured (bad resource type {{ type }}).</span>
|
||||||
<span v-else class="error">An unknown error occurred while loading track data.</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>
|
</div>
|
||||||
<a title="Funkwhale" href="https://funkwhale.audio" target="_blank" rel="noopener noreferrer" class="logo-wrapper">
|
<a
|
||||||
<logo :fill="currentTheme.textColor" class="logo"></logo>
|
title="Funkwhale"
|
||||||
|
href="https://funkwhale.audio"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="logo-wrapper"
|
||||||
|
>
|
||||||
|
<logo
|
||||||
|
:fill="currentTheme.textColor"
|
||||||
|
class="logo"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</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">
|
<table class="queue">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<tr
|
||||||
:id="'queue-item-' + index"
|
v-for="(track, index) in tracks"
|
||||||
role="button"
|
|
||||||
v-if="track.sources.length > 0"
|
v-if="track.sources.length > 0"
|
||||||
|
:id="'queue-item-' + index"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
role="button"
|
||||||
:class="[{active: index === currentIndex}]"
|
:class="[{active: index === currentIndex}]"
|
||||||
@click="play(index)"
|
@click="play(index)"
|
||||||
@keyup.enter="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">
|
<span class="position">
|
||||||
{{ index + 1 }}
|
{{ index + 1 }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="title" :title="track.title" ><div colspan="2" class="ellipsis">{{ track.title }}</div></td>
|
<td
|
||||||
<td class="artist" :title="track.artist.name" ><div class="ellipsis">{{ track.artist.name }}</div></td>
|
class="title"
|
||||||
<td class="album">
|
:title="track.title"
|
||||||
<div class="ellipsis" v-if="track.album" :title="track.album.title">{{ track.album.title }}</div>
|
>
|
||||||
|
<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>
|
||||||
<td width="50">{{ time.durationFormatted(track.sources[0].duration) }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -116,26 +240,24 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import Logo from "@/components/Logo"
|
import Logo from '@/components/Logo'
|
||||||
import url from '@/utils/url'
|
import url from '@/utils/url'
|
||||||
import time from '@/utils/time'
|
import time from '@/utils/time'
|
||||||
|
|
||||||
function getURLParams () {
|
function getURLParams () {
|
||||||
var urlParams
|
let match
|
||||||
var match,
|
const pl = /\+/g // Regex for replacing addition symbol with a space
|
||||||
pl = /\+/g, // Regex for replacing addition symbol with a space
|
const urlParams = {}
|
||||||
search = /([^&=]+)=?([^&]*)/g,
|
const search = /([^&=]+)=?([^&]*)/g
|
||||||
decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); },
|
const decode = function (s) { return decodeURIComponent(s.replace(pl, ' ')) }
|
||||||
query = window.location.search.substring(1);
|
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
|
return urlParams
|
||||||
}
|
}
|
||||||
export default {
|
export default {
|
||||||
name: 'app',
|
name: 'App',
|
||||||
components: {Logo},
|
components: { Logo },
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
time,
|
time,
|
||||||
|
@ -152,13 +274,59 @@ export default {
|
||||||
currentIndex: -1,
|
currentIndex: -1,
|
||||||
themes: {
|
themes: {
|
||||||
dark: {
|
dark: {
|
||||||
textColor: 'white',
|
textColor: 'white'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
currentTrack () {
|
||||||
|
if (this.tracks.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return this.tracks[this.currentIndex]
|
||||||
|
},
|
||||||
|
currentTheme () {
|
||||||
|
return this.themes[this.theme]
|
||||||
|
},
|
||||||
|
controls () {
|
||||||
|
return [
|
||||||
|
'play', // Play/pause playback
|
||||||
|
'progress', // The progress bar and scrubber for playback and buffering
|
||||||
|
'current-time', // The current time of playback
|
||||||
|
'mute', // Toggle mute
|
||||||
|
'volume' // Volume control
|
||||||
|
]
|
||||||
|
},
|
||||||
|
hasPrevious () {
|
||||||
|
return this.currentIndex > 0
|
||||||
|
},
|
||||||
|
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 () {
|
created () {
|
||||||
let params = getURLParams()
|
const params = getURLParams()
|
||||||
this.baseUrl = params.b || ''
|
this.baseUrl = params.b || ''
|
||||||
this.type = params.type
|
this.type = params.type
|
||||||
if (this.supportedTypes.indexOf(this.type) === -1) {
|
if (this.supportedTypes.indexOf(this.type) === -1) {
|
||||||
|
@ -172,44 +340,18 @@ export default {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!!params.instance) {
|
if (params.instance) {
|
||||||
this.baseUrl = params.instance
|
this.baseUrl = params.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
this.autoplay = params.autoplay != undefined || params.auto_play != undefined
|
this.autoplay = params.autoplay !== undefined || params.auto_play !== undefined
|
||||||
this.fetch(this.type, this.id)
|
this.fetch(this.type, this.id)
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
var parser = document.createElement('a')
|
const parser = document.createElement('a')
|
||||||
parser.href = this.baseUrl
|
parser.href = this.baseUrl
|
||||||
this.url = parser
|
this.url = parser
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
currentTrack () {
|
|
||||||
if (this.tracks.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return this.tracks[this.currentIndex]
|
|
||||||
},
|
|
||||||
currentTheme () {
|
|
||||||
return this.themes[this.theme]
|
|
||||||
},
|
|
||||||
controls () {
|
|
||||||
return [
|
|
||||||
'play', // Play/pause playback
|
|
||||||
'progress', // The progress bar and scrubber for playback and buffering
|
|
||||||
'current-time', // The current time of playback
|
|
||||||
'mute', // Toggle mute
|
|
||||||
'volume', // Volume control
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hasPrevious () {
|
|
||||||
return this.currentIndex > 0
|
|
||||||
},
|
|
||||||
hasNext () {
|
|
||||||
return this.currentIndex < this.tracks.length - 1
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
next () {
|
next () {
|
||||||
if (this.hasNext) {
|
if (this.hasNext) {
|
||||||
|
@ -221,11 +363,11 @@ export default {
|
||||||
this.play(this.currentIndex - 1)
|
this.play(this.currentIndex - 1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setControlFocus(event, enable) {
|
setControlFocus (event, enable) {
|
||||||
if (enable) {
|
if (enable) {
|
||||||
event.target.classList.add("plyr__tab-focus");
|
event.target.classList.add('plyr__tab-focus')
|
||||||
} else {
|
} else {
|
||||||
event.target.classList.remove("plyr__tab-focus");
|
event.target.classList.remove('plyr__tab-focus')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetch (type, id) {
|
fetch (type, id) {
|
||||||
|
@ -233,13 +375,13 @@ export default {
|
||||||
this.fetchTrack(id)
|
this.fetchTrack(id)
|
||||||
}
|
}
|
||||||
if (type === 'album') {
|
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') {
|
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') {
|
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') {
|
if (type === 'playlist') {
|
||||||
this.fetchTracks({}, `/api/v1/playlists/${id}/tracks/`)
|
this.fetchTracks({}, `/api/v1/playlists/${id}/tracks/`)
|
||||||
|
@ -247,67 +389,61 @@ export default {
|
||||||
},
|
},
|
||||||
play (index) {
|
play (index) {
|
||||||
this.currentIndex = index
|
this.currentIndex = index
|
||||||
let self = this
|
const self = this
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
self.$refs.player.player.play()
|
self.$refs.player.player.play()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
fetchTrack (id) {
|
fetchTrack (id) {
|
||||||
let self = this
|
const self = this
|
||||||
let url = `${this.baseUrl}/api/v1/tracks/${id}/`
|
const url = `${this.baseUrl}/api/v1/tracks/${id}/`
|
||||||
axios.get(url).then(response => {
|
axios.get(url).then(response => {
|
||||||
self.tracks = self.parseTracks([response.data])
|
self.tracks = self.parseTracks([response.data])
|
||||||
self.isLoading = false;
|
self.isLoading = false
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
if (error.response.status === 404) {
|
if (error.response.status === 404) {
|
||||||
self.error = 'server_not_found'
|
self.error = 'server_not_found'
|
||||||
}
|
} else if (error.response.status === 403) {
|
||||||
else if (error.response.status === 403) {
|
|
||||||
self.error = 'server_requires_auth'
|
self.error = 'server_requires_auth'
|
||||||
}
|
} else if (error.response.status === 500) {
|
||||||
else if (error.response.status === 500) {
|
|
||||||
self.error = 'server_error'
|
self.error = 'server_error'
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
self.error = 'server_unknown_error'
|
self.error = 'server_unknown_error'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.error = 'server_unknown_error'
|
self.error = 'server_unknown_error'
|
||||||
}
|
}
|
||||||
self.isLoading = false;
|
self.isLoading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
fetchTracks (filters, path) {
|
fetchTracks (filters, path) {
|
||||||
path = path || "/api/v1/tracks/"
|
path = path || '/api/v1/tracks/'
|
||||||
filters.include_channels = "true"
|
filters.include_channels = 'true'
|
||||||
let self = this
|
const self = this
|
||||||
let url = `${this.baseUrl}${path}`
|
const url = `${this.baseUrl}${path}`
|
||||||
axios.get(url, {params: filters}).then(response => {
|
axios.get(url, { params: filters }).then(response => {
|
||||||
self.tracks = self.parseTracks(response.data.results)
|
self.tracks = self.parseTracks(response.data.results)
|
||||||
self.isLoading = false;
|
self.isLoading = false
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
if (error.response.status === 404) {
|
if (error.response.status === 404) {
|
||||||
self.error = 'server_not_found'
|
self.error = 'server_not_found'
|
||||||
}
|
} else if (error.response.status === 403) {
|
||||||
else if (error.response.status === 403) {
|
|
||||||
self.error = 'server_requires_auth'
|
self.error = 'server_requires_auth'
|
||||||
}
|
} else if (error.response.status === 500) {
|
||||||
else if (error.response.status === 500) {
|
|
||||||
self.error = 'server_error'
|
self.error = 'server_error'
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
self.error = 'server_unknown_error'
|
self.error = 'server_unknown_error'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.error = 'server_unknown_error'
|
self.error = 'server_unknown_error'
|
||||||
}
|
}
|
||||||
self.isLoading = false;
|
self.isLoading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
parseTracks (tracks) {
|
parseTracks (tracks) {
|
||||||
let self = this
|
const self = this
|
||||||
if (this.type === 'playlist') {
|
if (this.type === 'playlist') {
|
||||||
tracks = tracks.map((t) => {
|
tracks = tracks.map((t) => {
|
||||||
return t.track
|
return t.track
|
||||||
|
@ -325,7 +461,7 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
bindEvents () {
|
bindEvents () {
|
||||||
let self = this
|
const self = this
|
||||||
this.$refs.player.player.on('ended', () => {
|
this.$refs.player.player.on('ended', () => {
|
||||||
self.next()
|
self.next()
|
||||||
})
|
})
|
||||||
|
@ -336,17 +472,17 @@ export default {
|
||||||
}
|
}
|
||||||
return path
|
return path
|
||||||
},
|
},
|
||||||
getCover(albumCover) {
|
getCover (albumCover) {
|
||||||
if (albumCover) {
|
if (albumCover) {
|
||||||
return albumCover.urls.medium_square_crop
|
return albumCover.urls.medium_square_crop
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getSources (uploads) {
|
getSources (uploads) {
|
||||||
let self = this;
|
const self = this
|
||||||
let a = document.createElement('audio')
|
const a = document.createElement('audio')
|
||||||
let allowed = ['probably', 'maybe']
|
const allowed = ['probably', 'maybe']
|
||||||
let sources = uploads.filter(u => {
|
const sources = uploads.filter(u => {
|
||||||
let canPlay = a.canPlayType(u.mimetype)
|
const canPlay = a.canPlayType(u.mimetype)
|
||||||
return allowed.indexOf(canPlay) > -1
|
return allowed.indexOf(canPlay) > -1
|
||||||
}).map(u => {
|
}).map(u => {
|
||||||
return {
|
return {
|
||||||
|
@ -371,26 +507,6 @@ export default {
|
||||||
}
|
}
|
||||||
return sources
|
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>
|
</script>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
var Album = {
|
const Album = {
|
||||||
clean (album) {
|
clean (album) {
|
||||||
// we manually rebind the album and artist to each child track
|
// we manually rebind the album and artist to each child track
|
||||||
album.tracks = album.tracks.map((track) => {
|
album.tracks = album.tracks.map((track) => {
|
||||||
|
@ -8,7 +8,7 @@ var Album = {
|
||||||
return album
|
return album
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var Artist = {
|
const Artist = {
|
||||||
clean (artist) {
|
clean (artist) {
|
||||||
// clean data as given by the API
|
// clean data as given by the API
|
||||||
artist.albums = artist.albums.map((album) => {
|
artist.albums = artist.albums.map((album) => {
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
const DYNAMIC_RANGE = 40 // dB
|
const DYNAMIC_RANGE = 40 // dB
|
||||||
|
|
||||||
function toLinearVolumeScale(v) {
|
function toLinearVolumeScale (v) {
|
||||||
if (v <= 0.0) {
|
if (v <= 0.0) {
|
||||||
return 0.0
|
return 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
// (1.0; 0.0) -> (0; -DYNAMIC_RANGE) dB
|
// (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)
|
return Math.pow(10, dB / 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toLogarithmicVolumeScale(v) {
|
function toLogarithmicVolumeScale (v) {
|
||||||
if (v <= 0.0) {
|
if (v <= 0.0) {
|
||||||
return 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)
|
// (0; -DYNAMIC_RANGE) [dB] -> (1.0; 0.0)
|
||||||
return 1 - (dB / -DYNAMIC_RANGE)
|
return 1 - (dB / -DYNAMIC_RANGE)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.toLinearVolumeScale = toLinearVolumeScale
|
exports.toLinearVolumeScale = toLinearVolumeScale
|
||||||
|
|
|
@ -1,77 +1,208 @@
|
||||||
<template>
|
<template>
|
||||||
<footer id="footer" role="contentinfo" class="ui vertical footer segment" aria-labelledby="footer-label">
|
<footer
|
||||||
<h1 id="footer-label" class="visually-hidden">
|
id="footer"
|
||||||
<translate translate-context="*/*/*">Application footer</translate>
|
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>
|
</h1>
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
<div class="ui stackable equal height stackable grid">
|
<div class="ui stackable equal height stackable grid">
|
||||||
<section class="four wide column">
|
<section class="four wide column">
|
||||||
<h4 v-if="podName" class="ui header ellipsis">
|
<h4
|
||||||
<span v-translate="{instanceName: podName}" translate-context="Footer/About/Title">About %{instanceName}</span>
|
v-if="podName"
|
||||||
|
class="ui header ellipsis"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-translate="{instanceName: podName}"
|
||||||
|
translate-context="Footer/About/Title"
|
||||||
|
>About %{instanceName}</span>
|
||||||
</h4>
|
</h4>
|
||||||
<h4 v-else class="ui header ellipsis">
|
<h4
|
||||||
<span v-translate="{instanceUrl: instanceHostname}" translate-context="Footer/About/Title">About %{instanceUrl}</span>
|
v-else
|
||||||
|
class="ui header ellipsis"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-translate="{instanceUrl: instanceHostname}"
|
||||||
|
translate-context="Footer/About/Title"
|
||||||
|
>About %{instanceUrl}</span>
|
||||||
</h4>
|
</h4>
|
||||||
<div class="ui list">
|
<div class="ui list">
|
||||||
<router-link v-if="this.$route.path != '/about'" class="link item" to="/about">
|
<router-link
|
||||||
<translate translate-context="Footer/About/List item.Link">About</translate>
|
v-if="$route.path != '/about'"
|
||||||
|
class="link item"
|
||||||
|
to="/about"
|
||||||
|
>
|
||||||
|
<translate translate-context="Footer/About/List item.Link">
|
||||||
|
About
|
||||||
|
</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link v-else-if="this.$route.path == '/about' && $store.state.auth.authenticated" class="link item" to="/library">
|
<router-link
|
||||||
<translate translate-context="Footer/*/List item.Link">Go to Library</translate>
|
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>
|
||||||
<router-link v-else class="link item" to="/">
|
<router-link
|
||||||
<translate translate-context="Footer/*/List item.Link">Home Page</translate>
|
v-else
|
||||||
|
class="link item"
|
||||||
|
to="/"
|
||||||
|
>
|
||||||
|
<translate translate-context="Footer/*/List item.Link">
|
||||||
|
Home Page
|
||||||
|
</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
<a v-if="version" class="link item" href="https://docs.funkwhale.audio/changelog.html" target="_blank">
|
<a
|
||||||
<translate translate-context="Footer/*/List item" :translate-params="{version: version}" >Version %{version}</translate>
|
v-if="version"
|
||||||
</a>
|
class="link item"
|
||||||
<a role="button" href="" class="link item" @click.prevent="$emit('show:set-instance-modal')" >
|
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')"
|
||||||
|
>
|
||||||
<translate translate-context="Footer/*/List item.Link">Use another instance</translate>
|
<translate translate-context="Footer/*/List item.Link">Use another instance</translate>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui form">
|
<div class="ui form">
|
||||||
<div class="ui field">
|
<div class="ui field">
|
||||||
<label for="language-select"><translate translate-context="Footer/Settings/Dropdown.Label/Short, Verb">Change language</translate></label>
|
<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)">
|
<select
|
||||||
<option v-for="(language, key) in $language.available" :key="key" :value="key">{{ language }}</option>
|
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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="four wide column">
|
<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">
|
<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
|
||||||
<a href="https://funkwhale.audio/apps" class="link item" target="_blank"><translate translate-context="Footer/*/List item.Link">Mobile and desktop apps</translate></a>
|
href="https://docs.funkwhale.audio"
|
||||||
<a hrelf="" class="link item" @click.prevent="$emit('show:shortcuts-modal')"><translate translate-context="*/*/*/Noun">Keyboard shortcuts</translate></a>
|
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>
|
||||||
<div class="ui form">
|
<div class="ui form">
|
||||||
<div class="ui field">
|
<div class="ui field">
|
||||||
<label for="theme-select"><translate translate-context="Footer/Settings/Dropdown.Label/Short, Verb">Change theme</translate></label>
|
<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)">
|
<select
|
||||||
<option v-for="theme in themes" :key="theme.key" :value="theme.key">{{ theme.name }}</option>
|
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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="four wide column">
|
<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">
|
<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
|
||||||
<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>
|
href="https://forum.funkwhale.audio/"
|
||||||
<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>
|
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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="four wide column">
|
<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">
|
<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
|
||||||
<a href="https://contribute.funkwhale.audio" class="link item" target="_blank"><translate translate-context="Footer/*/List item.Link">Contribute</translate></a>
|
href="https://funkwhale.audio"
|
||||||
<a href="https://dev.funkwhale.audio/funkwhale/funkwhale" class="link item" target="_blank"><translate translate-context="Footer/*/List item.Link">Source code</translate></a>
|
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>
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<p>
|
<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>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
@ -80,24 +211,22 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Vue from "vue"
|
import { mapState } from 'vuex'
|
||||||
import { mapState } from "vuex"
|
|
||||||
import axios from 'axios'
|
|
||||||
import _ from '@/lodash'
|
import _ from '@/lodash'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ["version"],
|
props: { version: { type: String, required: true } },
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
messages: state => state.ui.messages,
|
messages: state => state.ui.messages,
|
||||||
nodeinfo: state => state.instance.nodeinfo,
|
nodeinfo: state => state.instance.nodeinfo
|
||||||
}),
|
}),
|
||||||
podName() {
|
podName () {
|
||||||
return _.get(this.nodeinfo, 'metadata.nodeName')
|
return _.get(this.nodeinfo, 'metadata.nodeName')
|
||||||
},
|
},
|
||||||
instanceHostname() {
|
instanceHostname () {
|
||||||
let url = this.$store.state.instance.instanceUrl
|
const url = this.$store.state.instance.instanceUrl
|
||||||
let parser = document.createElement("a")
|
const parser = document.createElement('a')
|
||||||
parser.href = url
|
parser.href = url
|
||||||
return parser.hostname
|
return parser.hostname
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,15 +1,25 @@
|
||||||
<template>
|
<template>
|
||||||
<main class="main pusher page-home" v-title="labels.title">
|
<main
|
||||||
<section :class="['ui', 'head', {'with-background': banner}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" :style="headerStyle">
|
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">
|
<div class="segment-content">
|
||||||
<h1 class="ui center aligned large header">
|
<h1 class="ui center aligned large header">
|
||||||
<span
|
<span
|
||||||
v-translate="{podName: podName}"
|
v-translate="{podName: podName}"
|
||||||
translate-context="Content/Home/Header"
|
translate-context="Content/Home/Header"
|
||||||
:translate-params="{podName: podName}">
|
:translate-params="{podName: podName}"
|
||||||
|
>
|
||||||
Welcome to %{ podName }!
|
Welcome to %{ podName }!
|
||||||
</span>
|
</span>
|
||||||
<div v-if="shortDescription" class="sub header">
|
<div
|
||||||
|
v-if="shortDescription"
|
||||||
|
class="sub header"
|
||||||
|
>
|
||||||
{{ shortDescription }}
|
{{ shortDescription }}
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -19,31 +29,61 @@
|
||||||
<div class="ui stackable grid">
|
<div class="ui stackable grid">
|
||||||
<div class="ten wide column">
|
<div class="ten wide column">
|
||||||
<h2 class="header">
|
<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>
|
</h2>
|
||||||
<div class="ui raised segment" id="pod">
|
<div
|
||||||
|
id="pod"
|
||||||
|
class="ui raised segment"
|
||||||
|
>
|
||||||
<div class="ui stackable grid">
|
<div class="ui stackable grid">
|
||||||
<div class="eight wide column">
|
<div class="eight wide column">
|
||||||
<p v-if="!truncatedDescription">
|
<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>
|
</p>
|
||||||
<template v-if="truncatedDescription || rules">
|
<template v-if="truncatedDescription || rules">
|
||||||
<div v-if="truncatedDescription" v-html="truncatedDescription"></div>
|
<div
|
||||||
<div v-if="truncatedDescription" class="ui hidden divider"></div>
|
v-if="truncatedDescription"
|
||||||
|
v-html="truncatedDescription"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="truncatedDescription"
|
||||||
|
class="ui hidden divider"
|
||||||
|
/>
|
||||||
<div class="ui relaxed list">
|
<div class="ui relaxed list">
|
||||||
<div class="item" v-if="truncatedDescription">
|
<div
|
||||||
<i class="arrow right icon"></i>
|
v-if="truncatedDescription"
|
||||||
|
class="item"
|
||||||
|
>
|
||||||
|
<i class="arrow right icon" />
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<router-link class="ui link" :to="{name: 'about'}">
|
<router-link
|
||||||
<translate translate-context="Content/Home/Link">Learn more</translate>
|
class="ui link"
|
||||||
|
:to="{name: 'about'}"
|
||||||
|
>
|
||||||
|
<translate translate-context="Content/Home/Link">
|
||||||
|
Learn more
|
||||||
|
</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item" v-if="rules">
|
<div
|
||||||
<i class="book open icon"></i>
|
v-if="rules"
|
||||||
|
class="item"
|
||||||
|
>
|
||||||
|
<i class="book open icon" />
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<router-link class="ui link" v-if="rules" :to="{name: 'about', hash: '#rules'}">
|
<router-link
|
||||||
<translate translate-context="Content/Home/Link">Server rules</translate>
|
v-if="rules"
|
||||||
|
class="ui link"
|
||||||
|
:to="{name: 'about', hash: '#rules'}"
|
||||||
|
>
|
||||||
|
<translate translate-context="Content/Home/Link">
|
||||||
|
Server rules
|
||||||
|
</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,71 +93,130 @@
|
||||||
<div class="eight wide column">
|
<div class="eight wide column">
|
||||||
<template v-if="stats">
|
<template v-if="stats">
|
||||||
<h3 class="sub header">
|
<h3 class="sub header">
|
||||||
<translate translate-context="Content/Home/Header">Statistics</translate>
|
<translate translate-context="Content/Home/Header">
|
||||||
|
Statistics
|
||||||
|
</translate>
|
||||||
</h3>
|
</h3>
|
||||||
<p>
|
<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>
|
||||||
<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>
|
</p>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
<template v-if="contactEmail">
|
<template v-if="contactEmail">
|
||||||
<h3 class="sub header">
|
<h3 class="sub header">
|
||||||
<translate translate-context="Content/Home/Header/Name">Contact</translate>
|
<translate translate-context="Content/Home/Header/Name">
|
||||||
|
Contact
|
||||||
|
</translate>
|
||||||
</h3>
|
</h3>
|
||||||
<i class="at icon"></i>
|
<i class="at icon" />
|
||||||
<a :href="`mailto:${contactEmail}`">{{ contactEmail }}</a>
|
<a :href="`mailto:${contactEmail}`">{{ contactEmail }}</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="six wide column">
|
<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>
|
</div>
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<div class="ui stackable grid">
|
<div class="ui stackable grid">
|
||||||
<div class="four wide column">
|
<div class="four wide column">
|
||||||
<h3 class="header">
|
<h3 class="header">
|
||||||
<translate translate-context="Footer/*/Title/Short">About Funkwhale</translate>
|
<translate translate-context="Footer/*/Title/Short">
|
||||||
|
About Funkwhale
|
||||||
|
</translate>
|
||||||
</h3>
|
</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
|
||||||
<p v-translate translate-context="Content/Home/Paragraph">Funkwhale is free and developed by a friendly community of volunteers.</p>
|
v-translate
|
||||||
<a target="_blank" rel="noopener" href="https://funkwhale.audio">
|
translate-context="Content/Home/Paragraph"
|
||||||
<i class="external alternate icon"></i>
|
>
|
||||||
|
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>
|
<translate translate-context="Content/Home/Link">Visit funkwhale.audio</translate>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="four wide column">
|
<div class="four wide column">
|
||||||
<h3 class="header">
|
<h3 class="header">
|
||||||
<translate translate-context="Head/Login/Title">Log In</translate>
|
<translate translate-context="Head/Login/Title">
|
||||||
|
Log In
|
||||||
|
</translate>
|
||||||
</h3>
|
</h3>
|
||||||
<login-form button-classes="success" :show-signup="false"></login-form>
|
<login-form
|
||||||
<div class="ui hidden clearing divider"></div>
|
button-classes="success"
|
||||||
|
:show-signup="false"
|
||||||
|
/>
|
||||||
|
<div class="ui hidden clearing divider" />
|
||||||
</div>
|
</div>
|
||||||
<div class="four wide column">
|
<div class="four wide column">
|
||||||
<h3 class="header">
|
<h3 class="header">
|
||||||
<translate translate-context="*/Signup/Title">Sign up</translate>
|
<translate translate-context="*/Signup/Title">
|
||||||
|
Sign up
|
||||||
|
</translate>
|
||||||
</h3>
|
</h3>
|
||||||
<template v-if="openRegistrations">
|
<template v-if="openRegistrations">
|
||||||
<p>
|
<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>
|
||||||
<p v-if="defaultUploadQuota">
|
<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>
|
</p>
|
||||||
<signup-form button-classes="success" :show-login="false"></signup-form>
|
<signup-form
|
||||||
|
button-classes="success"
|
||||||
|
:show-login="false"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div v-else>
|
<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>
|
<p translate-context="Content/Home/Paragraph">
|
||||||
<a target="_blank" rel="noopener" href="https://funkwhale.audio/#get-started">
|
Registrations are closed on this pod. You can signup on another pod using the link below.
|
||||||
<i class="external alternate icon"></i>
|
</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>
|
<translate translate-context="Content/Home/Link">Find another pod</translate>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -125,39 +224,63 @@
|
||||||
|
|
||||||
<div class="four wide column">
|
<div class="four wide column">
|
||||||
<h3 class="header">
|
<h3 class="header">
|
||||||
<translate translate-context="Content/Home/Header">Useful links</translate>
|
<translate translate-context="Content/Home/Header">
|
||||||
|
Useful links
|
||||||
|
</translate>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="ui relaxed list">
|
<div class="ui relaxed list">
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<i class="headphones icon"></i>
|
<i class="headphones icon" />
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<router-link v-if="anonymousCanListen" class="header" to="/library">
|
<router-link
|
||||||
<translate translate-context="Content/Home/Link">Browse public content</translate>
|
v-if="anonymousCanListen"
|
||||||
|
class="header"
|
||||||
|
to="/library"
|
||||||
|
>
|
||||||
|
<translate translate-context="Content/Home/Link">
|
||||||
|
Browse public content
|
||||||
|
</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="description">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<i class="mobile alternate icon"></i>
|
<i class="mobile alternate icon" />
|
||||||
<div class="content">
|
<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>
|
<translate translate-context="Content/Home/Link">Mobile apps</translate>
|
||||||
</a>
|
</a>
|
||||||
<div class="description">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<i class="book icon"></i>
|
<i class="book icon" />
|
||||||
<div class="content">
|
<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>
|
<translate translate-context="Content/Home/Link">User guides</translate>
|
||||||
</a>
|
</a>
|
||||||
<div class="description">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -165,20 +288,37 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section v-if="anonymousCanListen" class="ui vertical stripe segment">
|
<section
|
||||||
<album-widget :filters="{playable: true, ordering: '-creation_date'}" :limit="10">
|
v-if="anonymousCanListen"
|
||||||
<template slot="title"><translate translate-context="Content/Home/Title">Recently added albums</translate></template>
|
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">
|
<router-link to="/library">
|
||||||
<translate translate-context="Content/Home/Link">View more…</translate>
|
<translate translate-context="Content/Home/Link">
|
||||||
<div class="ui hidden divider"></div>
|
View more…
|
||||||
|
</translate>
|
||||||
|
<div class="ui hidden divider" />
|
||||||
</router-link>
|
</router-link>
|
||||||
</album-widget>
|
</album-widget>
|
||||||
<div class="ui hidden section divider"></div>
|
<div class="ui hidden section divider" />
|
||||||
<h3 class="ui header" >
|
<h3 class="ui header">
|
||||||
<translate translate-context="*/*/*">New channels</translate>
|
<translate translate-context="*/*/*">
|
||||||
|
New channels
|
||||||
|
</translate>
|
||||||
</h3>
|
</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>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
@ -186,20 +326,20 @@
|
||||||
<script>
|
<script>
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
import _ from '@/lodash'
|
import _ from '@/lodash'
|
||||||
import {mapState} from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import showdown from 'showdown'
|
import showdown from 'showdown'
|
||||||
import AlbumWidget from "@/components/audio/album/Widget"
|
import AlbumWidget from '@/components/audio/album/Widget'
|
||||||
import ChannelsWidget from "@/components/audio/ChannelsWidget"
|
import ChannelsWidget from '@/components/audio/ChannelsWidget'
|
||||||
import LoginForm from "@/components/auth/LoginForm"
|
import LoginForm from '@/components/auth/LoginForm'
|
||||||
import SignupForm from "@/components/auth/SignupForm"
|
import SignupForm from '@/components/auth/SignupForm'
|
||||||
import {humanSize } from '@/filters'
|
import { humanSize } from '@/filters'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
AlbumWidget,
|
AlbumWidget,
|
||||||
ChannelsWidget,
|
ChannelsWidget,
|
||||||
LoginForm,
|
LoginForm,
|
||||||
SignupForm,
|
SignupForm
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -210,15 +350,15 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
nodeinfo: state => state.instance.nodeinfo,
|
nodeinfo: state => state.instance.nodeinfo
|
||||||
}),
|
}),
|
||||||
labels() {
|
labels () {
|
||||||
return {
|
return {
|
||||||
title: this.$pgettext('Head/Home/Title', "Home")
|
title: this.$pgettext('Head/Home/Title', 'Home')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
podName() {
|
podName () {
|
||||||
return _.get(this.nodeinfo, 'metadata.nodeName') || "Funkwhale"
|
return _.get(this.nodeinfo, 'metadata.nodeName') || 'Funkwhale'
|
||||||
},
|
},
|
||||||
banner () {
|
banner () {
|
||||||
return _.get(this.nodeinfo, 'metadata.banner')
|
return _.get(this.nodeinfo, 'metadata.banner')
|
||||||
|
@ -236,12 +376,12 @@ export default {
|
||||||
if (!this.longDescription) {
|
if (!this.longDescription) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let doc = this.markdown.makeHtml(this.longDescription)
|
const doc = this.markdown.makeHtml(this.longDescription)
|
||||||
let nodes = $.parseHTML(doc)
|
const nodes = $.parseHTML(doc)
|
||||||
let excerptParts = []
|
const excerptParts = []
|
||||||
let handled = 0
|
let handled = 0
|
||||||
nodes.forEach((n) => {
|
nodes.forEach((n) => {
|
||||||
let content = n.innerHTML || n.nodeValue
|
const content = n.innerHTML || n.nodeValue
|
||||||
if (handled < this.excerptLength && content.trim()) {
|
if (handled < this.excerptLength && content.trim()) {
|
||||||
excerptParts.push(n)
|
excerptParts.push(n)
|
||||||
handled += 1
|
handled += 1
|
||||||
|
@ -250,9 +390,9 @@ export default {
|
||||||
return excerptParts.map((p) => { return p.outerHTML }).join('')
|
return excerptParts.map((p) => { return p.outerHTML }).join('')
|
||||||
},
|
},
|
||||||
stats () {
|
stats () {
|
||||||
let data = {
|
const data = {
|
||||||
users: _.get(this.nodeinfo, 'usage.users.activeMonth', null),
|
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) {
|
if (data.users === null || data.artists === null) {
|
||||||
return
|
return
|
||||||
|
@ -271,16 +411,16 @@ export default {
|
||||||
openRegistrations () {
|
openRegistrations () {
|
||||||
return _.get(this.nodeinfo, 'openRegistrations')
|
return _.get(this.nodeinfo, 'openRegistrations')
|
||||||
},
|
},
|
||||||
headerStyle() {
|
headerStyle () {
|
||||||
if (!this.banner) {
|
if (!this.banner) {
|
||||||
return ""
|
return ''
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
"background-image: url(" +
|
'background-image: url(' +
|
||||||
this.$store.getters["instance/absoluteUrl"](this.banner) +
|
this.$store.getters['instance/absoluteUrl'](this.banner) +
|
||||||
")"
|
')'
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'$store.state.auth.authenticated': {
|
'$store.state.auth.authenticated': {
|
||||||
|
|
|
@ -1,31 +1,50 @@
|
||||||
<template>
|
<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"
|
<svg
|
||||||
viewBox="0 0 141.7 141.7" enable-background="new 0 0 141.7 141.7" xml:space="preserve">
|
id="layer_1"
|
||||||
<g>
|
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
|
<g>
|
||||||
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
|
||||||
<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
|
: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"
|
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
|
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
|
</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
|
||||||
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
|
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
|
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>
|
/>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
fill: {type: String, default: '#222222'}
|
fill: { type: String, default: '#222222' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
<template>
|
<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 transform="translate(34.65295 -109.48195)">
|
||||||
<g>
|
<g>
|
||||||
<g transform="matrix(.3191 0 0 .3191 -45.91741 93.47184)">
|
<g transform="matrix(.3191 0 0 .3191 -45.91741 93.47184)">
|
||||||
|
@ -14,7 +17,11 @@
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</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="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="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" />
|
<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>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
primary: {type: String, default: '#009fe3'},
|
primary: { type: String, default: '#009fe3' },
|
||||||
secondary: {type: String, default: 'var(--text-color)'},
|
secondary: { type: String, default: 'var(--text-color)' },
|
||||||
text: {type: String, default: 'var(--text-color)'},
|
text: { type: String, default: 'var(--text-color)' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,19 +1,33 @@
|
||||||
<template>
|
<template>
|
||||||
<main class="main pusher" :v-title="labels.title">
|
<main
|
||||||
|
class="main pusher"
|
||||||
|
:v-title="labels.title"
|
||||||
|
>
|
||||||
<section class="ui vertical stripe segment">
|
<section class="ui vertical stripe segment">
|
||||||
<div class="ui text container">
|
<div class="ui text container">
|
||||||
<h1 class="ui huge header">
|
<h1 class="ui huge header">
|
||||||
<i class="warning icon"></i>
|
<i class="warning icon" />
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<translate translate-context="Content/*/Title">Page not found!</translate>
|
<translate translate-context="Content/*/Title">
|
||||||
|
Page not found!
|
||||||
|
</translate>
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</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>
|
<a :href="path">{{ path }}</a>
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<router-link class="ui icon labeled right button" to="/">
|
<router-link
|
||||||
<translate translate-context="Content/*/Button.Label/Verb">Go to home page</translate>
|
class="ui icon labeled right button"
|
||||||
<i class="right arrow icon"></i>
|
to="/"
|
||||||
|
>
|
||||||
|
<translate translate-context="Content/*/Button.Label/Verb">
|
||||||
|
Go to home page
|
||||||
|
</translate>
|
||||||
|
<i class="right arrow icon" />
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -22,15 +36,15 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
data: function() {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
path: window.location.href
|
path: window.location.href
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
labels() {
|
labels () {
|
||||||
return {
|
return {
|
||||||
title: this.$pgettext('Head/*/Title', "Page Not Found")
|
title: this.$pgettext('Head/*/Title', 'Page Not Found')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,57 +1,68 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if='maxPage > 1' class="ui pagination menu component-pagination" role="navigation" :aria-label="labels.pagination">
|
<div
|
||||||
<a href
|
v-if="maxPage > 1"
|
||||||
|
class="ui pagination menu component-pagination"
|
||||||
|
role="navigation"
|
||||||
|
:aria-label="labels.pagination"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href
|
||||||
:disabled="current - 1 < 1"
|
:disabled="current - 1 < 1"
|
||||||
role="button"
|
role="button"
|
||||||
:aria-label="labels.previousPage"
|
:aria-label="labels.previousPage"
|
||||||
|
:class="[{'disabled': current - 1 < 1}, 'item']"
|
||||||
@click.prevent.stop="selectPage(current - 1)"
|
@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">
|
<template v-if="!compact">
|
||||||
<a href
|
<a
|
||||||
v-for="page in pages"
|
v-for="page in pages"
|
||||||
:key="page"
|
:key="page"
|
||||||
|
href
|
||||||
|
:class="[{'active': page === current}, {'disabled': page === 'skip'}, 'item']"
|
||||||
@click.prevent.stop="selectPage(page)"
|
@click.prevent.stop="selectPage(page)"
|
||||||
:class="[{'active': page === current}, {'disabled': page === 'skip'}, 'item']">
|
>
|
||||||
<span v-if="page !== 'skip'">{{ page }}</span>
|
<span v-if="page !== 'skip'">{{ page }}</span>
|
||||||
<span v-else>…</span>
|
<span v-else>…</span>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
<a href
|
<a
|
||||||
|
href
|
||||||
:disabled="current + 1 > maxPage"
|
:disabled="current + 1 > maxPage"
|
||||||
role="button"
|
role="button"
|
||||||
:aria-label="labels.nextPage"
|
:aria-label="labels.nextPage"
|
||||||
|
:class="[{'disabled': current + 1 > maxPage}, 'item']"
|
||||||
@click.prevent.stop="selectPage(current + 1)"
|
@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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _ from "@/lodash"
|
import _ from '@/lodash'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
current: { type: Number, default: 1 },
|
current: { type: Number, default: 1 },
|
||||||
paginateBy: { type: Number, default: 25 },
|
paginateBy: { type: Number, default: 25 },
|
||||||
total: { type: Number },
|
total: { type: Number, required: true },
|
||||||
compact: { type: Boolean, default: false }
|
compact: { type: Boolean, default: false }
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
labels() {
|
labels () {
|
||||||
return {
|
return {
|
||||||
pagination: this.$pgettext('Content/*/Hidden text/Noun', "Pagination"),
|
pagination: this.$pgettext('Content/*/Hidden text/Noun', 'Pagination'),
|
||||||
previousPage: this.$pgettext('Content/*/Link', "Previous Page"),
|
previousPage: this.$pgettext('Content/*/Link', 'Previous Page'),
|
||||||
nextPage: this.$pgettext('Content/*/Link', "Next Page")
|
nextPage: this.$pgettext('Content/*/Link', 'Next Page')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pages: function() {
|
pages: function () {
|
||||||
let range = 2
|
const range = 2
|
||||||
let current = this.current
|
const current = this.current
|
||||||
let beginning = _.range(1, Math.min(this.maxPage, 1 + range))
|
const beginning = _.range(1, Math.min(this.maxPage, 1 + range))
|
||||||
let middle = _.range(
|
const middle = _.range(
|
||||||
Math.max(1, current - range + 1),
|
Math.max(1, current - range + 1),
|
||||||
Math.min(this.maxPage, current + range)
|
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)
|
let allowed = beginning.concat(middle, end)
|
||||||
allowed = _.uniq(allowed)
|
allowed = _.uniq(allowed)
|
||||||
allowed = _.sortBy(allowed, [
|
allowed = _.sortBy(allowed, [
|
||||||
|
@ -59,11 +70,11 @@ export default {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
let final = []
|
const final = []
|
||||||
allowed.forEach(p => {
|
allowed.forEach(p => {
|
||||||
let last = final.slice(-1)[0]
|
const last = final.slice(-1)[0]
|
||||||
let consecutive = true
|
let consecutive = true
|
||||||
if (last === "skip") {
|
if (last === 'skip') {
|
||||||
consecutive = false
|
consecutive = false
|
||||||
} else {
|
} else {
|
||||||
if (!last) {
|
if (!last) {
|
||||||
|
@ -75,25 +86,25 @@ export default {
|
||||||
if (consecutive) {
|
if (consecutive) {
|
||||||
final.push(p)
|
final.push(p)
|
||||||
} else {
|
} else {
|
||||||
if (p !== "skip") {
|
if (p !== 'skip') {
|
||||||
final.push("skip")
|
final.push('skip')
|
||||||
final.push(p)
|
final.push(p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return final
|
return final
|
||||||
},
|
},
|
||||||
maxPage: function() {
|
maxPage: function () {
|
||||||
return Math.ceil(this.total / this.paginateBy)
|
return Math.ceil(this.total / this.paginateBy)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
selectPage: function(page) {
|
selectPage: function (page) {
|
||||||
if (page > this.maxPage || page < 1) {
|
if (page > this.maxPage || page < 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.current !== page) {
|
if (this.current !== page) {
|
||||||
this.$emit("page-changed", page)
|
this.$emit('page-changed', page)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,77 +1,148 @@
|
||||||
<template>
|
<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 vertical stripe queue segment', playerFocused ? 'player-focused' : '']">
|
||||||
<div class="ui fluid container">
|
<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 six wide column current-track">
|
||||||
<div class="ui basic segment" id="player">
|
<div
|
||||||
|
id="player"
|
||||||
|
class="ui basic segment"
|
||||||
|
>
|
||||||
<template v-if="currentTrack">
|
<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
|
||||||
<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)">
|
v-if="currentTrack.cover && currentTrack.cover.urls.large_square_crop"
|
||||||
<img class="ui image" alt="" v-else src="../assets/audio/default-cover.png">
|
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">
|
<h1 class="ui header">
|
||||||
<div class="content ellipsis">
|
<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 }}
|
{{ currentTrack.title }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="sub header ellipsis">
|
<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>
|
<router-link
|
||||||
<template v-if="currentTrack.album"> /
|
class="discrete link artist"
|
||||||
<router-link class="discrete link album" :to="{name: 'library.albums.detail', params: {id: currentTrack.album.id }}">{{ currentTrack.album.title }}</router-link>
|
: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>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="ui small warning message" v-if="currentTrack && errored">
|
<div
|
||||||
|
v-if="currentTrack && errored"
|
||||||
|
class="ui small warning message"
|
||||||
|
>
|
||||||
<h3 class="header">
|
<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>
|
</h3>
|
||||||
<p v-if="hasNext && playing && $store.state.player.errorCount < $store.state.player.maxConsecutiveErrors">
|
<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>
|
<translate translate-context="Sidebar/Player/Error message.Paragraph">
|
||||||
<i class="loading spinner icon"></i>
|
The next track will play automatically in a few seconds…
|
||||||
|
</translate>
|
||||||
|
<i class="loading spinner icon" />
|
||||||
</p>
|
</p>
|
||||||
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="additional-controls tablet-and-below">
|
<div class="additional-controls tablet-and-below">
|
||||||
<track-favorite-icon
|
<track-favorite-icon
|
||||||
v-if="$store.state.auth.authenticated"
|
v-if="$store.state.auth.authenticated"
|
||||||
:track="currentTrack"></track-favorite-icon>
|
:track="currentTrack"
|
||||||
|
/>
|
||||||
<track-playlist-icon
|
<track-playlist-icon
|
||||||
v-if="$store.state.auth.authenticated"
|
v-if="$store.state.auth.authenticated"
|
||||||
:track="currentTrack"></track-playlist-icon>
|
:track="currentTrack"
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="$store.state.auth.authenticated"
|
v-if="$store.state.auth.authenticated"
|
||||||
@click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})"
|
|
||||||
:class="['ui', 'really', 'basic', 'circular', 'icon', 'button']"
|
:class="['ui', 'really', 'basic', 'circular', 'icon', 'button']"
|
||||||
:aria-label="labels.addArtistContentFilter"
|
:aria-label="labels.addArtistContentFilter"
|
||||||
:title="labels.addArtistContentFilter">
|
:title="labels.addArtistContentFilter"
|
||||||
<i :class="['eye slash outline', 'basic', 'icon']"></i>
|
@click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})"
|
||||||
|
>
|
||||||
|
<i :class="['eye slash outline', 'basic', 'icon']" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-wrapper">
|
<div class="progress-wrapper">
|
||||||
<div class="progress-area" v-if="currentTrack && !errored">
|
<div
|
||||||
|
v-if="currentTrack && !errored"
|
||||||
|
class="progress-area"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
ref="progress"
|
ref="progress"
|
||||||
:class="['ui', 'small', 'vibrant', {'indicating': isLoadingAudio}, 'progress']"
|
:class="['ui', 'small', 'vibrant', {'indicating': isLoadingAudio}, 'progress']"
|
||||||
@click="touchProgress">
|
@click="touchProgress"
|
||||||
<div class="buffer bar" :data-percent="bufferProgress" :style="{ 'width': bufferProgress + '%' }"></div>
|
>
|
||||||
<div class="position bar" :data-percent="progress" :style="{ 'width': progress + '%' }"></div>
|
<div
|
||||||
|
class="buffer bar"
|
||||||
|
:data-percent="bufferProgress"
|
||||||
|
:style="{ 'width': bufferProgress + '%' }"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="position bar"
|
||||||
|
:data-percent="progress"
|
||||||
|
:style="{ 'width': progress + '%' }"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-area" v-else>
|
<div
|
||||||
|
v-else
|
||||||
|
class="progress-area"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
ref="progress"
|
ref="progress"
|
||||||
:class="['ui', 'small', 'vibrant', 'progress']">
|
:class="['ui', 'small', 'vibrant', 'progress']"
|
||||||
<div class="buffer bar"></div>
|
>
|
||||||
<div class="position bar"></div>
|
<div class="buffer bar" />
|
||||||
|
<div class="position bar" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress">
|
<div class="progress">
|
||||||
<template v-if="!isLoadingAudio">
|
<template v-if="!isLoadingAudio">
|
||||||
<a href="" :aria-label="labels.restart" class="left floated timer discrete start" @click.prevent="setCurrentTime(0)">{{currentTimeFormatted}}</a>
|
<a
|
||||||
<span class="right floated timer total">{{durationFormatted}}</span>
|
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>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span class="left floated timer">00:00</span>
|
<span class="left floated timer">00:00</span>
|
||||||
|
@ -80,45 +151,47 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="player-controls tablet-and-below">
|
<div class="player-controls tablet-and-below">
|
||||||
<template>
|
<span
|
||||||
<span
|
role="button"
|
||||||
role="button"
|
:title="labels.previousTrack"
|
||||||
:title="labels.previousTrack"
|
:aria-label="labels.previousTrack"
|
||||||
:aria-label="labels.previousTrack"
|
class="control"
|
||||||
class="control"
|
:disabled="emptyQueue"
|
||||||
@click.prevent.stop="$store.dispatch('queue/previous')"
|
@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>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
role="button"
|
v-if="!playing"
|
||||||
v-if="!playing"
|
role="button"
|
||||||
:title="labels.play"
|
:title="labels.play"
|
||||||
:aria-label="labels.play"
|
:aria-label="labels.play"
|
||||||
@click.prevent.stop="resumePlayback"
|
class="control"
|
||||||
class="control">
|
@click.prevent.stop="resumePlayback"
|
||||||
<i :class="['ui', 'play', {'disabled': !currentTrack}, 'icon']"></i>
|
>
|
||||||
</span>
|
<i :class="['ui', 'play', {'disabled': !currentTrack}, 'icon']" />
|
||||||
<span
|
</span>
|
||||||
role="button"
|
<span
|
||||||
v-else
|
v-else
|
||||||
:title="labels.pause"
|
role="button"
|
||||||
:aria-label="labels.pause"
|
:title="labels.pause"
|
||||||
@click.prevent.stop="pausePlayback"
|
:aria-label="labels.pause"
|
||||||
class="control">
|
class="control"
|
||||||
<i :class="['ui', 'pause', {'disabled': !currentTrack}, 'icon']"></i>
|
@click.prevent.stop="pausePlayback"
|
||||||
</span>
|
>
|
||||||
<span
|
<i :class="['ui', 'pause', {'disabled': !currentTrack}, 'icon']" />
|
||||||
role="button"
|
</span>
|
||||||
:title="labels.next"
|
<span
|
||||||
:aria-label="labels.next"
|
role="button"
|
||||||
class="control"
|
:title="labels.next"
|
||||||
@click.prevent.stop="$store.dispatch('queue/next')"
|
:aria-label="labels.next"
|
||||||
:disabled="!hasNext">
|
class="control"
|
||||||
<i :class="['ui', {'disabled': !hasNext}, 'forward step', 'icon']" ></i>
|
:disabled="!hasNext"
|
||||||
</span>
|
@click.prevent.stop="$store.dispatch('queue/next')"
|
||||||
</template>
|
>
|
||||||
|
<i :class="['ui', {'disabled': !hasNext}, 'forward step', 'icon']" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -129,20 +202,30 @@
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<button
|
<button
|
||||||
class="ui right floated basic button"
|
class="ui right floated basic button"
|
||||||
@click="$store.commit('ui/queueFocused', null)">
|
@click="$store.commit('ui/queueFocused', null)"
|
||||||
<translate translate-context="*/Queue/*/Verb">Close</translate>
|
>
|
||||||
|
<translate translate-context="*/Queue/*/Verb">
|
||||||
|
Close
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="ui right floated basic button danger"
|
class="ui right floated basic button danger"
|
||||||
@click="$store.dispatch('queue/clean')">
|
@click="$store.dispatch('queue/clean')"
|
||||||
<translate translate-context="*/Queue/*/Verb">Clear</translate>
|
>
|
||||||
|
<translate translate-context="*/Queue/*/Verb">
|
||||||
|
Clear
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
{{ labels.queue }}
|
{{ labels.queue }}
|
||||||
<div class="sub header">
|
<div class="sub header">
|
||||||
<div>
|
<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 }
|
Track %{ index } of %{ length }
|
||||||
</translate><template v-if="!$store.state.radios.running"> -
|
</translate><template v-if="!$store.state.radios.running">
|
||||||
|
-
|
||||||
<span :title="labels.duration">
|
<span :title="labels.duration">
|
||||||
{{ timeLeft }}
|
{{ timeLeft }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -153,22 +236,53 @@
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<table class="ui compact very basic fixed single line selectable unstackable table">
|
<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
|
<tr
|
||||||
v-for="(track, index) in tracks"
|
v-for="(track, index) in tracks"
|
||||||
:key="index"
|
:key="index"
|
||||||
:class="['queue-item', {'active': index === queue.currentIndex}]">
|
:class="['queue-item', {'active': index === queue.currentIndex}]"
|
||||||
|
>
|
||||||
<td class="handle">
|
<td class="handle">
|
||||||
<i class="grip lines icon"></i>
|
<i class="grip lines icon" />
|
||||||
</td>
|
</td>
|
||||||
<td class="image-cell" @click="$store.dispatch('queue/currentIndex', index)">
|
<td
|
||||||
<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)">
|
class="image-cell"
|
||||||
<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)">
|
@click="$store.dispatch('queue/currentIndex', index)"
|
||||||
<img class="ui mini image" alt="" v-else src="../assets/audio/default-cover.png">
|
>
|
||||||
|
<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>
|
||||||
<td colspan="3" @click="$store.dispatch('queue/currentIndex', index)">
|
<td
|
||||||
<button class="title reset ellipsis" :title="track.title" :aria-label="labels.selectTrack">
|
colspan="3"
|
||||||
<strong>{{ track.title }}</strong><br />
|
@click="$store.dispatch('queue/currentIndex', index)"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="title reset ellipsis"
|
||||||
|
:title="track.title"
|
||||||
|
:aria-label="labels.selectTrack"
|
||||||
|
>
|
||||||
|
<strong>{{ track.title }}</strong><br>
|
||||||
<span>
|
<span>
|
||||||
{{ track.artist.name }}
|
{{ track.artist.name }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -181,23 +295,44 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="controls">
|
<td class="controls">
|
||||||
<template v-if="$store.getters['favorites/isFavorite'](track.id)">
|
<template v-if="$store.getters['favorites/isFavorite'](track.id)">
|
||||||
<i class="pink heart icon"></i>
|
<i class="pink heart icon" />
|
||||||
</template>
|
</template>
|
||||||
<button :aria-label="labels.removeFromQueue" :title="labels.removeFromQueue" @click.stop="cleanTrack(index)" :class="['ui', 'really', 'tiny', 'basic', 'circular', 'icon', 'button']">
|
<button
|
||||||
<i class="x icon"></i>
|
:aria-label="labels.removeFromQueue"
|
||||||
|
:title="labels.removeFromQueue"
|
||||||
|
:class="['ui', 'really', 'tiny', 'basic', 'circular', 'icon', 'button']"
|
||||||
|
@click.stop="cleanTrack(index)"
|
||||||
|
>
|
||||||
|
<i class="x icon" />
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</draggable>
|
</draggable>
|
||||||
</table>
|
</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">
|
<div class="content">
|
||||||
<h3 class="header">
|
<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>
|
</h3>
|
||||||
<p><translate translate-context="Sidebar/Player/Paragraph">New tracks will be appended here automatically.</translate></p>
|
<p>
|
||||||
<button @click="$store.dispatch('radios/stop')" class="ui basic primary button"><translate translate-context="*/Player/Button.Label/Short, Verb">Stop radio</translate></button>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -229,16 +364,6 @@ export default {
|
||||||
time
|
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: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
currentIndex: state => state.queue.currentIndex,
|
currentIndex: state => state.queue.currentIndex,
|
||||||
|
@ -298,6 +423,46 @@ export default {
|
||||||
return this.$store.state.ui.queueFocused === 'player'
|
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: {
|
methods: {
|
||||||
...mapActions({
|
...mapActions({
|
||||||
cleanTrack: 'queue/cleanTrack',
|
cleanTrack: 'queue/cleanTrack',
|
||||||
|
@ -348,36 +513,6 @@ export default {
|
||||||
})
|
})
|
||||||
}, 100)
|
}, 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>
|
</script>
|
||||||
|
|
|
@ -1,19 +1,51 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="type === 'both' || type === undefined" class="two ui buttons">
|
<div
|
||||||
<button class="ui left floated labeled icon button" @click.prevent="changeType('rss')"><i class="feed icon"></i>
|
v-if="type === 'both' || type === undefined"
|
||||||
<translate translate-context="Content/Search/Input.Label/Noun">RSS</translate>
|
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>
|
</button>
|
||||||
<div class="or"></div>
|
<div class="or" />
|
||||||
<button class="ui right floated right labeled icon button" @click.prevent="changeType('artists')"><i class="globe icon"></i>
|
<button
|
||||||
<translate translate-context="Content/Search/Input.Label/Noun">Fediverse</translate>
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<form id="remote-search" :class="['ui', {loading: isLoading}, 'form']" @submit.stop.prevent="submit">
|
<form
|
||||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
id="remote-search"
|
||||||
<h3 class="header"><translate translate-context="Content/*/Error message.Title">Error while fetching object</translate></h3>
|
: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">
|
<ul class="list">
|
||||||
<li v-for="error in errors">{{ error }}</li>
|
<li
|
||||||
|
v-for="(error, key) in errors"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui required field">
|
<div class="ui required field">
|
||||||
|
@ -21,19 +53,45 @@
|
||||||
{{ labels.fieldLabel }}
|
{{ labels.fieldLabel }}
|
||||||
</label>
|
</label>
|
||||||
<p v-if="type === 'rss'">
|
<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>
|
||||||
<p v-else-if="type === 'artists'">
|
<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>
|
</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>
|
</div>
|
||||||
<button v-if="showSubmit" type="submit" :class="['ui', 'primary', {loading: isLoading}, 'button']" :disabled="isLoading || !id || id.length === 0">
|
<button
|
||||||
<translate translate-context="Content/Search/Input.Label/Noun">Search</translate>
|
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>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div v-if="!isLoading && fetch && fetch.status === 'finished' && !redirectRoute" role="alert" class="ui warning message">
|
<div
|
||||||
<p><translate translate-context="Content/*/Error message.Title">This kind of object isn't supported yet</translate></p>
|
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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -42,20 +100,87 @@ import axios from 'axios'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
initialId: { type: String, required: false},
|
initialId: { type: String, required: false, default: '' },
|
||||||
type: { type: String, required: false},
|
initialType: { type: String, required: false, default: '' },
|
||||||
redirect: { type: Boolean, default: true},
|
redirect: { type: Boolean, default: true },
|
||||||
showSubmit: { type: Boolean, default: true},
|
showSubmit: { type: Boolean, default: true },
|
||||||
standalone: { type: Boolean, default: true},
|
standalone: { type: Boolean, default: true }
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
type: this.initialType,
|
||||||
id: this.initialId,
|
id: this.initialId,
|
||||||
fetch: null,
|
fetch: null,
|
||||||
obj: null,
|
obj: null,
|
||||||
isLoading: false,
|
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 () {
|
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: {
|
methods: {
|
||||||
changeType(newType) {
|
changeType (newType) {
|
||||||
this.type = newType
|
this.type = newType
|
||||||
},
|
},
|
||||||
submit () {
|
submit () {
|
||||||
|
@ -135,13 +209,13 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.standalone) {
|
if (this.standalone) {
|
||||||
this.$router.replace({name: "search", query: {id: this.id}})
|
this.$router.replace({ name: 'search', query: { id: this.id } })
|
||||||
}
|
}
|
||||||
this.fetch = null
|
this.fetch = null
|
||||||
let self = this
|
const self = this
|
||||||
self.errors = []
|
self.errors = []
|
||||||
self.isLoading = true
|
self.isLoading = true
|
||||||
let payload = {
|
const payload = {
|
||||||
object: this.id
|
object: this.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,7 +224,7 @@ export default {
|
||||||
self.fetch = response.data
|
self.fetch = response.data
|
||||||
if (self.fetch.status === 'errored' || self.fetch.status === 'skipped') {
|
if (self.fetch.status === 'errored' || self.fetch.status === 'skipped') {
|
||||||
self.errors.push(
|
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 => {
|
}, error => {
|
||||||
|
@ -163,40 +237,27 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.standalone) {
|
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
|
this.fetch = null
|
||||||
let self = this
|
const self = this
|
||||||
self.errors = []
|
self.errors = []
|
||||||
self.isLoading = true
|
self.isLoading = true
|
||||||
let payload = {
|
const payload = {
|
||||||
url: this.id
|
url: this.id
|
||||||
}
|
}
|
||||||
|
|
||||||
axios.post('channels/rss-subscribe/', payload).then((response) => {
|
axios.post('channels/rss-subscribe/', payload).then((response) => {
|
||||||
self.isLoading = false
|
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)
|
self.$emit('subscribed', response.data)
|
||||||
if (self.redirect) {
|
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 => {
|
}, error => {
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
self.errors = error.backendErrors
|
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>
|
<template>
|
||||||
<div class="ui toast-container">
|
<div class="ui toast-container">
|
||||||
<message v-for="message in $store.state.ui.messages" :message="message" :key="message.key"></message>
|
<message
|
||||||
<slot></slot>
|
v-for="message in $store.state.ui.messages"
|
||||||
|
:key="message.key"
|
||||||
|
:message="message"
|
||||||
|
/>
|
||||||
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -1,41 +1,105 @@
|
||||||
<template>
|
<template>
|
||||||
<modal @update:show="$emit('update:show', $event); isError = false" :show="show">
|
<modal
|
||||||
<h3 class="header"><translate translate-context="Popup/Instance/Title">Choose your instance</translate></h3>
|
: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 class="scrolling content">
|
||||||
<div v-if="isError" role="alert" class="ui negative message">
|
<div
|
||||||
<h4 class="header"><translate translate-context="Popup/Instance/Error message.Title">It is not possible to connect to the given URL</translate></h4>
|
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">
|
<ul class="list">
|
||||||
<li><translate translate-context="Popup/Instance/Error message.List item">The server might be down</translate></li>
|
<li>
|
||||||
<li><translate translate-context="Popup/Instance/Error message.List item">The given address is not a Funkwhale server</translate></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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<form class="ui form" @submit.prevent="checkAndSwitch(instanceUrl)">
|
<form
|
||||||
<p v-if="$store.state.instance.instanceUrl" class="description" translate-context="Popup/Login/Paragraph" v-translate="{url: $store.state.instance.instanceUrl, hostname: instanceHostname }">
|
class="ui form"
|
||||||
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.
|
@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>
|
||||||
<p v-else>
|
<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>
|
</p>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="instance-picker"><translate translate-context="Popup/Instance/Input.Label/Noun">Instance URL</translate></label>
|
<label for="instance-picker"><translate translate-context="Popup/Instance/Input.Label/Noun">Instance URL</translate></label>
|
||||||
<div class="ui action input">
|
<div class="ui action input">
|
||||||
<input id ="instance-picker" type="text" v-model="instanceUrl" placeholder="https://funkwhale.server">
|
<input
|
||||||
<button type="submit" :class="['ui', 'icon', {loading: isLoading}, 'button']">
|
id="instance-picker"
|
||||||
<translate translate-context="*/*/Button.Label/Verb">Submit</translate>
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<form class="ui form" @submit.prevent="">
|
<form
|
||||||
|
class="ui form"
|
||||||
|
@submit.prevent=""
|
||||||
|
>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<h4><translate translate-context="Popup/Instance/List.Label">Suggested choices</translate></h4>
|
<h4>
|
||||||
<button v-for="url in suggestedInstances" @click="checkAndSwitch(url)" class="ui basic button">{{ url }}</button>
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<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>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
</template>
|
</template>
|
||||||
|
@ -43,25 +107,52 @@
|
||||||
<script>
|
<script>
|
||||||
import Modal from '@/components/semantic/Modal'
|
import Modal from '@/components/semantic/Modal'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import _ from "@/lodash"
|
import _ from '@/lodash'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['show'],
|
|
||||||
components: {
|
components: {
|
||||||
Modal,
|
Modal
|
||||||
},
|
},
|
||||||
data() {
|
props: { show: { type: Boolean, required: true } },
|
||||||
|
data () {
|
||||||
return {
|
return {
|
||||||
instanceUrl: null,
|
instanceUrl: null,
|
||||||
nodeinfo: null,
|
nodeinfo: null,
|
||||||
isError: false,
|
isError: false,
|
||||||
isLoading: 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: {
|
methods: {
|
||||||
fetchNodeInfo () {
|
fetchNodeInfo () {
|
||||||
let self = this
|
const self = this
|
||||||
axios.get('instance/nodeinfo/2.0/').then(response => {
|
axios.get('instance/nodeinfo/2.0/').then(response => {
|
||||||
self.nodeinfo = response.data
|
self.nodeinfo = response.data
|
||||||
})
|
})
|
||||||
|
@ -71,7 +162,7 @@ export default {
|
||||||
if (!urlFetch.endsWith('/')) {
|
if (!urlFetch.endsWith('/')) {
|
||||||
urlFetch = `${urlFetch}/${this.path}`
|
urlFetch = `${urlFetch}/${this.path}`
|
||||||
} else {
|
} else {
|
||||||
urlFetch = `${urlFetch}${this.path}`
|
urlFetch = `${urlFetch}${this.path}`
|
||||||
}
|
}
|
||||||
if (!urlFetch.startsWith('https://') && !urlFetch.startsWith('http://')) {
|
if (!urlFetch.startsWith('https://') && !urlFetch.startsWith('http://')) {
|
||||||
urlFetch = `https://${urlFetch}`
|
urlFetch = `https://${urlFetch}`
|
||||||
|
@ -79,14 +170,14 @@ export default {
|
||||||
return urlFetch
|
return urlFetch
|
||||||
},
|
},
|
||||||
requestDistantNodeInfo (url) {
|
requestDistantNodeInfo (url) {
|
||||||
var self = this
|
const self = this
|
||||||
axios.get(this.fetchUrl(url)).then(function (response) {
|
axios.get(this.fetchUrl(url)).then(function (response) {
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
if(!url.startsWith('https://') && !url.startsWith('http://')) {
|
if (!url.startsWith('https://') && !url.startsWith('http://')) {
|
||||||
url = `https://${url}`
|
url = `https://${url}`
|
||||||
}
|
}
|
||||||
self.switchInstance(url)
|
self.switchInstance(url)
|
||||||
}).catch(function (error) {
|
}).catch(function () {
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
self.isError = true
|
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…
|
// Here we disconnect from the current instance and reconnect to the new one. No check is performed…
|
||||||
this.$emit('update:show', false)
|
this.$emit('update:show', false)
|
||||||
this.isError = 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', {
|
this.$store.commit('ui/addMessage', {
|
||||||
content: this.$gettextInterpolate(msg, {url: url}),
|
content: this.$gettextInterpolate(msg, { url: url }),
|
||||||
date: new Date()
|
date: new Date()
|
||||||
})
|
})
|
||||||
let self = this
|
const self = this
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
self.$store.commit('instance/instanceUrl', null)
|
self.$store.commit('instance/instanceUrl', null)
|
||||||
self.$store.dispatch('instance/setUrl', url)
|
self.$store.dispatch('instance/setUrl', url)
|
||||||
|
@ -111,34 +202,7 @@ export default {
|
||||||
this.isError = false // Clear error message if any…
|
this.isError = false // Clear error message if any…
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.requestDistantNodeInfo(url)
|
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>
|
</script>
|
||||||
|
|
|
@ -1,42 +1,59 @@
|
||||||
<template>
|
<template>
|
||||||
<modal @update:show="$emit('update:show', $event)" :show="show">
|
<modal
|
||||||
|
:show="show"
|
||||||
|
@update:show="$emit('update:show', $event)"
|
||||||
|
>
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<translate translate-context="*/*/*/Noun">Keyboard shortcuts</translate>
|
<translate translate-context="*/*/*/Noun">
|
||||||
|
Keyboard shortcuts
|
||||||
|
</translate>
|
||||||
</header>
|
</header>
|
||||||
<section class="scrolling content">
|
<section class="scrolling content">
|
||||||
<div class="ui stackable two column grid">
|
<div class="ui stackable two column grid">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<table
|
<table
|
||||||
class="ui compact basic table"
|
|
||||||
v-for="section in player"
|
v-for="section in player"
|
||||||
:key="section.title">
|
:key="section.title"
|
||||||
<caption>{{ section.title }}</caption>
|
class="ui compact basic table"
|
||||||
<tbody>
|
>
|
||||||
<tr v-for="shortcut in section.shortcuts" :key="shortcut.summary">
|
<caption>{{ section.title }}</caption>
|
||||||
<td>{{ shortcut.summary }}</td>
|
<tbody>
|
||||||
<td><span class="ui label">{{ shortcut.key }}</span></td>
|
<tr
|
||||||
</tr>
|
v-for="shortcut in section.shortcuts"
|
||||||
</tbody>
|
:key="shortcut.summary"
|
||||||
|
>
|
||||||
|
<td>{{ shortcut.summary }}</td>
|
||||||
|
<td><span class="ui label">{{ shortcut.key }}</span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<table
|
<table
|
||||||
class="ui compact basic table"
|
|
||||||
v-for="section in general"
|
v-for="section in general"
|
||||||
:key="section.title">
|
:key="section.title"
|
||||||
<caption>{{ section.title }}</caption>
|
class="ui compact basic table"
|
||||||
<tbody>
|
>
|
||||||
<tr v-for="shortcut in section.shortcuts" :key="shortcut.summary">
|
<caption>{{ section.title }}</caption>
|
||||||
<td>{{ shortcut.summary }}</td>
|
<tbody>
|
||||||
<td><span class="ui label">{{ shortcut.key }}</span></td>
|
<tr
|
||||||
</tr>
|
v-for="shortcut in section.shortcuts"
|
||||||
</tbody>
|
:key="shortcut.summary"
|
||||||
|
>
|
||||||
|
<td>{{ shortcut.summary }}</td>
|
||||||
|
<td><span class="ui label">{{ shortcut.key }}</span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<footer class="actions">
|
<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>
|
</footer>
|
||||||
</modal>
|
</modal>
|
||||||
</template>
|
</template>
|
||||||
|
@ -44,10 +61,10 @@
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['show'],
|
|
||||||
components: {
|
components: {
|
||||||
Modal: () => import(/* webpackChunkName: "modal" */ "@/components/semantic/Modal"),
|
Modal: () => import(/* webpackChunkName: "modal" */ '@/components/semantic/Modal')
|
||||||
},
|
},
|
||||||
|
props: { show: { type: Boolean, required: true } },
|
||||||
computed: {
|
computed: {
|
||||||
general () {
|
general () {
|
||||||
return [
|
return [
|
||||||
|
@ -65,9 +82,9 @@ export default {
|
||||||
{
|
{
|
||||||
key: 'esc',
|
key: 'esc',
|
||||||
summary: this.$pgettext('Popup/Keyboard shortcuts/Table.Label/Verb', 'Unfocus searchbar')
|
summary: this.$pgettext('Popup/Keyboard shortcuts/Table.Label/Verb', 'Unfocus searchbar')
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -135,7 +152,7 @@ export default {
|
||||||
{
|
{
|
||||||
key: 'f',
|
key: 'f',
|
||||||
summary: this.$pgettext('Popup/Keyboard shortcuts/Table.Label/Verb', 'Toggle favorite')
|
summary: this.$pgettext('Popup/Keyboard shortcuts/Table.Label/Verb', 'Toggle favorite')
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,86 +1,156 @@
|
||||||
<template>
|
<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" />
|
<div class="ui divider" />
|
||||||
<h3 class="ui header">{{ group.label }}</h3>
|
<h3 class="ui header">
|
||||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
{{ group.label }}
|
||||||
<h4 class="header"><translate translate-context="Content/*/Error message.Title">Error while saving settings</translate></h4>
|
</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">
|
<ul class="list">
|
||||||
<li v-for="error in errors">{{ error }}</li>
|
<li
|
||||||
|
v-for="(error, key) in errors"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="result" class="ui positive message">
|
<div
|
||||||
<translate translate-context="Content/Settings/Paragraph">Settings updated successfully.</translate>
|
v-if="result"
|
||||||
|
class="ui positive message"
|
||||||
|
>
|
||||||
|
<translate translate-context="Content/Settings/Paragraph">
|
||||||
|
Settings updated successfully.
|
||||||
|
</translate>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="group.help">{{ group.help }}</p>
|
<p v-if="group.help">
|
||||||
<div v-for="setting in settings" class="ui field">
|
{{ group.help }}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
v-for="(setting, key) in settings"
|
||||||
|
:key="key"
|
||||||
|
class="ui field"
|
||||||
|
>
|
||||||
<template v-if="setting.field.widget.class !== 'CheckboxInput'">
|
<template v-if="setting.field.widget.class !== 'CheckboxInput'">
|
||||||
<label :for="setting.identifier">{{ setting.verbose_name }}</label>
|
<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>
|
</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
|
<signup-form-builder
|
||||||
v-else-if="setting.fieldType === 'formBuilder'"
|
v-else-if="setting.fieldType === 'formBuilder'"
|
||||||
:value="values[setting.identifier]"
|
:value="values[setting.identifier]"
|
||||||
:signup-approval-enabled="values.moderation__signup_approval_enabled"
|
:signup-approval-enabled="values.moderation__signup_approval_enabled"
|
||||||
@input="set(setting.identifier, $event)" />
|
@input="set(setting.identifier, $event)"
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
:id="setting.identifier"
|
|
||||||
:name="setting.identifier"
|
|
||||||
v-else-if="setting.field.widget.class === 'PasswordInput'"
|
v-else-if="setting.field.widget.class === 'PasswordInput'"
|
||||||
|
:id="setting.identifier"
|
||||||
|
v-model="values[setting.identifier]"
|
||||||
|
:name="setting.identifier"
|
||||||
type="password"
|
type="password"
|
||||||
class="ui input"
|
class="ui input"
|
||||||
v-model="values[setting.identifier]" />
|
>
|
||||||
<input
|
<input
|
||||||
:id="setting.identifier"
|
|
||||||
:name="setting.identifier"
|
|
||||||
v-else-if="setting.field.widget.class === 'TextInput'"
|
v-else-if="setting.field.widget.class === 'TextInput'"
|
||||||
|
:id="setting.identifier"
|
||||||
|
v-model="values[setting.identifier]"
|
||||||
|
:name="setting.identifier"
|
||||||
type="text"
|
type="text"
|
||||||
class="ui input"
|
class="ui input"
|
||||||
v-model="values[setting.identifier]" />
|
>
|
||||||
<input
|
<input
|
||||||
:id="setting.identifier"
|
|
||||||
:name="setting.identifier"
|
|
||||||
v-else-if="setting.field.class === 'IntegerField'"
|
v-else-if="setting.field.class === 'IntegerField'"
|
||||||
|
:id="setting.identifier"
|
||||||
|
v-model.number="values[setting.identifier]"
|
||||||
|
:name="setting.identifier"
|
||||||
type="number"
|
type="number"
|
||||||
class="ui input"
|
class="ui input"
|
||||||
v-model.number="values[setting.identifier]" />
|
>
|
||||||
<textarea
|
<textarea
|
||||||
:id="setting.identifier"
|
|
||||||
:name="setting.identifier"
|
|
||||||
v-else-if="setting.field.widget.class === 'Textarea'"
|
v-else-if="setting.field.widget.class === 'Textarea'"
|
||||||
|
:id="setting.identifier"
|
||||||
|
v-model="values[setting.identifier]"
|
||||||
|
:name="setting.identifier"
|
||||||
type="text"
|
type="text"
|
||||||
class="ui input"
|
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
|
<input
|
||||||
:id="setting.identifier"
|
:id="setting.identifier"
|
||||||
:name="setting.identifier"
|
|
||||||
v-model="values[setting.identifier]"
|
v-model="values[setting.identifier]"
|
||||||
type="checkbox" />
|
:name="setting.identifier"
|
||||||
|
type="checkbox"
|
||||||
|
>
|
||||||
<label :for="setting.identifier">{{ setting.verbose_name }}</label>
|
<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>
|
</div>
|
||||||
<select
|
<select
|
||||||
:id="setting.identifier"
|
|
||||||
v-else-if="setting.field.class === 'MultipleChoiceField'"
|
v-else-if="setting.field.class === 'MultipleChoiceField'"
|
||||||
|
:id="setting.identifier"
|
||||||
v-model="values[setting.identifier]"
|
v-model="values[setting.identifier]"
|
||||||
multiple
|
multiple
|
||||||
class="ui search selection dropdown">
|
class="ui search selection dropdown"
|
||||||
<option v-for="v in setting.additional_data.choices" :value="v[0]">{{ v[1] }}</option>
|
>
|
||||||
|
<option
|
||||||
|
v-for="(v, index) in setting.additional_data.choices"
|
||||||
|
:key="index"
|
||||||
|
:value="v[0]"
|
||||||
|
>
|
||||||
|
{{ v[1] }}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<div v-else-if="setting.field.widget.class === 'ImageWidget'">
|
<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 v-if="values[setting.identifier]">
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<h3 class="ui header"><translate translate-context="Content/Settings/Title/Noun">Current image</translate></h3>
|
<h3 class="ui header">
|
||||||
<img class="ui image" alt="" v-if="values[setting.identifier]" :src="$store.getters['instance/absoluteUrl'](values[setting.identifier])" />
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']">
|
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']"
|
||||||
<translate translate-context="Content/*/Button.Label/Verb">Save</translate>
|
>
|
||||||
|
<translate translate-context="Content/*/Button.Label/Verb">
|
||||||
|
Save
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
@ -91,12 +161,12 @@ import axios from 'axios'
|
||||||
import lodash from '@/lodash'
|
import lodash from '@/lodash'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
group: {type: Object, required: true},
|
|
||||||
settingsData: {type: Array, required: true}
|
|
||||||
},
|
|
||||||
components: {
|
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 () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -106,28 +176,44 @@ export default {
|
||||||
isLoading: false
|
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 () {
|
created () {
|
||||||
let self = this
|
const self = this
|
||||||
this.settings.forEach(e => {
|
this.settings.forEach(e => {
|
||||||
self.values[e.identifier] = e.value
|
self.values[e.identifier] = e.value
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
save () {
|
save () {
|
||||||
let self = this
|
const self = this
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
self.errors = []
|
self.errors = []
|
||||||
self.result = null
|
self.result = null
|
||||||
let postData = self.values
|
let postData = self.values
|
||||||
let contentType = 'application/json'
|
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) {
|
if (fileSettingsIDs.length > 0) {
|
||||||
contentType = 'multipart/form-data'
|
contentType = 'multipart/form-data'
|
||||||
postData = new FormData()
|
postData = new FormData()
|
||||||
this.settings.forEach((s) => {
|
this.settings.forEach((s) => {
|
||||||
if (fileSettingsIDs.indexOf(s.identifier) > -1) {
|
if (fileSettingsIDs.indexOf(s.identifier) > -1) {
|
||||||
let input = self.$refs[s.identifier][0]
|
const input = self.$refs[s.identifier][0]
|
||||||
let files = input.files
|
const files = input.files
|
||||||
console.log('ref', input, files)
|
console.log('ref', input, files)
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
postData.append(s.identifier, files[0])
|
postData.append(s.identifier, files[0])
|
||||||
|
@ -139,8 +225,8 @@ export default {
|
||||||
}
|
}
|
||||||
axios.post('instance/admin/settings/bulk/', postData, {
|
axios.post('instance/admin/settings/bulk/', postData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': contentType,
|
'Content-Type': contentType
|
||||||
},
|
}
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
self.result = true
|
self.result = true
|
||||||
response.data.forEach((s) => {
|
response.data.forEach((s) => {
|
||||||
|
@ -158,22 +244,6 @@ export default {
|
||||||
this.values = lodash.cloneDeep(this.values)
|
this.values = lodash.cloneDeep(this.values)
|
||||||
this.$set(this.values, key, value)
|
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>
|
</script>
|
||||||
|
|
|
@ -1,116 +1,173 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<div class="ui top attached tabular menu">
|
<div class="ui top attached tabular menu">
|
||||||
<button :class="[{active: !isPreviewing}, 'item']" @click.stop.prevent="isPreviewing = false">
|
<button
|
||||||
<translate translate-context="Content/*/Button.Label/Verb">Edit form</translate>
|
:class="[{active: !isPreviewing}, 'item']"
|
||||||
|
@click.stop.prevent="isPreviewing = false"
|
||||||
|
>
|
||||||
|
<translate translate-context="Content/*/Button.Label/Verb">
|
||||||
|
Edit form
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
<button :class="[{active: isPreviewing}, 'item']" @click.stop.prevent="isPreviewing = true">
|
<button
|
||||||
<translate translate-context="*/Form/Menu.item">Preview form</translate>
|
:class="[{active: isPreviewing}, 'item']"
|
||||||
|
@click.stop.prevent="isPreviewing = true"
|
||||||
|
>
|
||||||
|
<translate translate-context="*/Form/Menu.item">
|
||||||
|
Preview form
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isPreviewing" class="ui bottom attached segment">
|
<div
|
||||||
|
v-if="isPreviewing"
|
||||||
|
class="ui bottom attached segment"
|
||||||
|
>
|
||||||
<signup-form
|
<signup-form
|
||||||
:customization="local"
|
:customization="local"
|
||||||
:signup-approval-enabled="signupApprovalEnabled"
|
:signup-approval-enabled="signupApprovalEnabled"
|
||||||
:fetch-description-html="true"></signup-form>
|
:fetch-description-html="true"
|
||||||
<div class="ui clearing hidden divider"></div>
|
/>
|
||||||
|
<div class="ui clearing hidden divider" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="ui bottom attached segment">
|
<div
|
||||||
|
v-else
|
||||||
|
class="ui bottom attached segment"
|
||||||
|
>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="help-text">
|
<label for="help-text">
|
||||||
<translate translate-context="*/*/Label">Help text</translate>
|
<translate translate-context="*/*/Label">Help text</translate>
|
||||||
</label>
|
</label>
|
||||||
<p>
|
<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>
|
</p>
|
||||||
<content-form
|
<content-form
|
||||||
field-id="help-text"
|
field-id="help-text"
|
||||||
:permissive="true"
|
:permissive="true"
|
||||||
:value="(local.help_text || {}).text"
|
:value="(local.help_text || {}).text"
|
||||||
@input="update('help_text.text', $event)"></content-form>
|
@input="update('help_text.text', $event)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>
|
<label>
|
||||||
<translate translate-context="*/*/Label">Additional fields</translate>
|
<translate translate-context="*/*/Label">Additional fields</translate>
|
||||||
</label>
|
</label>
|
||||||
<p>
|
<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>
|
</p>
|
||||||
<table v-if="local.fields.length > 0">
|
<table v-if="local.fields.length > 0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
<translate translate-context="*/*/Form-builder,Help">Field label</translate>
|
<translate translate-context="*/*/Form-builder,Help">
|
||||||
|
Field label
|
||||||
|
</translate>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<translate translate-context="*/*/Form-builder,Help">Field type</translate>
|
<translate translate-context="*/*/Form-builder,Help">
|
||||||
|
Field type
|
||||||
|
</translate>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<translate translate-context="*/*/Form-builder,Help">Required</translate>
|
<translate translate-context="*/*/Form-builder,Help">
|
||||||
|
Required
|
||||||
|
</translate>
|
||||||
</th>
|
</th>
|
||||||
<th><span class="visually-hidden"><translate translate-context="*/*/Form-builder,Help">Actions</translate></span></th>
|
<th><span class="visually-hidden"><translate translate-context="*/*/Form-builder,Help">Actions</translate></span></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(field, idx) in local.fields">
|
<tr
|
||||||
|
v-for="(field, idx) in local.fields"
|
||||||
|
:key="idx"
|
||||||
|
>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" v-model="field.label" required>
|
<input
|
||||||
|
v-model="field.label"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<select v-model="field.input_type">
|
<select v-model="field.input_type">
|
||||||
<option value="short_text">
|
<option value="short_text">
|
||||||
<translate translate-context="*/*/Form-builder">Short text</translate>
|
<translate translate-context="*/*/Form-builder">
|
||||||
|
Short text
|
||||||
|
</translate>
|
||||||
</option>
|
</option>
|
||||||
<option value="long_text">
|
<option value="long_text">
|
||||||
<translate translate-context="*/*/Form-builder">Long text</translate>
|
<translate translate-context="*/*/Form-builder">
|
||||||
|
Long text
|
||||||
|
</translate>
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<select v-model="field.required">
|
<select v-model="field.required">
|
||||||
<option :value="true">
|
<option :value="true">
|
||||||
<translate translate-context="*/*/*">Yes</translate>
|
<translate translate-context="*/*/*">
|
||||||
|
Yes
|
||||||
|
</translate>
|
||||||
</option>
|
</option>
|
||||||
<option :value="false">
|
<option :value="false">
|
||||||
<translate translate-context="*/*/*">No</translate>
|
<translate translate-context="*/*/*">
|
||||||
|
No
|
||||||
|
</translate>
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<i
|
<i
|
||||||
:disabled="idx === 0"
|
:disabled="idx === 0"
|
||||||
@click="move(idx, -1)" role="button"
|
role="button"
|
||||||
:title="labels.up"
|
:title="labels.up"
|
||||||
:class="['up', 'arrow', {disabled: idx === 0}, 'icon']"></i>
|
:class="['up', 'arrow', {disabled: idx === 0}, 'icon']"
|
||||||
|
@click="move(idx, -1)"
|
||||||
|
/>
|
||||||
<i
|
<i
|
||||||
:disabled="idx >= local.fields.length - 1"
|
:disabled="idx >= local.fields.length - 1"
|
||||||
@click="move(idx, 1)" role="button"
|
role="button"
|
||||||
:title="labels.down"
|
:title="labels.down"
|
||||||
:class="['down', 'arrow', {disabled: idx >= local.fields.length - 1}, 'icon']"></i>
|
:class="['down', 'arrow', {disabled: idx >= local.fields.length - 1}, 'icon']"
|
||||||
<i @click="remove(idx)" role="button" :title="labels.delete" class="x icon"></i>
|
@click="move(idx, 1)"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
role="button"
|
||||||
|
:title="labels.delete"
|
||||||
|
class="x icon"
|
||||||
|
@click="remove(idx)"
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<button v-if="local.fields.length < maxFields" class="ui basic button" @click.stop.prevent="addField">
|
<button
|
||||||
<translate translate-context="*/*/Form-builder">Add a new field</translate>
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import lodash from '@/lodash'
|
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) {
|
if (newIndex >= arr.length) {
|
||||||
var k = newIndex - arr.length + 1
|
let k = newIndex - arr.length + 1
|
||||||
while (k--) {
|
while (k--) {
|
||||||
arr.push(undefined)
|
arr.push(undefined)
|
||||||
}
|
}
|
||||||
|
@ -122,40 +179,40 @@ function arrayMove(arr, oldIndex, newIndex) {
|
||||||
// v-model with objects is complex, cf
|
// v-model with objects is complex, cf
|
||||||
// https://simonkollross.de/posts/vuejs-using-v-model-with-objects-for-custom-components
|
// https://simonkollross.de/posts/vuejs-using-v-model-with-objects-for-custom-components
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
value: {type: Object},
|
|
||||||
signupApprovalEnabled: {type: Boolean},
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
SignupForm
|
SignupForm
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
value: { type: Object, required: true },
|
||||||
|
signupApprovalEnabled: { type: Boolean }
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
maxFields: 10,
|
maxFields: 10,
|
||||||
isPreviewing: false
|
isPreviewing: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
|
||||||
this.$emit('input', this.local)
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
labels () {
|
labels () {
|
||||||
return {
|
return {
|
||||||
delete: this.$pgettext('*/*/*', 'Delete'),
|
delete: this.$pgettext('*/*/*', 'Delete'),
|
||||||
up: this.$pgettext('*/*/*', 'Move up'),
|
up: this.$pgettext('*/*/*', 'Move up'),
|
||||||
down: this.$pgettext('*/*/*', 'Move down'),
|
down: this.$pgettext('*/*/*', 'Move down')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
local() {
|
local () {
|
||||||
return (this.value && this.value.fields) ? this.value : { help_text: {text: null, content_type: "text/markdown"}, fields: [] }
|
return (this.value && this.value.fields) ? this.value : { help_text: { text: null, content_type: 'text/markdown' }, fields: [] }
|
||||||
},
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.$emit('input', this.local)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addField () {
|
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),
|
label: this.$pgettext('*/*/Form-builder', 'Additional field') + ' ' + (this.local.fields.length + 1),
|
||||||
required: true,
|
required: true,
|
||||||
input_type: 'short_text',
|
input_type: 'short_text'
|
||||||
}))
|
}))
|
||||||
this.$emit('input', newValue)
|
this.$emit('input', newValue)
|
||||||
},
|
},
|
||||||
|
@ -169,10 +226,10 @@ export default {
|
||||||
if (idx + incr >= this.local.fields.length) {
|
if (idx + incr >= this.local.fields.length) {
|
||||||
return
|
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)
|
this.update('fields', newFields)
|
||||||
},
|
},
|
||||||
update(key, value) {
|
update (key, value) {
|
||||||
if (key === 'help_text.text') {
|
if (key === 'help_text.text') {
|
||||||
key = 'help_text'
|
key = 'help_text'
|
||||||
if (!value || value.length === 0) {
|
if (!value || value.length === 0) {
|
||||||
|
@ -180,12 +237,12 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
value = {
|
value = {
|
||||||
text: 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)))
|
this.$emit('input', lodash.tap(lodash.cloneDeep(this.local), v => lodash.set(v, key, value)))
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,25 +1,34 @@
|
||||||
<template>
|
<template>
|
||||||
<router-link class="artist-label ui image label" :to="route">
|
<router-link
|
||||||
<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)" />
|
class="artist-label ui image label"
|
||||||
<i :class="[artist.content_category != 'podcast' ? 'circular' : 'bordered', 'inverted violet users icon']" v-else />
|
: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 }}
|
{{ artist.name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import {momentFormat} from '@/filters'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
artist: Object,
|
artist: { type: Object, required: true }
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
route () {
|
route () {
|
||||||
if (this.artist.channel) {
|
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>
|
<template>
|
||||||
<div class="card app-card">
|
<div class="card app-card">
|
||||||
<div
|
<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}})"
|
@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>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<strong>
|
<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 }}
|
{{ object.artist.name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</strong>
|
</strong>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<translate class="meta ellipsis" translate-context="Content/Channel/Paragraph"
|
<translate
|
||||||
key="1"
|
|
||||||
v-if="object.artist.content_category === 'podcast'"
|
v-if="object.artist.content_category === 'podcast'"
|
||||||
|
key="1"
|
||||||
|
class="meta ellipsis"
|
||||||
|
translate-context="Content/Channel/Paragraph"
|
||||||
translate-plural="%{ count } episodes"
|
translate-plural="%{ count } episodes"
|
||||||
:translate-n="object.artist.tracks_count"
|
:translate-n="object.artist.tracks_count"
|
||||||
:translate-params="{count: object.artist.tracks_count}">
|
:translate-params="{count: object.artist.tracks_count}"
|
||||||
|
>
|
||||||
%{ count } episode
|
%{ count } episode
|
||||||
</translate>
|
</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>
|
<translate
|
||||||
<tags-list label-classes="tiny" :truncate-size="20" :limit="2" :show-more="false" :tags="object.artist.tags"></tags-list>
|
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>
|
</div>
|
||||||
<div class="extra content">
|
<div class="extra content">
|
||||||
<time
|
<time
|
||||||
v-translate
|
v-translate
|
||||||
class="meta ellipsis"
|
class="meta ellipsis"
|
||||||
:datetime="object.artist.modification_date"
|
:datetime="object.artist.modification_date"
|
||||||
:title="updatedTitle">
|
:title="updatedTitle"
|
||||||
|
>
|
||||||
%{ updatedAgo }
|
%{ updatedAgo }
|
||||||
</time>
|
</time>
|
||||||
<play-button
|
<play-button
|
||||||
class="right floated basic icon"
|
class="right floated basic icon"
|
||||||
:dropdown-only="true"
|
:dropdown-only="true"
|
||||||
:is-playable="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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import PlayButton from '@/components/audio/PlayButton'
|
import PlayButton from '@/components/audio/PlayButton'
|
||||||
import TagsList from "@/components/tags/List"
|
import TagsList from '@/components/tags/List'
|
||||||
|
|
||||||
import {momentFormat} from '@/filters'
|
import { momentFormat } from '@/filters'
|
||||||
import moment from "moment"
|
import moment from 'moment'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
object: {type: Object},
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
PlayButton,
|
PlayButton,
|
||||||
TagsList
|
TagsList
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
object: { type: Object, required: true }
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
imageUrl () {
|
imageUrl () {
|
||||||
if (this.object.artist.cover) {
|
if (this.object.artist.cover) {
|
||||||
return this.$store.getters['instance/absoluteUrl'](this.object.artist.cover.urls.medium_square_crop)
|
return this.$store.getters['instance/absoluteUrl'](this.object.artist.cover.urls.medium_square_crop)
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
},
|
},
|
||||||
urlId () {
|
urlId () {
|
||||||
if (this.object.actor && this.object.actor.is_local) {
|
if (this.object.actor && this.object.actor.is_local) {
|
||||||
|
@ -73,9 +106,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updatedTitle () {
|
updatedTitle () {
|
||||||
let d = momentFormat(this.object.artist.modification_date)
|
const d = momentFormat(this.object.artist.modification_date)
|
||||||
let message = this.$pgettext('*/*/*', 'Updated on %{ date }')
|
const message = this.$pgettext('*/*/*', 'Updated on %{ date }')
|
||||||
return this.$gettextInterpolate(message, {date: d})
|
return this.$gettextInterpolate(message, { date: d })
|
||||||
},
|
},
|
||||||
updatedAgo () {
|
updatedAgo () {
|
||||||
return moment(this.object.artist.modification_date).fromNow()
|
return moment(this.object.artist.modification_date).fromNow()
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<slot></slot>
|
<slot />
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
<div
|
||||||
<div class="ui loader"></div>
|
v-if="isLoading"
|
||||||
|
class="ui inverted active dimmer"
|
||||||
|
>
|
||||||
|
<div class="ui loader" />
|
||||||
</div>
|
</div>
|
||||||
<podcast-table
|
<podcast-table
|
||||||
v-if="isPodcast"
|
v-if="isPodcast"
|
||||||
|
@ -16,9 +19,10 @@
|
||||||
:show-album="false"
|
:show-album="false"
|
||||||
:paginate-results="true"
|
:paginate-results="true"
|
||||||
:total="count"
|
:total="count"
|
||||||
@page-changed="updatePage"
|
|
||||||
:page="page"
|
:page="page"
|
||||||
:paginate-by="limit"></podcast-table>
|
:paginate-by="limit"
|
||||||
|
@page-changed="updatePage"
|
||||||
|
/>
|
||||||
<track-table
|
<track-table
|
||||||
v-else
|
v-else
|
||||||
:default-cover="defaultCover"
|
:default-cover="defaultCover"
|
||||||
|
@ -30,13 +34,19 @@
|
||||||
:show-album="false"
|
:show-album="false"
|
||||||
:paginate-results="true"
|
:paginate-results="true"
|
||||||
:total="count"
|
:total="count"
|
||||||
@page-changed="updatePage"
|
|
||||||
:page="page"
|
:page="page"
|
||||||
:paginate-by="limit"></track-table>
|
:paginate-by="limit"
|
||||||
|
@page-changed="updatePage"
|
||||||
|
/>
|
||||||
<template v-if="!isLoading && objects.length === 0">
|
<template v-if="!isLoading && objects.length === 0">
|
||||||
<empty-state @refresh="fetchData('tracks/')" :refresh="true">
|
<empty-state
|
||||||
|
:refresh="true"
|
||||||
|
@refresh="fetchData('tracks/')"
|
||||||
|
>
|
||||||
<p>
|
<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>
|
</p>
|
||||||
</empty-state>
|
</empty-state>
|
||||||
</template>
|
</template>
|
||||||
|
@ -50,26 +60,31 @@ import PodcastTable from '@/components/audio/podcast/Table'
|
||||||
import TrackTable from '@/components/audio/track/Table'
|
import TrackTable from '@/components/audio/track/Table'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
filters: {type: Object, required: true},
|
|
||||||
limit: {type: Number, default: 10},
|
|
||||||
defaultCover: {type: Object},
|
|
||||||
isPodcast: {type: Boolean, required: true},
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
PodcastTable,
|
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 () {
|
data () {
|
||||||
return {
|
return {
|
||||||
objects: [],
|
objects: [],
|
||||||
count: 0,
|
count: 0,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
errors: null,
|
errors: [],
|
||||||
nextPage: null,
|
nextPage: null,
|
||||||
page: 1
|
page: 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
page () {
|
||||||
|
this.fetchData('tracks/')
|
||||||
|
}
|
||||||
|
},
|
||||||
created () {
|
created () {
|
||||||
this.fetchData('tracks/')
|
this.fetchData('tracks/')
|
||||||
},
|
},
|
||||||
|
@ -79,31 +94,26 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let self = this
|
const self = this
|
||||||
let params = _.clone(this.filters)
|
const params = _.clone(this.filters)
|
||||||
params.page_size = this.limit
|
params.page_size = this.limit
|
||||||
params.page = this.page
|
params.page = this.page
|
||||||
params.include_channels = true
|
params.include_channels = true
|
||||||
try {
|
try {
|
||||||
let channelsPromise = await axios.get(url, {params: params})
|
const channelsPromise = await axios.get(url, { params: params })
|
||||||
self.nextPage = channelsPromise.data.next
|
self.nextPage = channelsPromise.data.next
|
||||||
self.objects = channelsPromise.data.results
|
self.objects = channelsPromise.data.results
|
||||||
self.count = channelsPromise.data.count
|
self.count = channelsPromise.data.count
|
||||||
self.$emit('fetched', channelsPromise.data)
|
self.$emit('fetched', channelsPromise.data)
|
||||||
self.isLoading = false
|
|
||||||
} catch(e) {
|
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
self.errors = error.backendErrors
|
} catch (e) {
|
||||||
|
self.isLoading = false
|
||||||
|
self.errors = e.backendErrors
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updatePage: function(page) {
|
updatePage: function (page) {
|
||||||
this.page = page
|
this.page = page
|
||||||
}
|
}
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
page() {
|
|
||||||
this.fetchData('tracks/')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,48 +1,77 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="[{active: currentTrack && isPlaying && entry.id === currentTrack.id}, 'channel-entry-card']">
|
<div :class="[{active: currentTrack && isPlaying && entry.id === currentTrack.id}, 'channel-entry-card']">
|
||||||
<div class="controls">
|
<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>
|
</div>
|
||||||
<img
|
<img
|
||||||
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
|
|
||||||
alt=""
|
|
||||||
class="channel-image image"
|
|
||||||
v-if="cover && cover.urls.original"
|
v-if="cover && cover.urls.original"
|
||||||
v-lazy="$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)">
|
v-lazy="$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)"
|
||||||
<img
|
alt=""
|
||||||
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
|
|
||||||
class="channel-image image"
|
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-else-if="entry.artist.content_category === 'podcast' && defaultCover != undefined"
|
||||||
v-lazy="$store.getters['instance/absoluteUrl'](defaultCover.urls.medium_square_crop)">
|
v-lazy="$store.getters['instance/absoluteUrl'](defaultCover.urls.medium_square_crop)"
|
||||||
<img
|
|
||||||
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
|
|
||||||
alt=""
|
|
||||||
class="channel-image image"
|
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-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)">
|
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}})"
|
|
||||||
alt=""
|
alt=""
|
||||||
class="channel-image image"
|
class="channel-image image"
|
||||||
|
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
|
||||||
|
>
|
||||||
|
<img
|
||||||
v-else
|
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">
|
<div class="ellipsis content">
|
||||||
<strong>
|
<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 }}
|
{{ entry.title }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</strong>
|
</strong>
|
||||||
<br>
|
<br>
|
||||||
<human-date class="really discrete" :date="entry.creation_date"></human-date>
|
<human-date
|
||||||
|
class="really discrete"
|
||||||
|
:date="entry.creation_date"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<template v-if="$store.state.auth.authenticated && $store.getters['favorites/isFavorite'](entry.id)">
|
<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
|
||||||
</template>
|
class="tiny"
|
||||||
<human-duration v-if="duration" :duration="duration"></human-duration>
|
:track="entry"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<human-duration
|
||||||
|
v-if="duration"
|
||||||
|
:duration="duration"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -50,19 +79,21 @@
|
||||||
<script>
|
<script>
|
||||||
import PlayButton from '@/components/audio/PlayButton'
|
import PlayButton from '@/components/audio/PlayButton'
|
||||||
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
||||||
import { mapGetters } from "vuex"
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['entry', 'defaultCover'],
|
|
||||||
components: {
|
components: {
|
||||||
PlayButton,
|
PlayButton,
|
||||||
TrackFavoriteIcon,
|
TrackFavoriteIcon
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
entry: { type: Object, required: true },
|
||||||
|
defaultCover: { type: Object, required: true }
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentTrack: "queue/currentTrack",
|
currentTrack: 'queue/currentTrack'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
isPlaying () {
|
isPlaying () {
|
||||||
|
@ -72,14 +103,16 @@ export default {
|
||||||
if (this.entry.cover) {
|
if (this.entry.cover) {
|
||||||
return this.entry.cover
|
return this.entry.cover
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
},
|
},
|
||||||
duration () {
|
duration () {
|
||||||
let uploads = this.entry.uploads.filter((e) => {
|
const uploads = this.entry.uploads.filter((e) => {
|
||||||
return e.duration
|
return e.duration
|
||||||
})
|
})
|
||||||
if (uploads.length > 0) {
|
if (uploads.length > 0) {
|
||||||
return uploads[0].duration
|
return uploads[0].duration
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,55 @@
|
||||||
<template>
|
<template>
|
||||||
<form class="ui form" @submit.prevent.stop="submit">
|
<form
|
||||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
class="ui form"
|
||||||
<h4 class="header"><translate translate-context="Content/*/Error message.Title">Error while saving channel</translate></h4>
|
@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">
|
<ul class="list">
|
||||||
<li v-for="error in errors">{{ error }}</li>
|
<li
|
||||||
|
v-for="(error, key) in errors"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="metadataChoices">
|
<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>
|
<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>
|
</legend>
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div :class="['ui', 'radio', 'checkbox', {selected: choice.value == newValues.content_category}]" v-for="choice in categoryChoices">
|
<div
|
||||||
<input type="radio" name="channel-category" :id="`category-${choice.value}`" :value="choice.value" v-model="newValues.content_category">
|
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}`">
|
<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>
|
<strong>{{ choice.label }}</strong>
|
||||||
<div class="ui small hidden divider"></div>
|
<div class="ui small hidden divider" />
|
||||||
{{ choice.helpText }}
|
{{ choice.helpText }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -29,20 +60,35 @@
|
||||||
<label for="channel-name">
|
<label for="channel-name">
|
||||||
<translate translate-context="Content/Channel/*">Name</translate>
|
<translate translate-context="Content/Channel/*">Name</translate>
|
||||||
</label>
|
</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>
|
||||||
<div class="ui required field">
|
<div class="ui required field">
|
||||||
<label for="channel-username">
|
<label for="channel-username">
|
||||||
<translate translate-context="Content/Channel/*">Fediverse handle</translate>
|
<translate translate-context="Content/Channel/*">Fediverse handle</translate>
|
||||||
</label>
|
</label>
|
||||||
<div class="ui left labeled input">
|
<div class="ui left labeled input">
|
||||||
<div class="ui basic label">@</div>
|
<div class="ui basic label">
|
||||||
<input type="text" :required="creating" :disabled="!creating" :placeholder="labels.usernamePlaceholder" v-model="newValues.username">
|
@
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model="newValues.username"
|
||||||
|
type="text"
|
||||||
|
:required="creating"
|
||||||
|
:disabled="!creating"
|
||||||
|
:placeholder="labels.usernamePlaceholder"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="creating">
|
<template v-if="creating">
|
||||||
<div class="ui small hidden divider"></div>
|
<div class="ui small hidden divider" />
|
||||||
<p>
|
<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>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,12 +97,17 @@
|
||||||
v-model="newValues.cover"
|
v-model="newValues.cover"
|
||||||
:required="false"
|
:required="false"
|
||||||
:image-class="newValues.content_category === 'podcast' ? '' : 'circular'"
|
:image-class="newValues.content_category === 'podcast' ? '' : 'circular'"
|
||||||
@delete="newValues.cover = null">
|
@delete="newValues.cover = null"
|
||||||
<translate translate-context="Content/Channel/*" slot="label">Channel Picture</translate>
|
>
|
||||||
|
<translate
|
||||||
|
slot="label"
|
||||||
|
translate-context="Content/Channel/*"
|
||||||
|
>
|
||||||
|
Channel Picture
|
||||||
|
</translate>
|
||||||
</attachment-input>
|
</attachment-input>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="ui small hidden divider"></div>
|
<div class="ui small hidden divider" />
|
||||||
<div class="ui stackable grid row">
|
<div class="ui stackable grid row">
|
||||||
<div class="ten wide column">
|
<div class="ten wide column">
|
||||||
<div class="ui field">
|
<div class="ui field">
|
||||||
|
@ -64,46 +115,67 @@
|
||||||
<translate translate-context="*/*/*">Tags</translate>
|
<translate translate-context="*/*/*">Tags</translate>
|
||||||
</label>
|
</label>
|
||||||
<tags-selector
|
<tags-selector
|
||||||
v-model="newValues.tags"
|
|
||||||
id="channel-tags"
|
id="channel-tags"
|
||||||
:required="false"></tags-selector>
|
v-model="newValues.tags"
|
||||||
|
:required="false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="ui required field">
|
||||||
<label for="channel-language">
|
<label for="channel-language">
|
||||||
<translate translate-context="*/*/*">Language</translate>
|
<translate translate-context="*/*/*">Language</translate>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
name="channel-language"
|
|
||||||
id="channel-language"
|
id="channel-language"
|
||||||
v-model="newValues.metadata.language"
|
v-model="newValues.metadata.language"
|
||||||
|
name="channel-language"
|
||||||
required
|
required
|
||||||
class="ui search selection dropdown">
|
class="ui search selection dropdown"
|
||||||
<option v-for="v in metadataChoices.language" :value="v.value">{{ v.label }}</option>
|
>
|
||||||
|
<option
|
||||||
|
v-for="(v, key) in metadataChoices.language"
|
||||||
|
:key="key"
|
||||||
|
:value="v.value"
|
||||||
|
>
|
||||||
|
{{ v.label }}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui small hidden divider"></div>
|
<div class="ui small hidden divider" />
|
||||||
<div class="ui field">
|
<div class="ui field">
|
||||||
<label for="channel-name">
|
<label for="channel-name">
|
||||||
<translate translate-context="*/*/*">Description</translate>
|
<translate translate-context="*/*/*">Description</translate>
|
||||||
</label>
|
</label>
|
||||||
<content-form v-model="newValues.description"></content-form>
|
<content-form v-model="newValues.description" />
|
||||||
</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 required field">
|
<div class="ui required field">
|
||||||
<label for="channel-itunes-category">
|
<label for="channel-itunes-category">
|
||||||
<translate translate-context="*/*/*">Category</translate>
|
<translate translate-context="*/*/*">Category</translate>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
name="itunes-category"
|
|
||||||
id="itunes-category"
|
id="itunes-category"
|
||||||
v-model="newValues.metadata.itunes_category"
|
v-model="newValues.metadata.itunes_category"
|
||||||
|
name="itunes-category"
|
||||||
required
|
required
|
||||||
class="ui dropdown">
|
class="ui dropdown"
|
||||||
<option v-for="v in metadataChoices.itunes_category" :value="v.value">{{ v.label }}</option>
|
>
|
||||||
|
<option
|
||||||
|
v-for="(v, key) in metadataChoices.itunes_category"
|
||||||
|
:key="key"
|
||||||
|
:value="v.value"
|
||||||
|
>
|
||||||
|
{{ v.label }}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui field">
|
<div class="ui field">
|
||||||
|
@ -111,45 +183,64 @@
|
||||||
<translate translate-context="*/*/*">Subcategory</translate>
|
<translate translate-context="*/*/*">Subcategory</translate>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
name="itunes-category"
|
|
||||||
id="itunes-category"
|
id="itunes-category"
|
||||||
v-model="newValues.metadata.itunes_subcategory"
|
v-model="newValues.metadata.itunes_subcategory"
|
||||||
|
name="itunes-category"
|
||||||
:disabled="!newValues.metadata.itunes_category"
|
:disabled="!newValues.metadata.itunes_category"
|
||||||
class="ui dropdown">
|
class="ui dropdown"
|
||||||
<option v-for="v in itunesSubcategories" :value="v">{{ v }}</option>
|
>
|
||||||
|
<option
|
||||||
|
v-for="(v, key) in itunesSubcategories"
|
||||||
|
:key="key"
|
||||||
|
:value="v"
|
||||||
|
>
|
||||||
|
{{ v }}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="ui field">
|
||||||
<label for="channel-itunes-email">
|
<label for="channel-itunes-email">
|
||||||
<translate translate-context="*/*/*">Owner e-mail address</translate>
|
<translate translate-context="*/*/*">Owner e-mail address</translate>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
name="channel-itunes-email"
|
|
||||||
id="channel-itunes-email"
|
id="channel-itunes-email"
|
||||||
|
v-model="newValues.metadata.owner_email"
|
||||||
|
name="channel-itunes-email"
|
||||||
type="email"
|
type="email"
|
||||||
v-model="newValues.metadata.owner_email">
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui field">
|
<div class="ui field">
|
||||||
<label for="channel-itunes-name">
|
<label for="channel-itunes-name">
|
||||||
<translate translate-context="*/*/*">Owner name</translate>
|
<translate translate-context="*/*/*">Owner name</translate>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
name="channel-itunes-name"
|
|
||||||
id="channel-itunes-name"
|
id="channel-itunes-name"
|
||||||
|
v-model="newValues.metadata.owner_name"
|
||||||
|
name="channel-itunes-name"
|
||||||
maxlength="255"
|
maxlength="255"
|
||||||
v-model="newValues.metadata.owner_name">
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<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>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="ui active inverted dimmer">
|
<div
|
||||||
|
v-else
|
||||||
|
class="ui active inverted dimmer"
|
||||||
|
>
|
||||||
<div class="ui text loader">
|
<div class="ui text loader">
|
||||||
<translate translate-context="*/*/*">Loading</translate>
|
<translate translate-context="*/*/*">
|
||||||
|
Loading
|
||||||
|
</translate>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -161,29 +252,25 @@ import axios from 'axios'
|
||||||
import AttachmentInput from '@/components/common/AttachmentInput'
|
import AttachmentInput from '@/components/common/AttachmentInput'
|
||||||
import TagsSelector from '@/components/library/TagsSelector'
|
import TagsSelector from '@/components/library/TagsSelector'
|
||||||
|
|
||||||
function slugify(text) {
|
function slugify (text) {
|
||||||
return text.toString().toLowerCase()
|
return text.toString().toLowerCase()
|
||||||
.replace(/\s+/g, '') // Remove spaces
|
.replace(/\s+/g, '') // Remove spaces
|
||||||
.replace(/[^\w]+/g, '') // Remove all non-word chars
|
.replace(/[^\w]+/g, '') // Remove all non-word chars
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
object: {type: Object, required: false, default: null},
|
|
||||||
step: {type: Number, required: false, default: 1},
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
AttachmentInput,
|
AttachmentInput,
|
||||||
TagsSelector
|
TagsSelector
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
created () {
|
object: { type: Object, required: false, default: null },
|
||||||
this.fetchMetadataChoices()
|
step: { type: Number, required: false, default: 1 }
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
let oldValues = {}
|
const oldValues = {}
|
||||||
if (this.object) {
|
if (this.object) {
|
||||||
oldValues.metadata = {...(this.object.metadata || {})}
|
oldValues.metadata = { ...(this.object.metadata || {}) }
|
||||||
oldValues.name = this.object.artist.name
|
oldValues.name = this.object.artist.name
|
||||||
oldValues.description = this.object.artist.description
|
oldValues.description = this.object.artist.description
|
||||||
oldValues.cover = this.object.artist.cover
|
oldValues.cover = this.object.artist.cover
|
||||||
|
@ -196,13 +283,13 @@ export default {
|
||||||
errors: [],
|
errors: [],
|
||||||
metadataChoices: null,
|
metadataChoices: null,
|
||||||
newValues: {
|
newValues: {
|
||||||
name: oldValues.name || "",
|
name: oldValues.name || '',
|
||||||
username: oldValues.username || "",
|
username: oldValues.username || '',
|
||||||
tags: oldValues.tags || [],
|
tags: oldValues.tags || [],
|
||||||
description: (oldValues.description || {}).text || "",
|
description: (oldValues.description || {}).text || '',
|
||||||
cover: (oldValues.cover || {}).uuid || null,
|
cover: (oldValues.cover || {}).uuid || null,
|
||||||
content_category: oldValues.content_category || "podcast",
|
content_category: oldValues.content_category || 'podcast',
|
||||||
metadata: oldValues.metadata || {},
|
metadata: oldValues.metadata || {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -213,20 +300,20 @@ export default {
|
||||||
categoryChoices () {
|
categoryChoices () {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
value: "podcast",
|
value: 'podcast',
|
||||||
label: this.$pgettext('*/*/*', "Podcasts"),
|
label: this.$pgettext('*/*/*', 'Podcasts'),
|
||||||
helpText: this.$pgettext('Content/Channels/Help', "Host your episodes and keep your community updated."),
|
helpText: this.$pgettext('Content/Channels/Help', 'Host your episodes and keep your community updated.')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "music",
|
value: 'music',
|
||||||
label: this.$pgettext('*/*/*', "Artist discography"),
|
label: this.$pgettext('*/*/*', 'Artist discography'),
|
||||||
helpText: this.$pgettext('Content/Channels/Help', "Publish music you make as a nice discography of albums and singles."),
|
helpText: this.$pgettext('Content/Channels/Help', 'Publish music you make as a nice discography of albums and singles.')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
itunesSubcategories () {
|
itunesSubcategories () {
|
||||||
for (let index = 0; index < this.metadataChoices.itunes_category.length; index++) {
|
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) {
|
if (element.value === this.newValues.metadata.itunes_category) {
|
||||||
return element.children || []
|
return element.children || []
|
||||||
}
|
}
|
||||||
|
@ -235,8 +322,8 @@ export default {
|
||||||
},
|
},
|
||||||
labels () {
|
labels () {
|
||||||
return {
|
return {
|
||||||
namePlaceholder: this.$pgettext('Content/Channel/Form.Field.Placeholder', "Awesome channel name"),
|
namePlaceholder: this.$pgettext('Content/Channel/Form.Field.Placeholder', 'Awesome channel name'),
|
||||||
usernamePlaceholder: this.$pgettext('Content/Channel/Form.Field.Placeholder', "awesomechannelname"),
|
usernamePlaceholder: this.$pgettext('Content/Channel/Form.Field.Placeholder', 'awesomechannelname')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
submittable () {
|
submittable () {
|
||||||
|
@ -247,9 +334,41 @@ export default {
|
||||||
return !!v
|
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: {
|
methods: {
|
||||||
fetchMetadataChoices () {
|
fetchMetadataChoices () {
|
||||||
let self = this
|
const self = this
|
||||||
axios.get('channels/metadata-choices').then((response) => {
|
axios.get('channels/metadata-choices').then((response) => {
|
||||||
self.metadataChoices = response.data
|
self.metadataChoices = response.data
|
||||||
}, error => {
|
}, error => {
|
||||||
|
@ -258,21 +377,21 @@ export default {
|
||||||
},
|
},
|
||||||
submit () {
|
submit () {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let self = this
|
const self = this
|
||||||
let handler = this.creating ? axios.post : axios.patch
|
const handler = this.creating ? axios.post : axios.patch
|
||||||
let url = this.creating ? `channels/` : `channels/${this.object.uuid}`
|
const url = this.creating ? 'channels/' : `channels/${this.object.uuid}`
|
||||||
let payload = {
|
const payload = {
|
||||||
name: this.newValues.name,
|
name: this.newValues.name,
|
||||||
username: this.newValues.username,
|
username: this.newValues.username,
|
||||||
tags: this.newValues.tags,
|
tags: this.newValues.tags,
|
||||||
content_category: this.newValues.content_category,
|
content_category: this.newValues.content_category,
|
||||||
cover: this.newValues.cover,
|
cover: this.newValues.cover,
|
||||||
metadata: this.newValues.metadata,
|
metadata: this.newValues.metadata
|
||||||
}
|
}
|
||||||
if (this.newValues.description) {
|
if (this.newValues.description) {
|
||||||
payload.description = {
|
payload.description = {
|
||||||
content_type: 'text/markdown',
|
content_type: 'text/markdown',
|
||||||
text: this.newValues.description,
|
text: this.newValues.description
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
payload.description = null
|
payload.description = null
|
||||||
|
@ -291,34 +410,6 @@ export default {
|
||||||
self.$emit('errored', self.errors)
|
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>
|
</script>
|
||||||
|
|
|
@ -1,28 +1,62 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="channel-serie-card">
|
<div class="channel-serie-card">
|
||||||
<div class="two-images">
|
<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
|
||||||
<img alt="" @click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})" class="channel-image" v-else src="../../assets/audio/default-cover.png">
|
v-if="cover && cover.urls.original"
|
||||||
<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)">
|
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">
|
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>
|
||||||
<div class="content ellipsis">
|
<div class="content ellipsis">
|
||||||
<strong>
|
<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 }}
|
{{ serie.title }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</strong>
|
</strong>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<translate translate-context="Content/Channel/Paragraph"
|
<translate
|
||||||
|
translate-context="Content/Channel/Paragraph"
|
||||||
translate-plural="%{ count } episodes"
|
translate-plural="%{ count } episodes"
|
||||||
:translate-n="serie.tracks_count"
|
:translate-n="serie.tracks_count"
|
||||||
:translate-params="{count: serie.tracks_count}">
|
:translate-params="{count: serie.tracks_count}"
|
||||||
|
>
|
||||||
%{ count } episode
|
%{ count } episode
|
||||||
</translate>
|
</translate>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -31,18 +65,19 @@
|
||||||
import PlayButton from '@/components/audio/PlayButton'
|
import PlayButton from '@/components/audio/PlayButton'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['serie'],
|
|
||||||
components: {
|
components: {
|
||||||
PlayButton,
|
PlayButton
|
||||||
},
|
},
|
||||||
|
props: { serie: { type: Object, required: true } },
|
||||||
computed: {
|
computed: {
|
||||||
cover () {
|
cover () {
|
||||||
if (this.serie.cover) {
|
if (this.serie.cover) {
|
||||||
return this.serie.cover
|
return this.serie.cover
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
},
|
},
|
||||||
duration () {
|
duration () {
|
||||||
let uploads = this.serie.uploads.filter((e) => {
|
const uploads = this.serie.uploads.filter((e) => {
|
||||||
return e.duration
|
return e.duration
|
||||||
})
|
})
|
||||||
return uploads[0].duration
|
return uploads[0].duration
|
||||||
|
|
|
@ -1,26 +1,51 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<slot></slot>
|
<slot />
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
<div
|
||||||
<div class="ui loader"></div>
|
v-if="isLoading"
|
||||||
|
class="ui inverted active dimmer"
|
||||||
|
>
|
||||||
|
<div class="ui loader" />
|
||||||
</div>
|
</div>
|
||||||
<template v-if="isPodcast">
|
<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>
|
</template>
|
||||||
<div v-else class="ui app-cards cards">
|
<div
|
||||||
<album-card v-for="album in objects" :album="album" :key="album.id" />
|
v-else
|
||||||
|
class="ui app-cards cards"
|
||||||
|
>
|
||||||
|
<album-card
|
||||||
|
v-for="album in objects"
|
||||||
|
:key="album.id"
|
||||||
|
:album="album"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="nextPage">
|
<template v-if="nextPage">
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<button v-if="nextPage" @click="fetchData(nextPage)" :class="['ui', 'basic', 'button']">
|
<button
|
||||||
<translate translate-context="*/*/Button,Label">Show more</translate>
|
v-if="nextPage"
|
||||||
|
:class="['ui', 'basic', 'button']"
|
||||||
|
@click="fetchData(nextPage)"
|
||||||
|
>
|
||||||
|
<translate translate-context="*/*/Button,Label">
|
||||||
|
Show more
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="!isLoading && objects.length === 0">
|
<template v-if="!isLoading && objects.length === 0">
|
||||||
<empty-state @refresh="fetchData('albums/')" :refresh="true">
|
<empty-state
|
||||||
|
:refresh="true"
|
||||||
|
@refresh="fetchData('albums/')"
|
||||||
|
>
|
||||||
<p>
|
<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>
|
</p>
|
||||||
</empty-state>
|
</empty-state>
|
||||||
</template>
|
</template>
|
||||||
|
@ -33,16 +58,15 @@ import axios from 'axios'
|
||||||
import ChannelSerieCard from '@/components/audio/ChannelSerieCard'
|
import ChannelSerieCard from '@/components/audio/ChannelSerieCard'
|
||||||
import AlbumCard from '@/components/audio/album/Card'
|
import AlbumCard from '@/components/audio/album/Card'
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
filters: {type: Object, required: true},
|
|
||||||
isPodcast: {type: Boolean, default: true},
|
|
||||||
limit: {type: Number, default: 5},
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
ChannelSerieCard,
|
ChannelSerieCard,
|
||||||
AlbumCard,
|
AlbumCard
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
filters: { type: Object, required: true },
|
||||||
|
isPodcast: { type: Boolean, default: true },
|
||||||
|
limit: { type: Number, default: 5 }
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -62,11 +86,11 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let self = this
|
const self = this
|
||||||
let params = _.clone(this.filters)
|
const params = _.clone(this.filters)
|
||||||
params.page_size = this.limit
|
params.page_size = this.limit
|
||||||
params.include_channels = true
|
params.include_channels = true
|
||||||
axios.get(url, {params: params}).then((response) => {
|
axios.get(url, { params: params }).then((response) => {
|
||||||
self.nextPage = response.data.next
|
self.nextPage = response.data.next
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
self.objects = self.objects.concat(response.data.results)
|
self.objects = self.objects.concat(response.data.results)
|
||||||
|
@ -75,7 +99,7 @@ export default {
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
self.errors = error.backendErrors
|
self.errors = error.backendErrors
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,21 +1,37 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<slot></slot>
|
<slot />
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<div class="ui app-cards cards">
|
<div class="ui app-cards cards">
|
||||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
<div
|
||||||
<div class="ui loader"></div>
|
v-if="isLoading"
|
||||||
|
class="ui inverted active dimmer"
|
||||||
|
>
|
||||||
|
<div class="ui loader" />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<template v-if="nextPage">
|
<template v-if="nextPage">
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<button v-if="nextPage" @click="fetchData(nextPage)" :class="['ui', 'basic', 'button']">
|
<button
|
||||||
<translate translate-context="*/*/Button,Label">Show more</translate>
|
v-if="nextPage"
|
||||||
|
:class="['ui', 'basic', 'button']"
|
||||||
|
@click="fetchData(nextPage)"
|
||||||
|
>
|
||||||
|
<translate translate-context="*/*/Button,Label">
|
||||||
|
Show more
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="!isLoading && objects.length === 0">
|
<template v-if="!isLoading && objects.length === 0">
|
||||||
<empty-state @refresh="fetchData('channels/')" :refresh="true"></empty-state>
|
<empty-state
|
||||||
|
:refresh="true"
|
||||||
|
@refresh="fetchData('channels/')"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -26,13 +42,13 @@ import axios from 'axios'
|
||||||
import ChannelCard from '@/components/audio/ChannelCard'
|
import ChannelCard from '@/components/audio/ChannelCard'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
filters: {type: Object, required: true},
|
|
||||||
limit: {type: Number, default: 5},
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
ChannelCard
|
ChannelCard
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
filters: { type: Object, required: true },
|
||||||
|
limit: { type: Number, default: 5 }
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
objects: [],
|
objects: [],
|
||||||
|
@ -51,11 +67,11 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let self = this
|
const self = this
|
||||||
let params = _.clone(this.filters)
|
const params = _.clone(this.filters)
|
||||||
params.page_size = this.limit
|
params.page_size = this.limit
|
||||||
params.include_channels = true
|
params.include_channels = true
|
||||||
axios.get(url, {params: params}).then((response) => {
|
axios.get(url, { params: params }).then((response) => {
|
||||||
self.nextPage = response.data.next
|
self.nextPage = response.data.next
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
self.objects = self.objects.concat(response.data.results)
|
self.objects = self.objects.concat(response.data.results)
|
||||||
|
@ -65,7 +81,7 @@ export default {
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
self.errors = error.backendErrors
|
self.errors = error.backendErrors
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div role="alert" class="ui warning message" v-if="!anonymousCanListen">
|
<div
|
||||||
|
v-if="!anonymousCanListen"
|
||||||
|
role="alert"
|
||||||
|
class="ui warning message"
|
||||||
|
>
|
||||||
<p>
|
<p>
|
||||||
<strong>
|
<strong>
|
||||||
<translate translate-context="Content/Embed/Message">Sharing will not work because this pod doesn't allow anonymous users to access content.</translate>
|
<translate translate-context="Content/Embed/Message">Sharing will not work because this pod doesn't allow anonymous users to access content.</translate>
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui form">
|
<div class="ui form">
|
||||||
|
@ -15,49 +21,100 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="embed-width"><translate translate-context="Popup/Embed/Input.Label">Widget width</translate></label>
|
<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>
|
<p>
|
||||||
<input id="embed-width" type="number" v-model.number="width" min="0" step="10" />
|
<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>
|
</div>
|
||||||
<template v-if="type != 'track'">
|
<template v-if="type != 'track'">
|
||||||
<br>
|
<br>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="embed-height"><translate translate-context="Popup/Embed/Input.Label">Widget height</translate></label>
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<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>
|
<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>
|
<p>
|
||||||
<textarea ref="textarea" :value="embedCode" rows="5" readonly>
|
<translate translate-context="Popup/Embed/Paragraph">
|
||||||
</textarea>
|
Copy/paste this code in your website HTML
|
||||||
|
</translate>
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
ref="textarea"
|
||||||
|
:value="embedCode"
|
||||||
|
rows="5"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
<div class="ui right">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="preview">
|
<div class="preview">
|
||||||
<h3>
|
<h3>
|
||||||
<a :href="iframeSrc" target="_blank">
|
<a
|
||||||
|
:href="iframeSrc"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
<translate translate-context="Popup/Embed/Title/Noun">Preview</translate>
|
<translate translate-context="Popup/Embed/Title/Noun">Preview</translate>
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import { mapState } from "vuex"
|
import { mapState } from 'vuex'
|
||||||
import _ from '@/lodash'
|
import _ from '@/lodash'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['type', 'id'],
|
props: {
|
||||||
|
type: { type: String, required: true },
|
||||||
|
id: { type: Number, required: true }
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
let d = {
|
const d = {
|
||||||
width: null,
|
width: null,
|
||||||
height: 150,
|
height: 150,
|
||||||
minHeight: 100,
|
minHeight: 100,
|
||||||
|
@ -71,7 +128,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
nodeinfo: state => state.instance.nodeinfo,
|
nodeinfo: state => state.instance.nodeinfo
|
||||||
}),
|
}),
|
||||||
anonymousCanListen () {
|
anonymousCanListen () {
|
||||||
return _.get(this.nodeinfo, 'metadata.library.anonymousCanListen', false)
|
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
|
// include hostname/protocol too so that the iframe link is absolute
|
||||||
base = `${window.location.protocol}//${window.location.host}${base}`
|
base = `${window.location.protocol}//${window.location.host}${base}`
|
||||||
}
|
}
|
||||||
let instanceUrl = this.$store.state.instance.instanceUrl
|
const instanceUrl = this.$store.state.instance.instanceUrl
|
||||||
let b = ''
|
let b = ''
|
||||||
if (!window.location.href.startsWith(instanceUrl)) {
|
if (!window.location.href.startsWith(instanceUrl)) {
|
||||||
// the frontend is running on a separate domain, so we need to provide
|
// the frontend is running on a separate domain, so we need to provide
|
||||||
|
@ -98,15 +155,15 @@ export default {
|
||||||
return '100%'
|
return '100%'
|
||||||
},
|
},
|
||||||
embedCode () {
|
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>`
|
return `<iframe width="${this.frameWidth}" height="${this.height}" scrolling="no" frameborder="no" src="${src}"></iframe>`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
copy () {
|
copy () {
|
||||||
this.$refs.textarea.select()
|
this.$refs.textarea.select()
|
||||||
document.execCommand("Copy")
|
document.execCommand('Copy')
|
||||||
let self = this
|
const self = this
|
||||||
self.copied = true
|
self.copied = true
|
||||||
this.timeout = setTimeout(() => {
|
this.timeout = setTimeout(() => {
|
||||||
self.copied = false
|
self.copied = false
|
||||||
|
|
|
@ -1,16 +1,34 @@
|
||||||
<template>
|
<template>
|
||||||
<button @click.stop="toggle" :class="['ui', 'pink', {'inverted': isApproved || isPending}, {'favorited': isApproved}, 'icon', 'labeled', 'button']">
|
<button
|
||||||
<i class="heart icon"></i>
|
:class="['ui', 'pink', {'inverted': isApproved || isPending}, {'favorited': isApproved}, 'icon', 'labeled', 'button']"
|
||||||
<translate v-if="isApproved" translate-context="Content/Library/Card.Button.Label/Verb">Unfollow</translate>
|
@click.stop="toggle"
|
||||||
<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>
|
<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>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
library: {type: Object},
|
library: { type: Object, required: true }
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isPending () {
|
isPending () {
|
||||||
|
@ -34,6 +52,5 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,52 +1,121 @@
|
||||||
<template>
|
<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
|
<button
|
||||||
v-if="!dropdownOnly"
|
v-if="!dropdownOnly"
|
||||||
@click.stop.prevent="replacePlay"
|
|
||||||
:disabled="!playable"
|
:disabled="!playable"
|
||||||
:aria-label="labels.replacePlay"
|
:aria-label="labels.replacePlay"
|
||||||
:class="buttonClasses.concat(['ui', {loading: isLoading}, {'mini': discrete}, {disabled: !playable}])">
|
:class="buttonClasses.concat(['ui', {loading: isLoading}, {'mini': discrete}, {disabled: !playable}])"
|
||||||
<i v-if="playing" class="pause icon"></i>
|
@click.stop.prevent="replacePlay"
|
||||||
<i v-else :class="[playIconClass, 'icon']"></i>
|
>
|
||||||
|
<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>
|
<template v-if="!discrete && !iconOnly"> <slot><translate translate-context="*/Queue/Button.Label/Short, Verb">Play</translate></slot></template>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!discrete && !iconOnly"
|
v-if="!discrete && !iconOnly"
|
||||||
|
:class="['ui', {disabled: !playable && !filterableArtist}, 'floating', 'dropdown', {'icon': !dropdownOnly}, {'button': !dropdownOnly}]"
|
||||||
@click.stop.prevent="clicked = true"
|
@click.stop.prevent="clicked = true"
|
||||||
:class="['ui', {disabled: !playable && !filterableArtist}, 'floating', 'dropdown', {'icon': !dropdownOnly}, {'button': !dropdownOnly}]">
|
>
|
||||||
<i :class="dropdownIconClasses.concat(['icon'])" :title="title" ></i>
|
<i
|
||||||
<div class="menu" v-if="clicked">
|
:class="dropdownIconClasses.concat(['icon'])"
|
||||||
<button class="item basic" ref="add" data-ref="add" :disabled="!playable" @click.stop.prevent="add" :title="labels.addToQueue">
|
:title="title"
|
||||||
<i class="plus icon"></i><translate translate-context="*/Queue/Dropdown/Button/Label/Short">Add to queue</translate>
|
/>
|
||||||
|
<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>
|
||||||
<button class="item basic" ref="addNext" data-ref="addNext" :disabled="!playable" @click.stop.prevent="addNext()" :title="labels.playNext">
|
<button
|
||||||
<i class="step forward icon"></i>{{ labels.playNext }}
|
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>
|
||||||
<button class="item basic" ref="playNow" data-ref="playNow" :disabled="!playable" @click.stop.prevent="addNext(true)" :title="labels.playNow">
|
<button
|
||||||
<i class="play icon"></i>{{ labels.playNow }}
|
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>
|
||||||
<button v-if="track" class="item basic" :disabled="!playable" @click.stop.prevent="$store.dispatch('radios/start', {type: 'similar', objectId: track.id})" :title="labels.startRadio">
|
<button
|
||||||
<i class="feed icon"></i><translate translate-context="*/Queue/Button.Label/Short, Verb">Play radio</translate>
|
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>
|
||||||
<button v-if="track" class="item basic" :disabled="!playable" @click.stop="$store.commit('playlists/chooseTrack', track)">
|
<button
|
||||||
<i class="list icon"></i>
|
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>
|
<translate translate-context="Sidebar/Player/Icon.Tooltip/Verb">Add to playlist…</translate>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="track" class="item basic" @click.stop.prevent="$router.push(`/library/tracks/${track.id}/`)">
|
<button
|
||||||
<i class="info icon"></i>
|
v-if="track"
|
||||||
<translate v-if="track.artist.content_category === 'podcast'" translate-context="*/Queue/Dropdown/Button/Label/Short">Episode details</translate>
|
class="item basic"
|
||||||
<translate v-else translate-context="*/Queue/Dropdown/Button/Label/Short">Track details</translate>
|
@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>
|
</button>
|
||||||
<div class="divider"></div>
|
<div class="divider" />
|
||||||
<button v-if="filterableArtist" ref="filterArtist" data-ref="filterArtist" class="item basic" :disabled="!filterableArtist" @click.stop.prevent="filterArtist" :title="labels.hideArtist">
|
<button
|
||||||
<i class="eye slash outline icon"></i><translate translate-context="*/Queue/Dropdown/Button/Label/Short">Hide content from this artist</translate>
|
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>
|
||||||
<button
|
<button
|
||||||
v-for="obj in getReportableObjs({track, album, artist, playlist, account, channel})"
|
v-for="obj in getReportableObjs({track, album, artist, playlist, account, channel})"
|
||||||
:key="obj.target.type + obj.target.id"
|
:key="obj.target.type + obj.target.id"
|
||||||
|
:ref="`report${obj.target.type}${obj.target.id}`"
|
||||||
class="item basic"
|
class="item basic"
|
||||||
:ref="`report${obj.target.type}${obj.target.id}`" :data-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)">
|
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)"
|
||||||
|
>
|
||||||
<i class="share icon" /> {{ obj.label }}
|
<i class="share icon" /> {{ obj.label }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -55,7 +124,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
|
||||||
import jQuery from 'jquery'
|
import jQuery from 'jquery'
|
||||||
|
|
||||||
import ReportMixin from '@/components/mixins/Report'
|
import ReportMixin from '@/components/mixins/Report'
|
||||||
|
@ -65,23 +133,23 @@ export default {
|
||||||
mixins: [ReportMixin, PlayOptionsMixin],
|
mixins: [ReportMixin, PlayOptionsMixin],
|
||||||
props: {
|
props: {
|
||||||
// we can either have a single or multiple tracks to play when clicked
|
// we can either have a single or multiple tracks to play when clicked
|
||||||
tracks: {type: Array, required: false},
|
tracks: { type: Array, required: false, default: () => { return [] } },
|
||||||
track: {type: Object, required: false},
|
track: { type: Object, required: false, default: () => { return {} } },
|
||||||
account: {type: Object, required: false},
|
account: { type: Object, required: false, default: () => { return {} } },
|
||||||
dropdownIconClasses: {type: Array, required: false, default: () => { return ['dropdown'] }},
|
dropdownIconClasses: { type: Array, required: false, default: () => { return ['dropdown'] } },
|
||||||
playIconClass: {type: String, required: false, default: 'play icon'},
|
playIconClass: { type: String, required: false, default: 'play icon' },
|
||||||
buttonClasses: {type: Array, required: false, default: () => { return ['button'] }},
|
buttonClasses: { type: Array, required: false, default: () => { return ['button'] } },
|
||||||
playlist: {type: Object, required: false},
|
playlist: { type: Object, required: false, default: () => { return {} } },
|
||||||
discrete: {type: Boolean, default: false},
|
discrete: { type: Boolean, default: false },
|
||||||
dropdownOnly: {type: Boolean, default: false},
|
dropdownOnly: { type: Boolean, default: false },
|
||||||
iconOnly: {type: Boolean, default: false},
|
iconOnly: { type: Boolean, default: false },
|
||||||
artist: {type: Object, required: false},
|
artist: { type: Object, required: false, default: () => { return {} } },
|
||||||
album: {type: Object, required: false},
|
album: { type: Object, required: false, default: () => { return {} } },
|
||||||
library: {type: Object, required: false},
|
library: { type: Object, required: false, default: () => { return {} } },
|
||||||
channel: {type: Object, required: false},
|
channel: { type: Object, required: false, default: () => { return {} } },
|
||||||
isPlayable: {type: Boolean, required: false, default: null},
|
isPlayable: { type: Boolean, required: false, default: null },
|
||||||
playing: {type: Boolean, required: false, default: false},
|
playing: { type: Boolean, required: false, default: false },
|
||||||
paused: {type: Boolean, required: false, default: false}
|
paused: { type: Boolean, required: false, default: false }
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -111,7 +179,7 @@ export default {
|
||||||
startRadio: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play similar songs'),
|
startRadio: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play similar songs'),
|
||||||
report: this.$pgettext('*/Moderation/*/Button/Label,Verb', 'Report…'),
|
report: this.$pgettext('*/Moderation/*/Button/Label,Verb', 'Report…'),
|
||||||
addToPlaylist: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Add to playlist…'),
|
addToPlaylist: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Add to playlist…'),
|
||||||
replacePlay,
|
replacePlay
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title () {
|
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 this.$pgettext('*/Queue/Button/Title', 'This track is not available in any library you have access to')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
return null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
clicked () {
|
clicked () {
|
||||||
let self = this
|
const self = this
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
jQuery(this.$el).find('.ui.dropdown').dropdown({
|
jQuery(this.$el).find('.ui.dropdown').dropdown({
|
||||||
selectOnKeydown: false,
|
selectOnKeydown: false,
|
||||||
action: function (text, value, $el) {
|
action: function (text, value, $el) {
|
||||||
// used to ensure focusing the dropdown and clicking via keyboard
|
// used to ensure focusing the dropdown and clicking via keyboard
|
||||||
// works as expected
|
// works as expected
|
||||||
let button = self.$refs[$el.data('ref')]
|
const button = self.$refs[$el.data('ref')]
|
||||||
if (Array.isArray(button)) {
|
if (Array.isArray(button)) {
|
||||||
button[0].click()
|
button[0].click()
|
||||||
} else {
|
} else {
|
||||||
button.click()
|
button.click()
|
||||||
}
|
}
|
||||||
jQuery(self.$el).find('.ui.dropdown').dropdown('hide')
|
jQuery(self.$el).find('.ui.dropdown').dropdown('hide')
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
jQuery(this.$el).find('.ui.dropdown').dropdown('show', function () {
|
jQuery(this.$el).find('.ui.dropdown').dropdown('show', function () {
|
||||||
// little magic to ensure the menu is always visible in the viewport
|
// 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
|
// By default, try to diplay it on the right if there is enough room
|
||||||
let menu = jQuery(self.$el).find('.ui.dropdown').find(".menu")
|
const menu = jQuery(self.$el).find('.ui.dropdown').find('.menu')
|
||||||
let viewportOffset = menu.get(0).getBoundingClientRect();
|
const viewportOffset = menu.get(0).getBoundingClientRect()
|
||||||
let left = viewportOffset.left;
|
const viewportWidth = document.documentElement.clientWidth
|
||||||
let viewportWidth = document.documentElement.clientWidth
|
const rightOverflow = viewportOffset.right - viewportWidth
|
||||||
let rightOverflow = viewportOffset.right - viewportWidth
|
const leftOverflow = -viewportOffset.left
|
||||||
let leftOverflow = -viewportOffset.left
|
|
||||||
let offset = 0
|
let offset = 0
|
||||||
if (rightOverflow > 0) {
|
if (rightOverflow > 0) {
|
||||||
offset = -rightOverflow - 5
|
offset = -rightOverflow - 5
|
||||||
menu.css({cssText: `left: ${offset}px !important;`});
|
menu.css({ cssText: `left: ${offset}px !important;` })
|
||||||
}
|
} else if (leftOverflow > 0) {
|
||||||
else if (leftOverflow > 0) {
|
offset = leftOverflow + 5
|
||||||
offset = leftOverflow + 5
|
menu.css({ cssText: `right: -${offset}px !important;` })
|
||||||
menu.css({cssText: `right: -${offset}px !important;`});
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,64 +1,144 @@
|
||||||
<template>
|
<template>
|
||||||
<section role="complementary" v-if="currentTrack" class="player-wrapper ui bottom-player component-player" aria-labelledby="player-label">
|
<section
|
||||||
<h1 id="player-label" class="visually-hidden">
|
v-if="currentTrack"
|
||||||
<translate translate-context="*/*/*">Audio player and controls</translate>
|
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>
|
</h1>
|
||||||
<div class="ui inverted segment fixed-controls" @click.prevent.stop="toggleMobilePlayer">
|
<div
|
||||||
|
class="ui inverted segment fixed-controls"
|
||||||
|
@click.prevent.stop="toggleMobilePlayer"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
:class="['ui', 'top attached', 'small', 'inverted', {'indicating': isLoadingAudio}, 'progress']">
|
: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>
|
<div
|
||||||
|
class="buffer bar"
|
||||||
|
:data-percent="bufferProgress"
|
||||||
|
:style="{ 'width': bufferProgress + '%' }"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="position bar"
|
||||||
|
:data-percent="progress"
|
||||||
|
:style="{ 'width': progress + '%' }"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls-row">
|
<div class="controls-row">
|
||||||
|
|
||||||
<div class="controls track-controls queue-not-focused desktop-and-up">
|
<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 }})">
|
<div
|
||||||
<img alt="" ref="cover" v-if="currentTrack.cover && currentTrack.cover.urls.original" :src="$store.getters['instance/absoluteUrl'](currentTrack.cover.urls.medium_square_crop)">
|
class="ui tiny image"
|
||||||
<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)">
|
@click.stop.prevent="$router.push({name: 'library.tracks.detail', params: {id: currentTrack.id }})"
|
||||||
<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 && 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>
|
||||||
<div @click.stop.prevent="" class="middle aligned content ellipsis">
|
<div
|
||||||
|
class="middle aligned content ellipsis"
|
||||||
|
@click.stop.prevent=""
|
||||||
|
>
|
||||||
<strong>
|
<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 }}
|
{{ currentTrack.title }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</strong>
|
</strong>
|
||||||
<div class="meta">
|
<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>
|
<router-link
|
||||||
<template v-if="currentTrack.album"> /
|
class="discrete link"
|
||||||
<router-link @click.stop.prevent="" class="discrete link" :to="{name: 'library.albums.detail', params: {id: currentTrack.album.id }}">{{ currentTrack.album.title }}</router-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>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls track-controls queue-not-focused tablet-and-below">
|
<div class="controls track-controls queue-not-focused tablet-and-below">
|
||||||
<div class="ui tiny image">
|
<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
|
||||||
<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)">
|
v-if="currentTrack.cover && currentTrack.cover.urls.original"
|
||||||
<img alt="" v-else src="../../assets/audio/default-cover.png">
|
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>
|
||||||
<div class="middle aligned content ellipsis">
|
<div class="middle aligned content ellipsis">
|
||||||
<strong>
|
<strong>
|
||||||
{{ currentTrack.title }}
|
{{ currentTrack.title }}
|
||||||
</strong>
|
</strong>
|
||||||
<div class="meta">
|
<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>
|
||||||
</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
|
<track-favorite-icon
|
||||||
class="control white"
|
class="control white"
|
||||||
:track="currentTrack"></track-favorite-icon>
|
:track="currentTrack"
|
||||||
|
/>
|
||||||
<track-playlist-icon
|
<track-playlist-icon
|
||||||
class="control white"
|
class="control white"
|
||||||
:track="currentTrack"></track-playlist-icon>
|
:track="currentTrack"
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
@click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})"
|
|
||||||
:class="['ui', 'really', 'basic', 'circular', 'icon', 'button', 'control']"
|
:class="['ui', 'really', 'basic', 'circular', 'icon', 'button', 'control']"
|
||||||
:aria-label="labels.addArtistContentFilter"
|
:aria-label="labels.addArtistContentFilter"
|
||||||
:title="labels.addArtistContentFilter">
|
:title="labels.addArtistContentFilter"
|
||||||
<i :class="['eye slash outline', 'basic', 'icon']"></i>
|
@click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})"
|
||||||
|
>
|
||||||
|
<i :class="['eye slash outline', 'basic', 'icon']" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="player-controls controls queue-not-focused">
|
<div class="player-controls controls queue-not-focused">
|
||||||
|
@ -66,41 +146,48 @@
|
||||||
:title="labels.previous"
|
:title="labels.previous"
|
||||||
:aria-label="labels.previous"
|
:aria-label="labels.previous"
|
||||||
class="circular button control tablet-and-up"
|
class="circular button control tablet-and-up"
|
||||||
|
:disabled="!hasPrevious"
|
||||||
@click.prevent.stop="$store.dispatch('queue/previous')"
|
@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>
|
||||||
<button
|
<button
|
||||||
v-if="!playing"
|
v-if="!playing"
|
||||||
:title="labels.play"
|
:title="labels.play"
|
||||||
:aria-label="labels.play"
|
:aria-label="labels.play"
|
||||||
|
class="circular button control"
|
||||||
@click.prevent.stop="resumePlayback"
|
@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>
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
:title="labels.pause"
|
:title="labels.pause"
|
||||||
:aria-label="labels.pause"
|
:aria-label="labels.pause"
|
||||||
|
class="circular button control"
|
||||||
@click.prevent.stop="pausePlayback"
|
@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>
|
||||||
<button
|
<button
|
||||||
:title="labels.next"
|
:title="labels.next"
|
||||||
:aria-label="labels.next"
|
:aria-label="labels.next"
|
||||||
class="circular button control"
|
class="circular button control"
|
||||||
|
:disabled="!hasNext"
|
||||||
@click.prevent.stop="$store.dispatch('queue/next')"
|
@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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="controls progress-controls queue-not-focused tablet-and-up small align-left">
|
<div class="controls progress-controls queue-not-focused tablet-and-up small align-left">
|
||||||
<div class="timer">
|
<div class="timer">
|
||||||
<template v-if="!isLoadingAudio">
|
<template v-if="!isLoadingAudio">
|
||||||
<span class="start" @click.stop.prevent="setCurrentTime(0)">{{currentTimeFormatted}}</span>
|
<span
|
||||||
| <span class="total">{{durationFormatted}}</span>
|
class="start"
|
||||||
|
@click.stop.prevent="setCurrentTime(0)"
|
||||||
|
>{{ currentTimeFormatted }}</span>
|
||||||
|
| <span class="total">{{ durationFormatted }}</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
00:00 | 00:00
|
00:00 | 00:00
|
||||||
|
@ -111,35 +198,40 @@
|
||||||
<div class="group">
|
<div class="group">
|
||||||
<volume-control class="expandable" />
|
<volume-control class="expandable" />
|
||||||
<button
|
<button
|
||||||
class="circular control button"
|
|
||||||
v-if="looping === 0"
|
v-if="looping === 0"
|
||||||
|
class="circular control button"
|
||||||
:title="labels.loopingDisabled"
|
:title="labels.loopingDisabled"
|
||||||
:aria-label="labels.loopingDisabled"
|
:aria-label="labels.loopingDisabled"
|
||||||
|
:disabled="!currentTrack"
|
||||||
@click.prevent.stop="$store.commit('player/looping', 1)"
|
@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>
|
||||||
<button
|
<button
|
||||||
|
v-if="looping === 1"
|
||||||
class="looping circular control button"
|
class="looping circular control button"
|
||||||
@click.prevent.stop="$store.commit('player/looping', 2)"
|
|
||||||
:title="labels.loopingSingle"
|
:title="labels.loopingSingle"
|
||||||
:aria-label="labels.loopingSingle"
|
:aria-label="labels.loopingSingle"
|
||||||
v-if="looping === 1"
|
:disabled="!currentTrack"
|
||||||
:disabled="!currentTrack">
|
@click.prevent.stop="$store.commit('player/looping', 2)"
|
||||||
|
>
|
||||||
<i
|
<i
|
||||||
class="repeat icon">
|
class="repeat icon"
|
||||||
|
>
|
||||||
<span class="ui circular tiny vibrant label">1</span>
|
<span class="ui circular tiny vibrant label">1</span>
|
||||||
</i>
|
</i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-if="looping === 2"
|
||||||
class="looping circular control button"
|
class="looping circular control button"
|
||||||
:title="labels.loopingWhole"
|
:title="labels.loopingWhole"
|
||||||
:aria-label="labels.loopingWhole"
|
:aria-label="labels.loopingWhole"
|
||||||
v-if="looping === 2"
|
|
||||||
:disabled="!currentTrack"
|
:disabled="!currentTrack"
|
||||||
@click.prevent.stop="$store.commit('player/looping', 0)">
|
@click.prevent.stop="$store.commit('player/looping', 0)"
|
||||||
|
>
|
||||||
<i
|
<i
|
||||||
class="repeat icon">
|
class="repeat icon"
|
||||||
|
>
|
||||||
<span class="ui circular tiny vibrant label">∞</span>
|
<span class="ui circular tiny vibrant label">∞</span>
|
||||||
</i>
|
</i>
|
||||||
</button>
|
</button>
|
||||||
|
@ -148,55 +240,80 @@
|
||||||
:disabled="queue.tracks.length === 0"
|
:disabled="queue.tracks.length === 0"
|
||||||
:title="labels.shuffle"
|
:title="labels.shuffle"
|
||||||
:aria-label="labels.shuffle"
|
:aria-label="labels.shuffle"
|
||||||
@click.prevent.stop="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>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="group">
|
<div class="group">
|
||||||
<div class="fake-dropdown">
|
<div class="fake-dropdown">
|
||||||
<button class="position circular control button desktop-and-up" @click.stop="toggleMobilePlayer" aria-expanded="true">
|
<button
|
||||||
<i class="stream icon"></i>
|
class="position circular control button desktop-and-up"
|
||||||
<translate translate-context="Sidebar/Queue/Text" :translate-params="{index: queue.currentIndex + 1, length: queue.tracks.length}">
|
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 }
|
%{ index } of %{ length }
|
||||||
</translate>
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
<button class="position circular control button tablet-and-below" @click.stop="switchTab">
|
<button
|
||||||
<i class="stream icon"></i>
|
class="position circular control button tablet-and-below"
|
||||||
<translate translate-context="Sidebar/Queue/Text" :translate-params="{index: queue.currentIndex + 1, length: queue.tracks.length}">
|
@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 }
|
%{ index } of %{ length }
|
||||||
</translate>
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="circular control button close-control desktop-and-up"
|
|
||||||
v-if="$store.state.ui.queueFocused"
|
v-if="$store.state.ui.queueFocused"
|
||||||
@click.stop="toggleMobilePlayer">
|
class="circular control button close-control desktop-and-up"
|
||||||
<i class="large down angle icon"></i>
|
@click.stop="toggleMobilePlayer"
|
||||||
|
>
|
||||||
|
<i class="large down angle icon" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="circular control button desktop-and-up"
|
|
||||||
v-else
|
v-else
|
||||||
@click.stop="toggleMobilePlayer">
|
class="circular control button desktop-and-up"
|
||||||
<i class="large up angle icon"></i>
|
@click.stop="toggleMobilePlayer"
|
||||||
|
>
|
||||||
|
<i class="large up angle icon" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="circular control button close-control tablet-and-below"
|
|
||||||
v-if="$store.state.ui.queueFocused === 'player'"
|
v-if="$store.state.ui.queueFocused === 'player'"
|
||||||
@click.stop="switchTab">
|
class="circular control button close-control tablet-and-below"
|
||||||
<i class="large up angle icon"></i>
|
@click.stop="switchTab"
|
||||||
|
>
|
||||||
|
<i class="large up angle icon" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="circular control button tablet-and-below"
|
|
||||||
v-if="$store.state.ui.queueFocused === 'queue'"
|
v-if="$store.state.ui.queueFocused === 'queue'"
|
||||||
@click.stop="switchTab">
|
class="circular control button tablet-and-below"
|
||||||
<i class="large down angle icon"></i>
|
@click.stop="switchTab"
|
||||||
|
>
|
||||||
|
<i class="large down angle icon" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="circular control button close-control tablet-and-below"
|
class="circular control button close-control tablet-and-below"
|
||||||
@click.stop="$store.commit('ui/queueFocused', null)">
|
@click.stop="$store.commit('ui/queueFocused', null)"
|
||||||
<i class="x icon"></i>
|
>
|
||||||
|
<i class="x icon" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -219,7 +336,7 @@
|
||||||
@keydown.f.exact="$store.dispatch('favorites/toggle', currentTrack.id)"
|
@keydown.f.exact="$store.dispatch('favorites/toggle', currentTrack.id)"
|
||||||
@keydown.q.exact="clean"
|
@keydown.q.exact="clean"
|
||||||
@keydown.e.exact="toggleMobilePlayer"
|
@keydown.e.exact="toggleMobilePlayer"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -259,6 +376,124 @@ export default {
|
||||||
nextTrackPreloaded: false
|
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 () {
|
mounted () {
|
||||||
this.$store.dispatch('player/updateProgress', 0)
|
this.$store.dispatch('player/updateProgress', 0)
|
||||||
this.$store.commit('player/playing', false)
|
this.$store.commit('player/playing', false)
|
||||||
|
@ -661,124 +896,6 @@ export default {
|
||||||
navigator.mediaSession.metadata = new window.MediaMetadata(metadata)
|
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>
|
</script>
|
||||||
|
|
|
@ -1,29 +1,69 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<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', {'loading': isLoading }, 'search']">
|
||||||
<div class="ui icon big input">
|
<div class="ui icon big input">
|
||||||
<i class="search icon"></i>
|
<i class="search icon" />
|
||||||
<input ref="search" class="prompt" :placeholder="labels.searchPlaceholder" v-model.trim="query" type="text" />
|
<input
|
||||||
|
ref="search"
|
||||||
|
v-model.trim="query"
|
||||||
|
class="prompt"
|
||||||
|
:placeholder="labels.searchPlaceholder"
|
||||||
|
type="text"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="query.length > 0">
|
<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 v-if="results.artists.length > 0">
|
||||||
<div class="ui cards">
|
<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>
|
||||||
</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>
|
||||||
<template v-if="query.length > 0">
|
<template v-if="query.length > 0">
|
||||||
<h3 class="ui title"><translate translate-context="*/*/*">Albums</translate></h3>
|
<h3 class="ui title">
|
||||||
<div v-if="results.albums.length > 0" class="ui stackable three column grid">
|
<translate translate-context="*/*/*">
|
||||||
<div class="column" :key="album.id" v-for="album in results.albums">
|
Albums
|
||||||
<album-card class="fluid" :album="album" ></album-card>
|
</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>
|
||||||
</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>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -41,7 +81,7 @@ export default {
|
||||||
ArtistCard
|
ArtistCard
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
autofocus: {type: Boolean, default: false}
|
autofocus: { type: Boolean, default: false }
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -53,12 +93,6 @@ export default {
|
||||||
isLoading: false
|
isLoading: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
|
||||||
if (this.autofocus) {
|
|
||||||
this.$refs.search.focus()
|
|
||||||
}
|
|
||||||
this.search()
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
labels () {
|
labels () {
|
||||||
return {
|
return {
|
||||||
|
@ -66,15 +100,26 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
query () {
|
||||||
|
this.search()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
if (this.autofocus) {
|
||||||
|
this.$refs.search.focus()
|
||||||
|
}
|
||||||
|
this.search()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
search: _.debounce(function () {
|
search: _.debounce(function () {
|
||||||
if (this.query.length < 1) {
|
if (this.query.length < 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var self = this
|
const self = this
|
||||||
self.isLoading = true
|
self.isLoading = true
|
||||||
logger.default.debug('Searching track matching "' + this.query + '"')
|
logger.default.debug('Searching track matching "' + this.query + '"')
|
||||||
let params = {
|
const params = {
|
||||||
query: this.query
|
query: this.query
|
||||||
}
|
}
|
||||||
axios.get('search', {
|
axios.get('search', {
|
||||||
|
@ -90,11 +135,6 @@ export default {
|
||||||
artists: results.artists
|
artists: results.artists
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
query () {
|
|
||||||
this.search()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="ui fluid category search">
|
<div class="ui fluid category search">
|
||||||
<slot></slot><div class="ui icon input">
|
<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()">
|
<input
|
||||||
<i class="search icon"></i>
|
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>
|
||||||
<div class="results"></div>
|
<div class="results" />
|
||||||
<slot name="after"></slot>
|
<slot name="after" />
|
||||||
<GlobalEvents
|
<GlobalEvents
|
||||||
@keydown.shift.f.prevent.exact="focusSearch"
|
@keydown.shift.f.prevent.exact="focusSearch"
|
||||||
/>
|
/>
|
||||||
|
@ -16,11 +24,11 @@
|
||||||
import jQuery from 'jquery'
|
import jQuery from 'jquery'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import lodash from '@/lodash'
|
import lodash from '@/lodash'
|
||||||
import GlobalEvents from "@/components/utils/global-events"
|
import GlobalEvents from '@/components/utils/global-events'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
GlobalEvents,
|
GlobalEvents
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
labels () {
|
labels () {
|
||||||
|
@ -31,22 +39,21 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
let artistLabel = this.$pgettext('*/*/*/Noun', 'Artist')
|
const artistLabel = this.$pgettext('*/*/*/Noun', 'Artist')
|
||||||
let albumLabel = this.$pgettext('*/*/*', 'Album')
|
const albumLabel = this.$pgettext('*/*/*', 'Album')
|
||||||
let trackLabel = this.$pgettext('*/*/*/Noun', 'Track')
|
const trackLabel = this.$pgettext('*/*/*/Noun', 'Track')
|
||||||
let tagLabel = this.$pgettext('*/*/*/Noun', 'Tag')
|
const tagLabel = this.$pgettext('*/*/*/Noun', 'Tag')
|
||||||
let self = this
|
const self = this
|
||||||
var searchQuery;
|
let searchQuery
|
||||||
|
|
||||||
jQuery(this.$el).keypress(function(e) {
|
jQuery(this.$el).keypress(function (e) {
|
||||||
if(e.which == 13) {
|
if (e.which === 13) {
|
||||||
// Cancel any API search request to backend…
|
// 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…
|
// Go direct to the artist page…
|
||||||
router.push(`/search?q=${searchQuery}&type=artists`);
|
router.push(`/search?q=${searchQuery}&type=artists`)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
|
|
||||||
jQuery(this.$el).search({
|
jQuery(this.$el).search({
|
||||||
type: 'category',
|
type: 'category',
|
||||||
|
@ -57,9 +64,9 @@ export default {
|
||||||
noResults: this.$pgettext('Sidebar/Search/Error.Label', 'Sorry, there are no results for this search')
|
noResults: this.$pgettext('Sidebar/Search/Error.Label', 'Sorry, there are no results for this search')
|
||||||
},
|
},
|
||||||
onSelect (result, response) {
|
onSelect (result, response) {
|
||||||
jQuery(self.$el).search("set value", searchQuery)
|
jQuery(self.$el).search('set value', searchQuery)
|
||||||
router.push(result.routerUrl)
|
router.push(result.routerUrl)
|
||||||
jQuery(self.$el).search("hide results")
|
jQuery(self.$el).search('hide results')
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
onSearchQuery (query) {
|
onSearchQuery (query) {
|
||||||
|
@ -78,17 +85,17 @@ export default {
|
||||||
return xhrObject
|
return xhrObject
|
||||||
},
|
},
|
||||||
onResponse: function (initialResponse) {
|
onResponse: function (initialResponse) {
|
||||||
let objId = self.extractObjId(searchQuery)
|
const objId = self.extractObjId(searchQuery)
|
||||||
let results = {}
|
const results = {}
|
||||||
let isEmptyResults = true
|
let isEmptyResults = true
|
||||||
let categories = [
|
const categories = [
|
||||||
{
|
{
|
||||||
code: 'federation',
|
code: 'federation',
|
||||||
name: self.$pgettext('*/*/*', 'Federation'),
|
name: self.$pgettext('*/*/*', 'Federation')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'podcasts',
|
code: 'podcasts',
|
||||||
name: self.$pgettext('*/*/*', 'Podcasts'),
|
name: self.$pgettext('*/*/*', 'Podcasts')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'artists',
|
code: 'artists',
|
||||||
|
@ -148,12 +155,12 @@ export default {
|
||||||
},
|
},
|
||||||
getId (t) {
|
getId (t) {
|
||||||
return t.name
|
return t.name
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'more',
|
code: 'more',
|
||||||
name: '',
|
name: ''
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
categories.forEach(category => {
|
categories.forEach(category => {
|
||||||
results[category.code] = {
|
results[category.code] = {
|
||||||
|
@ -161,29 +168,27 @@ export default {
|
||||||
results: []
|
results: []
|
||||||
}
|
}
|
||||||
if (category.code === 'federation') {
|
if (category.code === 'federation') {
|
||||||
|
|
||||||
if (objId) {
|
if (objId) {
|
||||||
isEmptyResults = false
|
isEmptyResults = false
|
||||||
let searchMessage = self.$pgettext('Search/*/*', 'Search on the fediverse')
|
const searchMessage = self.$pgettext('Search/*/*', 'Search on the fediverse')
|
||||||
results['federation'] = {
|
results.federation = {
|
||||||
name: self.$pgettext('*/*/*', 'Federation'),
|
name: self.$pgettext('*/*/*', 'Federation'),
|
||||||
results: [{
|
results: [{
|
||||||
title: searchMessage,
|
title: searchMessage,
|
||||||
routerUrl: {
|
routerUrl: {
|
||||||
name: 'search',
|
name: 'search',
|
||||||
query: {
|
query: {
|
||||||
id: objId,
|
id: objId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else if (category.code === 'podcasts') {
|
||||||
else if (category.code === 'podcasts') {
|
|
||||||
if (objId) {
|
if (objId) {
|
||||||
isEmptyResults = false
|
isEmptyResults = false
|
||||||
let searchMessage = self.$pgettext('Search/*/*', 'Subscribe to podcast via RSS')
|
const searchMessage = self.$pgettext('Search/*/*', 'Subscribe to podcast via RSS')
|
||||||
results['podcasts'] = {
|
results.podcasts = {
|
||||||
name: self.$pgettext('*/*/*', 'Podcasts'),
|
name: self.$pgettext('*/*/*', 'Podcasts'),
|
||||||
results: [{
|
results: [{
|
||||||
title: searchMessage,
|
title: searchMessage,
|
||||||
|
@ -191,33 +196,31 @@ export default {
|
||||||
name: 'search',
|
name: 'search',
|
||||||
query: {
|
query: {
|
||||||
id: objId,
|
id: objId,
|
||||||
type: "rss"
|
type: 'rss'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else if (category.code === 'more') {
|
||||||
else if (category.code === 'more') {
|
const searchMessage = self.$pgettext('Search/*/*', 'More results 🡒')
|
||||||
let searchMessage = self.$pgettext('Search/*/*', 'More results 🡒')
|
results.more = {
|
||||||
results['more'] = {
|
|
||||||
name: '',
|
name: '',
|
||||||
results: [{
|
results: [{
|
||||||
title: searchMessage,
|
title: searchMessage,
|
||||||
routerUrl: {
|
routerUrl: {
|
||||||
name: 'search',
|
name: 'search',
|
||||||
query: {
|
query: {
|
||||||
type: "artists",
|
type: 'artists',
|
||||||
q: searchQuery
|
q: searchQuery
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
initialResponse[category.code].forEach(result => {
|
initialResponse[category.code].forEach(result => {
|
||||||
isEmptyResults = false
|
isEmptyResults = false
|
||||||
let id = category.getId(result)
|
const id = category.getId(result)
|
||||||
results[category.code].results.push({
|
results[category.code].results.push({
|
||||||
title: category.getTitle(result),
|
title: category.getTitle(result),
|
||||||
id,
|
id,
|
||||||
|
|
|
@ -1,74 +1,87 @@
|
||||||
<template>
|
<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
|
<span
|
||||||
role="button"
|
|
||||||
v-if="sliderVolume === 0"
|
v-if="sliderVolume === 0"
|
||||||
|
role="button"
|
||||||
:title="labels.unmute"
|
:title="labels.unmute"
|
||||||
:aria-label="labels.unmute"
|
:aria-label="labels.unmute"
|
||||||
@click.prevent.stop="unmute">
|
@click.prevent.stop="unmute"
|
||||||
<i class="volume off icon"></i>
|
>
|
||||||
|
<i class="volume off icon" />
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
role="button"
|
|
||||||
v-else-if="sliderVolume < 0.5"
|
v-else-if="sliderVolume < 0.5"
|
||||||
|
role="button"
|
||||||
:title="labels.mute"
|
:title="labels.mute"
|
||||||
:aria-label="labels.mute"
|
:aria-label="labels.mute"
|
||||||
@click.prevent.stop="mute">
|
@click.prevent.stop="mute"
|
||||||
<i class="volume down icon"></i>
|
>
|
||||||
|
<i class="volume down icon" />
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
role="button"
|
|
||||||
v-else
|
v-else
|
||||||
|
role="button"
|
||||||
:title="labels.mute"
|
:title="labels.mute"
|
||||||
:aria-label="labels.mute"
|
:aria-label="labels.mute"
|
||||||
@click.prevent.stop="mute">
|
@click.prevent.stop="mute"
|
||||||
<i class="volume up icon"></i>
|
>
|
||||||
|
<i class="volume up icon" />
|
||||||
</span>
|
</span>
|
||||||
<div class="popup">
|
<div class="popup">
|
||||||
<label for="volume-slider" class="visually-hidden">{{ labels.slider }}</label>
|
<label
|
||||||
|
for="volume-slider"
|
||||||
|
class="visually-hidden"
|
||||||
|
>{{ labels.slider }}</label>
|
||||||
<input
|
<input
|
||||||
id="volume-slider"
|
id="volume-slider"
|
||||||
|
v-model="sliderVolume"
|
||||||
type="range"
|
type="range"
|
||||||
step="any"
|
step="any"
|
||||||
min="0"
|
min="0"
|
||||||
v-bind:max="volumeSteps"
|
:max="volumeSteps"
|
||||||
v-model="sliderVolume" />
|
>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapGetters, mapActions } from "vuex"
|
import mapActions from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
expanded: false,
|
expanded: false,
|
||||||
timeout: null,
|
timeout: null,
|
||||||
volumeSteps: 100,
|
volumeSteps: 100
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
sliderVolume: {
|
sliderVolume: {
|
||||||
get () {
|
get () {
|
||||||
return this.$store.state.player.volume * this.volumeSteps;
|
return this.$store.state.player.volume * this.volumeSteps
|
||||||
},
|
},
|
||||||
set (v) {
|
set (v) {
|
||||||
this.$store.commit("player/volume", v / this.volumeSteps)
|
this.$store.commit('player/volume', v / this.volumeSteps)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
labels () {
|
labels () {
|
||||||
return {
|
return {
|
||||||
unmute: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Unmute"),
|
unmute: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Unmute'),
|
||||||
mute: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Mute"),
|
mute: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Mute'),
|
||||||
slider: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Adjust volume")
|
slider: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Adjust volume')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions({
|
...mapActions({
|
||||||
mute: "player/mute",
|
mute: 'player/mute',
|
||||||
unmute: "player/unmute",
|
unmute: 'player/unmute',
|
||||||
toggleMute: "player/toggleMute",
|
toggleMute: 'player/toggleMute'
|
||||||
}),
|
}),
|
||||||
handleOver () {
|
handleOver () {
|
||||||
if (this.timeout) {
|
if (this.timeout) {
|
||||||
|
@ -80,7 +93,7 @@ export default {
|
||||||
if (this.timeout) {
|
if (this.timeout) {
|
||||||
clearTimeout(this.timeout)
|
clearTimeout(this.timeout)
|
||||||
}
|
}
|
||||||
this.timeout = setTimeout(() => {this.expanded = false}, 500)
|
this.timeout = setTimeout(() => { this.expanded = false }, 500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,32 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="card app-card component-album-card">
|
<div class="card app-card component-album-card">
|
||||||
<div
|
<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}})"
|
@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>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<strong>
|
<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 }}
|
{{ album.title }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</strong>
|
</strong>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<span>
|
<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 }}
|
{{ album.artist.name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
|
@ -21,8 +34,21 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="extra content">
|
<div class="extra content">
|
||||||
<span v-if="album.release_date">{{ album.release_date | moment('Y') }} · </span>
|
<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>
|
<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-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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -31,17 +57,18 @@
|
||||||
import PlayButton from '@/components/audio/PlayButton'
|
import PlayButton from '@/components/audio/PlayButton'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
album: {type: Object},
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
PlayButton
|
PlayButton
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
album: { type: Object, required: true }
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
imageUrl () {
|
imageUrl () {
|
||||||
if (this.album.cover && this.album.cover.urls.original) {
|
if (this.album.cover && this.album.cover.urls.original) {
|
||||||
return this.$store.getters['instance/absoluteUrl'](this.album.cover.urls.medium_square_crop)
|
return this.$store.getters['instance/absoluteUrl'](this.album.cover.urls.medium_square_crop)
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,54 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<h3 v-if="!!this.$slots.title" class="ui header">
|
<h3
|
||||||
<slot name="title"></slot>
|
v-if="!!$slots.title"
|
||||||
<span v-if="showCount" class="ui tiny circular label">{{ count }}</span>
|
class="ui header"
|
||||||
|
>
|
||||||
|
<slot name="title" />
|
||||||
|
<span
|
||||||
|
v-if="showCount"
|
||||||
|
class="ui tiny circular label"
|
||||||
|
>{{ count }}</span>
|
||||||
</h3>
|
</h3>
|
||||||
<slot></slot>
|
<slot />
|
||||||
<inline-search-bar v-model="query" v-if="search" @search="albums = []; fetchData()"></inline-search-bar>
|
<inline-search-bar
|
||||||
<div class="ui hidden divider"></div>
|
v-if="search"
|
||||||
|
v-model="query"
|
||||||
|
@search="albums = []; fetchData()"
|
||||||
|
/>
|
||||||
|
<div class="ui hidden divider" />
|
||||||
<div class="ui app-cards cards">
|
<div class="ui app-cards cards">
|
||||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
<div
|
||||||
<div class="ui loader"></div>
|
v-if="isLoading"
|
||||||
|
class="ui inverted active dimmer"
|
||||||
|
>
|
||||||
|
<div class="ui loader" />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<slot v-if="!isLoading && albums.length === 0" name="empty-state">
|
<slot
|
||||||
<empty-state @refresh="fetchData" :refresh="true"></empty-state>
|
v-if="!isLoading && albums.length === 0"
|
||||||
|
name="empty-state"
|
||||||
|
>
|
||||||
|
<empty-state
|
||||||
|
:refresh="true"
|
||||||
|
@refresh="fetchData"
|
||||||
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
<template v-if="nextPage">
|
<template v-if="nextPage">
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<button v-if="nextPage" @click="fetchData(nextPage)" :class="['ui', 'basic', 'button']">
|
<button
|
||||||
<translate translate-context="*/*/Button,Label">Show more</translate>
|
v-if="nextPage"
|
||||||
|
:class="['ui', 'basic', 'button']"
|
||||||
|
@click="fetchData(nextPage)"
|
||||||
|
>
|
||||||
|
<translate translate-context="*/*/Button,Label">
|
||||||
|
Show more
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -30,16 +59,16 @@ import axios from 'axios'
|
||||||
import AlbumCard from '@/components/audio/album/Card'
|
import AlbumCard from '@/components/audio/album/Card'
|
||||||
|
|
||||||
export default {
|
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: {
|
components: {
|
||||||
AlbumCard
|
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 () {
|
data () {
|
||||||
return {
|
return {
|
||||||
albums: [],
|
albums: [],
|
||||||
|
@ -48,7 +77,15 @@ export default {
|
||||||
errors: null,
|
errors: null,
|
||||||
previousPage: null,
|
previousPage: null,
|
||||||
nextPage: null,
|
nextPage: null,
|
||||||
query: '',
|
query: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
offset () {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
'$store.state.moderation.lastUpdate': function () {
|
||||||
|
this.fetchData()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
@ -58,11 +95,11 @@ export default {
|
||||||
fetchData (url) {
|
fetchData (url) {
|
||||||
url = url || 'albums/'
|
url = url || 'albums/'
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let self = this
|
const self = this
|
||||||
let params = {q: this.query, ...this.filters}
|
const params = { q: this.query, ...this.filters }
|
||||||
params.page_size = this.limit
|
params.page_size = this.limit
|
||||||
params.offset = this.offset
|
params.offset = this.offset
|
||||||
axios.get(url, {params: params}).then((response) => {
|
axios.get(url, { params: params }).then((response) => {
|
||||||
self.previousPage = response.data.previous
|
self.previousPage = response.data.previous
|
||||||
self.nextPage = response.data.next
|
self.nextPage = response.data.next
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
|
@ -79,14 +116,6 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
this.offset = Math.max(this.offset - this.limit, 0)
|
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>
|
<template>
|
||||||
<div class="app-card card">
|
<div class="app-card card">
|
||||||
<div
|
<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}})"
|
@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>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<strong>
|
<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) }}
|
{{ artist.name|truncate(30) }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</strong>
|
</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>
|
||||||
<div class="extra content">
|
<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
|
||||||
<translate v-else translate-context="*/*/*" :translate-params="{count: artist.tracks_count}" :translate-n="artist.tracks_count" translate-plural="%{ count } episodes">%{ count } episode</translate>
|
v-if="artist.content_category === 'music'"
|
||||||
<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-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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import PlayButton from '@/components/audio/PlayButton'
|
import PlayButton from '@/components/audio/PlayButton'
|
||||||
import TagsList from "@/components/tags/List"
|
import TagsList from '@/components/tags/List'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['artist'],
|
|
||||||
components: {
|
components: {
|
||||||
PlayButton,
|
PlayButton,
|
||||||
TagsList
|
TagsList
|
||||||
},
|
},
|
||||||
|
props: { artist: { type: Object, required: true } },
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
initialAlbums: 30,
|
initialAlbums: 30,
|
||||||
showAllAlbums: true,
|
showAllAlbums: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
imageUrl () {
|
imageUrl () {
|
||||||
let cover = this.cover
|
const cover = this.cover
|
||||||
if (cover && cover.urls.original) {
|
if (cover && cover.urls.original) {
|
||||||
return this.$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)
|
return this.$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
},
|
},
|
||||||
cover () {
|
cover () {
|
||||||
if (this.artist.cover && this.artist.cover.urls.original) {
|
if (this.artist.cover && this.artist.cover.urls.original) {
|
||||||
|
@ -54,7 +93,7 @@ export default {
|
||||||
}).filter((c) => {
|
}).filter((c) => {
|
||||||
return c && c.urls.original
|
return c && c.urls.original
|
||||||
})[0]
|
})[0]
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,24 +1,50 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<h3 v-if="header" class="ui header">
|
<h3
|
||||||
<slot name="title"></slot>
|
v-if="header"
|
||||||
|
class="ui header"
|
||||||
|
>
|
||||||
|
<slot name="title" />
|
||||||
<span class="ui tiny circular label">{{ count }}</span>
|
<span class="ui tiny circular label">{{ count }}</span>
|
||||||
</h3>
|
</h3>
|
||||||
<inline-search-bar v-model="query" v-if="search" @search="objects = []; fetchData()"></inline-search-bar>
|
<inline-search-bar
|
||||||
<div class="ui hidden divider"></div>
|
v-if="search"
|
||||||
|
v-model="query"
|
||||||
|
@search="objects = []; fetchData()"
|
||||||
|
/>
|
||||||
|
<div class="ui hidden divider" />
|
||||||
<div class="ui five app-cards cards">
|
<div class="ui five app-cards cards">
|
||||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
<div
|
||||||
<div class="ui loader"></div>
|
v-if="isLoading"
|
||||||
|
class="ui inverted active dimmer"
|
||||||
|
>
|
||||||
|
<div class="ui loader" />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<slot v-if="!isLoading && objects.length === 0" name="empty-state">
|
<slot
|
||||||
<empty-state @refresh="fetchData" :refresh="true"></empty-state>
|
v-if="!isLoading && objects.length === 0"
|
||||||
|
name="empty-state"
|
||||||
|
>
|
||||||
|
<empty-state
|
||||||
|
:refresh="true"
|
||||||
|
@refresh="fetchData"
|
||||||
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
<template v-if="nextPage">
|
<template v-if="nextPage">
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<button v-if="nextPage" @click="fetchData(nextPage)" :class="['ui', 'basic', 'button']">
|
<button
|
||||||
<translate translate-context="*/*/Button,Label">Show more</translate>
|
v-if="nextPage"
|
||||||
|
:class="['ui', 'basic', 'button']"
|
||||||
|
@click="fetchData(nextPage)"
|
||||||
|
>
|
||||||
|
<translate translate-context="*/*/Button,Label">
|
||||||
|
Show more
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,17 +52,17 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import ArtistCard from "@/components/audio/artist/Card"
|
import ArtistCard from '@/components/audio/artist/Card'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
filters: {type: Object, required: true},
|
|
||||||
controls: {type: Boolean, default: true},
|
|
||||||
header: {type: Boolean, default: true},
|
|
||||||
search: {type: Boolean, default: false},
|
|
||||||
},
|
|
||||||
components: {
|
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 () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -47,7 +73,15 @@ export default {
|
||||||
errors: null,
|
errors: null,
|
||||||
previousPage: null,
|
previousPage: null,
|
||||||
nextPage: null,
|
nextPage: null,
|
||||||
query: '',
|
query: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
offset () {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
'$store.state.moderation.lastUpdate': function () {
|
||||||
|
this.fetchData()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
@ -57,11 +91,11 @@ export default {
|
||||||
fetchData (url) {
|
fetchData (url) {
|
||||||
url = url || 'artists/'
|
url = url || 'artists/'
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let self = this
|
const self = this
|
||||||
let params = {q: this.query, ...this.filters}
|
const params = { q: this.query, ...this.filters }
|
||||||
params.page_size = this.limit
|
params.page_size = this.limit
|
||||||
params.offset = this.offset
|
params.offset = this.offset
|
||||||
axios.get(url, {params: params}).then((response) => {
|
axios.get(url, { params: params }).then((response) => {
|
||||||
self.previousPage = response.data.previous
|
self.previousPage = response.data.previous
|
||||||
self.nextPage = response.data.next
|
self.nextPage = response.data.next
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
|
@ -78,14 +112,6 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
this.offset = Math.max(this.offset - this.limit, 0)
|
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
|
<div
|
||||||
v-if="showArt"
|
v-if="showArt"
|
||||||
@click.prevent.exact="activateTrack(track, index)"
|
|
||||||
class="image left floated column"
|
class="image left floated column"
|
||||||
|
@click.prevent.exact="activateTrack(track, index)"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt=""
|
|
||||||
class="ui artist-track mini image"
|
|
||||||
v-if="
|
v-if="
|
||||||
track.album && track.album.cover && track.album.cover.urls.original
|
track.album && track.album.cover && track.album.cover.urls.original
|
||||||
"
|
"
|
||||||
|
@ -21,10 +19,10 @@
|
||||||
track.album.cover.urls.medium_square_crop
|
track.album.cover.urls.medium_square_crop
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt=""
|
alt=""
|
||||||
class="ui artist-track mini image"
|
class="ui artist-track mini image"
|
||||||
|
>
|
||||||
|
<img
|
||||||
v-else-if="
|
v-else-if="
|
||||||
track.cover
|
track.cover
|
||||||
"
|
"
|
||||||
|
@ -33,10 +31,10 @@
|
||||||
track.cover.urls.medium_square_crop
|
track.cover.urls.medium_square_crop
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt=""
|
alt=""
|
||||||
class="ui artist-track mini image"
|
class="ui artist-track mini image"
|
||||||
|
>
|
||||||
|
<img
|
||||||
v-else-if="
|
v-else-if="
|
||||||
track.artist.cover
|
track.artist.cover
|
||||||
"
|
"
|
||||||
|
@ -45,19 +43,21 @@
|
||||||
track.artist.cover.urls.medium_square_crop
|
track.artist.cover.urls.medium_square_crop
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt=""
|
alt=""
|
||||||
class="ui artist-track mini image"
|
class="ui artist-track mini image"
|
||||||
|
>
|
||||||
|
<img
|
||||||
v-else
|
v-else
|
||||||
|
alt=""
|
||||||
|
class="ui artist-track mini image"
|
||||||
src="../../../assets/audio/default-cover.png"
|
src="../../../assets/audio/default-cover.png"
|
||||||
/>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
tabindex=0
|
tabindex="0"
|
||||||
@click="activateTrack(track, index)"
|
|
||||||
role="button"
|
role="button"
|
||||||
class="content ellipsis left floated column"
|
class="content ellipsis left floated column"
|
||||||
|
@click="activateTrack(track, index)"
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
:class="[
|
:class="[
|
||||||
|
@ -68,24 +68,33 @@
|
||||||
>
|
>
|
||||||
{{ track.title }}
|
{{ track.title }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="track.artist.content_category === 'podcast'" class="track-meta mobile">
|
<p
|
||||||
<human-date class="really discrete" :date="track.creation_date"></human-date>
|
v-if="track.artist.content_category === 'podcast'"
|
||||||
|
class="track-meta mobile"
|
||||||
|
>
|
||||||
|
<human-date
|
||||||
|
class="really discrete"
|
||||||
|
:date="track.creation_date"
|
||||||
|
/>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<human-duration
|
<human-duration
|
||||||
v-if="track.uploads[0] && track.uploads[0].duration"
|
v-if="track.uploads[0] && track.uploads[0].duration"
|
||||||
:duration="track.uploads[0].duration"
|
:duration="track.uploads[0].duration"
|
||||||
></human-duration>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<p v-else class="track-meta mobile">
|
<p
|
||||||
|
v-else
|
||||||
|
class="track-meta mobile"
|
||||||
|
>
|
||||||
{{ track.artist.name }} <span>·</span>
|
{{ track.artist.name }} <span>·</span>
|
||||||
<human-duration
|
<human-duration
|
||||||
v-if="track.uploads[0] && track.uploads[0].duration"
|
v-if="track.uploads[0] && track.uploads[0].duration"
|
||||||
:duration="track.uploads[0].duration"
|
:duration="track.uploads[0].duration"
|
||||||
></human-duration>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<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="[
|
:class="[
|
||||||
'meta',
|
'meta',
|
||||||
'right',
|
'right',
|
||||||
|
@ -100,12 +109,11 @@
|
||||||
class="tiny"
|
class="tiny"
|
||||||
:border="false"
|
:border="false"
|
||||||
:track="track"
|
:track="track"
|
||||||
></track-favorite-icon>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
:aria-label="actionsButtonLabel"
|
:aria-label="actionsButtonLabel"
|
||||||
@click.prevent.exact="showTrackModal = !showTrackModal"
|
|
||||||
:class="[
|
:class="[
|
||||||
'modal-button',
|
'modal-button',
|
||||||
'right',
|
'right',
|
||||||
|
@ -114,36 +122,36 @@
|
||||||
'mobile',
|
'mobile',
|
||||||
{ 'with-art': showArt },
|
{ 'with-art': showArt },
|
||||||
]"
|
]"
|
||||||
|
@click.prevent.exact="showTrackModal = !showTrackModal"
|
||||||
>
|
>
|
||||||
<i class="ellipsis large vertical icon" />
|
<i class="ellipsis large vertical icon" />
|
||||||
</div>
|
</div>
|
||||||
<track-modal
|
<track-modal
|
||||||
@update:show="showTrackModal = $event;"
|
|
||||||
:show="showTrackModal"
|
:show="showTrackModal"
|
||||||
:track="track"
|
:track="track"
|
||||||
:index="index"
|
:index="index"
|
||||||
:is-artist="isArtist"
|
:is-artist="isArtist"
|
||||||
:is-album="isAlbum"
|
:is-album="isAlbum"
|
||||||
></track-modal>
|
@update:show="showTrackModal = $event;"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import PlayIndicator from "@/components/audio/track/PlayIndicator";
|
import { mapActions, mapGetters } from 'vuex'
|
||||||
import { mapActions, mapGetters } from "vuex";
|
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
||||||
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon";
|
import TrackModal from '@/components/audio/track/Modal'
|
||||||
import TrackModal from "@/components/audio/track/Modal";
|
import PlayOptionsMixin from '@/components/mixins/PlayOptions'
|
||||||
import PlayOptionsMixin from "@/components/mixins/PlayOptions"
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [PlayOptionsMixin],
|
|
||||||
data() {
|
components: {
|
||||||
return {
|
TrackFavoriteIcon,
|
||||||
showTrackModal: false,
|
TrackModal
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
mixins: [PlayOptionsMixin],
|
||||||
props: {
|
props: {
|
||||||
tracks: Array,
|
tracks: { type: Array, required: true },
|
||||||
showAlbum: { type: Boolean, required: false, default: true },
|
showAlbum: { type: Boolean, required: false, default: true },
|
||||||
showArtist: { type: Boolean, required: false, default: true },
|
showArtist: { type: Boolean, required: false, default: true },
|
||||||
showPosition: { type: Boolean, required: false, default: false },
|
showPosition: { type: Boolean, required: false, default: false },
|
||||||
|
@ -155,41 +163,40 @@ export default {
|
||||||
showDuration: { type: Boolean, required: false, default: true },
|
showDuration: { type: Boolean, required: false, default: true },
|
||||||
index: { type: Number, required: true },
|
index: { type: Number, required: true },
|
||||||
track: { type: Object, required: true },
|
track: { type: Object, required: true },
|
||||||
isArtist: {type: Boolean, required: false, default: false},
|
isArtist: { type: Boolean, required: false, default: false },
|
||||||
isAlbum: {type: Boolean, required: false, default: false},
|
isAlbum: { type: Boolean, required: false, default: false }
|
||||||
},
|
},
|
||||||
|
data () {
|
||||||
components: {
|
return {
|
||||||
PlayIndicator,
|
showTrackModal: false
|
||||||
TrackFavoriteIcon,
|
}
|
||||||
TrackModal,
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentTrack: "queue/currentTrack",
|
currentTrack: 'queue/currentTrack'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
isPlaying() {
|
isPlaying () {
|
||||||
return this.$store.state.player.playing;
|
return this.$store.state.player.playing
|
||||||
},
|
},
|
||||||
actionsButtonLabel () {
|
actionsButtonLabel () {
|
||||||
return this.$pgettext('Content/Track/Icon.Tooltip/Verb', 'Show track actions')
|
return this.$pgettext('Content/Track/Icon.Tooltip/Verb', 'Show track actions')
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
prettyPosition(position, size) {
|
prettyPosition (position, size) {
|
||||||
var s = String(position);
|
let s = String(position)
|
||||||
while (s.length < (size || 2)) {
|
while (s.length < (size || 2)) {
|
||||||
s = "0" + s;
|
s = '0' + s
|
||||||
}
|
}
|
||||||
return s;
|
return s
|
||||||
},
|
},
|
||||||
|
|
||||||
...mapActions({
|
...mapActions({
|
||||||
resumePlayback: "player/resumePlayback",
|
resumePlayback: 'player/resumePlayback',
|
||||||
pausePlayback: "player/pausePlayback",
|
pausePlayback: 'player/pausePlayback'
|
||||||
}),
|
})
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -15,8 +15,6 @@
|
||||||
@click.prevent.exact="activateTrack(track, index)"
|
@click.prevent.exact="activateTrack(track, index)"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt=""
|
|
||||||
class="ui artist-track mini image"
|
|
||||||
v-if="
|
v-if="
|
||||||
track.cover && track.cover.urls.original
|
track.cover && track.cover.urls.original
|
||||||
"
|
"
|
||||||
|
@ -25,10 +23,10 @@
|
||||||
track.cover.urls.medium_square_crop
|
track.cover.urls.medium_square_crop
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt=""
|
alt=""
|
||||||
class="ui artist-track mini image"
|
class="ui artist-track mini image"
|
||||||
|
>
|
||||||
|
<img
|
||||||
v-else-if="
|
v-else-if="
|
||||||
defaultCover
|
defaultCover
|
||||||
"
|
"
|
||||||
|
@ -37,21 +35,32 @@
|
||||||
defaultCover.cover.urls.medium_square_crop
|
defaultCover.cover.urls.medium_square_crop
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt=""
|
alt=""
|
||||||
class="ui artist-track mini image"
|
class="ui artist-track mini image"
|
||||||
|
>
|
||||||
|
<img
|
||||||
v-else
|
v-else
|
||||||
|
alt=""
|
||||||
|
class="ui artist-track mini image"
|
||||||
src="../../../assets/audio/default-cover.png"
|
src="../../../assets/audio/default-cover.png"
|
||||||
/>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div tabindex=0 class="content left floated column">
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
class="content left floated column"
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
class="podcast-episode-title ellipsis"
|
class="podcast-episode-title ellipsis"
|
||||||
@click.prevent.exact="activateTrack(track, index)">{{ track.title }}</a>
|
@click.prevent.exact="activateTrack(track, index)"
|
||||||
<p class="podcast-episode-meta">{{ description.text }}</p>
|
>{{ track.title }}</a>
|
||||||
|
<p class="podcast-episode-meta">
|
||||||
|
{{ description.text }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="displayActions" class="meta right floated column">
|
<div
|
||||||
|
v-if="displayActions"
|
||||||
|
class="meta right floated column"
|
||||||
|
>
|
||||||
<play-button
|
<play-button
|
||||||
id="playmenu"
|
id="playmenu"
|
||||||
class="play-button basic icon"
|
class="play-button basic icon"
|
||||||
|
@ -63,22 +72,25 @@
|
||||||
'large really discrete',
|
'large really discrete',
|
||||||
]"
|
]"
|
||||||
:track="track"
|
:track="track"
|
||||||
></play-button>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import PlayIndicator from "@/components/audio/track/PlayIndicator";
|
import { mapActions, mapGetters } from 'vuex'
|
||||||
import { mapActions, mapGetters } from "vuex";
|
import PlayButton from '@/components/audio/PlayButton'
|
||||||
import PlayButton from "@/components/audio/PlayButton";
|
import PlayOptions from '@/components/mixins/PlayOptions'
|
||||||
import PlayOptions from "@/components/mixins/PlayOptions";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
|
components: {
|
||||||
|
PlayButton
|
||||||
|
},
|
||||||
mixins: [PlayOptions],
|
mixins: [PlayOptions],
|
||||||
props: {
|
props: {
|
||||||
tracks: Array,
|
tracks: { type: Array, required: true },
|
||||||
showAlbum: { type: Boolean, required: false, default: true },
|
showAlbum: { type: Boolean, required: false, default: true },
|
||||||
showArtist: { type: Boolean, required: false, default: true },
|
showArtist: { type: Boolean, required: false, default: true },
|
||||||
showPosition: { type: Boolean, required: false, default: false },
|
showPosition: { type: Boolean, required: false, default: false },
|
||||||
|
@ -90,34 +102,29 @@ export default {
|
||||||
showDuration: { type: Boolean, required: false, default: true },
|
showDuration: { type: Boolean, required: false, default: true },
|
||||||
index: { type: Number, required: true },
|
index: { type: Number, required: true },
|
||||||
track: { type: Object, required: true },
|
track: { type: Object, required: true },
|
||||||
defaultCover: { type: Object, required: false },
|
defaultCover: { type: Object, required: false, default: () => { return {} } }
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data () {
|
||||||
return {
|
return {
|
||||||
hover: null,
|
hover: null,
|
||||||
errors: null,
|
errors: null,
|
||||||
description: null,
|
description: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
created () {
|
|
||||||
this.fetchData('tracks/' + this.track.id + '/' )
|
|
||||||
},
|
|
||||||
|
|
||||||
components: {
|
|
||||||
PlayIndicator,
|
|
||||||
PlayButton,
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentTrack: "queue/currentTrack",
|
currentTrack: 'queue/currentTrack'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
isPlaying() {
|
isPlaying () {
|
||||||
return this.$store.state.player.playing;
|
return this.$store.state.player.playing
|
||||||
},
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created () {
|
||||||
|
this.fetchData('tracks/' + this.track.id + '/')
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -126,29 +133,29 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let self = this
|
const self = this
|
||||||
try {
|
try {
|
||||||
let channelsPromise = await axios.get(url)
|
const channelsPromise = await axios.get(url)
|
||||||
self.description = channelsPromise.data.description
|
self.description = channelsPromise.data.description
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
self.errors = error.backendErrors
|
self.errors = e.backendErrors
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
prettyPosition(position, size) {
|
prettyPosition (position, size) {
|
||||||
var s = String(position);
|
let s = String(position)
|
||||||
while (s.length < (size || 2)) {
|
while (s.length < (size || 2)) {
|
||||||
s = "0" + s;
|
s = '0' + s
|
||||||
}
|
}
|
||||||
return s;
|
return s
|
||||||
},
|
},
|
||||||
|
|
||||||
...mapActions({
|
...mapActions({
|
||||||
resumePlayback: "player/resumePlayback",
|
resumePlayback: 'player/resumePlayback',
|
||||||
pausePlayback: "player/pausePlayback",
|
pausePlayback: 'player/pausePlayback'
|
||||||
}),
|
})
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
|
|
||||||
<!-- Add a header if needed -->
|
<!-- Add a header if needed -->
|
||||||
|
|
||||||
<slot name="header"></slot>
|
<slot name="header" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
|
@ -13,38 +13,44 @@
|
||||||
<!-- For each item, build a row -->
|
<!-- For each item, build a row -->
|
||||||
<podcast-row
|
<podcast-row
|
||||||
v-for="(track, index) in tracks"
|
v-for="(track, index) in tracks"
|
||||||
:track="track"
|
|
||||||
:key="track.id"
|
:key="track.id"
|
||||||
|
:track="track"
|
||||||
:index="index"
|
:index="index"
|
||||||
:tracks="tracks"
|
:tracks="tracks"
|
||||||
:display-actions="displayActions"
|
:display-actions="displayActions"
|
||||||
:show-duration="showDuration"
|
:show-duration="showDuration"
|
||||||
:is-podcast="isPodcast"
|
:is-podcast="isPodcast"
|
||||||
></podcast-row>
|
/>
|
||||||
</div>
|
</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
|
<pagination
|
||||||
:total="total"
|
:total="total"
|
||||||
:current="page"
|
:current="page"
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
v-on="$listeners">
|
v-on="$listeners"
|
||||||
</pagination>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-below']"
|
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-below']"
|
||||||
>
|
>
|
||||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
<div
|
||||||
<div class="ui loader"></div>
|
v-if="isLoading"
|
||||||
|
class="ui inverted active dimmer"
|
||||||
|
>
|
||||||
|
<div class="ui loader" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- For each item, build a row -->
|
<!-- For each item, build a row -->
|
||||||
|
|
||||||
<track-mobile-row
|
<track-mobile-row
|
||||||
v-for="(track, index) in tracks"
|
v-for="(track, index) in tracks"
|
||||||
:track="track"
|
|
||||||
:key="track.id"
|
:key="track.id"
|
||||||
|
:track="track"
|
||||||
:index="index"
|
:index="index"
|
||||||
:tracks="tracks"
|
:tracks="tracks"
|
||||||
:show-position="showPosition"
|
:show-position="showPosition"
|
||||||
|
@ -53,36 +59,37 @@
|
||||||
:is-artist="isArtist"
|
:is-artist="isArtist"
|
||||||
:is-album="isAlbum"
|
:is-album="isAlbum"
|
||||||
:is-podcast="isPodcast"
|
: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
|
<pagination
|
||||||
v-if="paginateResults"
|
v-if="paginateResults"
|
||||||
:total="total"
|
:total="total"
|
||||||
:current="page"
|
:current="page"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
v-on="$listeners"></pagination>
|
v-on="$listeners"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _ from "@/lodash";
|
import PodcastRow from '@/components/audio/podcast/Row'
|
||||||
import TrackRow from "@/components/audio/track/Row";
|
import TrackMobileRow from '@/components/audio/track/MobileRow'
|
||||||
import PodcastRow from "@/components/audio/podcast/Row";
|
import Pagination from '@/components/Pagination'
|
||||||
import TrackMobileRow from "@/components/audio/track/MobileRow";
|
|
||||||
import Pagination from "@/components/Pagination";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
TrackRow,
|
|
||||||
TrackMobileRow,
|
TrackMobileRow,
|
||||||
Pagination,
|
Pagination,
|
||||||
PodcastRow,
|
PodcastRow
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
tracks: Array,
|
tracks: { type: Array, required: true },
|
||||||
showAlbum: { type: Boolean, required: false, default: true },
|
showAlbum: { type: Boolean, required: false, default: true },
|
||||||
showArtist: { type: Boolean, required: false, default: true },
|
showArtist: { type: Boolean, required: false, default: true },
|
||||||
showPosition: { type: Boolean, required: false, default: false },
|
showPosition: { type: Boolean, required: false, default: false },
|
||||||
|
@ -94,33 +101,33 @@ export default {
|
||||||
showDuration: { type: Boolean, required: false, default: true },
|
showDuration: { type: Boolean, required: false, default: true },
|
||||||
isArtist: { type: Boolean, required: false, default: false },
|
isArtist: { type: Boolean, required: false, default: false },
|
||||||
isAlbum: { type: Boolean, required: false, default: false },
|
isAlbum: { type: Boolean, required: false, default: false },
|
||||||
paginateResults: { type: Boolean, required: false, default: true},
|
paginateResults: { type: Boolean, required: false, default: true },
|
||||||
total: { type: Number, required: false},
|
total: { type: Number, required: false, default: 0 },
|
||||||
page: {type: Number, required: false, default: 1},
|
page: { type: Number, required: false, default: 1 },
|
||||||
paginateBy: {type: Number, required: false, default: 25},
|
paginateBy: { type: Number, required: false, default: 25 },
|
||||||
isPodcast: {type: Boolean, required: true},
|
isPodcast: { type: Boolean, required: true },
|
||||||
defaultCover: {type: Object, required: false},
|
defaultCover: { type: Object, required: false, default: () => { return {} } }
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data () {
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
labels() {
|
labels () {
|
||||||
return {
|
return {
|
||||||
title: this.$pgettext("*/*/*/Noun", "Title"),
|
title: this.$pgettext('*/*/*/Noun', 'Title'),
|
||||||
album: this.$pgettext("*/*/*/Noun", "Album"),
|
album: this.$pgettext('*/*/*/Noun', 'Album'),
|
||||||
artist: this.$pgettext("*/*/*/Noun", "Artist"),
|
artist: this.$pgettext('*/*/*/Noun', 'Artist')
|
||||||
};
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updatePage: function(page) {
|
|
||||||
this.$emit('page-changed', page)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
methods: {
|
||||||
|
updatePage: function (page) {
|
||||||
|
this.$emit('page-changed', page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -7,12 +7,10 @@
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="showArt"
|
v-if="showArt"
|
||||||
@click.prevent.exact="activateTrack(track, index)"
|
|
||||||
class="image left floated column"
|
class="image left floated column"
|
||||||
|
@click.prevent.exact="activateTrack(track, index)"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt=""
|
|
||||||
class="ui artist-track mini image"
|
|
||||||
v-if="
|
v-if="
|
||||||
track.album && track.album.cover && track.album.cover.urls.original
|
track.album && track.album.cover && track.album.cover.urls.original
|
||||||
"
|
"
|
||||||
|
@ -21,10 +19,10 @@
|
||||||
track.album.cover.urls.medium_square_crop
|
track.album.cover.urls.medium_square_crop
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt=""
|
alt=""
|
||||||
class="ui artist-track mini image"
|
class="ui artist-track mini image"
|
||||||
|
>
|
||||||
|
<img
|
||||||
v-else-if="
|
v-else-if="
|
||||||
track.cover
|
track.cover
|
||||||
"
|
"
|
||||||
|
@ -33,10 +31,10 @@
|
||||||
track.cover.urls.medium_square_crop
|
track.cover.urls.medium_square_crop
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt=""
|
alt=""
|
||||||
class="ui artist-track mini image"
|
class="ui artist-track mini image"
|
||||||
|
>
|
||||||
|
<img
|
||||||
v-else-if="
|
v-else-if="
|
||||||
track.artist.cover
|
track.artist.cover
|
||||||
"
|
"
|
||||||
|
@ -45,19 +43,21 @@
|
||||||
track.artist.cover.urls.medium_square_crop
|
track.artist.cover.urls.medium_square_crop
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt=""
|
alt=""
|
||||||
class="ui artist-track mini image"
|
class="ui artist-track mini image"
|
||||||
|
>
|
||||||
|
<img
|
||||||
v-else
|
v-else
|
||||||
|
alt=""
|
||||||
|
class="ui artist-track mini image"
|
||||||
src="../../../assets/audio/default-cover.png"
|
src="../../../assets/audio/default-cover.png"
|
||||||
/>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
tabindex=0
|
tabindex="0"
|
||||||
@click="activateTrack(track, index)"
|
|
||||||
role="button"
|
role="button"
|
||||||
class="content ellipsis left floated column"
|
class="content ellipsis left floated column"
|
||||||
|
@click="activateTrack(track, index)"
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
:class="[
|
:class="[
|
||||||
|
@ -73,7 +73,7 @@
|
||||||
<human-duration
|
<human-duration
|
||||||
v-if="track.uploads[0] && track.uploads[0].duration"
|
v-if="track.uploads[0] && track.uploads[0].duration"
|
||||||
:duration="track.uploads[0].duration"
|
:duration="track.uploads[0].duration"
|
||||||
></human-duration>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -92,12 +92,11 @@
|
||||||
class="tiny"
|
class="tiny"
|
||||||
:border="false"
|
:border="false"
|
||||||
:track="track"
|
:track="track"
|
||||||
></track-favorite-icon>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
:aria-label="actionsButtonLabel"
|
:aria-label="actionsButtonLabel"
|
||||||
@click.prevent.exact="showTrackModal = !showTrackModal"
|
|
||||||
:class="[
|
:class="[
|
||||||
'modal-button',
|
'modal-button',
|
||||||
'right',
|
'right',
|
||||||
|
@ -106,36 +105,36 @@
|
||||||
'mobile',
|
'mobile',
|
||||||
{ 'with-art': showArt },
|
{ 'with-art': showArt },
|
||||||
]"
|
]"
|
||||||
|
@click.prevent.exact="showTrackModal = !showTrackModal"
|
||||||
>
|
>
|
||||||
<i class="ellipsis large vertical icon" />
|
<i class="ellipsis large vertical icon" />
|
||||||
</div>
|
</div>
|
||||||
<track-modal
|
<track-modal
|
||||||
@update:show="showTrackModal = $event;"
|
|
||||||
:show="showTrackModal"
|
:show="showTrackModal"
|
||||||
:track="track"
|
:track="track"
|
||||||
:index="index"
|
:index="index"
|
||||||
:is-artist="isArtist"
|
:is-artist="isArtist"
|
||||||
:is-album="isAlbum"
|
:is-album="isAlbum"
|
||||||
></track-modal>
|
@update:show="showTrackModal = $event;"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import PlayIndicator from "@/components/audio/track/PlayIndicator";
|
import { mapActions, mapGetters } from 'vuex'
|
||||||
import { mapActions, mapGetters } from "vuex";
|
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
||||||
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon";
|
import TrackModal from '@/components/audio/track/Modal'
|
||||||
import TrackModal from "@/components/audio/track/Modal";
|
import PlayOptionsMixin from '@/components/mixins/PlayOptions'
|
||||||
import PlayOptionsMixin from "@/components/mixins/PlayOptions"
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [PlayOptionsMixin],
|
|
||||||
data() {
|
components: {
|
||||||
return {
|
TrackFavoriteIcon,
|
||||||
showTrackModal: false,
|
TrackModal
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
mixins: [PlayOptionsMixin],
|
||||||
props: {
|
props: {
|
||||||
tracks: Array,
|
tracks: { type: Array, required: true },
|
||||||
showAlbum: { type: Boolean, required: false, default: true },
|
showAlbum: { type: Boolean, required: false, default: true },
|
||||||
showArtist: { type: Boolean, required: false, default: true },
|
showArtist: { type: Boolean, required: false, default: true },
|
||||||
showPosition: { type: Boolean, required: false, default: false },
|
showPosition: { type: Boolean, required: false, default: false },
|
||||||
|
@ -147,41 +146,40 @@ export default {
|
||||||
showDuration: { type: Boolean, required: false, default: true },
|
showDuration: { type: Boolean, required: false, default: true },
|
||||||
index: { type: Number, required: true },
|
index: { type: Number, required: true },
|
||||||
track: { type: Object, required: true },
|
track: { type: Object, required: true },
|
||||||
isArtist: {type: Boolean, required: false, default: false},
|
isArtist: { type: Boolean, required: false, default: false },
|
||||||
isAlbum: {type: Boolean, required: false, default: false},
|
isAlbum: { type: Boolean, required: false, default: false }
|
||||||
},
|
},
|
||||||
|
data () {
|
||||||
components: {
|
return {
|
||||||
PlayIndicator,
|
showTrackModal: false
|
||||||
TrackFavoriteIcon,
|
}
|
||||||
TrackModal,
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentTrack: "queue/currentTrack",
|
currentTrack: 'queue/currentTrack'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
isPlaying() {
|
isPlaying () {
|
||||||
return this.$store.state.player.playing;
|
return this.$store.state.player.playing
|
||||||
},
|
},
|
||||||
actionsButtonLabel () {
|
actionsButtonLabel () {
|
||||||
return this.$pgettext('Content/Track/Icon.Tooltip/Verb', 'Show track actions')
|
return this.$pgettext('Content/Track/Icon.Tooltip/Verb', 'Show track actions')
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
prettyPosition(position, size) {
|
prettyPosition (position, size) {
|
||||||
var s = String(position);
|
let s = String(position)
|
||||||
while (s.length < (size || 2)) {
|
while (s.length < (size || 2)) {
|
||||||
s = "0" + s;
|
s = '0' + s
|
||||||
}
|
}
|
||||||
return s;
|
return s
|
||||||
},
|
},
|
||||||
|
|
||||||
...mapActions({
|
...mapActions({
|
||||||
resumePlayback: "player/resumePlayback",
|
resumePlayback: 'player/resumePlayback',
|
||||||
pausePlayback: "player/pausePlayback",
|
pausePlayback: 'player/pausePlayback'
|
||||||
}),
|
})
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="audio-bars">
|
<div id="audio-bars">
|
||||||
<div class="audio-bar"></div>
|
<div class="audio-bar" />
|
||||||
<div class="audio-bar"></div>
|
<div class="audio-bar" />
|
||||||
<div class="audio-bar"></div>
|
<div class="audio-bar" />
|
||||||
<div class="audio-bar"></div>
|
<div class="audio-bar" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
|
@ -16,19 +16,18 @@
|
||||||
<play-indicator
|
<play-indicator
|
||||||
v-if="
|
v-if="
|
||||||
!$store.state.player.isLoadingAudio &&
|
!$store.state.player.isLoadingAudio &&
|
||||||
currentTrack &&
|
currentTrack &&
|
||||||
isPlaying &&
|
isPlaying &&
|
||||||
track.id === currentTrack.id &&
|
track.id === currentTrack.id &&
|
||||||
!(track.id == hover)
|
!(track.id == hover)
|
||||||
"
|
"
|
||||||
>
|
/>
|
||||||
</play-indicator>
|
|
||||||
<button
|
<button
|
||||||
v-else-if="
|
v-else-if="
|
||||||
currentTrack &&
|
currentTrack &&
|
||||||
!isPlaying &&
|
!isPlaying &&
|
||||||
track.id === currentTrack.id &&
|
track.id === currentTrack.id &&
|
||||||
!track.id == hover
|
!track.id == hover
|
||||||
"
|
"
|
||||||
class="ui really tiny basic icon button play-button paused"
|
class="ui really tiny basic icon button play-button paused"
|
||||||
>
|
>
|
||||||
|
@ -37,9 +36,9 @@
|
||||||
<button
|
<button
|
||||||
v-else-if="
|
v-else-if="
|
||||||
currentTrack &&
|
currentTrack &&
|
||||||
isPlaying &&
|
isPlaying &&
|
||||||
track.id === currentTrack.id &&
|
track.id === currentTrack.id &&
|
||||||
track.id == hover
|
track.id == hover
|
||||||
"
|
"
|
||||||
class="ui really tiny basic icon button play-button"
|
class="ui really tiny basic icon button play-button"
|
||||||
>
|
>
|
||||||
|
@ -51,7 +50,10 @@
|
||||||
>
|
>
|
||||||
<i class="play icon" />
|
<i class="play icon" />
|
||||||
</button>
|
</button>
|
||||||
<span class="track-position" v-else-if="showPosition">
|
<span
|
||||||
|
v-else-if="showPosition"
|
||||||
|
class="track-position"
|
||||||
|
>
|
||||||
{{ prettyPosition(track.position) }}
|
{{ prettyPosition(track.position) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,8 +64,6 @@
|
||||||
@click.prevent.exact="activateTrack(track, index)"
|
@click.prevent.exact="activateTrack(track, index)"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt=""
|
|
||||||
class="ui artist-track mini image"
|
|
||||||
v-if="
|
v-if="
|
||||||
track.album && track.album.cover && track.album.cover.urls.original
|
track.album && track.album.cover && track.album.cover.urls.original
|
||||||
"
|
"
|
||||||
|
@ -72,10 +72,10 @@
|
||||||
track.album.cover.urls.medium_square_crop
|
track.album.cover.urls.medium_square_crop
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt=""
|
alt=""
|
||||||
class="ui artist-track mini image"
|
class="ui artist-track mini image"
|
||||||
|
>
|
||||||
|
<img
|
||||||
v-else-if="
|
v-else-if="
|
||||||
track.cover && track.cover.urls.original
|
track.cover && track.cover.urls.original
|
||||||
"
|
"
|
||||||
|
@ -84,10 +84,10 @@
|
||||||
track.cover.urls.medium_square_crop
|
track.cover.urls.medium_square_crop
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt=""
|
alt=""
|
||||||
class="ui artist-track mini image"
|
class="ui artist-track mini image"
|
||||||
|
>
|
||||||
|
<img
|
||||||
v-else-if="
|
v-else-if="
|
||||||
track.artist && track.artist.cover && track.album.cover.urls.original
|
track.artist && track.artist.cover && track.album.cover.urls.original
|
||||||
"
|
"
|
||||||
|
@ -96,36 +96,49 @@
|
||||||
track.cover.urls.medium_square_crop
|
track.cover.urls.medium_square_crop
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt=""
|
alt=""
|
||||||
class="ui artist-track mini image"
|
class="ui artist-track mini image"
|
||||||
|
>
|
||||||
|
<img
|
||||||
v-else
|
v-else
|
||||||
|
alt=""
|
||||||
|
class="ui artist-track mini image"
|
||||||
src="../../../assets/audio/default-cover.png"
|
src="../../../assets/audio/default-cover.png"
|
||||||
/>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div tabindex=0 class="content ellipsis left floated column">
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
class="content ellipsis left floated column"
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
@click="activateTrack(track, index)"
|
@click="activateTrack(track, index)"
|
||||||
>
|
>
|
||||||
{{ track.title }}
|
{{ track.title }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showAlbum" class="content ellipsis left floated column">
|
<div
|
||||||
|
v-if="showAlbum"
|
||||||
|
class="content ellipsis left floated column"
|
||||||
|
>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'library.albums.detail', params: { id: track.album.id } }"
|
:to="{ name: 'library.albums.detail', params: { id: track.album.id } }"
|
||||||
>{{ track.album.title }}</router-link
|
|
||||||
>
|
>
|
||||||
|
{{ track.album.title }}
|
||||||
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showArtist" class="content ellipsis left floated column">
|
<div
|
||||||
|
v-if="showArtist"
|
||||||
|
class="content ellipsis left floated column"
|
||||||
|
>
|
||||||
<router-link
|
<router-link
|
||||||
class="artist link"
|
class="artist link"
|
||||||
:to="{
|
:to="{
|
||||||
name: 'library.artists.detail',
|
name: 'library.artists.detail',
|
||||||
params: { id: track.artist.id },
|
params: { id: track.artist.id },
|
||||||
}"
|
}"
|
||||||
>{{ track.artist.name }}</router-link
|
|
||||||
>
|
>
|
||||||
|
{{ track.artist.name }}
|
||||||
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="$store.state.auth.authenticated"
|
v-if="$store.state.auth.authenticated"
|
||||||
|
@ -135,15 +148,21 @@
|
||||||
class="tiny"
|
class="tiny"
|
||||||
:border="false"
|
:border="false"
|
||||||
:track="track"
|
:track="track"
|
||||||
></track-favorite-icon>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showDuration" class="meta right floated column">
|
<div
|
||||||
|
v-if="showDuration"
|
||||||
|
class="meta right floated column"
|
||||||
|
>
|
||||||
<human-duration
|
<human-duration
|
||||||
v-if="track.uploads[0] && track.uploads[0].duration"
|
v-if="track.uploads[0] && track.uploads[0].duration"
|
||||||
:duration="track.uploads[0].duration"
|
:duration="track.uploads[0].duration"
|
||||||
></human-duration>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="displayActions" class="meta right floated column">
|
<div
|
||||||
|
v-if="displayActions"
|
||||||
|
class="meta right floated column"
|
||||||
|
>
|
||||||
<play-button
|
<play-button
|
||||||
id="playmenu"
|
id="playmenu"
|
||||||
class="play-button basic icon"
|
class="play-button basic icon"
|
||||||
|
@ -155,22 +174,28 @@
|
||||||
'large really discrete',
|
'large really discrete',
|
||||||
]"
|
]"
|
||||||
:track="track"
|
:track="track"
|
||||||
></play-button>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import PlayIndicator from "@/components/audio/track/PlayIndicator";
|
import PlayIndicator from '@/components/audio/track/PlayIndicator'
|
||||||
import { mapActions, mapGetters } from "vuex";
|
import { mapActions, mapGetters } from 'vuex'
|
||||||
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon";
|
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
||||||
import PlayButton from "@/components/audio/PlayButton";
|
import PlayButton from '@/components/audio/PlayButton'
|
||||||
import PlayOptions from "@/components/mixins/PlayOptions";
|
import PlayOptions from '@/components/mixins/PlayOptions'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
|
components: {
|
||||||
|
PlayIndicator,
|
||||||
|
TrackFavoriteIcon,
|
||||||
|
PlayButton
|
||||||
|
},
|
||||||
mixins: [PlayOptions],
|
mixins: [PlayOptions],
|
||||||
props: {
|
props: {
|
||||||
tracks: Array,
|
tracks: { type: Array, required: true },
|
||||||
showAlbum: { type: Boolean, required: false, default: true },
|
showAlbum: { type: Boolean, required: false, default: true },
|
||||||
showArtist: { type: Boolean, required: false, default: true },
|
showArtist: { type: Boolean, required: false, default: true },
|
||||||
showPosition: { type: Boolean, required: false, default: false },
|
showPosition: { type: Boolean, required: false, default: false },
|
||||||
|
@ -181,45 +206,39 @@ export default {
|
||||||
displayActions: { type: Boolean, required: false, default: true },
|
displayActions: { type: Boolean, required: false, default: true },
|
||||||
showDuration: { type: Boolean, required: false, default: true },
|
showDuration: { type: Boolean, required: false, default: true },
|
||||||
index: { type: Number, required: true },
|
index: { type: Number, required: true },
|
||||||
track: { type: Object, required: true },
|
track: { type: Object, required: true }
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data () {
|
||||||
return {
|
return {
|
||||||
hover: null,
|
hover: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
components: {
|
|
||||||
PlayIndicator,
|
|
||||||
TrackFavoriteIcon,
|
|
||||||
PlayButton,
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentTrack: "queue/currentTrack",
|
currentTrack: 'queue/currentTrack'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
isPlaying() {
|
isPlaying () {
|
||||||
return this.$store.state.player.playing;
|
return this.$store.state.player.playing
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
prettyPosition(position, size) {
|
prettyPosition (position, size) {
|
||||||
var s = String(position);
|
let s = String(position)
|
||||||
while (s.length < (size || 2)) {
|
while (s.length < (size || 2)) {
|
||||||
s = "0" + s;
|
s = '0' + s
|
||||||
}
|
}
|
||||||
return s;
|
return s
|
||||||
},
|
},
|
||||||
|
|
||||||
...mapActions({
|
...mapActions({
|
||||||
resumePlayback: "player/resumePlayback",
|
resumePlayback: 'player/resumePlayback',
|
||||||
pausePlayback: "player/pausePlayback",
|
pausePlayback: 'player/pausePlayback'
|
||||||
}),
|
})
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -2,65 +2,95 @@
|
||||||
<div>
|
<div>
|
||||||
<!-- Show the search bar if search is true -->
|
<!-- Show the search bar if search is true -->
|
||||||
<inline-search-bar
|
<inline-search-bar
|
||||||
v-model="query"
|
|
||||||
v-if="search"
|
v-if="search"
|
||||||
|
v-model="query"
|
||||||
@search="
|
@search="
|
||||||
additionalTracks = [];
|
additionalTracks = [];
|
||||||
fetchData();
|
fetchData();
|
||||||
"
|
"
|
||||||
></inline-search-bar>
|
/>
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
|
|
||||||
<!-- Add a header if needed -->
|
<!-- Add a header if needed -->
|
||||||
|
|
||||||
<slot name="header"></slot>
|
<slot name="header" />
|
||||||
|
|
||||||
<!-- Show a message if no tracks are available -->
|
<!-- 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
|
<empty-state
|
||||||
@refresh="fetchData('tracks/')"
|
|
||||||
:refresh="true"
|
:refresh="true"
|
||||||
></empty-state>
|
@refresh="fetchData('tracks/')"
|
||||||
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div
|
<div
|
||||||
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-up']"
|
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-up']"
|
||||||
>
|
>
|
||||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
<div
|
||||||
<div class="ui loader"></div>
|
v-if="isLoading"
|
||||||
|
class="ui inverted active dimmer"
|
||||||
|
>
|
||||||
|
<div class="ui loader" />
|
||||||
</div>
|
</div>
|
||||||
<div class="track-table row">
|
<div class="track-table row">
|
||||||
<div v-if="showPosition" class="actions left floated column">
|
<div
|
||||||
<i class="hashtag icon"></i>
|
v-if="showPosition"
|
||||||
|
class="actions left floated column"
|
||||||
|
>
|
||||||
|
<i class="hashtag icon" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="actions left floated column"></div>
|
<div
|
||||||
<div v-if="showArt" class="image left floated column"></div>
|
v-else
|
||||||
|
class="actions left floated column"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="showArt"
|
||||||
|
class="image left floated column"
|
||||||
|
/>
|
||||||
<div class="content ellipsis left floated column">
|
<div class="content ellipsis left floated column">
|
||||||
<b>{{ labels.title }}</b>
|
<b>{{ labels.title }}</b>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showAlbum" class="content ellipsisleft floated column">
|
<div
|
||||||
|
v-if="showAlbum"
|
||||||
|
class="content ellipsisleft floated column"
|
||||||
|
>
|
||||||
<b>{{ labels.album }}</b>
|
<b>{{ labels.album }}</b>
|
||||||
</div>
|
</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>
|
<b>{{ labels.artist }}</b>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="$store.state.auth.authenticated"
|
v-if="$store.state.auth.authenticated"
|
||||||
class="meta right floated column"
|
class="meta right floated column"
|
||||||
></div>
|
/>
|
||||||
<div v-if="showDuration" class="meta right floated column">
|
<div
|
||||||
<i class="clock outline icon" style="padding: 0.5rem" />
|
v-if="showDuration"
|
||||||
|
class="meta right floated column"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="clock outline icon"
|
||||||
|
style="padding: 0.5rem"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="displayActions" class="meta right floated column"></div>
|
<div
|
||||||
|
v-if="displayActions"
|
||||||
|
class="meta right floated column"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- For each item, build a row -->
|
<!-- For each item, build a row -->
|
||||||
|
|
||||||
<track-row
|
<track-row
|
||||||
v-for="(track, index) in allTracks"
|
v-for="(track, index) in allTracks"
|
||||||
:track="track"
|
|
||||||
:key="track.id"
|
:key="track.id"
|
||||||
|
:track="track"
|
||||||
:index="index"
|
:index="index"
|
||||||
:tracks="allTracks"
|
:tracks="allTracks"
|
||||||
:show-album="showAlbum"
|
:show-album="showAlbum"
|
||||||
|
@ -70,31 +100,37 @@
|
||||||
:display-actions="displayActions"
|
:display-actions="displayActions"
|
||||||
:show-duration="showDuration"
|
:show-duration="showDuration"
|
||||||
:is-podcast="isPodcast"
|
:is-podcast="isPodcast"
|
||||||
></track-row>
|
/>
|
||||||
</div>
|
</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
|
<pagination
|
||||||
:total="total"
|
:total="total"
|
||||||
:current="page"
|
:current="page"
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
v-on="$listeners">
|
v-on="$listeners"
|
||||||
</pagination>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-below']"
|
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-below']"
|
||||||
>
|
>
|
||||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
<div
|
||||||
<div class="ui loader"></div>
|
v-if="isLoading"
|
||||||
|
class="ui inverted active dimmer"
|
||||||
|
>
|
||||||
|
<div class="ui loader" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- For each item, build a row -->
|
<!-- For each item, build a row -->
|
||||||
|
|
||||||
<track-mobile-row
|
<track-mobile-row
|
||||||
v-for="(track, index) in allTracks"
|
v-for="(track, index) in allTracks"
|
||||||
:track="track"
|
|
||||||
:key="track.id"
|
:key="track.id"
|
||||||
|
:track="track"
|
||||||
:index="index"
|
:index="index"
|
||||||
:tracks="allTracks"
|
:tracks="allTracks"
|
||||||
:show-position="showPosition"
|
:show-position="showPosition"
|
||||||
|
@ -103,35 +139,39 @@
|
||||||
:is-artist="isArtist"
|
:is-artist="isArtist"
|
||||||
:is-album="isAlbum"
|
:is-album="isAlbum"
|
||||||
:is-podcast="isPodcast"
|
: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
|
<pagination
|
||||||
v-if="paginateResults"
|
v-if="paginateResults"
|
||||||
:total="total"
|
:total="total"
|
||||||
:current="page"
|
:current="page"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
v-on="$listeners"></pagination>
|
v-on="$listeners"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _ from "@/lodash";
|
import _ from '@/lodash'
|
||||||
import axios from "axios";
|
import axios from 'axios'
|
||||||
import TrackRow from "@/components/audio/track/Row";
|
import TrackRow from '@/components/audio/track/Row'
|
||||||
import TrackMobileRow from "@/components/audio/track/MobileRow";
|
import TrackMobileRow from '@/components/audio/track/MobileRow'
|
||||||
import Pagination from "@/components/Pagination";
|
import Pagination from '@/components/Pagination'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
TrackRow,
|
TrackRow,
|
||||||
TrackMobileRow,
|
TrackMobileRow,
|
||||||
Pagination,
|
Pagination
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
tracks: Array,
|
tracks: { type: Array, default: () => { return [] } },
|
||||||
showAlbum: { type: Boolean, required: false, default: true },
|
showAlbum: { type: Boolean, required: false, default: true },
|
||||||
showArtist: { type: Boolean, required: false, default: true },
|
showArtist: { type: Boolean, required: false, default: true },
|
||||||
showPosition: { type: Boolean, required: false, default: false },
|
showPosition: { type: Boolean, required: false, default: false },
|
||||||
|
@ -144,66 +184,66 @@ export default {
|
||||||
isArtist: { type: Boolean, required: false, default: false },
|
isArtist: { type: Boolean, required: false, default: false },
|
||||||
isAlbum: { type: Boolean, required: false, default: false },
|
isAlbum: { type: Boolean, required: false, default: false },
|
||||||
isPodcast: { type: Boolean, required: false, default: false },
|
isPodcast: { type: Boolean, required: false, default: false },
|
||||||
paginateResults: { type: Boolean, required: false, default: true},
|
paginateResults: { type: Boolean, required: false, default: true },
|
||||||
total: { type: Number, required: false},
|
total: { type: Number, required: false, default: 0 },
|
||||||
page: {type: Number, required: false, default: 1},
|
page: { type: Number, required: false, default: 1 },
|
||||||
paginateBy: {type: Number, required: false, default: 25}
|
paginateBy: { type: Number, required: false, default: 25 }
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data () {
|
||||||
return {
|
return {
|
||||||
fetchDataUrl: this.nextUrl,
|
fetchDataUrl: this.nextUrl,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
additionalTracks: [],
|
additionalTracks: [],
|
||||||
query: "",
|
query: ''
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
allTracks() {
|
allTracks () {
|
||||||
return (this.tracks || []).concat(this.additionalTracks);
|
return (this.tracks || []).concat(this.additionalTracks)
|
||||||
},
|
},
|
||||||
|
|
||||||
labels() {
|
labels () {
|
||||||
return {
|
return {
|
||||||
title: this.$pgettext("*/*/*/Noun", "Title"),
|
title: this.$pgettext('*/*/*/Noun', 'Title'),
|
||||||
album: this.$pgettext("*/*/*/Noun", "Album"),
|
album: this.$pgettext('*/*/*/Noun', 'Album'),
|
||||||
artist: this.$pgettext("*/*/*/Noun", "Artist"),
|
artist: this.$pgettext('*/*/*/Noun', 'Artist')
|
||||||
};
|
}
|
||||||
},
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
if (!this.tracks) {
|
||||||
|
this.fetchData('tracks/')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async fetchData(url) {
|
async fetchData (url) {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
this.isLoading = true;
|
this.isLoading = true
|
||||||
let self = this;
|
const self = this
|
||||||
let params = _.clone(this.filters);
|
const params = _.clone(this.filters)
|
||||||
let tracksPromise = axios.get(url, { params: params })
|
const tracksPromise = axios.get(url, { params: params })
|
||||||
params.page_size = this.limit;
|
params.page_size = this.limit
|
||||||
params.page = this.page;
|
params.page = this.page
|
||||||
params.include_channels = true;
|
params.include_channels = true
|
||||||
try {
|
try {
|
||||||
await tracksPromise
|
await tracksPromise
|
||||||
self.nextPage = tracksPromise.data.next;
|
self.nextPage = tracksPromise.data.next
|
||||||
self.objects = tracksPromise.data.results;
|
self.objects = tracksPromise.data.results
|
||||||
self.count = tracksPromise.data.count;
|
self.count = tracksPromise.data.count
|
||||||
self.$emit("fetched", tracksPromise.data);
|
self.$emit('fetched', tracksPromise.data)
|
||||||
self.isLoading = false;
|
self.isLoading = false
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
self.isLoading = false;
|
self.isLoading = false
|
||||||
self.errors = error.backendErrors;
|
self.errors = e.backendErrors
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updatePage: function(page) {
|
updatePage: function (page) {
|
||||||
this.$emit('page-changed', page)
|
this.$emit('page-changed', page)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
created() {
|
}
|
||||||
if (!this.tracks) {
|
|
||||||
this.fetchData("tracks/");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,17 +1,48 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="component-track-widget">
|
<div class="component-track-widget">
|
||||||
<h3 v-if="!!this.$slots.title">
|
<h3 v-if="!!$slots.title">
|
||||||
<slot name="title"></slot>
|
<slot name="title" />
|
||||||
<span v-if="showCount" class="ui tiny circular label">{{ count }}</span>
|
<span
|
||||||
|
v-if="showCount"
|
||||||
|
class="ui tiny circular label"
|
||||||
|
>{{ count }}</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div v-if="count > 0" class="ui divided unstackable items">
|
<div
|
||||||
<div :class="['item', itemClasses]" v-for="object in objects" :key="object.id">
|
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">
|
<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
|
||||||
<img alt="" v-else-if="object.track.cover" v-lazy="$store.getters['instance/absoluteUrl'](object.track.cover.urls.medium_square_crop)"/>
|
v-if="object.track.album && object.track.album.cover"
|
||||||
<img alt="" v-else-if="object.track.artist.cover" v-lazy="$store.getters['instance/absoluteUrl'](object.track.artist.cover.urls.medium_square_crop)"/>
|
v-lazy="$store.getters['instance/absoluteUrl'](object.track.album.cover.urls.medium_square_crop)"
|
||||||
<img alt="" v-else src="../../../assets/audio/default-cover.png">
|
alt=""
|
||||||
<play-button class="play-overlay" :icon-only="true" :button-classes="['ui', 'circular', 'tiny', 'vibrant', 'icon', 'button']" :track="object.track"></play-button>
|
>
|
||||||
|
<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>
|
||||||
<div class="middle aligned content">
|
<div class="middle aligned content">
|
||||||
<div class="ui unstackable grid">
|
<div class="ui unstackable grid">
|
||||||
|
@ -23,15 +54,32 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="meta ellipsis">
|
<div class="meta ellipsis">
|
||||||
<span>
|
<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 }}
|
{{ object.track.artist.name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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">
|
<div
|
||||||
<router-link class="left floated" :to="{name: 'profile.overview', params: {username: object.user.username}}">@{{ object.user.username }}</router-link>
|
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>
|
<span class="right floated"><human-date :date="object.creation_date" /></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,30 +89,46 @@
|
||||||
:account="object.actor"
|
:account="object.actor"
|
||||||
:dropdown-only="true"
|
:dropdown-only="true"
|
||||||
:dropdown-icon-classes="['ellipsis', 'vertical', 'large really discrete']"
|
:dropdown-icon-classes="['ellipsis', 'vertical', 'large really discrete']"
|
||||||
:track="object.track"></play-button>
|
:track="object.track"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
<div
|
||||||
<div class="ui loader"></div>
|
v-if="isLoading"
|
||||||
|
class="ui inverted active dimmer"
|
||||||
|
>
|
||||||
|
<div class="ui loader" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="ui placeholder segment">
|
<div
|
||||||
|
v-else
|
||||||
|
class="ui placeholder segment"
|
||||||
|
>
|
||||||
<div class="ui icon header">
|
<div class="ui icon header">
|
||||||
<i class="music icon"></i>
|
<i class="music icon" />
|
||||||
<translate translate-context="Content/Home/Placeholder">
|
<translate translate-context="Content/Home/Placeholder">
|
||||||
Nothing found
|
Nothing found
|
||||||
</translate>
|
</translate>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
<div
|
||||||
<div class="ui loader"></div>
|
v-if="isLoading"
|
||||||
|
class="ui inverted active dimmer"
|
||||||
|
>
|
||||||
|
<div class="ui loader" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="nextPage">
|
<template v-if="nextPage">
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<button v-if="nextPage" @click="fetchData(nextPage)" :class="['ui', 'basic', 'button']">
|
<button
|
||||||
<translate translate-context="*/*/Button,Label">Show more</translate>
|
v-if="nextPage"
|
||||||
|
:class="['ui', 'basic', 'button']"
|
||||||
|
@click="fetchData(nextPage)"
|
||||||
|
>
|
||||||
|
<translate translate-context="*/*/Button,Label">
|
||||||
|
Show more
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -74,21 +138,21 @@
|
||||||
import _ from '@/lodash'
|
import _ from '@/lodash'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import PlayButton from '@/components/audio/PlayButton'
|
import PlayButton from '@/components/audio/PlayButton'
|
||||||
import TagsList from "@/components/tags/List"
|
import TagsList from '@/components/tags/List'
|
||||||
|
|
||||||
export default {
|
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: {
|
components: {
|
||||||
PlayButton,
|
PlayButton,
|
||||||
TagsList
|
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 () {
|
data () {
|
||||||
return {
|
return {
|
||||||
objects: [],
|
objects: [],
|
||||||
|
@ -99,6 +163,17 @@ export default {
|
||||||
nextPage: null
|
nextPage: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
offset () {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
'$store.state.moderation.lastUpdate': function () {
|
||||||
|
this.fetchData(this.url)
|
||||||
|
},
|
||||||
|
count (v) {
|
||||||
|
this.$emit('count', v)
|
||||||
|
}
|
||||||
|
},
|
||||||
created () {
|
created () {
|
||||||
this.fetchData(this.url)
|
this.fetchData(this.url)
|
||||||
},
|
},
|
||||||
|
@ -108,11 +183,11 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let self = this
|
const self = this
|
||||||
let params = _.clone(this.filters)
|
const params = _.clone(this.filters)
|
||||||
params.page_size = this.limit
|
params.page_size = this.limit
|
||||||
params.offset = this.offset
|
params.offset = this.offset
|
||||||
axios.get(url, {params: params}).then((response) => {
|
axios.get(url, { params: params }).then((response) => {
|
||||||
self.previousPage = response.data.previous
|
self.previousPage = response.data.previous
|
||||||
self.nextPage = response.data.next
|
self.nextPage = response.data.next
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
|
@ -123,7 +198,7 @@ export default {
|
||||||
newObjects = response.data.results
|
newObjects = response.data.results
|
||||||
} else {
|
} else {
|
||||||
newObjects = response.data.results.map((r) => {
|
newObjects = response.data.results.map((r) => {
|
||||||
return {track: r}
|
return { track: r }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
self.objects = [...self.objects, ...newObjects]
|
self.objects = [...self.objects, ...newObjects]
|
||||||
|
@ -139,17 +214,6 @@ export default {
|
||||||
this.offset = Math.max(this.offset - this.limit, 0)
|
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>
|
</script>
|
||||||
|
|
|
@ -1,16 +1,26 @@
|
||||||
<template>
|
<template>
|
||||||
<main class="main pusher" v-title="labels.title">
|
<main
|
||||||
|
v-title="labels.title"
|
||||||
|
class="main pusher"
|
||||||
|
>
|
||||||
<div class="ui vertical stripe segment">
|
<div class="ui vertical stripe segment">
|
||||||
<section class="ui text container">
|
<section class="ui text container">
|
||||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
<div
|
||||||
<div class="ui loader"></div>
|
v-if="isLoading"
|
||||||
|
class="ui inverted active dimmer"
|
||||||
|
>
|
||||||
|
<div class="ui loader" />
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<router-link :to="{name: 'settings'}">
|
<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>
|
</router-link>
|
||||||
<h2 class="ui header">
|
<h2 class="ui header">
|
||||||
<translate translate-context="Content/Applications/Title">Application details</translate>
|
<translate translate-context="Content/Applications/Title">
|
||||||
|
Application details
|
||||||
|
</translate>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="ui form">
|
<div class="ui form">
|
||||||
<p>
|
<p>
|
||||||
|
@ -20,25 +30,45 @@
|
||||||
</p>
|
</p>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="copy-id"><translate translate-context="Content/Applications/Label">Application ID</translate></label>
|
<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>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="copy-secret"><translate translate-context="Content/Applications/Label">Application secret</translate></label>
|
<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>
|
||||||
<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>
|
<label for="copy-secret"><translate translate-context="Content/Applications/Label">Access token</translate></label>
|
||||||
<copy-input id="copy-secret" :value="application.token" />
|
<copy-input
|
||||||
<a href="" @click.prevent="refreshToken">
|
id="copy-secret"
|
||||||
<i class="refresh icon"></i>
|
:value="application.token"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
href=""
|
||||||
|
@click.prevent="refreshToken"
|
||||||
|
>
|
||||||
|
<i class="refresh icon" />
|
||||||
<translate translate-context="Content/Applications/Label">Regenerate token</translate>
|
<translate translate-context="Content/Applications/Label">Regenerate token</translate>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="ui header">
|
<h2 class="ui header">
|
||||||
<translate translate-context="Content/Applications/Title">Edit application</translate>
|
<translate translate-context="Content/Applications/Title">
|
||||||
|
Edit application
|
||||||
|
</translate>
|
||||||
</h2>
|
</h2>
|
||||||
<application-form @updated="application = $event" :app="application" />
|
<application-form
|
||||||
|
:app="application"
|
||||||
|
@updated="application = $event"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,19 +76,26 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios"
|
import axios from 'axios'
|
||||||
|
|
||||||
import ApplicationForm from "@/components/auth/ApplicationForm"
|
import ApplicationForm from '@/components/auth/ApplicationForm'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['id'],
|
|
||||||
components: {
|
components: {
|
||||||
ApplicationForm
|
ApplicationForm
|
||||||
},
|
},
|
||||||
data() {
|
props: { id: { type: Number, required: true } },
|
||||||
|
data () {
|
||||||
return {
|
return {
|
||||||
application: null,
|
application: null,
|
||||||
isLoading: false,
|
isLoading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
labels () {
|
||||||
|
return {
|
||||||
|
title: this.$pgettext('Content/Applications/Title', 'Edit application')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
@ -67,7 +104,7 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
fetchApplication () {
|
fetchApplication () {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let self = this
|
const self = this
|
||||||
axios.get(`oauth/apps/${this.id}/`).then((response) => {
|
axios.get(`oauth/apps/${this.id}/`).then((response) => {
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
self.application = response.data
|
self.application = response.data
|
||||||
|
@ -78,17 +115,10 @@ export default {
|
||||||
},
|
},
|
||||||
async refreshToken () {
|
async refreshToken () {
|
||||||
self.isLoading = true
|
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
|
this.application = response.data
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
}
|
}
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labels() {
|
|
||||||
return {
|
|
||||||
title: this.$pgettext('Content/Applications/Title', "Edit application")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,19 +1,45 @@
|
||||||
<template>
|
<template>
|
||||||
|
<form
|
||||||
<form class="ui form component-form" role="alert" @submit.prevent="submit()">
|
class="ui form component-form"
|
||||||
<div v-if="errors.length > 0" class="ui negative message">
|
role="alert"
|
||||||
<h4 class="header"><translate translate-context="Content/*/Error message.Title">We cannot save your changes</translate></h4>
|
@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">
|
<ul class="list">
|
||||||
<li v-for="error in errors">{{ error }}</li>
|
<li
|
||||||
|
v-for="(error, key) in errors"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui field">
|
<div class="ui field">
|
||||||
<label for="application-name"><translate translate-context="*/*/*/Noun">Name</translate></label>
|
<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>
|
||||||
<div class="ui field">
|
<div class="ui field">
|
||||||
<label for="redirect-uris"><translate translate-context="Content/Applications/Input.Label/Noun">Redirect URI</translate></label>
|
<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">
|
<p class="help">
|
||||||
<translate translate-context="Content/Applications/Help Text">
|
<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.
|
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>
|
</translate>
|
||||||
</p>
|
</p>
|
||||||
<div class="ui stackable two column grid">
|
<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">
|
<div class="ui parent checkbox">
|
||||||
<input
|
<input
|
||||||
|
:id="parent.id"
|
||||||
v-model="scopeArray"
|
v-model="scopeArray"
|
||||||
:value="parent.id"
|
:value="parent.id"
|
||||||
:id="parent.id"
|
type="checkbox"
|
||||||
type="checkbox">
|
>
|
||||||
<label :for="parent.id">
|
<label :for="parent.id">
|
||||||
{{ parent.label }}
|
{{ parent.label }}
|
||||||
<p class="help">
|
<p class="help">
|
||||||
|
@ -43,13 +74,17 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="child in parent.children">
|
<div
|
||||||
|
v-for="(child, index) in parent.children"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
<div class="ui child checkbox">
|
<div class="ui child checkbox">
|
||||||
<input
|
<input
|
||||||
|
:id="child.id"
|
||||||
v-model="scopeArray"
|
v-model="scopeArray"
|
||||||
:value="child.id"
|
:value="child.id"
|
||||||
:id="child.id"
|
type="checkbox"
|
||||||
type="checkbox">
|
>
|
||||||
<label :for="child.id">
|
<label :for="child.id">
|
||||||
{{ child.id }}
|
{{ child.id }}
|
||||||
<p class="help">
|
<p class="help">
|
||||||
|
@ -60,29 +95,43 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
<button
|
||||||
<button :class="['ui', {'loading': isLoading}, 'success', 'button']" type="submit">
|
:class="['ui', {'loading': isLoading}, 'success', 'button']"
|
||||||
<translate v-if="updating" key="2" translate-context="Content/Applications/Button.Label/Verb">Update application</translate>
|
type="submit"
|
||||||
<translate v-else key="3" translate-context="Content/Applications/Button.Label/Verb">Create application</translate>
|
>
|
||||||
|
<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>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _ from "@/lodash"
|
import _ from '@/lodash'
|
||||||
import axios from "axios"
|
import axios from 'axios'
|
||||||
import TranslationsMixin from "@/components/mixins/Translations"
|
import TranslationsMixin from '@/components/mixins/Translations'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [TranslationsMixin],
|
mixins: [TranslationsMixin],
|
||||||
props: {
|
props: {
|
||||||
app: {type: Object, required: false},
|
app: { type: Object, required: false, default: () => { return {} } },
|
||||||
defaults: {type: Object, required: false}
|
defaults: { type: Object, required: false, default: () => { return {} } }
|
||||||
},
|
},
|
||||||
data() {
|
data () {
|
||||||
let app = this.app || {}
|
const app = this.app || {}
|
||||||
let defaults = this.defaults || {}
|
const defaults = this.defaults || {}
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
errors: [],
|
errors: [],
|
||||||
|
@ -92,45 +141,19 @@ export default {
|
||||||
scopes: app.scopes || defaults.scopes || 'read'
|
scopes: app.scopes || defaults.scopes || 'read'
|
||||||
},
|
},
|
||||||
scopes: [
|
scopes: [
|
||||||
{id: "profile", icon: 'user'},
|
{ id: 'profile', icon: 'user' },
|
||||||
{id: "libraries", icon: 'book'},
|
{ id: 'libraries', icon: 'book' },
|
||||||
{id: "favorites", icon: 'heart'},
|
{ id: 'favorites', icon: 'heart' },
|
||||||
{id: "listenings", icon: 'music'},
|
{ id: 'listenings', icon: 'music' },
|
||||||
{id: "follows", icon: 'users'},
|
{ id: 'follows', icon: 'users' },
|
||||||
{id: "playlists", icon: 'list'},
|
{ id: 'playlists', icon: 'list' },
|
||||||
{id: "radios", icon: 'rss'},
|
{ id: 'radios', icon: 'rss' },
|
||||||
{id: "filters", icon: 'eye slash'},
|
{ id: 'filters', icon: 'eye slash' },
|
||||||
{id: "notifications", icon: 'bell'},
|
{ id: 'notifications', icon: 'bell' },
|
||||||
{id: "edits", icon: 'pencil alternate'},
|
{ 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: {
|
computed: {
|
||||||
updating () {
|
updating () {
|
||||||
return this.app
|
return this.app
|
||||||
|
@ -144,8 +167,8 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
allScopes () {
|
allScopes () {
|
||||||
let self = this
|
const self = this
|
||||||
let parents = [
|
const parents = [
|
||||||
{
|
{
|
||||||
id: 'read',
|
id: 'read',
|
||||||
label: this.$pgettext('Content/OAuth Scopes/Label/Verb', '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'),
|
label: this.$pgettext('Content/OAuth Scopes/Label/Verb', 'Write'),
|
||||||
description: this.$pgettext('Content/OAuth Scopes/Help Text', 'Write-only access to user data'),
|
description: this.$pgettext('Content/OAuth Scopes/Help Text', 'Write-only access to user data'),
|
||||||
value: this.scopeArray.indexOf('write') > -1
|
value: this.scopeArray.indexOf('write') > -1
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
parents.forEach((p) => {
|
parents.forEach((p) => {
|
||||||
p.children = self.scopes.map(s => {
|
p.children = self.scopes.map(s => {
|
||||||
let id = `${p.id}:${s.id}`
|
const id = `${p.id}:${s.id}`
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
value: this.scopeArray.indexOf(id) > -1,
|
value: this.scopeArray.indexOf(id) > -1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return parents
|
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>
|
</script>
|
||||||
|
|
|
@ -1,46 +1,58 @@
|
||||||
<template>
|
<template>
|
||||||
<main class="main pusher" v-title="labels.title">
|
<main
|
||||||
|
v-title="labels.title"
|
||||||
|
class="main pusher"
|
||||||
|
>
|
||||||
<div class="ui vertical stripe segment">
|
<div class="ui vertical stripe segment">
|
||||||
<section class="ui text container">
|
<section class="ui text container">
|
||||||
<router-link :to="{name: 'settings'}">
|
<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>
|
</router-link>
|
||||||
<h2 class="ui header">
|
<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>
|
</h2>
|
||||||
<application-form
|
<application-form
|
||||||
:defaults="defaults"
|
: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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ApplicationForm from "@/components/auth/ApplicationForm"
|
import ApplicationForm from '@/components/auth/ApplicationForm'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['name', 'redirect_uris', 'scopes'],
|
|
||||||
components: {
|
components: {
|
||||||
ApplicationForm
|
ApplicationForm
|
||||||
},
|
},
|
||||||
data() {
|
props: {
|
||||||
|
name: { type: String, required: true },
|
||||||
|
redirectUris: { type: String, required: true },
|
||||||
|
scopes: { type: Array, required: true }
|
||||||
|
},
|
||||||
|
data () {
|
||||||
return {
|
return {
|
||||||
application: null,
|
application: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
defaults: {
|
defaults: {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
redirect_uris: this.redirect_uris,
|
redirectUris: this.redirectUris,
|
||||||
scopes: this.scopes,
|
scopes: this.scopes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
labels() {
|
labels () {
|
||||||
return {
|
return {
|
||||||
title: this.$pgettext('Content/Settings/Button.Label', "Create a new application")
|
title: this.$pgettext('Content/Settings/Button.Label', 'Create a new application')
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,34 +1,91 @@
|
||||||
<template>
|
<template>
|
||||||
<main class="main pusher" v-title="labels.title">
|
<main
|
||||||
|
v-title="labels.title"
|
||||||
|
class="main pusher"
|
||||||
|
>
|
||||||
<section class="ui vertical stripe segment">
|
<section class="ui vertical stripe segment">
|
||||||
<div class="ui small text container">
|
<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>
|
<h2>
|
||||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
<i class="lock open icon" /><translate translate-context="Content/Auth/Title/Verb">
|
||||||
<h4 v-if="application" class="header"><translate translate-context="Popup/Moderation/Error message">Error while authorizing application</translate></h4>
|
Authorize third-party app
|
||||||
<h4 v-else class="header"><translate translate-context="Popup/Moderation/Error message">Error while fetching application data</translate></h4>
|
</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">
|
<ul class="list">
|
||||||
<li v-for="error in errors">{{ error }}</li>
|
<li
|
||||||
|
v-for="(error, key) in errors"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
<div
|
||||||
<div class="ui loader"></div>
|
v-if="isLoading"
|
||||||
|
class="ui inverted active dimmer"
|
||||||
|
>
|
||||||
|
<div class="ui loader" />
|
||||||
</div>
|
</div>
|
||||||
<form v-else-if="application && !code" :class="['ui', {loading: isLoading}, 'form']" @submit.prevent="submit">
|
<form
|
||||||
<h3><translate translate-context="Content/Auth/Title" :translate-params="{app: application.name}">%{ app } wants to access your Funkwhale account</translate></h3>
|
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">
|
<h4
|
||||||
<span v-if="topic.write && !topic.read" :class="['ui', 'basic', 'right floated', 'tiny', 'vertically-spaced component-label label']">
|
v-for="(topic, key) in topicScopes"
|
||||||
<i class="pencil icon"></i>
|
: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>
|
<translate translate-context="Content/Auth/Label/Noun">Write-only</translate>
|
||||||
</span>
|
</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>
|
<translate translate-context="Content/Auth/Label/Noun">Read-only</translate>
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="topic.write && topic.read" :class="['ui', 'basic', 'right floated', 'tiny', 'vertically-spaced component-label label']">
|
<span
|
||||||
<i class="pencil icon"></i>
|
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>
|
<translate translate-context="Content/Auth/Label/Noun">Full access</translate>
|
||||||
</span>
|
</span>
|
||||||
<i :class="[topic.icon, 'icon']"></i>
|
<i :class="[topic.icon, 'icon']" />
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{ topic.label }}
|
{{ topic.label }}
|
||||||
<div class="sub header">
|
<div class="sub header">
|
||||||
|
@ -38,23 +95,46 @@
|
||||||
</h4>
|
</h4>
|
||||||
<div v-if="unknownRequestedScopes.length > 0">
|
<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>
|
<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">
|
<ul
|
||||||
<li>{{ scope }}</li>
|
v-for="(unknownscope, key) in unknownRequestedScopes"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
<li>{{ unknownscope }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<button class="ui success labeled icon button" type="submit">
|
<button
|
||||||
<i class="lock open icon"></i>
|
class="ui success labeled icon button"
|
||||||
<translate translate-context="Content/Signup/Button.Label/Verb" :translate-params="{app: application.name}">Authorize %{ app }</translate>
|
type="submit"
|
||||||
|
>
|
||||||
|
<i class="lock open icon" />
|
||||||
|
<translate
|
||||||
|
translate-context="Content/Signup/Button.Label/Verb"
|
||||||
|
:translate-params="{app: application.name}"
|
||||||
|
>
|
||||||
|
Authorize %{ app }
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
<p v-if="redirectUri === 'urn:ietf:wg:oauth:2.0:oob'" key="1" v-translate translate-context="Content/Auth/Paragraph">
|
<p
|
||||||
You will be shown a code to copy-paste in the application.</p>
|
v-if="redirectUri === 'urn:ietf:wg:oauth:2.0:oob'"
|
||||||
<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>
|
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>
|
</form>
|
||||||
<div v-else-if="code">
|
<div v-else-if="code">
|
||||||
<p><strong><translate translate-context="Content/Auth/Paragraph">Copy-paste the following code in the application:</translate></strong></p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -62,60 +142,54 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import TranslationsMixin from "@/components/mixins/Translations"
|
import TranslationsMixin from '@/components/mixins/Translations'
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import {checkRedirectToLogin} from '@/utils'
|
import { checkRedirectToLogin } from '@/utils'
|
||||||
export default {
|
export default {
|
||||||
mixins: [TranslationsMixin],
|
mixins: [TranslationsMixin],
|
||||||
props: [
|
props: {
|
||||||
'clientId',
|
clientId: { type: String, required: true },
|
||||||
'redirectUri',
|
redirectUri: { type: String, required: true },
|
||||||
'scope',
|
scope: { type: String, required: true },
|
||||||
'responseType',
|
responseType: { type: String, required: true },
|
||||||
'nonce',
|
nonce: { type: String, required: true },
|
||||||
'state',
|
state: { type: String, required: true }
|
||||||
],
|
},
|
||||||
data() {
|
data () {
|
||||||
return {
|
return {
|
||||||
application: null,
|
application: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
errors: [],
|
errors: [],
|
||||||
code: null,
|
code: null,
|
||||||
knownScopes: [
|
knownScopes: [
|
||||||
{id: "profile", icon: 'user'},
|
{ id: 'profile', icon: 'user' },
|
||||||
{id: "libraries", icon: 'book'},
|
{ id: 'libraries', icon: 'book' },
|
||||||
{id: "favorites", icon: 'heart'},
|
{ id: 'favorites', icon: 'heart' },
|
||||||
{id: "listenings", icon: 'music'},
|
{ id: 'listenings', icon: 'music' },
|
||||||
{id: "follows", icon: 'users'},
|
{ id: 'follows', icon: 'users' },
|
||||||
{id: "playlists", icon: 'list'},
|
{ id: 'playlists', icon: 'list' },
|
||||||
{id: "radios", icon: 'rss'},
|
{ id: 'radios', icon: 'rss' },
|
||||||
{id: "filters", icon: 'eye slash'},
|
{ id: 'filters', icon: 'eye slash' },
|
||||||
{id: "notifications", icon: 'bell'},
|
{ id: 'notifications', icon: 'bell' },
|
||||||
{id: "edits", icon: 'pencil alternate'},
|
{ id: 'edits', icon: 'pencil alternate' },
|
||||||
{id: "security", icon: 'lock'},
|
{ id: 'security', icon: 'lock' },
|
||||||
{id: "reports", icon: 'warning sign'},
|
{ id: 'reports', icon: 'warning sign' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
|
||||||
checkRedirectToLogin(this.$store, this.$router)
|
|
||||||
if (this.clientId) {
|
|
||||||
this.fetchApplication()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
labels () {
|
labels () {
|
||||||
return {
|
return {
|
||||||
title: this.$pgettext('Head/Authorize/Title', "Allow application")
|
title: this.$pgettext('Head/Authorize/Title', 'Allow application')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
requestedScopes () {
|
requestedScopes () {
|
||||||
return (this.scope || '').split(' ')
|
return (this.scope || '').split(' ')
|
||||||
},
|
},
|
||||||
supportedScopes () {
|
supportedScopes () {
|
||||||
let supported = ['read', 'write']
|
const supported = ['read', 'write']
|
||||||
this.knownScopes.forEach(s => {
|
this.knownScopes.forEach(s => {
|
||||||
supported.push(`read:${s.id}`)
|
supported.push(`read:${s.id}`)
|
||||||
supported.push(`write:${s.id}`)
|
supported.push(`write:${s.id}`)
|
||||||
|
@ -123,14 +197,14 @@ export default {
|
||||||
return supported
|
return supported
|
||||||
},
|
},
|
||||||
unknownRequestedScopes () {
|
unknownRequestedScopes () {
|
||||||
let self = this
|
const self = this
|
||||||
return this.requestedScopes.filter(s => {
|
return this.requestedScopes.filter(s => {
|
||||||
return self.supportedScopes.indexOf(s) < 0
|
return self.supportedScopes.indexOf(s) < 0
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
topicScopes () {
|
topicScopes () {
|
||||||
let self = this
|
const self = this
|
||||||
let requested = this.requestedScopes
|
const requested = this.requestedScopes
|
||||||
let write = false
|
let write = false
|
||||||
let read = false
|
let read = false
|
||||||
if (requested.indexOf('read') > -1) {
|
if (requested.indexOf('read') > -1) {
|
||||||
|
@ -141,24 +215,30 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.knownScopes.map(s => {
|
return this.knownScopes.map(s => {
|
||||||
let id = s.id
|
const id = s.id
|
||||||
return {
|
return {
|
||||||
id: id,
|
id: id,
|
||||||
icon: s.icon,
|
icon: s.icon,
|
||||||
label: self.sharedLabels.scopes[s.id].label,
|
label: self.sharedLabels.scopes[s.id].label,
|
||||||
description: self.sharedLabels.scopes[s.id].description,
|
description: self.sharedLabels.scopes[s.id].description,
|
||||||
read: read || requested.indexOf(`read:${id}`) > -1,
|
read: read || requested.indexOf(`read:${id}`) > -1,
|
||||||
write: write || requested.indexOf(`write:${id}`) > -1,
|
write: write || requested.indexOf(`write:${id}`) > -1
|
||||||
}
|
}
|
||||||
}).filter(c => {
|
}).filter(c => {
|
||||||
return c.read || c.write
|
return c.read || c.write
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created () {
|
||||||
|
checkRedirectToLogin(this.$store, this.$router)
|
||||||
|
if (this.clientId) {
|
||||||
|
this.fetchApplication()
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchApplication () {
|
fetchApplication () {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let self = this
|
const self = this
|
||||||
axios.get(`oauth/apps/${this.clientId}/`).then((response) => {
|
axios.get(`oauth/apps/${this.clientId}/`).then((response) => {
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
self.application = response.data
|
self.application = response.data
|
||||||
|
@ -169,8 +249,8 @@ export default {
|
||||||
},
|
},
|
||||||
submit () {
|
submit () {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let self = this
|
const self = this
|
||||||
let data = new FormData();
|
const data = new FormData()
|
||||||
data.set('redirect_uri', this.redirectUri)
|
data.set('redirect_uri', this.redirectUri)
|
||||||
data.set('scope', this.scope)
|
data.set('scope', this.scope)
|
||||||
data.set('allow', true)
|
data.set('allow', true)
|
||||||
|
@ -178,7 +258,7 @@ export default {
|
||||||
data.set('response_type', this.responseType)
|
data.set('response_type', this.responseType)
|
||||||
data.set('state', this.state)
|
data.set('state', this.state)
|
||||||
data.set('nonce', this.nonce)
|
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') {
|
if (self.redirectUri === 'urn:ietf:wg:oauth:2.0:oob') {
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
self.code = response.data.code
|
self.code = response.data.code
|
||||||
|
|
|
@ -1,18 +1,35 @@
|
||||||
<template>
|
<template>
|
||||||
<form class="ui form" @submit.prevent="submit()">
|
<form
|
||||||
<div v-if="error" role="alert" class="ui negative message">
|
class="ui form"
|
||||||
<h4 class="header"><translate translate-context="Content/Login/Error message.Title">We cannot log you in</translate></h4>
|
@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">
|
<ul class="list">
|
||||||
<li v-if="error == 'invalid_credentials' && $store.state.instance.settings.moderation.signup_approval_enabled.value">
|
<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>
|
||||||
<li v-else-if="error == 'invalid_credentials'">
|
<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>
|
||||||
<li v-else>{{ error }}</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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">
|
<div class="field">
|
||||||
<label for="username-field">
|
<label for="username-field">
|
||||||
<translate translate-context="Content/Login/Input.Label/Noun">Username or e-mail address</translate>
|
<translate translate-context="Content/Login/Input.Label/Noun">Username or e-mail address</translate>
|
||||||
|
@ -24,14 +41,14 @@
|
||||||
</template>
|
</template>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
ref="username"
|
id="username-field"
|
||||||
required
|
ref="username"
|
||||||
name="username"
|
v-model="credentials.username"
|
||||||
type="text"
|
required
|
||||||
id="username-field"
|
name="username"
|
||||||
autofocus
|
type="text"
|
||||||
:placeholder="labels.usernamePlaceholder"
|
autofocus
|
||||||
v-model="credentials.username"
|
:placeholder="labels.usernamePlaceholder"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
@ -41,65 +58,78 @@
|
||||||
<translate translate-context="*/Login/*/Verb">Reset your password</translate>
|
<translate translate-context="*/Login/*/Verb">Reset your password</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
</label>
|
</label>
|
||||||
<password-input field-id="password-field" required v-model="credentials.password" />
|
<password-input
|
||||||
|
v-model="credentials.password"
|
||||||
|
field-id="password-field"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<p>
|
<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>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<button :class="['ui', {'loading': isLoading}, 'right', 'floated', buttonClasses, 'button']" type="submit">
|
<button
|
||||||
<translate translate-context="*/Login/*/Verb">Login</translate>
|
:class="['ui', {'loading': isLoading}, 'right', 'floated', buttonClasses, 'button']"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<translate translate-context="*/Login/*/Verb">
|
||||||
|
Login
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import PasswordInput from "@/components/forms/PasswordInput"
|
import PasswordInput from '@/components/forms/PasswordInput'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
next: { type: String, default: "/library" },
|
|
||||||
buttonClasses: { type: String, default: "success" },
|
|
||||||
showSignup: { type: Boolean, default: true},
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
PasswordInput
|
PasswordInput
|
||||||
},
|
},
|
||||||
data() {
|
props: {
|
||||||
|
next: { type: String, default: '/library' },
|
||||||
|
buttonClasses: { type: String, default: 'success' },
|
||||||
|
showSignup: { type: Boolean, default: true }
|
||||||
|
},
|
||||||
|
data () {
|
||||||
return {
|
return {
|
||||||
// We need to initialize the component with any
|
// We need to initialize the component with any
|
||||||
// properties that will be used in it
|
// properties that will be used in it
|
||||||
credentials: {
|
credentials: {
|
||||||
username: "",
|
username: '',
|
||||||
password: ""
|
password: ''
|
||||||
},
|
},
|
||||||
error: "",
|
error: '',
|
||||||
isLoading: false
|
isLoading: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
labels () {
|
||||||
|
const usernamePlaceholder = this.$pgettext('Content/Login/Input.Placeholder', 'Enter your username or e-mail address')
|
||||||
|
return {
|
||||||
|
usernamePlaceholder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
created () {
|
created () {
|
||||||
if (this.$store.state.auth.authenticated) {
|
if (this.$store.state.auth.authenticated) {
|
||||||
this.$router.push(this.next)
|
this.$router.push(this.next)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted () {
|
||||||
if (this.$refs.username) {
|
if (this.$refs.username) {
|
||||||
this.$refs.username.focus()
|
this.$refs.username.focus()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
labels() {
|
|
||||||
let usernamePlaceholder = this.$pgettext('Content/Login/Input.Placeholder', "Enter your username or e-mail address")
|
|
||||||
return {
|
|
||||||
usernamePlaceholder,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
async submit() {
|
async submit () {
|
||||||
if (this.$store.getters['instance/appDomain'] === this.$store.getters['instance/domain']) {
|
if (this.$store.getters['instance/appDomain'] === this.$store.getters['instance/domain']) {
|
||||||
return await this.submitSession()
|
return await this.submitSession()
|
||||||
} else {
|
} else {
|
||||||
|
@ -107,21 +137,21 @@ export default {
|
||||||
await this.$store.dispatch('auth/oauthLogin', this.next)
|
await this.$store.dispatch('auth/oauthLogin', this.next)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async submitSession() {
|
async submitSession () {
|
||||||
var self = this
|
const self = this
|
||||||
self.isLoading = true
|
self.isLoading = true
|
||||||
this.error = ""
|
this.error = ''
|
||||||
var credentials = {
|
const credentials = {
|
||||||
username: this.credentials.username,
|
username: this.credentials.username,
|
||||||
password: this.credentials.password
|
password: this.credentials.password
|
||||||
}
|
}
|
||||||
this.$store
|
this.$store
|
||||||
.dispatch("auth/login", {
|
.dispatch('auth/login', {
|
||||||
credentials,
|
credentials,
|
||||||
next: this.next,
|
next: this.next,
|
||||||
onError: error => {
|
onError: error => {
|
||||||
if (error.response.status === 400) {
|
if (error.response.status === 400) {
|
||||||
self.error = "invalid_credentials"
|
self.error = 'invalid_credentials'
|
||||||
} else {
|
} else {
|
||||||
self.error = error.backendErrors[0]
|
self.error = error.backendErrors[0]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,50 @@
|
||||||
<template>
|
<template>
|
||||||
<main class="main pusher" v-title="labels.title">
|
<main
|
||||||
|
v-title="labels.title"
|
||||||
|
class="main pusher"
|
||||||
|
>
|
||||||
<section class="ui vertical stripe segment">
|
<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>
|
<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>
|
</h2>
|
||||||
<p v-translate="{username: $store.state.auth.username}" translate-context="Content/Login/Paragraph">You are currently logged in as %{ username }</p>
|
<p
|
||||||
<button class="ui button" @click="$store.dispatch('auth/logout')"><translate translate-context="Content/Login/Button.Label">Yes, log me out!</translate></button>
|
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>
|
||||||
<div v-else class="ui small text container">
|
<div
|
||||||
|
v-else
|
||||||
|
class="ui small text container"
|
||||||
|
>
|
||||||
<h2>
|
<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>
|
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
@ -21,9 +53,9 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
computed: {
|
computed: {
|
||||||
labels() {
|
labels () {
|
||||||
return {
|
return {
|
||||||
title: this.$pgettext('Head/Login/Title', "Log Out")
|
title: this.$pgettext('Head/Login/Title', 'Log Out')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,96 +1,193 @@
|
||||||
<template>
|
<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>
|
<h3>{{ plugin.label }}</h3>
|
||||||
<div v-if="plugin.description" v-html="markdown.makeHtml(plugin.description)"></div>
|
<div
|
||||||
<template v-if="plugin.homepage" >
|
v-if="plugin.description"
|
||||||
<div class="ui small hidden divider"></div>
|
v-html="markdown.makeHtml(plugin.description)"
|
||||||
<a :href="plugin.homepage" target="_blank">
|
/>
|
||||||
<i class="external icon"></i>
|
<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>
|
<translate translate-context="Footer/*/List item.Link/Short, Noun">Documentation</translate>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
<div class="ui clearing hidden divider"></div>
|
<div class="ui clearing hidden divider" />
|
||||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
<div
|
||||||
<h4 class="header"><translate translate-context="Content/*/Error message.Title">Error while saving plugin</translate></h4>
|
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">
|
<ul class="list">
|
||||||
<li v-for="error in errors">{{ error }}</li>
|
<li
|
||||||
|
v-for="(error, key) in errors"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui toggle checkbox">
|
<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>
|
<label :for="`${plugin.name}-enabled`"><translate translate-context="*/*/*">Enabled</translate></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui clearing hidden divider"></div>
|
<div class="ui clearing hidden divider" />
|
||||||
<div v-if="plugin.source" class="field">
|
<div
|
||||||
|
v-if="plugin.source"
|
||||||
|
class="field"
|
||||||
|
>
|
||||||
<label for="plugin-library"><translate translate-context="*/*/*/Noun">Library</translate></label>
|
<label for="plugin-library"><translate translate-context="*/*/*/Noun">Library</translate></label>
|
||||||
<select id="plugin-library" v-model="values['library']">
|
<select
|
||||||
<option :value="l.uuid" v-for="l in libraries" :key="l.uuid">{{ l.name }}</option>
|
id="plugin-library"
|
||||||
|
v-model="values['library']"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="l in libraries"
|
||||||
|
:key="l.uuid"
|
||||||
|
:value="l.uuid"
|
||||||
|
>
|
||||||
|
{{ l.name }}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="plugin.conf && plugin.conf.length > 0" v-for="field in plugin.conf">
|
<template
|
||||||
<div v-if="field.type === 'text'" class="field">
|
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>
|
<label :for="`plugin-${field.name}`">{{ field.label || field.name }}</label>
|
||||||
<input :id="`plugin-${field.name}`" type="text" v-model="values[field.name]">
|
<input
|
||||||
<div v-if="field.help" v-html="markdown.makeHtml(field.help)"></div>
|
:id="`plugin-${field.name}`"
|
||||||
|
v-model="values[field.name]"
|
||||||
|
type="text"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="field.help"
|
||||||
|
v-html="markdown.makeHtml(field.help)"
|
||||||
|
/>
|
||||||
</div>
|
</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>
|
<label :for="`plugin-${field.name}`">{{ field.label || field.name }}</label>
|
||||||
<textarea :id="`plugin-${field.name}`" type="text" v-model="values[field.name]" rows="5" />
|
<textarea
|
||||||
<div v-if="field.help" v-html="markdown.makeHtml(field.help)"></div>
|
: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>
|
||||||
<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>
|
<label :for="`plugin-${field.name}`">{{ field.label || field.name }}</label>
|
||||||
<input :id="`plugin-${field.name}`" type="url" v-model="values[field.name]">
|
<input
|
||||||
<div v-if="field.help" v-html="markdown.makeHtml(field.help)"></div>
|
:id="`plugin-${field.name}`"
|
||||||
|
v-model="values[field.name]"
|
||||||
|
type="url"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="field.help"
|
||||||
|
v-html="markdown.makeHtml(field.help)"
|
||||||
|
/>
|
||||||
</div>
|
</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>
|
<label :for="`plugin-${field.name}`">{{ field.label || field.name }}</label>
|
||||||
<input :id="`plugin-${field.name}`" type="password" v-model="values[field.name]">
|
<input
|
||||||
<div v-if="field.help" v-html="markdown.makeHtml(field.help)"></div>
|
:id="`plugin-${field.name}`"
|
||||||
|
v-model="values[field.name]"
|
||||||
|
type="password"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="field.help"
|
||||||
|
v-html="markdown.makeHtml(field.help)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']">
|
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']"
|
||||||
<translate translate-context="Content/*/Button.Label/Verb">Save</translate>
|
>
|
||||||
|
<translate translate-context="Content/*/Button.Label/Verb">
|
||||||
|
Save
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="scan"
|
|
||||||
v-if="plugin.source"
|
v-if="plugin.source"
|
||||||
|
type="scan"
|
||||||
|
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']"
|
||||||
@click.prevent="submitAndScan"
|
@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>
|
</button>
|
||||||
<div class="ui clearing hidden divider"></div>
|
<div class="ui clearing hidden divider" />
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios"
|
import axios from 'axios'
|
||||||
import lodash from '@/lodash'
|
import lodash from '@/lodash'
|
||||||
import showdown from 'showdown'
|
import showdown from 'showdown'
|
||||||
export default {
|
export default {
|
||||||
props: ['plugin', "libraries"],
|
props: {
|
||||||
|
plugin: { type: Object, required: true },
|
||||||
|
libraries: { type: Array, required: true }
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
markdown: new showdown.Converter(),
|
markdown: new showdown.Converter(),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
enabled: this.plugin.enabled,
|
enabled: this.plugin.enabled,
|
||||||
values: lodash.clone(this.plugin.values || {}),
|
values: lodash.clone(this.plugin.values || {}),
|
||||||
errors: [],
|
errors: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async submit () {
|
async submit () {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.errors = []
|
this.errors = []
|
||||||
let url = `plugins/${this.plugin.name}`
|
const url = `plugins/${this.plugin.name}`
|
||||||
let enableUrl = this.enabled ? `${url}/enable` : `${url}/disable`
|
const enableUrl = this.enabled ? `${url}/enable` : `${url}/disable`
|
||||||
await axios.post(enableUrl)
|
await axios.post(enableUrl)
|
||||||
try {
|
try {
|
||||||
await axios.post(url, this.values)
|
await axios.post(url, this.values)
|
||||||
|
@ -101,19 +198,19 @@ export default {
|
||||||
},
|
},
|
||||||
async scan () {
|
async scan () {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.errors = []
|
this.errors = []
|
||||||
let url = `plugins/${this.plugin.name}/scan`
|
const url = `plugins/${this.plugin.name}/scan`
|
||||||
try {
|
try {
|
||||||
await axios.post(url, this.values)
|
await axios.post(url, this.values)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errors = e.backendErrors
|
this.errors = e.backendErrors
|
||||||
}
|
}
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
},
|
},
|
||||||
async submitAndScan () {
|
async submitAndScan () {
|
||||||
await this.submit()
|
await this.submit()
|
||||||
await this.scan()
|
await this.scan()
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -2,140 +2,197 @@
|
||||||
<div v-if="submitted">
|
<div v-if="submitted">
|
||||||
<div class="ui success message">
|
<div class="ui success message">
|
||||||
<p v-if="signupRequiresApproval">
|
<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>
|
||||||
<p v-else>
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<h2><translate translate-context="Content/Login/Title/Verb">Log in to your Funkwhale account</translate></h2>
|
<h2>
|
||||||
<login-form button-classes="basic success" :show-signup="false"></login-form>
|
<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>
|
</div>
|
||||||
<form
|
<form
|
||||||
v-else
|
v-else
|
||||||
:class="['ui', {'loading': isLoadingInstanceSetting}, 'form']"
|
:class="['ui', {'loading': isLoadingInstanceSetting}, 'form']"
|
||||||
@submit.prevent="submit()">
|
@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>
|
<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>
|
||||||
<p class="ui message" v-else-if="signupRequiresApproval">
|
<p
|
||||||
<translate translate-context="Content/Signup/Form/Paragraph">Registrations on this pod are open, but reviewed by moderators before approval.</translate>
|
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>
|
</p>
|
||||||
<template v-if="formCustomization && formCustomization.help_text">
|
<template v-if="formCustomization && formCustomization.help_text">
|
||||||
<rendered-description :content="formCustomization.help_text" :fetch-html="fetchDescriptionHtml" :permissive="true"></rendered-description>
|
<rendered-description
|
||||||
<div class="ui hidden divider"></div>
|
:content="formCustomization.help_text"
|
||||||
|
:fetch-html="fetchDescriptionHtml"
|
||||||
|
:permissive="true"
|
||||||
|
/>
|
||||||
|
<div class="ui hidden divider" />
|
||||||
</template>
|
</template>
|
||||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
<div
|
||||||
<h4 class="header"><translate translate-context="Content/Signup/Form/Paragraph">Your account cannot be created.</translate></h4>
|
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">
|
<ul class="list">
|
||||||
<li v-for="error in errors">{{ error }}</li>
|
<li
|
||||||
|
v-for="(error, key) in errors"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="required field">
|
<div class="required field">
|
||||||
<label for="username-field"><translate translate-context="Content/*/*">Username</translate></label>
|
<label for="username-field"><translate translate-context="Content/*/*">Username</translate></label>
|
||||||
<input
|
<input
|
||||||
ref="username"
|
id="username-field"
|
||||||
name="username"
|
ref="username"
|
||||||
required
|
v-model="username"
|
||||||
id="username-field"
|
name="username"
|
||||||
type="text"
|
required
|
||||||
autofocus
|
type="text"
|
||||||
:placeholder="labels.usernamePlaceholder"
|
autofocus
|
||||||
v-model="username">
|
:placeholder="labels.usernamePlaceholder"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="required field">
|
<div class="required field">
|
||||||
<label for="email-field"><translate translate-context="Content/*/*/Noun">E-mail address</translate></label>
|
<label for="email-field"><translate translate-context="Content/*/*/Noun">E-mail address</translate></label>
|
||||||
<input
|
<input
|
||||||
id="email-field"
|
id="email-field"
|
||||||
ref="email"
|
ref="email"
|
||||||
name="email"
|
v-model="email"
|
||||||
required
|
name="email"
|
||||||
type="email"
|
required
|
||||||
:placeholder="labels.emailPlaceholder"
|
type="email"
|
||||||
v-model="email">
|
:placeholder="labels.emailPlaceholder"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="required field">
|
<div class="required field">
|
||||||
<label for="password-field"><translate translate-context="*/*/*">Password</translate></label>
|
<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>
|
||||||
<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>
|
<label for="invitation-code"><translate translate-context="Content/*/Input.Label">Invitation code</translate></label>
|
||||||
<input
|
<input
|
||||||
id="invitation-code"
|
id="invitation-code"
|
||||||
required
|
v-model="invitation"
|
||||||
type="text"
|
required
|
||||||
name="invitation"
|
type="text"
|
||||||
:placeholder="labels.placeholder"
|
name="invitation"
|
||||||
v-model="invitation">
|
:placeholder="labels.placeholder"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="signupRequiresApproval && formCustomization && formCustomization.fields && formCustomization.fields.length > 0">
|
<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>
|
<label :for="`custom-field-${idx}`">{{ field.label }}</label>
|
||||||
<textarea
|
<textarea
|
||||||
v-if="field.input_type === 'long_text'"
|
v-if="field.input_type === 'long_text'"
|
||||||
:id="`custom-field-${idx}`"
|
:id="`custom-field-${idx}`"
|
||||||
:value="customFields[field.label]"
|
:value="customFields[field.label]"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
@input="$set(customFields, field.label, $event.target.value)" rows="5"></textarea>
|
rows="5"
|
||||||
<input v-else :id="`custom-field-${idx}`" type="text" :value="customFields[field.label]" :required="field.required" @input="$set(customFields, field.label, $event.target.value)">
|
@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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<button :class="['ui', buttonClasses, {'loading': isLoading}, ' right floated button']" type="submit">
|
<button
|
||||||
<translate translate-context="Content/Signup/Button.Label">Create my account</translate>
|
:class="['ui', buttonClasses, {'loading': isLoading}, ' right floated button']"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<translate translate-context="Content/Signup/Button.Label">
|
||||||
|
Create my account
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios"
|
import axios from 'axios'
|
||||||
import logger from "@/logging"
|
import logger from '@/logging'
|
||||||
|
|
||||||
import LoginForm from "@/components/auth/LoginForm"
|
import LoginForm from '@/components/auth/LoginForm'
|
||||||
import PasswordInput from "@/components/forms/PasswordInput"
|
import PasswordInput from '@/components/forms/PasswordInput'
|
||||||
|
|
||||||
export default {
|
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: {
|
components: {
|
||||||
LoginForm,
|
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 {
|
return {
|
||||||
username: "",
|
username: '',
|
||||||
email: "",
|
email: '',
|
||||||
password: "",
|
password: '',
|
||||||
isLoadingInstanceSetting: true,
|
isLoadingInstanceSetting: true,
|
||||||
errors: [],
|
errors: [],
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
invitation: this.defaultInvitation,
|
invitation: this.defaultInvitation,
|
||||||
customFields: {},
|
customFields: {},
|
||||||
submitted: false,
|
submitted: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
|
||||||
let self = this
|
|
||||||
this.$store.dispatch("instance/fetchSettings", {
|
|
||||||
callback: function() {
|
|
||||||
self.isLoadingInstanceSetting = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
labels() {
|
labels () {
|
||||||
let placeholder = this.$pgettext(
|
const placeholder = this.$pgettext(
|
||||||
"Content/Signup/Form/Placeholder",
|
'Content/Signup/Form/Placeholder',
|
||||||
"Enter your invitation code (case insensitive)"
|
'Enter your invitation code (case insensitive)'
|
||||||
)
|
)
|
||||||
let usernamePlaceholder = this.$pgettext("Content/Signup/Form/Placeholder", "Enter your username")
|
const usernamePlaceholder = this.$pgettext('Content/Signup/Form/Placeholder', 'Enter your username')
|
||||||
let emailPlaceholder = this.$pgettext("Content/Signup/Form/Placeholder", "Enter your e-mail address")
|
const emailPlaceholder = this.$pgettext('Content/Signup/Form/Placeholder', 'Enter your e-mail address')
|
||||||
return {
|
return {
|
||||||
usernamePlaceholder,
|
usernamePlaceholder,
|
||||||
emailPlaceholder,
|
emailPlaceholder,
|
||||||
|
@ -152,22 +209,30 @@ export default {
|
||||||
return this.signupApprovalEnabled
|
return this.signupApprovalEnabled
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created () {
|
||||||
|
const self = this
|
||||||
|
this.$store.dispatch('instance/fetchSettings', {
|
||||||
|
callback: function () {
|
||||||
|
self.isLoadingInstanceSetting = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submit() {
|
submit () {
|
||||||
var self = this
|
const self = this
|
||||||
self.isLoading = true
|
self.isLoading = true
|
||||||
this.errors = []
|
this.errors = []
|
||||||
var payload = {
|
const payload = {
|
||||||
username: this.username,
|
username: this.username,
|
||||||
password1: this.password,
|
password1: this.password,
|
||||||
password2: this.password,
|
password2: this.password,
|
||||||
email: this.email,
|
email: this.email,
|
||||||
invitation: this.invitation,
|
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 => {
|
response => {
|
||||||
logger.default.info("Successfully created account")
|
logger.default.info('Successfully created account')
|
||||||
self.submitted = true
|
self.submitted = true
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,61 +1,144 @@
|
||||||
<template>
|
<template>
|
||||||
<form class="ui form" @submit.prevent="requestNewToken()">
|
<form
|
||||||
<h2><translate translate-context="Content/Settings/Title">Subsonic API password</translate></h2>
|
class="ui form"
|
||||||
<p class="ui message" v-if="!subsonicEnabled">
|
@submit.prevent="requestNewToken()"
|
||||||
<translate translate-context="Content/Settings/Paragraph">The Subsonic API is not available on this Funkwhale instance.</translate>
|
>
|
||||||
|
<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>
|
||||||
<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>
|
||||||
<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>
|
||||||
<p><a href="https://docs.funkwhale.audio/users/apps.html#subsonic-compatible-clients" target="_blank">
|
<p>
|
||||||
<translate translate-context="Content/Settings/Link">Discover how to use Funkwhale from other apps</translate>
|
<a
|
||||||
</a></p>
|
href="https://docs.funkwhale.audio/users/apps.html#subsonic-compatible-clients"
|
||||||
<div v-if="success" class="ui positive message">
|
target="_blank"
|
||||||
<h4 class="header">{{ successMessage }}</h4>
|
>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="subsonicEnabled && errors.length > 0" role="alert" class="ui negative message">
|
<div
|
||||||
<h4 class="header"><translate translate-context="Content/*/Error message.Title">Error</translate></h4>
|
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">
|
<ul class="list">
|
||||||
<li v-for="error in errors">{{ error }}</li>
|
<li
|
||||||
|
v-for="(error, key) in errors"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="subsonicEnabled">
|
<template v-if="subsonicEnabled">
|
||||||
<div v-if="token" class="field">
|
<div
|
||||||
<label for="subsonic-password" class="visually-hidden">{{ labels.subsonicField }}</label>
|
v-if="token"
|
||||||
|
class="field"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
for="subsonic-password"
|
||||||
|
class="visually-hidden"
|
||||||
|
>{{ labels.subsonicField }}</label>
|
||||||
<password-input
|
<password-input
|
||||||
field-id="subsonic-password"
|
|
||||||
ref="passwordInput"
|
ref="passwordInput"
|
||||||
v-model="token"
|
|
||||||
:key="token"
|
:key="token"
|
||||||
|
v-model="token"
|
||||||
|
field-id="subsonic-password"
|
||||||
:copy-button="true"
|
:copy-button="true"
|
||||||
:default-show="showToken"/>
|
:default-show="showToken"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<dangerous-button
|
<dangerous-button
|
||||||
v-if="token"
|
v-if="token"
|
||||||
:class="['ui', {'loading': isLoading}, 'button']"
|
:class="['ui', {'loading': isLoading}, 'button']"
|
||||||
:action="requestNewToken">
|
: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>
|
<translate translate-context="*/Settings/Button.Label/Verb">
|
||||||
<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>
|
Request a new password
|
||||||
<div slot="modal-confirm"><translate translate-context="*/Settings/Button.Label/Verb">Request a new password</translate></div>
|
</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>
|
</dangerous-button>
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
color=""
|
color=""
|
||||||
:class="['ui', {'loading': isLoading}, 'button']"
|
:class="['ui', {'loading': isLoading}, 'button']"
|
||||||
@click="requestNewToken"><translate translate-context="Content/Settings/Button.Label/Verb">Request a password</translate></button>
|
@click="requestNewToken"
|
||||||
<dangerous-button
|
>
|
||||||
v-if="token"
|
<translate translate-context="Content/Settings/Button.Label/Verb">
|
||||||
:class="['ui', {'loading': isLoading}, 'warning', 'button']"
|
Request a password
|
||||||
:action="disable">
|
</translate>
|
||||||
<translate translate-context="Content/Settings/Button.Label/Verb">Disable Subsonic access</translate>
|
</button>
|
||||||
<p slot="modal-header"><translate translate-context="Popup/Settings/Title">Disable Subsonic API access?</translate></p>
|
<dangerous-button
|
||||||
<p slot="modal-content"><translate translate-context="Popup/Settings/Paragraph">This will completely disable access to the Subsonic API using from account.</translate></p>
|
v-if="token"
|
||||||
<div slot="modal-confirm"><translate translate-context="Popup/Settings/Button.Label">Disable access</translate></div>
|
:class="['ui', {'loading': isLoading}, 'warning', 'button']"
|
||||||
</dangerous-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>
|
||||||
|
</dangerous-button>
|
||||||
</template>
|
</template>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
@ -78,6 +161,16 @@ export default {
|
||||||
showToken: false
|
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 () {
|
created () {
|
||||||
this.fetchToken()
|
this.fetchToken()
|
||||||
},
|
},
|
||||||
|
@ -86,10 +179,10 @@ export default {
|
||||||
this.success = false
|
this.success = false
|
||||||
this.errors = []
|
this.errors = []
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let self = this
|
const self = this
|
||||||
let url = `users/${this.$store.state.auth.username}/subsonic-token/`
|
const url = `users/${this.$store.state.auth.username}/subsonic-token/`
|
||||||
return axios.get(url).then(response => {
|
return axios.get(url).then(response => {
|
||||||
self.token = response.data['subsonic_api_token']
|
self.token = response.data.subsonic_api_token
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
}, error => {
|
}, error => {
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
|
@ -101,11 +194,11 @@ export default {
|
||||||
this.success = false
|
this.success = false
|
||||||
this.errors = []
|
this.errors = []
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let self = this
|
const self = this
|
||||||
let url = `users/${this.$store.state.auth.username}/subsonic-token/`
|
const url = `users/${this.$store.state.auth.username}/subsonic-token/`
|
||||||
return axios.post(url, {}).then(response => {
|
return axios.post(url, {}).then(response => {
|
||||||
self.showToken = true
|
self.showToken = true
|
||||||
self.token = response.data['subsonic_api_token']
|
self.token = response.data.subsonic_api_token
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
self.success = true
|
self.success = true
|
||||||
}, error => {
|
}, error => {
|
||||||
|
@ -118,8 +211,8 @@ export default {
|
||||||
this.success = false
|
this.success = false
|
||||||
this.errors = []
|
this.errors = []
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let self = this
|
const self = this
|
||||||
let url = `users/${this.$store.state.auth.username}/subsonic-token/`
|
const url = `users/${this.$store.state.auth.username}/subsonic-token/`
|
||||||
return axios.delete(url).then(response => {
|
return axios.delete(url).then(response => {
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
self.token = null
|
self.token = null
|
||||||
|
@ -129,16 +222,6 @@ export default {
|
||||||
self.errors = error.backendErrors
|
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>
|
</script>
|
||||||
|
|
|
@ -1,16 +1,35 @@
|
||||||
<template>
|
<template>
|
||||||
<form @submit.stop.prevent :class="['ui', {loading: isLoading}, 'form']">
|
<form
|
||||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
:class="['ui', {loading: isLoading}, 'form']"
|
||||||
<h4 class="header"><translate translate-context="Content/*/Error message.Title">Error while creating</translate></h4>
|
@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">
|
<ul class="list">
|
||||||
<li v-for="error in errors">{{ error }}</li>
|
<li
|
||||||
|
v-for="(error, key) in errors"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui required field">
|
<div class="ui required field">
|
||||||
<label for="album-title">
|
<label for="album-title">
|
||||||
<translate translate-context="*/*/*/Noun">Title</translate>
|
<translate translate-context="*/*/*/Noun">Title</translate>
|
||||||
</label>
|
</label>
|
||||||
<input type="text" v-model="values.title">
|
<input
|
||||||
|
v-model="values.title"
|
||||||
|
type="text"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
@ -18,17 +37,17 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
channel: {type: Object, required: true},
|
|
||||||
},
|
|
||||||
components: {},
|
components: {},
|
||||||
|
props: {
|
||||||
|
channel: { type: Object, required: true }
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
errors: [],
|
errors: [],
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
values: {
|
values: {
|
||||||
title: "",
|
title: ''
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -36,20 +55,28 @@ export default {
|
||||||
return this.values.title.length > 0
|
return this.values.title.length > 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
submittable (v) {
|
||||||
|
this.$emit('submittable', v)
|
||||||
|
},
|
||||||
|
isLoading (v) {
|
||||||
|
this.$emit('loading', v)
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
submit () {
|
submit () {
|
||||||
let self = this
|
const self = this
|
||||||
self.isLoading = true
|
self.isLoading = true
|
||||||
self.errors = []
|
self.errors = []
|
||||||
let payload = {
|
const payload = {
|
||||||
...this.values,
|
...this.values,
|
||||||
artist: this.channel.artist.id,
|
artist: this.channel.artist.id
|
||||||
}
|
}
|
||||||
return axios.post('albums/', payload).then(
|
return axios.post('albums/', payload).then(
|
||||||
response => {
|
response => {
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
self.$emit("created")
|
self.$emit('created')
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
self.errors = error.backendErrors
|
self.errors = error.backendErrors
|
||||||
|
@ -57,14 +84,6 @@ export default {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
submittable (v) {
|
|
||||||
this.$emit("submittable", v)
|
|
||||||
},
|
|
||||||
isLoading (v) {
|
|
||||||
this.$emit("loading", v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,21 +1,47 @@
|
||||||
<template>
|
<template>
|
||||||
<modal class="small" :show.sync="show">
|
<modal
|
||||||
|
class="small"
|
||||||
|
:show.sync="show"
|
||||||
|
>
|
||||||
<h4 class="header">
|
<h4 class="header">
|
||||||
<translate key="1" v-if="channel.content_category === 'podcasts'" translate-context="Popup/Channels/Title/Verb">New series</translate>
|
<translate
|
||||||
<translate key="2" v-else translate-context="Popup/Channels/Title">New album</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>
|
</h4>
|
||||||
<div class="scrolling content">
|
<div class="scrolling content">
|
||||||
<channel-album-form
|
<channel-album-form
|
||||||
ref="albumForm"
|
ref="albumForm"
|
||||||
|
:channel="channel"
|
||||||
@loading="isLoading = $event"
|
@loading="isLoading = $event"
|
||||||
@submittable="submittable = $event"
|
@submittable="submittable = $event"
|
||||||
@created="$emit('created', $event)"
|
@created="$emit('created', $event)"
|
||||||
:channel="channel"></channel-album-form>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="ui basic cancel button"><translate translate-context="*/*/Button.Label/Verb">Cancel</translate></button>
|
<button class="ui basic cancel button">
|
||||||
<button :class="['ui', 'primary', {loading: isLoading}, 'button']" :disabled="!submittable" @click.stop.prevent="$refs.albumForm.submit()">
|
<translate translate-context="*/*/Button.Label/Verb">
|
||||||
<translate translate-context="*/*/Button.Label">Create</translate>
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
|
@ -26,16 +52,16 @@ import Modal from '@/components/semantic/Modal'
|
||||||
import ChannelAlbumForm from '@/components/channels/AlbumForm'
|
import ChannelAlbumForm from '@/components/channels/AlbumForm'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['channel'],
|
|
||||||
components: {
|
components: {
|
||||||
Modal,
|
Modal,
|
||||||
ChannelAlbumForm
|
ChannelAlbumForm
|
||||||
},
|
},
|
||||||
|
props: { channel: { type: Object, required: true } },
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
submittable: false,
|
submittable: false,
|
||||||
show: false,
|
show: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
|
@ -1,15 +1,41 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<label for="album-dropdown">
|
<label for="album-dropdown">
|
||||||
<translate v-if="channel && channel.artist.content_category === 'podcast'" key="1" translate-context="*/*/*">Series</translate>
|
<translate
|
||||||
<translate v-else key="2" translate-context="*/*/*">Album</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>
|
</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="">
|
<option value="">
|
||||||
<translate translate-context="*/*/*">None</translate>
|
<translate translate-context="*/*/*">
|
||||||
|
None
|
||||||
|
</translate>
|
||||||
</option>
|
</option>
|
||||||
<option v-for="album in albums" :key="album.id" :value="album.id">
|
<option
|
||||||
{{ album.title }} (<translate translate-context="*/*/*" :translate-params="{count: album.tracks_count}" :translate-n="album.tracks_count" translate-plural="%{ count } tracks">%{ count } track</translate>)
|
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>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,11 +44,19 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['value', 'channel'],
|
props: {
|
||||||
|
value: { type: String, required: true },
|
||||||
|
channel: { type: Object, required: true }
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
albums: [],
|
albums: [],
|
||||||
isLoading: false,
|
isLoading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
async channel () {
|
||||||
|
await this.fetchData()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created () {
|
async created () {
|
||||||
|
@ -35,14 +69,9 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.isLoading = true
|
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.albums = response.data.results
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
async channel () {
|
|
||||||
await this.fetchData()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,15 +3,36 @@
|
||||||
<label for="license-dropdown">
|
<label for="license-dropdown">
|
||||||
<translate translate-context="Content/*/*/Noun">License</translate>
|
<translate translate-context="Content/*/*/Noun">License</translate>
|
||||||
</label>
|
</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="">
|
<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>
|
||||||
<option v-for="l in featuredLicenses" :key="l.code" :value="l.code">{{ l.name }}</option>
|
|
||||||
</select>
|
</select>
|
||||||
<p class="help" v-if="value">
|
<div class="ui very small hidden divider" />
|
||||||
<div class="ui very small hidden divider"></div>
|
<p
|
||||||
<a :href="currentLicense.url" v-if="value" target="_blank" rel="noreferrer noopener">
|
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>
|
<translate translate-context="Content/*/*">About this license</translate>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
@ -21,7 +42,7 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['value'],
|
props: { value: { type: String, required: true } },
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
availableLicenses: [],
|
availableLicenses: [],
|
||||||
|
@ -32,38 +53,38 @@ export default {
|
||||||
'cc-by-nc-4.0',
|
'cc-by-nc-4.0',
|
||||||
'cc-by-nc-sa-4.0',
|
'cc-by-nc-sa-4.0',
|
||||||
'cc-by-nc-nd-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: {
|
computed: {
|
||||||
featuredLicenses () {
|
featuredLicenses () {
|
||||||
let self = this
|
const self = this
|
||||||
return this.availableLicenses.filter((l) => {
|
return this.availableLicenses.filter((l) => {
|
||||||
return self.featuredLicensesIds.indexOf(l.code) > -1
|
return self.featuredLicensesIds.indexOf(l.code) > -1
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
currentLicense () {
|
currentLicense () {
|
||||||
let self = this
|
const self = this
|
||||||
if (this.value) {
|
if (this.value) {
|
||||||
return this.availableLicenses.filter((l) => {
|
return this.availableLicenses.filter((l) => {
|
||||||
return l.code === self.value
|
return l.code === self.value
|
||||||
})[0]
|
})[0]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async created () {
|
||||||
|
await this.fetchLicenses()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async fetchLicenses () {
|
async fetchLicenses () {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let response = await axios.get('licenses/')
|
const response = await axios.get('licenses/')
|
||||||
this.availableLicenses = response.data.results
|
this.availableLicenses = response.data.results
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,20 +1,40 @@
|
||||||
<template>
|
<template>
|
||||||
<button v-if="$store.state.auth.authenticated" @click.stop="toggle" :class="['ui', 'pink', {'inverted': isSubscribed}, {'favorited': isSubscribed}, 'icon', 'labeled', 'button']">
|
<button
|
||||||
<i class="heart icon"></i>
|
v-if="$store.state.auth.authenticated"
|
||||||
<translate v-if="isSubscribed" translate-context="Content/Track/Button.Message">Unsubscribe</translate>
|
:class="['ui', 'pink', {'inverted': isSubscribed}, {'favorited': isSubscribed}, 'icon', 'labeled', 'button']"
|
||||||
<translate v-else translate-context="Content/Track/*/Verb">Subscribe</translate>
|
@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>
|
||||||
<button @click="$refs.loginModal.show = true" v-else :class="['ui', 'pink', 'icon', 'labeled', 'button']">
|
<button
|
||||||
<i class="heart icon"></i>
|
v-else
|
||||||
<translate translate-context="Content/Track/*/Verb">Subscribe</translate>
|
: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
|
<login-modal
|
||||||
ref="loginModal"
|
ref="loginModal"
|
||||||
class="small"
|
class="small"
|
||||||
:nextRoute='this.$route.fullPath'
|
:next-route="$route.fullPath"
|
||||||
:message='this.message.authMessage'
|
:message="message.authMessage"
|
||||||
:cover='this.channel.artist.cover'
|
:cover="channel.artist.cover"
|
||||||
@created="$refs.loginModal.show = false;">
|
@created="$refs.loginModal.show = false;"
|
||||||
</login-modal>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -22,12 +42,12 @@
|
||||||
import LoginModal from '@/components/common/LoginModal'
|
import LoginModal from '@/components/common/LoginModal'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
channel: {type: Object},
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
LoginModal
|
LoginModal
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
channel: { type: Object, required: true }
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
title () {
|
title () {
|
||||||
if (this.isSubscribed) {
|
if (this.isSubscribed) {
|
||||||
|
@ -43,7 +63,7 @@ export default {
|
||||||
return {
|
return {
|
||||||
authMessage: this.$pgettext('Popup/Message/Paragraph', 'You need to be logged in to subscribe to this channel')
|
authMessage: this.$pgettext('Popup/Message/Paragraph', 'You need to be logged in to subscribe to this channel')
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggle () {
|
toggle () {
|
||||||
|
@ -56,6 +76,5 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,70 +1,132 @@
|
||||||
<template>
|
<template>
|
||||||
<form @submit.stop.prevent :class="['ui', {loading: isLoadingStep1}, 'form component-file-upload']">
|
<form
|
||||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
:class="['ui', {loading: isLoadingStep1}, 'form component-file-upload']"
|
||||||
<h4 class="header"><translate translate-context="Content/*/Error message.Title">Error while publishing</translate></h4>
|
@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">
|
<ul class="list">
|
||||||
<li v-for="error in errors">{{ error }}</li>
|
<li
|
||||||
|
v-for="(error, key) in errors"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div :class="['ui', 'required', {hidden: step > 1}, 'field']">
|
<div :class="['ui', 'required', {hidden: step > 1}, 'field']">
|
||||||
<label for="channel-dropdown">
|
<label for="channel-dropdown">
|
||||||
<translate translate-context="*/*/*">Channel</translate>
|
<translate translate-context="*/*/*">Channel</translate>
|
||||||
</label>
|
</label>
|
||||||
<div id="channel-dropdown" class="ui search normal selection dropdown">
|
<div
|
||||||
<div class="text"></div>
|
id="channel-dropdown"
|
||||||
<i class="dropdown icon"></i>
|
class="ui search normal selection dropdown"
|
||||||
|
>
|
||||||
|
<div class="text" />
|
||||||
|
<i class="dropdown icon" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<album-select v-model.number="values.album" :channel="selectedChannel" :class="['ui', {hidden: step > 1}, 'field']"></album-select>
|
<album-select
|
||||||
<license-select v-model="values.license" :class="['ui', {hidden: step > 1}, 'field']"></license-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="['ui', {hidden: step > 1}, 'message']">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p>
|
<p>
|
||||||
<i class="copyright icon"></i>
|
<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>
|
<translate translate-context="Content/Channels/Popup.Paragraph">
|
||||||
|
Add a license to your upload to ensure some freedoms to your public.
|
||||||
|
</translate>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="step >= 2 && step < 4">
|
<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">
|
<div class="content">
|
||||||
<p>
|
<p>
|
||||||
<i class="warning icon"></i>
|
<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>
|
<translate translate-context="Content/Library/Paragraph">
|
||||||
|
You don't have any space left to upload your files. Please contact the moderators.
|
||||||
|
</translate>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<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>
|
<p>
|
||||||
<i class="redo icon"></i>
|
<i class="redo icon" />
|
||||||
<translate translate-context="Popup/Channels/Paragraph">You have some draft uploads pending publication.</translate>
|
<translate translate-context="Popup/Channels/Paragraph">
|
||||||
|
You have some draft uploads pending publication.
|
||||||
|
</translate>
|
||||||
</p>
|
</p>
|
||||||
<button @click.stop.prevent="includeDraftUploads = false" class="ui basic button">
|
<button
|
||||||
<translate translate-context="*/*/*">Ignore</translate>
|
class="ui basic button"
|
||||||
|
@click.stop.prevent="includeDraftUploads = false"
|
||||||
|
>
|
||||||
|
<translate translate-context="*/*/*">
|
||||||
|
Ignore
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
<button @click.stop.prevent="includeDraftUploads = true" class="ui basic button">
|
<button
|
||||||
<translate translate-context="*/*/*">Resume</translate>
|
class="ui basic button"
|
||||||
|
@click.stop.prevent="includeDraftUploads = true"
|
||||||
|
>
|
||||||
|
<translate translate-context="*/*/*">
|
||||||
|
Resume
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="uploadedFiles.length > 0" :class="[{hidden: step === 3}]">
|
<div
|
||||||
<div class="channel-file" v-for="(file, idx) in uploadedFiles">
|
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 class="content">
|
||||||
<div role="button"
|
<div
|
||||||
v-if="file.response.uuid"
|
v-if="file.response.uuid"
|
||||||
@click.stop.prevent="selectedUploadId = file.response.uuid"
|
role="button"
|
||||||
class="ui basic icon button"
|
class="ui basic icon button"
|
||||||
:title="labels.editTitle">
|
:title="labels.editTitle"
|
||||||
<i class="pencil icon"></i>
|
@click.stop.prevent="selectedUploadId = file.response.uuid"
|
||||||
|
>
|
||||||
|
<i class="pencil icon" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="file.error"
|
v-if="file.error"
|
||||||
@click.stop.prevent="selectedUploadId = file.response.uuid"
|
|
||||||
class="ui basic danger icon label"
|
class="ui basic danger icon label"
|
||||||
:title="file.error">
|
:title="file.error"
|
||||||
<i class="warning sign icon"></i>
|
@click.stop.prevent="selectedUploadId = file.response.uuid"
|
||||||
|
>
|
||||||
|
<i class="warning sign icon" />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<h4 class="ui header">
|
<h4 class="ui header">
|
||||||
<template v-if="file.metadata.title">
|
<template v-if="file.metadata.title">
|
||||||
|
@ -77,20 +139,39 @@
|
||||||
<template v-if="file.response.uuid">
|
<template v-if="file.response.uuid">
|
||||||
{{ file.size | humanSize }}
|
{{ file.size | humanSize }}
|
||||||
<template v-if="file.response.duration">
|
<template v-if="file.response.duration">
|
||||||
· <human-duration :duration="file.response.duration"></human-duration>
|
· <human-duration :duration="file.response.duration" />
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<translate key="1" v-if="file.active" translate-context="Channels/*/*">Uploading</translate>
|
<translate
|
||||||
<translate key="2" v-else-if="file.error" translate-context="Channels/*/*">Errored</translate>
|
v-if="file.active"
|
||||||
<translate key="3" v-else translate-context="Channels/*/*">Pending</translate>
|
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 }}
|
· {{ file.size | humanSize }}
|
||||||
· {{ parseInt(file.progress) }}%
|
· {{ parseInt(file.progress) }}%
|
||||||
</template>
|
</template>
|
||||||
· <a @click.stop.prevent="remove(file)">
|
· <a @click.stop.prevent="remove(file)">
|
||||||
<translate translate-context="Content/Radio/Button.Label/Verb">Remove</translate>
|
<translate translate-context="Content/Radio/Button.Label/Verb">Remove</translate>
|
||||||
</a>
|
</a>
|
||||||
<template v-if="file.error"> ·
|
<template v-if="file.error">
|
||||||
|
·
|
||||||
<a @click.stop.prevent="retry(file)">
|
<a @click.stop.prevent="retry(file)">
|
||||||
<translate translate-context="*/*/*">Retry</translate>
|
<translate translate-context="*/*/*">Retry</translate>
|
||||||
</a>
|
</a>
|
||||||
|
@ -100,20 +181,30 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<upload-metadata-form
|
<upload-metadata-form
|
||||||
:key="selectedUploadId"
|
|
||||||
v-if="selectedUpload"
|
v-if="selectedUpload"
|
||||||
|
:key="selectedUploadId"
|
||||||
:upload="selectedUpload"
|
:upload="selectedUpload"
|
||||||
:values="uploadImportData[selectedUploadId]"
|
:values="uploadImportData[selectedUploadId]"
|
||||||
@values="setDynamic('uploadImportData', selectedUploadId, $event)"></upload-metadata-form>
|
@values="setDynamic('uploadImportData', selectedUploadId, $event)"
|
||||||
<div class="ui message" v-if="step === 2">
|
/>
|
||||||
|
<div
|
||||||
|
v-if="step === 2"
|
||||||
|
class="ui message"
|
||||||
|
>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p>
|
<p>
|
||||||
<i class="info icon"></i>
|
<i class="info icon" />
|
||||||
<translate translate-context="Content/Library/Paragraph" :translate-params="{extensions: $store.state.ui.supportedExtensions.join(', ')}">Supported extensions: %{ extensions }</translate>
|
<translate
|
||||||
|
translate-context="Content/Library/Paragraph"
|
||||||
|
:translate-params="{extensions: $store.state.ui.supportedExtensions.join(', ')}"
|
||||||
|
>
|
||||||
|
Supported extensions: %{ extensions }
|
||||||
|
</translate>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<file-upload-widget
|
<file-upload-widget
|
||||||
|
ref="upload"
|
||||||
:class="['ui', 'icon', 'basic', 'button', 'channels', {hidden: step === 3}]"
|
:class="['ui', 'icon', 'basic', 'button', 'channels', {hidden: step === 3}]"
|
||||||
:post-action="uploadUrl"
|
:post-action="uploadUrl"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
|
@ -121,21 +212,25 @@
|
||||||
:drop="true"
|
:drop="true"
|
||||||
:extensions="$store.state.ui.supportedExtensions"
|
:extensions="$store.state.ui.supportedExtensions"
|
||||||
:value="files"
|
:value="files"
|
||||||
@input="updateFiles"
|
|
||||||
name="audio_file"
|
name="audio_file"
|
||||||
:thread="1"
|
:thread="1"
|
||||||
|
@input="updateFiles"
|
||||||
@input-file="inputFile"
|
@input-file="inputFile"
|
||||||
ref="upload">
|
>
|
||||||
<div>
|
<div>
|
||||||
<i class="upload icon"></i>
|
<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>
|
<translate translate-context="Content/Channels/Paragraph">
|
||||||
|
Drag and drop your files here or open the browser to upload your files
|
||||||
|
</translate>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui very small divider"></div>
|
<div class="ui very small divider" />
|
||||||
<div>
|
<div>
|
||||||
<translate translate-context="*/*/*">Browse…</translate>
|
<translate translate-context="*/*/*">
|
||||||
|
Browse…
|
||||||
|
</translate>
|
||||||
</div>
|
</div>
|
||||||
</file-upload-widget>
|
</file-upload-widget>
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</form>
|
</form>
|
||||||
|
@ -146,31 +241,31 @@ import $ from 'jquery'
|
||||||
|
|
||||||
import LicenseSelect from '@/components/channels/LicenseSelect'
|
import LicenseSelect from '@/components/channels/LicenseSelect'
|
||||||
import AlbumSelect from '@/components/channels/AlbumSelect'
|
import AlbumSelect from '@/components/channels/AlbumSelect'
|
||||||
import FileUploadWidget from "@/components/library/FileUploadWidget";
|
import FileUploadWidget from '@/components/library/FileUploadWidget'
|
||||||
import UploadMetadataForm from '@/components/channels/UploadMetadataForm'
|
import UploadMetadataForm from '@/components/channels/UploadMetadataForm'
|
||||||
|
|
||||||
function setIfEmpty (obj, k, v) {
|
function setIfEmpty (obj, k, v) {
|
||||||
if (obj[k] != undefined) {
|
if (obj[k] !== undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
obj[k] = v
|
obj[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
channel: {type: Object, default: null, required: false},
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
AlbumSelect,
|
AlbumSelect,
|
||||||
LicenseSelect,
|
LicenseSelect,
|
||||||
FileUploadWidget,
|
FileUploadWidget,
|
||||||
UploadMetadataForm,
|
UploadMetadataForm
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
channel: { type: Object, default: null, required: false }
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
availableChannels: {
|
availableChannels: {
|
||||||
results: [],
|
results: [],
|
||||||
count: 0,
|
count: 0
|
||||||
},
|
},
|
||||||
audioMetadata: {},
|
audioMetadata: {},
|
||||||
uploadData: {},
|
uploadData: {},
|
||||||
|
@ -180,29 +275,22 @@ export default {
|
||||||
errors: [],
|
errors: [],
|
||||||
removed: [],
|
removed: [],
|
||||||
includeDraftUploads: null,
|
includeDraftUploads: null,
|
||||||
uploadUrl: this.$store.getters['instance/absoluteUrl']("/api/v1/uploads/"),
|
uploadUrl: this.$store.getters['instance/absoluteUrl']('/api/v1/uploads/'),
|
||||||
quotaStatus: null,
|
quotaStatus: null,
|
||||||
isLoadingStep1: true,
|
isLoadingStep1: true,
|
||||||
step: 1,
|
step: 1,
|
||||||
values: {
|
values: {
|
||||||
channel: (this.channel || {}).uuid,
|
channel: (this.channel || {}).uuid,
|
||||||
license: null,
|
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: {
|
computed: {
|
||||||
labels () {
|
labels () {
|
||||||
return {
|
return {
|
||||||
editTitle: this.$pgettext('Content/*/Button.Label/Verb', 'Edit'),
|
editTitle: this.$pgettext('Content/*/Button.Label/Verb', 'Edit')
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -210,7 +298,7 @@ export default {
|
||||||
return {
|
return {
|
||||||
channel: this.values.channel,
|
channel: this.values.channel,
|
||||||
import_status: 'draft',
|
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 () {
|
remainingSpace () {
|
||||||
|
@ -220,18 +308,18 @@ export default {
|
||||||
return Math.max(0, this.quotaStatus.remaining - (this.uploadedSize / (1000 * 1000)))
|
return Math.max(0, this.quotaStatus.remaining - (this.uploadedSize / (1000 * 1000)))
|
||||||
},
|
},
|
||||||
selectedChannel () {
|
selectedChannel () {
|
||||||
let self = this
|
const self = this
|
||||||
return this.availableChannels.results.filter((c) => {
|
return this.availableChannels.results.filter((c) => {
|
||||||
return c.uuid === self.values.channel
|
return c.uuid === self.values.channel
|
||||||
})[0]
|
})[0]
|
||||||
},
|
},
|
||||||
selectedUpload () {
|
selectedUpload () {
|
||||||
let self = this
|
const self = this
|
||||||
if (!this.selectedUploadId) {
|
if (!this.selectedUploadId) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
let selected = this.uploadedFiles.filter((f) => {
|
const selected = this.uploadedFiles.filter((f) => {
|
||||||
return f.response && f.response.uuid == self.selectedUploadId
|
return f.response && f.response.uuid === self.selectedUploadId
|
||||||
})[0]
|
})[0]
|
||||||
return {
|
return {
|
||||||
...selected.response,
|
...selected.response,
|
||||||
|
@ -239,27 +327,24 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
uploadedFilesById () {
|
uploadedFilesById () {
|
||||||
let data = {}
|
const data = {}
|
||||||
this.uploadedFiles.forEach((u) => {
|
this.uploadedFiles.forEach((u) => {
|
||||||
data[u.response.uuid] = u
|
data[u.response.uuid] = u
|
||||||
})
|
})
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
uploadedFiles () {
|
uploadedFiles () {
|
||||||
let self = this
|
const self = this
|
||||||
self.uploadData
|
const files = this.files.map((f) => {
|
||||||
self.audioMetadata
|
const data = {
|
||||||
let files = this.files.map((f) => {
|
|
||||||
let data = {
|
|
||||||
...f,
|
...f,
|
||||||
_fileObj: f,
|
_fileObj: f,
|
||||||
metadata: {}
|
metadata: {}
|
||||||
}
|
}
|
||||||
let metadata = {}
|
|
||||||
if (f.response && f.response.uuid) {
|
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 = {
|
data.metadata = {
|
||||||
...uploadImportMetadata,
|
...uploadImportMetadata
|
||||||
}
|
}
|
||||||
data.removed = self.removed.indexOf(f.response.uuid) >= 0
|
data.removed = self.removed.indexOf(f.response.uuid) >= 0
|
||||||
}
|
}
|
||||||
|
@ -308,7 +393,7 @@ export default {
|
||||||
canSubmit: !this.activeFile && this.uploadedFiles.length > 0,
|
canSubmit: !this.activeFile && this.uploadedFiles.length > 0,
|
||||||
speed,
|
speed,
|
||||||
remaining,
|
remaining,
|
||||||
quotaStatus: this.quotaStatus,
|
quotaStatus: this.quotaStatus
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
totalSize () {
|
totalSize () {
|
||||||
|
@ -335,44 +420,92 @@ export default {
|
||||||
})[0]
|
})[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: {
|
methods: {
|
||||||
async fetchChannels () {
|
async fetchChannels () {
|
||||||
let response = await axios.get('channels/', {params: {scope: 'me'}})
|
const response = await axios.get('channels/', { params: { scope: 'me' } })
|
||||||
this.availableChannels = response.data
|
this.availableChannels = response.data
|
||||||
},
|
},
|
||||||
async patchUpload (id, 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.uploadData[id] = response.data
|
||||||
this.uploadImportData[id] = response.data.import_metadata
|
this.uploadImportData[id] = response.data.import_metadata
|
||||||
},
|
},
|
||||||
fetchQuota () {
|
fetchQuota () {
|
||||||
let self = this
|
const self = this
|
||||||
axios.get('users/me/').then((response) => {
|
axios.get('users/me/').then((response) => {
|
||||||
self.quotaStatus = response.data.quota_status
|
self.quotaStatus = response.data.quota_status
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
publish () {
|
publish () {
|
||||||
let self = this
|
const self = this
|
||||||
self.isLoading = true
|
self.isLoading = true
|
||||||
self.errors = []
|
self.errors = []
|
||||||
let ids = this.uploadedFiles.map((f) => {
|
const ids = this.uploadedFiles.map((f) => {
|
||||||
return f.response.uuid
|
return f.response.uuid
|
||||||
})
|
})
|
||||||
let payload = {
|
const payload = {
|
||||||
action: 'publish',
|
action: 'publish',
|
||||||
objects: ids,
|
objects: ids
|
||||||
}
|
}
|
||||||
return axios.post('uploads/action/', payload).then(
|
return axios.post('uploads/action/', payload).then(
|
||||||
response => {
|
response => {
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
self.$emit("published", {
|
self.$emit('published', {
|
||||||
uploads: self.uploadedFiles.map((u) => {
|
uploads: self.uploadedFiles.map((u) => {
|
||||||
return {
|
return {
|
||||||
...u.response,
|
...u.response,
|
||||||
import_status: 'pending',
|
import_status: 'pending'
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
channel: self.selectedChannel})
|
channel: self.selectedChannel
|
||||||
|
})
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
self.errors = error.backendErrors
|
self.errors = error.backendErrors
|
||||||
|
@ -380,32 +513,31 @@ export default {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
setupChannelsDropdown () {
|
setupChannelsDropdown () {
|
||||||
let self = this
|
const self = this
|
||||||
$(this.$el).find('#channel-dropdown').dropdown({
|
$(this.$el).find('#channel-dropdown').dropdown({
|
||||||
onChange (value, text, $choice) {
|
onChange (value, text, $choice) {
|
||||||
self.values.channel = value
|
self.values.channel = value
|
||||||
},
|
},
|
||||||
values: this.availableChannels.results.map((c) => {
|
values: this.availableChannels.results.map((c) => {
|
||||||
let d = {
|
const d = {
|
||||||
name: c.artist.name,
|
name: c.artist.name,
|
||||||
value: c.uuid,
|
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) {
|
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
|
d.image = coverUrl
|
||||||
if (c.artist.content_category === 'podcast') {
|
if (c.artist.content_category === 'podcast') {
|
||||||
d.imageClass = 'ui image'
|
d.imageClass = 'ui image'
|
||||||
} else {
|
} else {
|
||||||
d.imageClass = "ui avatar image"
|
d.imageClass = 'ui avatar image'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
d.icon = "user"
|
d.icon = 'user'
|
||||||
if (c.artist.content_category === 'podcast') {
|
if (c.artist.content_category === 'podcast') {
|
||||||
d.iconClass = "bordered icon"
|
d.iconClass = 'bordered icon'
|
||||||
} else {
|
} else {
|
||||||
d.iconClass = "circular icon"
|
d.iconClass = 'circular icon'
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return d
|
return d
|
||||||
|
@ -413,23 +545,23 @@ export default {
|
||||||
})
|
})
|
||||||
$(this.$el).find('#channel-dropdown').dropdown('hide')
|
$(this.$el).find('#channel-dropdown').dropdown('hide')
|
||||||
},
|
},
|
||||||
inputFile(newFile, oldFile) {
|
inputFile (newFile, oldFile) {
|
||||||
if (!newFile) {
|
if (!newFile) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.remainingSpace < newFile.size / (1000 * 1000)) {
|
if (this.remainingSpace < newFile.size / (1000 * 1000)) {
|
||||||
newFile.error = 'denied'
|
newFile.error = 'denied'
|
||||||
} else {
|
} else {
|
||||||
this.$refs.upload.active = true;
|
this.$refs.upload.active = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetchAudioMetadata (uuid) {
|
fetchAudioMetadata (uuid) {
|
||||||
let self = this
|
const self = this
|
||||||
self.audioMetadata[uuid] = null
|
self.audioMetadata[uuid] = null
|
||||||
axios.get(`uploads/${uuid}/audio-file-metadata/`).then((response) => {
|
axios.get(`uploads/${uuid}/audio-file-metadata/`).then((response) => {
|
||||||
self.setDynamic('audioMetadata', uuid, response.data)
|
self.setDynamic('audioMetadata', uuid, response.data)
|
||||||
let uploadedFile = self.uploadedFilesById[uuid]
|
const uploadedFile = self.uploadedFilesById[uuid]
|
||||||
if (uploadedFile._fileObj && uploadedFile.response.import_metadata.title === uploadedFile._fileObj.name.replace(/\.[^/.]+$/, "") && response.data.title) {
|
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
|
// replace existing title deduced from file by the one in audio file metadat, if any
|
||||||
self.uploadImportData[uuid].title = response.data.title
|
self.uploadImportData[uuid].title = response.data.title
|
||||||
} else {
|
} else {
|
||||||
|
@ -439,17 +571,17 @@ export default {
|
||||||
setIfEmpty(self.uploadImportData[uuid], 'position', response.data.position)
|
setIfEmpty(self.uploadImportData[uuid], 'position', response.data.position)
|
||||||
setIfEmpty(self.uploadImportData[uuid], 'tags', response.data.tags)
|
setIfEmpty(self.uploadImportData[uuid], 'tags', response.data.tags)
|
||||||
setIfEmpty(self.uploadImportData[uuid], 'description', (response.data.description || {}).text)
|
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) {
|
setDynamic (objName, key, data) {
|
||||||
// cf https://vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
|
// cf https://vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
|
||||||
let newData = {}
|
const newData = {}
|
||||||
newData[key] = data
|
newData[key] = data
|
||||||
this[objName] = Object.assign({}, this[objName], newData)
|
this[objName] = Object.assign({}, this[objName], newData)
|
||||||
},
|
},
|
||||||
updateFiles (value) {
|
updateFiles (value) {
|
||||||
let self = this
|
const self = this
|
||||||
this.files = value
|
this.files = value
|
||||||
this.files.forEach((f) => {
|
this.files.forEach((f) => {
|
||||||
if (f.response && f.response.uuid && self.audioMetadata[f.response.uuid] === undefined) {
|
if (f.response && f.response.uuid && self.audioMetadata[f.response.uuid] === undefined) {
|
||||||
|
@ -462,9 +594,9 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async fetchDraftUploads (channel) {
|
async fetchDraftUploads (channel) {
|
||||||
let self = this
|
const self = this
|
||||||
this.draftUploads = null
|
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 = response.data.results
|
||||||
this.draftUploads.forEach((u) => {
|
this.draftUploads.forEach((u) => {
|
||||||
self.uploadImportData[u.uuid] = u.import_metadata
|
self.uploadImportData[u.uuid] = u.import_metadata
|
||||||
|
@ -479,49 +611,8 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
retry (file) {
|
retry (file) {
|
||||||
this.$refs.upload.update(file, {error: '', progress: '0.00'})
|
this.$refs.upload.update(file, { error: '', progress: '0.00' })
|
||||||
this.$refs.upload.active = true;
|
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,
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,59 +4,79 @@
|
||||||
<label for="upload-title">
|
<label for="upload-title">
|
||||||
<translate translate-context="*/*/*/Noun">Title</translate>
|
<translate translate-context="*/*/*/Noun">Title</translate>
|
||||||
</label>
|
</label>
|
||||||
<input type="text" v-model="newValues.title">
|
<input
|
||||||
|
v-model="newValues.title"
|
||||||
|
type="text"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<attachment-input
|
<attachment-input
|
||||||
v-model="newValues.cover"
|
v-model="newValues.cover"
|
||||||
:required="false"
|
:required="false"
|
||||||
@delete="newValues.cover = null">
|
@delete="newValues.cover = null"
|
||||||
<translate translate-context="Content/Channel/*" slot="label">Track Picture</translate>
|
>
|
||||||
|
<translate
|
||||||
|
slot="label"
|
||||||
|
translate-context="Content/Channel/*"
|
||||||
|
>
|
||||||
|
Track Picture
|
||||||
|
</translate>
|
||||||
</attachment-input>
|
</attachment-input>
|
||||||
<div class="ui small hidden divider"></div>
|
<div class="ui small hidden divider" />
|
||||||
<div class="ui two fields">
|
<div class="ui two fields">
|
||||||
<div class="ui field">
|
<div class="ui field">
|
||||||
<label for="upload-tags">
|
<label for="upload-tags">
|
||||||
<translate translate-context="*/*/*/Noun">Tags</translate>
|
<translate translate-context="*/*/*/Noun">Tags</translate>
|
||||||
</label>
|
</label>
|
||||||
<tags-selector
|
<tags-selector
|
||||||
v-model="newValues.tags"
|
|
||||||
id="upload-tags"
|
id="upload-tags"
|
||||||
:required="false"></tags-selector>
|
v-model="newValues.tags"
|
||||||
|
:required="false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui field">
|
<div class="ui field">
|
||||||
<label for="upload-position">
|
<label for="upload-position">
|
||||||
<translate translate-context="*/*/*/Short, Noun">Position</translate>
|
<translate translate-context="*/*/*/Short, Noun">Position</translate>
|
||||||
</label>
|
</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>
|
</div>
|
||||||
<div class="ui field">
|
<div class="ui field">
|
||||||
<label for="upload-description">
|
<label for="upload-description">
|
||||||
<translate translate-context="*/*/*">Description</translate>
|
<translate translate-context="*/*/*">Description</translate>
|
||||||
</label>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
|
||||||
import TagsSelector from '@/components/library/TagsSelector'
|
import TagsSelector from '@/components/library/TagsSelector'
|
||||||
import AttachmentInput from '@/components/common/AttachmentInput'
|
import AttachmentInput from '@/components/common/AttachmentInput'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['upload', 'values'],
|
|
||||||
components: {
|
components: {
|
||||||
TagsSelector,
|
TagsSelector,
|
||||||
AttachmentInput
|
AttachmentInput
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
upload: { type: Object, required: true },
|
||||||
|
values: { type: Object, required: true }
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
newValues: {...this.values} || this.upload.import_metadata
|
newValues: { ...this.values } || this.upload.import_metadata
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isLoading () {
|
isLoading () {
|
||||||
return !!this.metadata
|
return !!this.metadata
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -66,7 +86,7 @@ export default {
|
||||||
this.$emit('values', v)
|
this.$emit('values', v)
|
||||||
},
|
},
|
||||||
immediate: true
|
immediate: true
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,60 +1,142 @@
|
||||||
<template>
|
<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">
|
<h4 class="header">
|
||||||
<translate key="1" v-if="step === 1" translate-context="Popup/Channels/Title/Verb">Publish audio</translate>
|
<translate
|
||||||
<translate key="2" v-else-if="step === 2" translate-context="Popup/Channels/Title">Files to upload</translate>
|
v-if="step === 1"
|
||||||
<translate key="3" v-else-if="step === 3" translate-context="Popup/Channels/Title">Upload details</translate>
|
key="1"
|
||||||
<translate key="4" v-else-if="step === 4" translate-context="Popup/Channels/Title">Processing uploads</translate>
|
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>
|
</h4>
|
||||||
<div class="scrolling content">
|
<div class="scrolling content">
|
||||||
<channel-upload-form
|
<channel-upload-form
|
||||||
ref="uploadForm"
|
ref="uploadForm"
|
||||||
|
:channel="$store.state.channels.uploadModalConfig.channel"
|
||||||
@step="step = $event"
|
@step="step = $event"
|
||||||
@loading="isLoading = $event"
|
@loading="isLoading = $event"
|
||||||
@published="$store.commit('channels/publish', $event)"
|
@published="$store.commit('channels/publish', $event)"
|
||||||
@status="statusData = $event"
|
@status="statusData = $event"
|
||||||
@submittable="submittable = $event"
|
@submittable="submittable = $event"
|
||||||
:channel="$store.state.channels.uploadModalConfig.channel"></channel-upload-form>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<div class="left floated text left align">
|
<div class="left floated text left align">
|
||||||
<template v-if="statusData && step >= 2">
|
<template v-if="statusData && step >= 2">
|
||||||
{{ statusInfo.join(' · ') }}
|
{{ statusInfo.join(' · ') }}
|
||||||
</template>
|
</template>
|
||||||
<div class="ui very small hidden divider"></div>
|
<div class="ui very small hidden divider" />
|
||||||
<template v-if="statusData && statusData.quotaStatus">
|
<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 }}
|
{{ (statusData.quotaStatus.remaining * 1000 * 1000) - statusData.uploadedSize | humanSize }}
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui hidden clearing divider mobile-only"></div>
|
<div class="ui hidden clearing divider mobile-only" />
|
||||||
<button class="ui basic cancel button" v-if="step === 1"><translate translate-context="*/*/Button.Label/Verb">Cancel</translate></button>
|
<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>
|
v-if="step === 1"
|
||||||
<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>
|
class="ui basic cancel 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>
|
<translate translate-context="*/*/Button.Label/Verb">
|
||||||
|
Cancel
|
||||||
|
</translate>
|
||||||
</button>
|
</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
|
<button
|
||||||
:class="['ui', 'primary button', {loading: isLoading}]"
|
:class="['ui', 'primary button', {loading: isLoading}]"
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="!statusData || !statusData.canSubmit"
|
:disabled="!statusData || !statusData.canSubmit"
|
||||||
@click.prevent.stop="$refs.uploadForm.publish">
|
@click.prevent.stop="$refs.uploadForm.publish"
|
||||||
<translate translate-context="*/Channels/Button.Label">Publish</translate>
|
>
|
||||||
|
<translate translate-context="*/Channels/Button.Label">
|
||||||
|
Publish
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
<button class="ui floating dropdown icon button" ref="dropdown" v-dropdown :disabled="!statusData || !statusData.canSubmit">
|
<button
|
||||||
<i class="dropdown icon"></i>
|
ref="dropdown"
|
||||||
|
v-dropdown
|
||||||
|
class="ui floating dropdown icon button"
|
||||||
|
:disabled="!statusData || !statusData.canSubmit"
|
||||||
|
>
|
||||||
|
<i class="dropdown icon" />
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
|
class="basic item"
|
||||||
@click="update(false)"
|
@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>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
</template>
|
</template>
|
||||||
|
@ -62,7 +144,7 @@
|
||||||
<script>
|
<script>
|
||||||
import Modal from '@/components/semantic/Modal'
|
import Modal from '@/components/semantic/Modal'
|
||||||
import ChannelUploadForm from '@/components/channels/UploadForm'
|
import ChannelUploadForm from '@/components/channels/UploadForm'
|
||||||
import {humanSize} from '@/filters'
|
import { humanSize } from '@/filters'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -74,14 +156,9 @@ export default {
|
||||||
step: 1,
|
step: 1,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
submittable: true,
|
submittable: true,
|
||||||
statusData: null,
|
statusData: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
update (v) {
|
|
||||||
this.$store.commit('channels/showUploadModal', {show: v})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
labels () {
|
labels () {
|
||||||
return {}
|
return {}
|
||||||
|
@ -90,14 +167,14 @@ export default {
|
||||||
if (!this.statusData) {
|
if (!this.statusData) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
let info = []
|
const info = []
|
||||||
if (this.statusData.totalSize) {
|
if (this.statusData.totalSize) {
|
||||||
info.push(humanSize(this.statusData.totalSize))
|
info.push(humanSize(this.statusData.totalSize))
|
||||||
}
|
}
|
||||||
if (this.statusData.totalFiles) {
|
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(
|
info.push(
|
||||||
this.$gettextInterpolate(msg, {count: this.statusData.totalFiles}),
|
this.$gettextInterpolate(msg, { count: this.statusData.totalFiles })
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (this.statusData.progress) {
|
if (this.statusData.progress) {
|
||||||
|
@ -107,13 +184,17 @@ export default {
|
||||||
info.push(`${humanSize(this.statusData.speed)}/s`)
|
info.push(`${humanSize(this.statusData.speed)}/s`)
|
||||||
}
|
}
|
||||||
return info
|
return info
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'$store.state.route.path' () {
|
'$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>
|
</script>
|
||||||
|
|
|
@ -1,32 +1,35 @@
|
||||||
<template>
|
<template>
|
||||||
<span class="feedback" v-if="isLoading || isDone">
|
<span
|
||||||
<span v-if="isLoading" :class="['ui', 'active', size, 'inline', 'loader']"></span>
|
v-if="isLoading || isDone"
|
||||||
<i v-if="isDone" :class="['success', size, 'check', 'icon']"></i>
|
class="feedback"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="isLoading"
|
||||||
|
:class="['ui', 'active', size, 'inline', 'loader']"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-if="isDone"
|
||||||
|
:class="['success', size, 'check', 'icon']"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {hashCode, intToRGB} from '@/utils/color'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
isLoading: {type: Boolean, required: true},
|
isLoading: { type: Boolean, required: true },
|
||||||
size: {type: String, default: 'small'},
|
size: { type: String, default: 'small' }
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
timer: null,
|
timer: null,
|
||||||
isDone: false,
|
isDone: false
|
||||||
}
|
|
||||||
},
|
|
||||||
destroyed () {
|
|
||||||
if (this.timer) {
|
|
||||||
clearTimeout(this.timer)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
isLoading (v) {
|
isLoading (v) {
|
||||||
let self = this
|
const self = this
|
||||||
if (v && this.timer) {
|
if (v && this.timer) {
|
||||||
clearTimeout(this.timer)
|
clearTimeout(this.timer)
|
||||||
}
|
}
|
||||||
|
@ -36,10 +39,14 @@ export default {
|
||||||
this.isDone = true
|
this.isDone = true
|
||||||
this.timer = setTimeout(() => {
|
this.timer = setTimeout(() => {
|
||||||
self.isDone = false
|
self.isDone = false
|
||||||
}, (2000));
|
}, (2000))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
if (this.timer) {
|
||||||
|
clearTimeout(this.timer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -4,111 +4,188 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="1000">
|
<th colspan="1000">
|
||||||
<div v-if="refreshable" class="right floated">
|
<div
|
||||||
|
v-if="refreshable"
|
||||||
|
class="right floated"
|
||||||
|
>
|
||||||
<span v-if="needsRefresh">
|
<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>
|
<translate translate-context="Content/*/Button.Help text.Paragraph">Content has been updated, click refresh to see up-to-date content</translate>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
@click="$emit('refresh')"
|
|
||||||
class="ui basic icon button"
|
class="ui basic icon button"
|
||||||
:title="labels.refresh"
|
:title="labels.refresh"
|
||||||
:aria-label="labels.refresh">
|
:aria-label="labels.refresh"
|
||||||
<i class="refresh icon"></i>
|
@click="$emit('refresh')"
|
||||||
|
>
|
||||||
|
<i class="refresh icon" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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="ui inline fields">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="actions-select"><translate translate-context="Content/*/*/Noun">Actions</translate></label>
|
<label for="actions-select"><translate translate-context="Content/*/*/Noun">Actions</translate></label>
|
||||||
<select id="actions-select" class="ui dropdown" v-model="currentActionName">
|
<select
|
||||||
<option v-for="action in actions" :value="action.name">
|
id="actions-select"
|
||||||
|
v-model="currentActionName"
|
||||||
|
class="ui dropdown"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="(action, key) in actions"
|
||||||
|
:key="key"
|
||||||
|
:value="action.name"
|
||||||
|
>
|
||||||
{{ action.label }}
|
{{ action.label }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<dangerous-button
|
<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-color="currentAction.confirmColor || 'success'"
|
||||||
@confirm="launchAction" :aria-label="labels.performAction">
|
:aria-label="labels.performAction"
|
||||||
<translate translate-context="Content/*/Button.Label/Short, Verb">Go</translate>
|
@confirm="launchAction"
|
||||||
|
>
|
||||||
|
<translate translate-context="Content/*/Button.Label/Short, Verb">
|
||||||
|
Go
|
||||||
|
</translate>
|
||||||
<p slot="modal-header">
|
<p slot="modal-header">
|
||||||
<translate translate-context="Modal/*/Title"
|
<translate
|
||||||
key="1"
|
key="1"
|
||||||
|
translate-context="Modal/*/Title"
|
||||||
:translate-n="affectedObjectsCount"
|
:translate-n="affectedObjectsCount"
|
||||||
:translate-params="{count: affectedObjectsCount, action: currentActionName}"
|
: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?
|
Do you want to launch %{ action } on %{ count } element?
|
||||||
</translate>
|
</translate>
|
||||||
</p>
|
</p>
|
||||||
<p slot="modal-content">
|
<p slot="modal-content">
|
||||||
<template v-if="currentAction.confirmationMessage">{{ currentAction.confirmationMessage }}</template>
|
<template v-if="currentAction.confirmationMessage">
|
||||||
<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>
|
{{ 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>
|
</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>
|
</dangerous-button>
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
@click="launchAction"
|
|
||||||
:disabled="checked.length === 0"
|
:disabled="checked.length === 0"
|
||||||
:aria-label="labels.performAction"
|
:aria-label="labels.performAction"
|
||||||
:class="['ui', {disabled: checked.length === 0}, {'loading': actionLoading}, 'button']">
|
:class="['ui', {disabled: checked.length === 0}, {'loading': actionLoading}, 'button']"
|
||||||
<translate translate-context="Content/*/Button.Label/Short, Verb">Go</translate></button>
|
@click="launchAction"
|
||||||
|
>
|
||||||
|
<translate translate-context="Content/*/Button.Label/Short, Verb">
|
||||||
|
Go
|
||||||
|
</translate>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="count field">
|
<div class="count field">
|
||||||
<translate translate-context="Content/*/Paragraph"
|
<translate
|
||||||
tag="span"
|
|
||||||
v-if="selectAll"
|
v-if="selectAll"
|
||||||
key="1"
|
key="1"
|
||||||
|
translate-context="Content/*/Paragraph"
|
||||||
|
tag="span"
|
||||||
:translate-n="objectsData.count"
|
:translate-n="objectsData.count"
|
||||||
:translate-params="{count: objectsData.count, total: 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
|
All %{ count } element selected
|
||||||
</translate>
|
</translate>
|
||||||
<translate translate-context="Content/*/Paragraph"
|
<translate
|
||||||
tag="span"
|
|
||||||
v-else
|
v-else
|
||||||
key="2"
|
key="2"
|
||||||
|
translate-context="Content/*/Paragraph"
|
||||||
|
tag="span"
|
||||||
:translate-n="checked.length"
|
:translate-n="checked.length"
|
||||||
:translate-params="{count: checked.length, total: objectsData.count}"
|
:translate-params="{count: checked.length, total: objectsData.count}"
|
||||||
translate-plural="%{ count } on %{ total } selected">
|
translate-plural="%{ count } on %{ total } selected"
|
||||||
|
>
|
||||||
%{ count } on %{ total } selected
|
%{ count } on %{ total } selected
|
||||||
</translate>
|
</translate>
|
||||||
<template v-if="currentAction.allowAll && checkable.length > 0 && checkable.length === checked.length">
|
<template v-if="currentAction.allowAll && checkable.length > 0 && checkable.length === checked.length">
|
||||||
<a @click.prevent="selectAll = true" v-if="!selectAll" href="">
|
<a
|
||||||
<translate translate-context="Content/*/Link/Verb"
|
v-if="!selectAll"
|
||||||
|
href=""
|
||||||
|
@click.prevent="selectAll = true"
|
||||||
|
>
|
||||||
|
<translate
|
||||||
key="3"
|
key="3"
|
||||||
|
translate-context="Content/*/Link/Verb"
|
||||||
:translate-n="objectsData.count"
|
:translate-n="objectsData.count"
|
||||||
:translate-params="{total: objectsData.count}"
|
:translate-params="{total: objectsData.count}"
|
||||||
translate-plural="Select all %{ total } elements">
|
translate-plural="Select all %{ total } elements"
|
||||||
|
>
|
||||||
Select one element
|
Select one element
|
||||||
</translate>
|
</translate>
|
||||||
</a>
|
</a>
|
||||||
<a @click.prevent="selectAll = false" v-else href="">
|
<a
|
||||||
<translate translate-context="Content/*/Link/Verb" key="4">Select only current page</translate>
|
v-else
|
||||||
|
href=""
|
||||||
|
@click.prevent="selectAll = false"
|
||||||
|
>
|
||||||
|
<translate
|
||||||
|
key="4"
|
||||||
|
translate-context="Content/*/Link/Verb"
|
||||||
|
>Select only current page</translate>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="actionErrors.length > 0" role="alert" class="ui negative message">
|
<div
|
||||||
<h4 class="header"><translate translate-context="Content/*/Error message/Header">Error while applying action</translate></h4>
|
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">
|
<ul class="list">
|
||||||
<li v-for="error in actionErrors">{{ error }}</li>
|
<li
|
||||||
|
v-for="(error, key) in actionErrors"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="actionResult" class="ui positive message">
|
<div
|
||||||
|
v-if="actionResult"
|
||||||
|
class="ui positive message"
|
||||||
|
>
|
||||||
<p>
|
<p>
|
||||||
<translate translate-context="Content/*/Paragraph"
|
<translate
|
||||||
|
translate-context="Content/*/Paragraph"
|
||||||
:translate-n="actionResult.updated"
|
:translate-n="actionResult.updated"
|
||||||
:translate-params="{count: actionResult.updated, action: actionResult.action}"
|
: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
|
Action %{ action } was launched successfully on %{ count } element
|
||||||
</translate>
|
</translate>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<slot name="action-success-footer" :result="actionResult">
|
<slot
|
||||||
</slot>
|
name="action-success-footer"
|
||||||
|
:result="actionResult"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
@ -118,26 +195,37 @@
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@change="toggleCheckAll"
|
|
||||||
:aria-label="labels.selectAllItems"
|
:aria-label="labels.selectAllItems"
|
||||||
:disabled="checkable.length === 0"
|
:disabled="checkable.length === 0"
|
||||||
:checked="checkable.length > 0 && checked.length === checkable.length">
|
:checked="checkable.length > 0 && checked.length === checkable.length"
|
||||||
|
@change="toggleCheckAll"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<slot name="header-cells"></slot>
|
<slot name="header-cells" />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody v-if="objectsData.count > 0">
|
<tbody v-if="objectsData.count > 0">
|
||||||
<tr v-for="(obj, index) in objects">
|
<tr
|
||||||
<td v-if="actions.length > 0" class="collapsing">
|
v-for="(obj, index) in objects"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
v-if="actions.length > 0"
|
||||||
|
class="collapsing"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:aria-label="labels.selectItem"
|
:aria-label="labels.selectItem"
|
||||||
:disabled="checkable.indexOf(getId(obj)) === -1"
|
:disabled="checkable.indexOf(getId(obj)) === -1"
|
||||||
|
:checked="checked.indexOf(getId(obj)) > -1"
|
||||||
@click="toggleCheck($event, getId(obj), index)"
|
@click="toggleCheck($event, getId(obj), index)"
|
||||||
:checked="checked.indexOf(getId(obj)) > -1">
|
>
|
||||||
</td>
|
</td>
|
||||||
<slot name="row-cells" :obj="obj"></slot>
|
<slot
|
||||||
|
name="row-cells"
|
||||||
|
:obj="obj"
|
||||||
|
/>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -147,19 +235,19 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
export default {
|
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: {},
|
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 () {
|
data () {
|
||||||
let d = {
|
const d = {
|
||||||
checked: [],
|
checked: [],
|
||||||
actionLoading: false,
|
actionLoading: false,
|
||||||
actionResult: null,
|
actionResult: null,
|
||||||
|
@ -173,86 +261,20 @@ export default {
|
||||||
}
|
}
|
||||||
return d
|
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: {
|
computed: {
|
||||||
currentAction () {
|
currentAction () {
|
||||||
let self = this
|
const self = this
|
||||||
return this.actions.filter((a) => {
|
return this.actions.filter((a) => {
|
||||||
return a.name === self.currentActionName
|
return a.name === self.currentActionName
|
||||||
})[0]
|
})[0]
|
||||||
},
|
},
|
||||||
checkable () {
|
checkable () {
|
||||||
let self = this
|
const self = this
|
||||||
if (!this.currentAction) {
|
if (!this.currentAction) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
let objs = this.objectsData.results
|
let objs = this.objectsData.results
|
||||||
let filter = this.currentAction.filterCheckable
|
const filter = this.currentAction.filterCheckable
|
||||||
if (filter) {
|
if (filter) {
|
||||||
objs = objs.filter((o) => {
|
objs = objs.filter((o) => {
|
||||||
return filter(o)
|
return filter(o)
|
||||||
|
@ -261,9 +283,9 @@ export default {
|
||||||
return objs.map((o) => { return self.getId(o) })
|
return objs.map((o) => { return self.getId(o) })
|
||||||
},
|
},
|
||||||
objects () {
|
objects () {
|
||||||
let self = this
|
const self = this
|
||||||
return this.objectsData.results.map((o) => {
|
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)
|
return self.getId(co) === self.getId(o)
|
||||||
})[0]
|
})[0]
|
||||||
if (custom) {
|
if (custom) {
|
||||||
|
@ -298,11 +320,77 @@ export default {
|
||||||
currentActionName () {
|
currentActionName () {
|
||||||
// we update checked status as some actions have specific filters
|
// we update checked status as some actions have specific filters
|
||||||
// on what is checkable or not
|
// on what is checkable or not
|
||||||
let self = this
|
const self = this
|
||||||
this.checked = this.checked.filter(r => {
|
this.checked = this.checked.filter(r => {
|
||||||
return self.checkable.indexOf(r) > -1
|
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>
|
</script>
|
||||||
|
|
|
@ -1,13 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<img alt="" v-if="actor.icon && actor.icon.urls.original" :src="actor.icon.urls.medium_square_crop" class="ui avatar circular image" />
|
<img
|
||||||
<span v-else :style="defaultAvatarStyle" class="ui avatar circular label">{{ actor.preferred_username[0]}}</span>
|
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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {hashCode, intToRGB} from '@/utils/color'
|
import { hashCode, intToRGB } from '@/utils/color'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['actor'],
|
props: { actor: { type: Object, required: true } },
|
||||||
computed: {
|
computed: {
|
||||||
actorColor () {
|
actorColor () {
|
||||||
return intToRGB(hashCode(this.actor.full_username))
|
return intToRGB(hashCode(this.actor.full_username))
|
||||||
|
|
|
@ -1,29 +1,33 @@
|
||||||
<template>
|
<template>
|
||||||
<router-link :to="url" :title="actor.full_username">
|
<router-link
|
||||||
<template v-if="avatar"><actor-avatar :actor="actor" /><span> </span></template><slot>{{ repr | truncate(truncateLength) }}</slot>
|
:to="url"
|
||||||
|
:title="actor.full_username"
|
||||||
|
>
|
||||||
|
<template v-if="avatar">
|
||||||
|
<actor-avatar :actor="actor" /><span> </span>
|
||||||
|
</template><slot>{{ repr | truncate(truncateLength) }}</slot>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {hashCode, intToRGB} from '@/utils/color'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
actor: {type: Object},
|
actor: { type: Object, required: true },
|
||||||
avatar: {type: Boolean, default: true},
|
avatar: { type: Boolean, default: true },
|
||||||
admin: {type: Boolean, default: false},
|
admin: { type: Boolean, default: false },
|
||||||
displayName: {type: Boolean, default: false},
|
displayName: { type: Boolean, default: false },
|
||||||
truncateLength: {type: Number, default: 30},
|
truncateLength: { type: Number, default: 30 }
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
url () {
|
url () {
|
||||||
if (this.admin) {
|
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) {
|
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 {
|
} 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 () {
|
repr () {
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<button @click="ajaxCall" :class="['ui', {loading: isLoading}, 'button']">
|
<button
|
||||||
<slot></slot>
|
:class="['ui', {loading: isLoading}, 'button']"
|
||||||
|
@click="ajaxCall"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
@ -8,17 +11,17 @@ import axios from 'axios'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
url: {type: String, required: true},
|
url: { type: String, required: true },
|
||||||
method: {type: String, required: true},
|
method: { type: String, required: true }
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
ajaxCall () {
|
ajaxCall () {
|
||||||
var self = this
|
const self = this
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
axios[this.method](this.url).then(response => {
|
axios[this.method](this.url).then(response => {
|
||||||
self.$emit('action-done', response.data)
|
self.$emit('action-done', response.data)
|
||||||
|
|
|
@ -1,36 +1,84 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="ui form">
|
<div class="ui form">
|
||||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
<div
|
||||||
<h4 class="header"><translate translate-context="Content/*/Error message.Title">Your attachment cannot be saved</translate></h4>
|
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">
|
<ul class="list">
|
||||||
<li v-for="error in errors">{{ error }}</li>
|
<li
|
||||||
|
v-for="(error, key) in errors"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui field">
|
<div class="ui field">
|
||||||
<span id="avatarLabel">
|
<span id="avatarLabel">
|
||||||
<slot name="label"></slot>
|
<slot name="label" />
|
||||||
</span>
|
</span>
|
||||||
<div class="ui stackable grid row">
|
<div class="ui stackable grid row">
|
||||||
<div class="three wide column">
|
<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
|
||||||
<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`)" />
|
v-if="value && value === initialValue"
|
||||||
<div :class="['ui', imageClass, 'static', 'large placeholder image']" v-else></div>
|
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>
|
||||||
<div class="eleven wide column">
|
<div class="eleven wide column">
|
||||||
<div class="file-input">
|
<div class="file-input">
|
||||||
<label :for="attachmentId">
|
<label :for="attachmentId">
|
||||||
<translate translate-context="*/*/*">Upload New Picture…</translate>
|
<translate translate-context="*/*/*">Upload New Picture…</translate>
|
||||||
</label>
|
</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>
|
||||||
<div class="ui very small hidden divider"></div>
|
<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>
|
<p>
|
||||||
<button class="ui basic tiny button" v-if="value" @click.stop.prevent="remove(value)">
|
<translate translate-context="Content/*/Paragraph">
|
||||||
<translate translate-context="Content/Radio/Button.Label/Verb">Remove</translate>
|
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>
|
</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">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -43,8 +91,8 @@ import axios from 'axios'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
value: {},
|
value: { type: String, required: true },
|
||||||
imageClass: {default: '', required: false}
|
imageClass: { type: String, default: '', required: false }
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -52,21 +100,29 @@ export default {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
errors: [],
|
errors: [],
|
||||||
initialValue: this.value,
|
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: {
|
methods: {
|
||||||
submit() {
|
submit () {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.errors = []
|
this.errors = []
|
||||||
let self = this
|
const self = this
|
||||||
this.file = this.$refs.attachment.files[0]
|
this.file = this.$refs.attachment.files[0]
|
||||||
let formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append("file", this.file)
|
formData.append('file', this.file)
|
||||||
axios
|
axios
|
||||||
.post(`attachments/`, formData, {
|
.post('attachments/', formData, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
'Content-Type': 'multipart/form-data'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(
|
.then(
|
||||||
|
@ -81,10 +137,10 @@ export default {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
remove(uuid) {
|
remove (uuid) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.errors = []
|
this.errors = []
|
||||||
let self = this
|
const self = this
|
||||||
axios.delete(`attachments/${uuid}/`)
|
axios.delete(`attachments/${uuid}/`)
|
||||||
.then(
|
.then(
|
||||||
response => {
|
response => {
|
||||||
|
@ -97,14 +153,6 @@ export default {
|
||||||
self.errors = error.backendErrors
|
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>
|
<template>
|
||||||
<a role="button" class="collapse link" @click.prevent="$emit('input', !value)">
|
<a
|
||||||
<translate v-if="isCollapsed" key="1" translate-context="*/*/Button,Label">Expand</translate>
|
role="button"
|
||||||
<translate v-else key="2" translate-context="*/*/Button,Label">Collapse</translate>
|
class="collapse link"
|
||||||
<i :class="[{down: !isCollapsed}, {right: isCollapsed}, 'angle', 'icon']"></i>
|
@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>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
value: {type: Boolean, required: true},
|
value: { type: Boolean, required: true }
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isCollapsed () {
|
isCollapsed () {
|
||||||
|
|
|
@ -2,48 +2,71 @@
|
||||||
<div class="content-form ui segments">
|
<div class="content-form ui segments">
|
||||||
<div class="ui segment">
|
<div class="ui segment">
|
||||||
<div class="ui tiny secondary pointing menu">
|
<div class="ui tiny secondary pointing menu">
|
||||||
<button @click.prevent="isPreviewing = false" :class="[{active: !isPreviewing}, 'item']">
|
<button
|
||||||
<translate translate-context="*/Form/Menu.item">Write</translate>
|
:class="[{active: !isPreviewing}, 'item']"
|
||||||
|
@click.prevent="isPreviewing = false"
|
||||||
|
>
|
||||||
|
<translate translate-context="*/Form/Menu.item">
|
||||||
|
Write
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
<button @click.prevent="isPreviewing = true" :class="[{active: isPreviewing}, 'item']">
|
<button
|
||||||
<translate translate-context="*/Form/Menu.item">Preview</translate>
|
:class="[{active: isPreviewing}, 'item']"
|
||||||
|
@click.prevent="isPreviewing = true"
|
||||||
|
>
|
||||||
|
<translate translate-context="*/Form/Menu.item">
|
||||||
|
Preview
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="isPreviewing" >
|
<template v-if="isPreviewing">
|
||||||
|
<div
|
||||||
<div class="ui placeholder" v-if="isLoadingPreview">
|
v-if="isLoadingPreview"
|
||||||
|
class="ui placeholder"
|
||||||
|
>
|
||||||
<div class="paragraph">
|
<div class="paragraph">
|
||||||
<div class="line"></div>
|
<div class="line" />
|
||||||
<div class="line"></div>
|
<div class="line" />
|
||||||
<div class="line"></div>
|
<div class="line" />
|
||||||
<div class="line"></div>
|
<div class="line" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-else-if="preview === null">
|
<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>
|
</p>
|
||||||
<div v-html="preview" v-else></div>
|
<div
|
||||||
|
v-else
|
||||||
|
v-html="preview"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="ui transparent input">
|
<div class="ui transparent input">
|
||||||
<textarea
|
<textarea
|
||||||
ref="textarea"
|
|
||||||
:name="fieldId"
|
|
||||||
:id="fieldId"
|
:id="fieldId"
|
||||||
:rows="rows"
|
ref="textarea"
|
||||||
v-model="newValue"
|
v-model="newValue"
|
||||||
|
:name="fieldId"
|
||||||
|
:rows="rows"
|
||||||
:required="required"
|
:required="required"
|
||||||
:placeholder="placeholder || labels.placeholder"></textarea>
|
:placeholder="placeholder || labels.placeholder"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui very small hidden divider"></div>
|
<div class="ui very small hidden divider" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui bottom attached segment">
|
<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 }}
|
{{ remainingChars }}
|
||||||
</span>
|
</span>
|
||||||
<p>
|
<p>
|
||||||
<translate translate-context="*/Form/Paragraph">Markdown syntax is supported.</translate>
|
<translate translate-context="*/Form/Paragraph">
|
||||||
|
Markdown syntax is supported.
|
||||||
|
</translate>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,50 +77,31 @@ import axios from 'axios'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
value: {type: String, default: ""},
|
value: { type: String, default: '' },
|
||||||
fieldId: {type: String, default: "change-content"},
|
fieldId: { type: String, default: 'change-content' },
|
||||||
placeholder: {type: String, default: null},
|
placeholder: { type: String, default: null },
|
||||||
autofocus: {type: Boolean, default: false},
|
autofocus: { type: Boolean, default: false },
|
||||||
charLimit: {type: Number, default: 5000, required: false},
|
charLimit: { type: Number, default: 5000, required: false },
|
||||||
rows: {type: Number, default: 5, required: false},
|
rows: { type: Number, default: 5, required: false },
|
||||||
permissive: {type: Boolean, default: false},
|
permissive: { type: Boolean, default: false },
|
||||||
required: {type: Boolean, default: false},
|
required: { type: Boolean, default: false }
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isPreviewing: false,
|
isPreviewing: false,
|
||||||
preview: null,
|
preview: null,
|
||||||
newValue: this.value,
|
newValue: this.value,
|
||||||
isLoadingPreview: false,
|
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
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
labels () {
|
labels () {
|
||||||
return {
|
return {
|
||||||
placeholder: this.$pgettext("*/Form/Placeholder", "Write a few words here…")
|
placeholder: this.$pgettext('*/Form/Placeholder', 'Write a few words here…')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
remainingChars () {
|
remainingChars () {
|
||||||
return this.charLimit - (this.value || "").length
|
return this.charLimit - (this.value || '').length
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -113,7 +117,7 @@ export default {
|
||||||
await this.loadPreview()
|
await this.loadPreview()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
immediate: true,
|
immediate: true
|
||||||
},
|
},
|
||||||
async isPreviewing (v) {
|
async isPreviewing (v) {
|
||||||
if (v && !!this.value && this.preview === null && !this.isLoadingPreview) {
|
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>
|
</script>
|
||||||
|
|
|
@ -1,21 +1,38 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="ui fluid action input component-copy-input">
|
<div class="ui fluid action input component-copy-input">
|
||||||
<p class="message" v-if="copied">
|
<p
|
||||||
<translate translate-context="Content/*/Paragraph">Text copied to clipboard!</translate>
|
v-if="copied"
|
||||||
|
class="message"
|
||||||
|
>
|
||||||
|
<translate translate-context="Content/*/Paragraph">
|
||||||
|
Text copied to clipboard!
|
||||||
|
</translate>
|
||||||
</p>
|
</p>
|
||||||
<input :id="id" :name="id" ref="input" :value="value" type="text" readonly>
|
<input
|
||||||
<button @click="copy" :class="['ui', buttonClasses, 'right', 'labeled', 'icon', 'button']">
|
:id="id"
|
||||||
<i class="copy icon"></i>
|
ref="input"
|
||||||
<translate translate-context="*/*/Button.Label/Short, Verb">Copy</translate>
|
: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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
value: {type: String},
|
value: { type: String, required: true },
|
||||||
buttonClasses: {type: String, default: 'accent'},
|
buttonClasses: { type: String, default: 'accent' },
|
||||||
id: {type: String, default: 'copy-input'},
|
id: { type: String, default: 'copy-input' }
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -29,8 +46,8 @@ export default {
|
||||||
clearTimeout(this.timeout)
|
clearTimeout(this.timeout)
|
||||||
}
|
}
|
||||||
this.$refs.input.select()
|
this.$refs.input.select()
|
||||||
document.execCommand("Copy")
|
document.execCommand('Copy')
|
||||||
let self = this
|
const self = this
|
||||||
self.copied = true
|
self.copied = true
|
||||||
this.timeout = setTimeout(() => {
|
this.timeout = setTimeout(() => {
|
||||||
self.copied = false
|
self.copied = false
|
||||||
|
|
|
@ -1,44 +1,59 @@
|
||||||
<template>
|
<template>
|
||||||
<button @click="showModal = true" :class="[{disabled: disabled}]" :disabled="disabled">
|
<button
|
||||||
<slot></slot>
|
:class="[{disabled: disabled}]"
|
||||||
|
:disabled="disabled"
|
||||||
|
@click="showModal = true"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
|
||||||
<modal class="small" :show.sync="showModal">
|
<modal
|
||||||
|
class="small"
|
||||||
|
:show.sync="showModal"
|
||||||
|
>
|
||||||
<h4 class="header">
|
<h4 class="header">
|
||||||
<slot name="modal-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>
|
</slot>
|
||||||
</h4>
|
</h4>
|
||||||
<div class="scrolling content">
|
<div class="scrolling content">
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<slot name="modal-content"></slot>
|
<slot name="modal-content" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="ui basic cancel button">
|
<button class="ui basic cancel button">
|
||||||
<translate translate-context="*/*/Button.Label/Verb">Cancel</translate>
|
<translate translate-context="*/*/Button.Label/Verb">
|
||||||
|
Cancel
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
<button :class="['ui', 'confirm', confirmButtonColor, 'button']" @click="confirm">
|
<button
|
||||||
|
:class="['ui', 'confirm', confirmButtonColor, 'button']"
|
||||||
|
@click="confirm"
|
||||||
|
>
|
||||||
<slot name="modal-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>
|
</slot>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import Modal from '@/components/semantic/Modal'
|
import Modal from '@/components/semantic/Modal'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
action: {type: Function, required: false},
|
|
||||||
disabled: {type: Boolean, default: false},
|
|
||||||
confirmColor: {type: String, default: "danger", required: false}
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
Modal
|
Modal
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
action: { type: Function, required: false, default: () => {} },
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
|
confirmColor: { type: String, default: 'danger', required: false }
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
showModal: false
|
showModal: false
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<span>
|
<span>
|
||||||
<translate translate-context="Content/*/Paragraph"
|
<translate
|
||||||
v-if="durationData.hours > 0"
|
v-if="durationData.hours > 0"
|
||||||
:translate-params="{minutes: durationData.minutes, hours: durationData.hours}">%{ hours } h %{ minutes } min</translate>
|
translate-context="Content/*/Paragraph"
|
||||||
<translate translate-context="Content/*/Paragraph"
|
:translate-params="{minutes: durationData.minutes, hours: durationData.hours}"
|
||||||
|
>%{ hours } h %{ minutes } min</translate>
|
||||||
|
<translate
|
||||||
v-else
|
v-else
|
||||||
:translate-params="{minutes: durationData.minutes}">%{ minutes } min</translate>
|
translate-context="Content/*/Paragraph"
|
||||||
|
:translate-params="{minutes: durationData.minutes}"
|
||||||
|
>%{ minutes } min</translate>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import {secondsToObject} from '@/filters'
|
import { secondsToObject } from '@/filters'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['seconds'],
|
props: { seconds: { type: Number, required: true } },
|
||||||
computed: {
|
computed: {
|
||||||
durationData () {
|
durationData () {
|
||||||
return secondsToObject(this.seconds)
|
return secondsToObject(this.seconds)
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
<h4 class="ui header">
|
<h4 class="ui header">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<slot name="title">
|
<slot name="title">
|
||||||
|
<i class="search icon" />
|
||||||
<i class="search icon"></i>
|
|
||||||
<translate translate-context="Content/*/Paragraph">
|
<translate translate-context="Content/*/Paragraph">
|
||||||
No results were found.
|
No results were found.
|
||||||
</translate>
|
</translate>
|
||||||
|
@ -12,8 +11,12 @@
|
||||||
</div>
|
</div>
|
||||||
</h4>
|
</h4>
|
||||||
<div class="inline center aligned text">
|
<div class="inline center aligned text">
|
||||||
<slot></slot>
|
<slot />
|
||||||
<button v-if="refresh" class="ui button" @click="$emit('refresh')">
|
<button
|
||||||
|
v-if="refresh"
|
||||||
|
class="ui button"
|
||||||
|
@click="$emit('refresh')"
|
||||||
|
>
|
||||||
<translate translate-context="Content/*/Button.Label/Short, Verb">
|
<translate translate-context="Content/*/Button.Label/Short, Verb">
|
||||||
Refresh
|
Refresh
|
||||||
</translate>
|
</translate>
|
||||||
|
@ -24,7 +27,7 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
refresh: {type: Boolean, default: false}
|
refresh: { type: Boolean, default: false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -3,10 +3,22 @@
|
||||||
<div :class="['expandable-content', {expandable: truncated.length < content.length}, {expanded: isExpanded}]">
|
<div :class="['expandable-content', {expandable: truncated.length < content.length}, {expanded: isExpanded}]">
|
||||||
<slot>{{ content }}</slot>
|
<slot>{{ content }}</slot>
|
||||||
</div>
|
</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>
|
<br>
|
||||||
<translate v-if="isExpanded" key="1" translate-context="*/*/Button,Label">Show less</translate>
|
<translate
|
||||||
<translate v-else key="2" translate-context="*/*/Button,Label">Show more</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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -15,12 +27,12 @@
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
content: {type: String, required: true},
|
content: { type: String, required: true },
|
||||||
length: {type: Number, default: 150, required: false},
|
length: { type: Number, default: 150, required: false }
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isExpanded: false,
|
isExpanded: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<time :datetime="date" :title="date | moment">
|
<time
|
||||||
<i v-if="icon" class="outline clock icon"></i>
|
:datetime="date"
|
||||||
|
:title="date | moment"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
v-if="icon"
|
||||||
|
class="outline clock icon"
|
||||||
|
/>
|
||||||
{{ realDate | ago($store.state.ui.momentLocale) }}
|
{{ realDate | ago($store.state.ui.momentLocale) }}
|
||||||
</time>
|
</time>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import {mapState} from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
date: {required: true},
|
date: { type: String, required: true },
|
||||||
icon: {type: Boolean, required: false, default: false},
|
icon: { type: Boolean, required: false, default: false }
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<time :datetime="`${duration}s`">
|
<time :datetime="`${duration}s`">
|
||||||
{{ duration | duration}}
|
{{ duration | duration }}
|
||||||
</time>
|
</time>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
duration: {required: true},
|
duration: { type: Object, required: true }
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,13 +1,34 @@
|
||||||
<template>
|
<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']">
|
<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>
|
<translate translate-context="Content/Search/Input.Label/Noun">Search</translate>
|
||||||
</label>
|
</label>
|
||||||
<input id="search-query" name="search-query" type="text" :placeholder="placeholder || labels.searchPlaceholder" :value="value" @input="$emit('input', $event.target.value)">
|
<input
|
||||||
<i v-if="isClearable" class="x link icon" :title="labels.clear" @click.stop.prevent="$emit('input', ''); $emit('search', value)"></i>
|
id="search-query"
|
||||||
<button type="submit" class="ui icon basic button">
|
name="search-query"
|
||||||
<i class="search icon"></i>
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -15,14 +36,14 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
value: {type: String, required: true},
|
value: { type: String, required: true },
|
||||||
placeholder: {type: String, required: false},
|
placeholder: { type: String, required: false, default: '' }
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
labels () {
|
labels () {
|
||||||
return {
|
return {
|
||||||
searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search…'),
|
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 () {
|
isClearable () {
|
||||||
|
|
|
@ -1,64 +1,81 @@
|
||||||
<template>
|
<template>
|
||||||
<modal :show.sync="show">
|
<modal :show.sync="show">
|
||||||
<h4 class="header">{{ labels.header }}</h4>
|
<h4 class="header">
|
||||||
<div v-if="cover" class="image content">
|
{{ labels.header }}
|
||||||
<div class="ui medium image">
|
</h4>
|
||||||
<img :src="cover.urls.medium_square_crop">
|
<div
|
||||||
</div>
|
v-if="cover"
|
||||||
<div class="description">
|
class="image content"
|
||||||
<div class="ui header">
|
>
|
||||||
{{ labels.description }}
|
<div class="ui medium image">
|
||||||
</div>
|
<img :src="cover.urls.medium_square_crop">
|
||||||
<p>
|
|
||||||
{{ message }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="content">
|
<div class="description">
|
||||||
<div class="ui centered header">
|
<div class="ui header">
|
||||||
{{ labels.description }}
|
{{ labels.description }}
|
||||||
</div>
|
</div>
|
||||||
<p style="text-align: center;">
|
<p>
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
</div>
|
||||||
<router-link :to="{path: '/login', query: { next: nextRoute }}" class="ui labeled icon button"><i class="key icon"></i>
|
<div
|
||||||
{{ labels.login }}
|
v-else
|
||||||
</router-link>
|
class="content"
|
||||||
<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>
|
>
|
||||||
{{ labels.signup }}
|
<div class="ui centered header">
|
||||||
</router-link>
|
{{ labels.description }}
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
<p style="text-align: center;">
|
||||||
|
{{ message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<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" />
|
||||||
|
{{ labels.signup }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Modal from '@/components/semantic/Modal'
|
import Modal from '@/components/semantic/Modal'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
nextRoute: {type: String},
|
|
||||||
message: {type: String},
|
|
||||||
cover: {type: Object},
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
Modal,
|
Modal
|
||||||
},
|
},
|
||||||
data() {
|
props: {
|
||||||
|
nextRoute: { type: String, required: true },
|
||||||
|
message: { type: String, required: true },
|
||||||
|
cover: { type: Object, required: true }
|
||||||
|
},
|
||||||
|
data () {
|
||||||
return {
|
return {
|
||||||
show: false,
|
show: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
labels() {
|
labels () {
|
||||||
return {
|
return {
|
||||||
header: this.$pgettext('Popup/Title/Noun', "Unauthenticated"),
|
header: this.$pgettext('Popup/Title/Noun', 'Unauthenticated'),
|
||||||
login: this.$pgettext('*/*/Button.Label/Verb', "Log in"),
|
login: this.$pgettext('*/*/Button.Label/Verb', 'Log in'),
|
||||||
signup: this.$pgettext('*/*/Button.Label/Verb', "Sign up"),
|
signup: this.$pgettext('*/*/Button.Label/Verb', 'Sign up'),
|
||||||
description: this.$pgettext('Popup/*/Paragraph', "You don't have access!"),
|
description: this.$pgettext('Popup/*/Paragraph', "You don't have access!")
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
<template>
|
<template>
|
||||||
<div></div>
|
<div />
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['message'],
|
props: { message: { type: Object, required: true } },
|
||||||
mounted () {
|
mounted () {
|
||||||
let self = this
|
const self = this
|
||||||
let params = {
|
const params = {
|
||||||
context: "#app",
|
context: '#app',
|
||||||
message: this.message.content,
|
message: this.message.content,
|
||||||
showProgress: 'top',
|
showProgress: 'top',
|
||||||
position: "bottom right",
|
position: 'bottom right',
|
||||||
progressUp: true,
|
progressUp: true,
|
||||||
onRemove () {
|
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>
|
</script>
|
||||||
|
|
|
@ -1,79 +1,113 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<template v-if="content && !isUpdating">
|
<template v-if="content && !isUpdating">
|
||||||
<div v-html="html"></div>
|
<div v-html="html" />
|
||||||
<template v-if="isTruncated">
|
<template v-if="isTruncated">
|
||||||
<div class="ui small hidden divider"></div>
|
<div class="ui small hidden divider" />
|
||||||
<a href="" @click.stop.prevent="showMore = true" v-if="showMore === false">
|
<a
|
||||||
|
v-if="showMore === false"
|
||||||
|
href=""
|
||||||
|
@click.stop.prevent="showMore = true"
|
||||||
|
>
|
||||||
<translate translate-context="*/*/Button,Label">Show more</translate>
|
<translate translate-context="*/*/Button,Label">Show more</translate>
|
||||||
</a>
|
</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>
|
<translate translate-context="*/*/Button,Label">Show less</translate>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<p v-else-if="!isUpdating">
|
<p v-else-if="!isUpdating">
|
||||||
<translate translate-context="*/*/Placeholder">No description available</translate>
|
<translate translate-context="*/*/Placeholder">
|
||||||
|
No description available
|
||||||
|
</translate>
|
||||||
</p>
|
</p>
|
||||||
<template v-if="!isUpdating && canUpdate && updateUrl">
|
<template v-if="!isUpdating && canUpdate && updateUrl">
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<span role="button" @click="isUpdating = true">
|
<span
|
||||||
<i class="pencil icon"></i>
|
role="button"
|
||||||
|
@click="isUpdating = true"
|
||||||
|
>
|
||||||
|
<i class="pencil icon" />
|
||||||
<translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
|
<translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<form v-if="isUpdating" class="ui form" @submit.prevent="submit()">
|
<form
|
||||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
v-if="isUpdating"
|
||||||
<h4 class="header"><translate translate-context="Content/Channels/Error message.Title">Error while updating description</translate></h4>
|
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">
|
<ul class="list">
|
||||||
<li v-for="error in errors">{{ error }}</li>
|
<li
|
||||||
|
v-for="(error, key) in errors"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<content-form v-model="newText" :autofocus="true"></content-form>
|
<content-form
|
||||||
<a @click.prevent="isUpdating = false" class="left floated">
|
v-model="newText"
|
||||||
|
:autofocus="true"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
class="left floated"
|
||||||
|
@click.prevent="isUpdating = false"
|
||||||
|
>
|
||||||
<translate translate-context="*/*/Button.Label/Verb">Cancel</translate>
|
<translate translate-context="*/*/Button.Label/Verb">Cancel</translate>
|
||||||
</a>
|
</a>
|
||||||
<button :class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']" type="submit" :disabled="isLoading">
|
<button
|
||||||
<translate translate-context="Content/Channels/Button.Label/Verb">Update description</translate>
|
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']"
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading"
|
||||||
|
>
|
||||||
|
<translate translate-context="Content/Channels/Button.Label/Verb">
|
||||||
|
Update description
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
<div class="ui clearing hidden divider"></div>
|
<div class="ui clearing hidden divider" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {secondsToObject} from '@/filters'
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import clip from 'text-clipper'
|
import clip from 'text-clipper'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
content: {required: true},
|
content: { type: String, required: true },
|
||||||
fieldName: {required: false, default: 'description'},
|
fieldName: { type: String, required: false, default: 'description' },
|
||||||
updateUrl: {required: false, type: String},
|
updateUrl: { required: false, type: String, default: '' },
|
||||||
canUpdate: {required: false, default: true, type: Boolean},
|
canUpdate: { required: false, default: true, type: Boolean },
|
||||||
fetchHtml: {required: false, default: false, type: Boolean},
|
fetchHtml: { required: false, default: false, type: Boolean },
|
||||||
permissive: {required: false, default: false, type: Boolean},
|
permissive: { required: false, default: false, type: Boolean },
|
||||||
truncateLength: {required: false, default: 500, type: Number},
|
truncateLength: { required: false, default: 500, type: Number }
|
||||||
|
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isUpdating: false,
|
isUpdating: false,
|
||||||
showMore: false,
|
showMore: false,
|
||||||
newText: (this.content || {text: ''}).text,
|
newText: (this.content || { text: '' }).text,
|
||||||
errors: null,
|
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
errors: [],
|
errors: [],
|
||||||
preview: null
|
preview: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created () {
|
|
||||||
if (this.fetchHtml) {
|
|
||||||
await this.fetchPreview()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
html () {
|
html () {
|
||||||
if (this.fetchHtml) {
|
if (this.fetchHtml) {
|
||||||
|
@ -91,21 +125,26 @@ export default {
|
||||||
return this.truncateLength > 0 && this.truncatedHtml.length < this.content.html.length
|
return this.truncateLength > 0 && this.truncatedHtml.length < this.content.html.length
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async created () {
|
||||||
|
if (this.fetchHtml) {
|
||||||
|
await this.fetchPreview()
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async fetchPreview () {
|
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
|
this.preview = response.data.rendered
|
||||||
},
|
},
|
||||||
submit () {
|
submit () {
|
||||||
let self = this
|
const self = this
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.errors = []
|
this.errors = []
|
||||||
let payload = {}
|
const payload = {}
|
||||||
payload[this.fieldName] = null
|
payload[this.fieldName] = null
|
||||||
if (this.newText) {
|
if (this.newText) {
|
||||||
payload[this.fieldName] = {
|
payload[this.fieldName] = {
|
||||||
content_type: "text/markdown",
|
content_type: 'text/markdown',
|
||||||
text: this.newText,
|
text: this.newText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
axios.patch(this.updateUrl, payload).then((response) => {
|
axios.patch(this.updateUrl, payload).then((response) => {
|
||||||
|
@ -116,7 +155,7 @@ export default {
|
||||||
self.errors = error.backendErrors
|
self.errors = error.backendErrors
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
<template>
|
<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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
content: {type: String, required: true},
|
content: { type: String, required: true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -2,11 +2,16 @@
|
||||||
<span class="component-user-link">
|
<span class="component-user-link">
|
||||||
<template v-if="avatar">
|
<template v-if="avatar">
|
||||||
<img
|
<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"
|
class="ui tiny circular avatar"
|
||||||
alt=""
|
alt=""
|
||||||
v-if="user.avatar && user.avatar.urls.medium_square_crop"
|
>
|
||||||
v-lazy="$store.getters['instance/absoluteUrl'](user.avatar.urls.medium_square_crop)" />
|
<span
|
||||||
<span v-else :style="defaultAvatarStyle" class="ui circular label">{{ user.username[0]}}</span>
|
v-else
|
||||||
|
:style="defaultAvatarStyle"
|
||||||
|
class="ui circular label"
|
||||||
|
>{{ user.username[0] }}</span>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
@{{ user.username }}
|
@{{ user.username }}
|
||||||
|
@ -14,12 +19,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {hashCode, intToRGB} from '@/utils/color'
|
import { hashCode, intToRGB } from '@/utils/color'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
user: {required: true},
|
user: { type: String, required: true },
|
||||||
avatar: {type: Boolean, default: true}
|
avatar: { type: Boolean, default: true }
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
userColor () {
|
userColor () {
|
||||||
|
|
|
@ -101,12 +101,12 @@
|
||||||
</template>
|
</template>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<a
|
<a
|
||||||
class="column"
|
class="column"
|
||||||
href="https://funkwhale.audio/help"
|
href="https://funkwhale.audio/help"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<i class="user-modal list-icon life ring outline icon" />
|
<i class="user-modal list-icon life ring outline icon" />
|
||||||
<span class="user-modal list-item">{{ labels.help }}</span>
|
<span class="user-modal list-item">{{ labels.help }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
|
@ -3,6 +3,6 @@
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['username']
|
props: { username: { type: String, required: true } }
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,98 +1,158 @@
|
||||||
<template>
|
<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">
|
<section class="ui vertical center aligned stripe segment">
|
||||||
<div :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']">
|
<div :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']">
|
||||||
<div class="ui text loader">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<h2 v-if="results" class="ui center aligned icon header">
|
<h2
|
||||||
<i class="circular inverted heart pink icon"></i>
|
v-if="results"
|
||||||
|
class="ui center aligned icon header"
|
||||||
|
>
|
||||||
|
<i class="circular inverted heart pink icon" />
|
||||||
<translate
|
<translate
|
||||||
translate-plural="%{ count } favorites"
|
translate-plural="%{ count } favorites"
|
||||||
:translate-n="$store.state.favorites.count"
|
:translate-n="$store.state.favorites.count"
|
||||||
:translate-params="{count: results.count}"
|
:translate-params="{count: results.count}"
|
||||||
translate-context="Content/Favorites/Title">
|
translate-context="Content/Favorites/Title"
|
||||||
%{ count } favorite
|
>
|
||||||
|
%{ count } favorite
|
||||||
</translate>
|
</translate>
|
||||||
</h2>
|
</h2>
|
||||||
<radio-button v-if="hasFavorites" type="favorites"></radio-button>
|
<radio-button
|
||||||
|
v-if="hasFavorites"
|
||||||
|
type="favorites"
|
||||||
|
/>
|
||||||
</section>
|
</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="['ui', {'loading': isLoading}, 'form']">
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="favorites-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label>
|
<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">
|
<select
|
||||||
<option v-for="option in orderingOptions" :value="option[0]" :key="option[0]">
|
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]] }}
|
{{ sharedLabels.filters[option[1]] }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="favorites-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Order</translate></label>
|
<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">
|
<select
|
||||||
<option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option>
|
id="favorites-ordering-direction"
|
||||||
<option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option>
|
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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="favorites-results"><translate translate-context="Content/Search/Dropdown.Label/Noun">Results per page</translate></label>
|
<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">
|
<select
|
||||||
<option :value="parseInt(12)">12</option>
|
id="favorites-results"
|
||||||
<option :value="parseInt(25)">25</option>
|
v-model="paginateBy"
|
||||||
<option :value="parseInt(50)">50</option>
|
class="ui dropdown"
|
||||||
|
>
|
||||||
|
<option :value="parseInt(12)">
|
||||||
|
12
|
||||||
|
</option>
|
||||||
|
<option :value="parseInt(25)">
|
||||||
|
25
|
||||||
|
</option>
|
||||||
|
<option :value="parseInt(50)">
|
||||||
|
50
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<div class="ui center aligned basic segment">
|
||||||
<pagination
|
<pagination
|
||||||
v-if="results && results.count > paginateBy"
|
v-if="results && results.count > paginateBy"
|
||||||
@page-changed="selectPage"
|
|
||||||
:current="page"
|
:current="page"
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:total="results.count"
|
:total="results.count"
|
||||||
></pagination>
|
@page-changed="selectPage"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div v-else class="ui placeholder segment">
|
<div
|
||||||
|
v-else
|
||||||
|
class="ui placeholder segment"
|
||||||
|
>
|
||||||
<div class="ui icon header">
|
<div class="ui icon header">
|
||||||
<i class="broken heart icon"></i>
|
<i class="broken heart icon" />
|
||||||
<translate
|
<translate
|
||||||
translate-context="Content/Home/Placeholder"
|
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>
|
</div>
|
||||||
<router-link :to="'/library'" class="ui success labeled icon button">
|
<router-link
|
||||||
<i class="headphones icon"></i>
|
:to="'/library'"
|
||||||
<translate translate-context="Content/*/Verb">Browse the library</translate>
|
class="ui success labeled icon button"
|
||||||
|
>
|
||||||
|
<i class="headphones icon" />
|
||||||
|
<translate translate-context="Content/*/Verb">
|
||||||
|
Browse the library
|
||||||
|
</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios"
|
import axios from 'axios'
|
||||||
import $ from "jquery"
|
import $ from 'jquery'
|
||||||
import logger from "@/logging"
|
import logger from '@/logging'
|
||||||
import RadioButton from "@/components/radios/Button"
|
import RadioButton from '@/components/radios/Button'
|
||||||
import Pagination from "@/components/Pagination"
|
import Pagination from '@/components/Pagination'
|
||||||
import OrderingMixin from "@/components/mixins/Ordering"
|
import OrderingMixin from '@/components/mixins/Ordering'
|
||||||
import PaginationMixin from "@/components/mixins/Pagination"
|
import PaginationMixin from '@/components/mixins/Pagination'
|
||||||
import TranslationsMixin from "@/components/mixins/Translations"
|
import TranslationsMixin from '@/components/mixins/Translations'
|
||||||
import {checkRedirectToLogin} from '@/utils'
|
import { checkRedirectToLogin } from '@/utils'
|
||||||
import TrackTable from '@/components/audio/track/Table'
|
import TrackTable from '@/components/audio/track/Table'
|
||||||
const FAVORITES_URL = "tracks/"
|
const FAVORITES_URL = 'tracks/'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [OrderingMixin, PaginationMixin, TranslationsMixin],
|
|
||||||
components: {
|
components: {
|
||||||
RadioButton,
|
RadioButton,
|
||||||
Pagination,
|
Pagination,
|
||||||
TrackTable
|
TrackTable
|
||||||
},
|
},
|
||||||
data() {
|
mixins: [OrderingMixin, PaginationMixin, TranslationsMixin],
|
||||||
|
data () {
|
||||||
return {
|
return {
|
||||||
results: null,
|
results: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
@ -100,33 +160,46 @@ export default {
|
||||||
previousLink: null,
|
previousLink: null,
|
||||||
page: parseInt(this.defaultPage),
|
page: parseInt(this.defaultPage),
|
||||||
orderingOptions: [
|
orderingOptions: [
|
||||||
["creation_date", "creation_date"],
|
['creation_date', 'creation_date'],
|
||||||
["title", "track_title"],
|
['title', 'track_title'],
|
||||||
["album__title", "album_title"],
|
['album__title', 'album_title'],
|
||||||
["artist__name", "artist_name"]
|
['artist__name', 'artist_name']
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
|
||||||
checkRedirectToLogin(this.$store, this.$router)
|
|
||||||
this.fetchFavorites(FAVORITES_URL)
|
|
||||||
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
$(".ui.dropdown").dropdown()
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
labels() {
|
labels () {
|
||||||
return {
|
return {
|
||||||
title: this.$pgettext('Head/Favorites/Title', 'Your Favorites')
|
title: this.$pgettext('Head/Favorites/Title', 'Your Favorites')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hasFavorites () {
|
hasFavorites () {
|
||||||
return this.$store.state.favorites.count > 0
|
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: {
|
methods: {
|
||||||
updateQueryString: function() {
|
updateQueryString: function () {
|
||||||
this.$router.replace({
|
this.$router.replace({
|
||||||
query: {
|
query: {
|
||||||
page: this.page,
|
page: this.page,
|
||||||
|
@ -136,44 +209,30 @@ export default {
|
||||||
})
|
})
|
||||||
this.fetchFavorites(FAVORITES_URL)
|
this.fetchFavorites(FAVORITES_URL)
|
||||||
},
|
},
|
||||||
fetchFavorites(url) {
|
fetchFavorites (url) {
|
||||||
var self = this
|
const self = this
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let params = {
|
const params = {
|
||||||
favorites: "true",
|
favorites: 'true',
|
||||||
page: this.page,
|
page: this.page,
|
||||||
page_size: this.paginateBy,
|
page_size: this.paginateBy,
|
||||||
ordering: this.getOrderingAsString()
|
ordering: this.getOrderingAsString()
|
||||||
}
|
}
|
||||||
logger.default.time("Loading user favorites")
|
logger.default.time('Loading user favorites')
|
||||||
axios.get(url, { params: params }).then(response => {
|
axios.get(url, { params: params }).then(response => {
|
||||||
self.results = response.data
|
self.results = response.data
|
||||||
self.nextLink = response.data.next
|
self.nextLink = response.data.next
|
||||||
self.previousLink = response.data.previous
|
self.previousLink = response.data.previous
|
||||||
self.results.results.forEach(track => {
|
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
|
self.isLoading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
selectPage: function(page) {
|
selectPage: function (page) {
|
||||||
this.page = page
|
this.page = page
|
||||||
}
|
}
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
page: function() {
|
|
||||||
this.updateQueryString()
|
|
||||||
},
|
|
||||||
paginateBy: function() {
|
|
||||||
this.updateQueryString()
|
|
||||||
},
|
|
||||||
orderingDirection: function() {
|
|
||||||
this.updateQueryString()
|
|
||||||
},
|
|
||||||
ordering: function() {
|
|
||||||
this.updateQueryString()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,25 +1,40 @@
|
||||||
<template>
|
<template>
|
||||||
<button @click.stop="$store.dispatch('favorites/toggle', track.id)" v-if="button" :class="['ui', 'pink', {'inverted': isFavorite}, {'favorited': isFavorite}, 'icon', 'labeled', 'button']">
|
<button
|
||||||
<i class="heart icon"></i>
|
v-if="button"
|
||||||
<translate v-if="isFavorite" translate-context="Content/Track/Button.Message">In favorites</translate>
|
:class="['ui', 'pink', {'inverted': isFavorite}, {'favorited': isFavorite}, 'icon', 'labeled', 'button']"
|
||||||
<translate v-else translate-context="Content/Track/*/Verb">Add to favorites</translate>
|
@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>
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
@click.stop="$store.dispatch('favorites/toggle', track.id)"
|
|
||||||
:class="['ui', 'favorite-icon', {'pink': isFavorite}, {'favorited': isFavorite}, 'basic', 'circular', 'icon', {'really': !border}, 'button']"
|
:class="['ui', 'favorite-icon', {'pink': isFavorite}, {'favorited': isFavorite}, 'basic', 'circular', 'icon', {'really': !border}, 'button']"
|
||||||
:aria-label="title"
|
:aria-label="title"
|
||||||
:title="title">
|
:title="title"
|
||||||
<i :class="['heart', {'pink': isFavorite}, 'basic', 'icon']"></i>
|
@click.stop="$store.dispatch('favorites/toggle', track.id)"
|
||||||
|
>
|
||||||
|
<i :class="['heart', {'pink': isFavorite}, 'basic', 'icon']" />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
track: {type: Object},
|
track: { type: Object, default: () => { return {} } },
|
||||||
button: {type: Boolean, default: false},
|
button: { type: Boolean, default: false },
|
||||||
border: {type: Boolean, default: false},
|
border: { type: Boolean, default: false }
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
title () {
|
title () {
|
||||||
|
|
|
@ -1,30 +1,73 @@
|
||||||
<template>
|
<template>
|
||||||
<div @click="createFetch" role="button">
|
<div
|
||||||
|
role="button"
|
||||||
|
@click="createFetch"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<slot></slot>
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<modal class="small" :show.sync="showModal">
|
<modal
|
||||||
|
class="small"
|
||||||
|
:show.sync="showModal"
|
||||||
|
>
|
||||||
<h3 class="header">
|
<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>
|
</h3>
|
||||||
<div class="scrolling content">
|
<div class="scrolling content">
|
||||||
<template v-if="fetch && fetch.status != 'pending'">
|
<template v-if="fetch && fetch.status != 'pending'">
|
||||||
<div v-if="fetch.status === 'skipped'" class="ui message">
|
<div
|
||||||
<h4 class="header"><translate translate-context="Popup/*/Message.Title">Refresh was skipped</translate></h4>
|
v-if="fetch.status === 'skipped'"
|
||||||
<p><translate translate-context="Popup/*/Message.Content">The remote server answered, but returned data was unsupported by Funkwhale.</translate></p>
|
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>
|
||||||
<div v-else-if="fetch.status === 'finished'" class="ui success message">
|
<div
|
||||||
<h4 class="header"><translate translate-context="Popup/*/Message.Title">Refresh successful</translate></h4>
|
v-else-if="fetch.status === 'finished'"
|
||||||
<p><translate translate-context="Popup/*/Message.Content">Data was refreshed successfully from remote server.</translate></p>
|
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>
|
||||||
<div v-else-if="fetch.status === 'errored'" class="ui error message">
|
<div
|
||||||
<h4 class="header"><translate translate-context="Popup/*/Message.Title">Refresh error</translate></h4>
|
v-else-if="fetch.status === 'errored'"
|
||||||
<p><translate translate-context="Popup/*/Message.Content">An error occurred while trying to refresh data:</translate></p>
|
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">
|
<table class="ui very basic collapsing celled table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<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>
|
||||||
<td>
|
<td>
|
||||||
{{ fetch.detail.error_code }}
|
{{ fetch.detail.error_code }}
|
||||||
|
@ -32,61 +75,136 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<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>
|
||||||
<td>
|
<td>
|
||||||
<translate
|
<translate
|
||||||
v-if="fetch.detail.error_code === 'http' && fetch.detail.status_code"
|
v-if="fetch.detail.error_code === 'http' && fetch.detail.status_code"
|
||||||
:translate-params="{status: 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
|
<translate
|
||||||
v-else-if="['http', 'request'].indexOf(fetch.detail.error_code) > -1"
|
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
|
<translate
|
||||||
v-else-if="fetch.detail.error_code === 'timeout'"
|
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
|
<translate
|
||||||
v-else-if="fetch.detail.error_code === 'connection'"
|
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
|
<translate
|
||||||
v-else-if="['invalid_json', 'invalid_jsonld', 'missing_jsonld_type'].indexOf(fetch.detail.error_code) > -1"
|
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-context="*/*/Error"
|
||||||
<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>
|
The remote server returned invalid JSON or JSON-LD data
|
||||||
<translate v-else translate-context="*/*/Error">Unknown error</translate>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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">
|
<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>
|
</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">
|
<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>
|
</div>
|
||||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
<div
|
||||||
<h4 class="header"><translate translate-context="Content/*/Error message.Title">Error while saving settings</translate></h4>
|
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">
|
<ul class="list">
|
||||||
<li v-for="error in errors">{{ error }}</li>
|
<li
|
||||||
|
v-for="(error, key) in errors"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="fetch && fetch.status === 'pending' && pollsCount >= maxPolls" role="alert" class="ui warning message">
|
<div
|
||||||
<h4 class="header"><translate translate-context="Popup/*/Message.Title">Refresh pending</translate></h4>
|
v-else-if="fetch && fetch.status === 'pending' && pollsCount >= maxPolls"
|
||||||
<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>
|
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>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="ui basic cancel button">
|
<button class="ui basic cancel button">
|
||||||
<translate translate-context="*/*/Button.Label/Verb">Close</translate>
|
<translate translate-context="*/*/Button.Label/Verb">
|
||||||
|
Close
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
<button @click.prevent="showModal = false; $emit('refresh')" class="ui confirm success button" v-if="fetch && fetch.status === 'finished'">
|
<button
|
||||||
<translate translate-context="*/*/Button.Label/Verb">Close and reload page</translate>
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
|
@ -94,14 +212,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios"
|
import axios from 'axios'
|
||||||
import Modal from '@/components/semantic/Modal'
|
import Modal from '@/components/semantic/Modal'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['url'],
|
|
||||||
components: {
|
components: {
|
||||||
Modal
|
Modal
|
||||||
},
|
},
|
||||||
|
props: { url: { type: String, required: true } },
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
fetch: null,
|
fetch: null,
|
||||||
|
@ -110,12 +228,12 @@ export default {
|
||||||
showModal: false,
|
showModal: false,
|
||||||
isWaitingFetch: false,
|
isWaitingFetch: false,
|
||||||
maxPolls: 15,
|
maxPolls: 15,
|
||||||
pollsCount: 0,
|
pollsCount: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
createFetch () {
|
createFetch () {
|
||||||
let self = this
|
const self = this
|
||||||
this.fetch = null
|
this.fetch = null
|
||||||
this.pollsCount = 0
|
this.pollsCount = 0
|
||||||
this.errors = []
|
this.errors = []
|
||||||
|
@ -134,8 +252,8 @@ export default {
|
||||||
pollFetch () {
|
pollFetch () {
|
||||||
this.isWaitingFetch = true
|
this.isWaitingFetch = true
|
||||||
this.pollsCount += 1
|
this.pollsCount += 1
|
||||||
let url = `federation/fetches/${this.fetch.id}/`
|
const url = `federation/fetches/${this.fetch.id}/`
|
||||||
let self = this
|
const self = this
|
||||||
self.showModal = true
|
self.showModal = true
|
||||||
axios.get(url).then((response) => {
|
axios.get(url).then((response) => {
|
||||||
self.isCreatingFetch = false
|
self.isCreatingFetch = false
|
||||||
|
|
|
@ -1,27 +1,52 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<h3 v-if="!!this.$slots.title" class="ui header">
|
<h3
|
||||||
<slot name="title"></slot>
|
v-if="!!$slots.title"
|
||||||
|
class="ui header"
|
||||||
|
>
|
||||||
|
<slot name="title" />
|
||||||
</h3>
|
</h3>
|
||||||
<p v-if="!isLoading && libraries.length > 0" class="ui subtitle"><slot name="subtitle"></slot></p>
|
<p
|
||||||
<p v-if="!isLoading && libraries.length === 0" class="ui subtitle"><translate translate-context="Content/Federation/Paragraph">No matching library.</translate></p>
|
v-if="!isLoading && libraries.length > 0"
|
||||||
<div class="ui hidden divider"></div>
|
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 class="ui cards">
|
||||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
<div
|
||||||
<div class="ui loader"></div>
|
v-if="isLoading"
|
||||||
|
class="ui inverted active dimmer"
|
||||||
|
>
|
||||||
|
<div class="ui loader" />
|
||||||
</div>
|
</div>
|
||||||
<library-card
|
<library-card
|
||||||
|
v-for="library in libraries"
|
||||||
|
:key="library.uuid"
|
||||||
:display-scan="false"
|
:display-scan="false"
|
||||||
:display-follow="$store.state.auth.authenticated && library.actor.full_username != $store.state.auth.fullUsername"
|
:display-follow="$store.state.auth.authenticated && library.actor.full_username != $store.state.auth.fullUsername"
|
||||||
:library="library"
|
:library="library"
|
||||||
:display-copy-fid="true"
|
:display-copy-fid="true"
|
||||||
v-for="library in libraries"
|
/>
|
||||||
:key="library.uuid"></library-card>
|
|
||||||
</div>
|
</div>
|
||||||
<template v-if="nextPage">
|
<template v-if="nextPage">
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<button v-if="nextPage" @click="fetchData(nextPage)" :class="['ui', 'basic', 'button']">
|
<button
|
||||||
<translate translate-context="*/*/Button,Label">Show more</translate>
|
v-if="nextPage"
|
||||||
|
:class="['ui', 'basic', 'button']"
|
||||||
|
@click="fetchData(nextPage)"
|
||||||
|
>
|
||||||
|
<translate translate-context="*/*/Button,Label">
|
||||||
|
Show more
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -33,12 +58,12 @@ import axios from 'axios'
|
||||||
import LibraryCard from '@/views/content/remote/Card'
|
import LibraryCard from '@/views/content/remote/Card'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
url: {type: String, required: true}
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
LibraryCard
|
LibraryCard
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
url: { type: String, required: true }
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
libraries: [],
|
libraries: [],
|
||||||
|
@ -49,17 +74,22 @@ export default {
|
||||||
nextPage: null
|
nextPage: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
offset () {
|
||||||
|
this.fetchData()
|
||||||
|
}
|
||||||
|
},
|
||||||
created () {
|
created () {
|
||||||
this.fetchData(this.url)
|
this.fetchData(this.url)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchData (url) {
|
fetchData (url) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let self = this
|
const self = this
|
||||||
let params = _.clone({})
|
const params = _.clone({})
|
||||||
params.page_size = this.limit
|
params.page_size = this.limit
|
||||||
params.offset = this.offset
|
params.offset = this.offset
|
||||||
axios.get(url, {params: params}).then((response) => {
|
axios.get(url, { params: params }).then((response) => {
|
||||||
self.previousPage = response.data.previous
|
self.previousPage = response.data.previous
|
||||||
self.nextPage = response.data.next
|
self.nextPage = response.data.next
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
|
@ -77,11 +107,6 @@ export default {
|
||||||
this.offset = Math.max(this.offset - this.limit, 0)
|
this.offset = Math.max(this.offset - this.limit, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
offset () {
|
|
||||||
this.fetchData()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,55 +1,60 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="ui fluid action input">
|
<div class="ui fluid action input">
|
||||||
<input
|
<input
|
||||||
|
:id="fieldId"
|
||||||
required
|
required
|
||||||
name="password"
|
name="password"
|
||||||
:type="passwordInputType"
|
:type="passwordInputType"
|
||||||
@input="$emit('input', $event.target.value)"
|
|
||||||
:id="fieldId"
|
|
||||||
:value="value"
|
:value="value"
|
||||||
/>
|
@input="$emit('input', $event.target.value)"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
@click.prevent="showPassword = !showPassword"
|
|
||||||
type="button"
|
type="button"
|
||||||
:title="labels.title"
|
:title="labels.title"
|
||||||
class="ui icon button"
|
class="ui icon button"
|
||||||
|
@click.prevent="showPassword = !showPassword"
|
||||||
>
|
>
|
||||||
<i class="eye icon"></i>
|
<i class="eye icon" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="copyButton"
|
v-if="copyButton"
|
||||||
@click.prevent="copyPassword"
|
|
||||||
type="button"
|
type="button"
|
||||||
class="ui icon button"
|
class="ui icon button"
|
||||||
:title="labels.copy"
|
:title="labels.copy"
|
||||||
|
@click.prevent="copyPassword"
|
||||||
>
|
>
|
||||||
<i class="copy icon"></i>
|
<i class="copy icon" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ["value", "defaultShow", "copyButton", "fieldId"],
|
props: {
|
||||||
data() {
|
value: { type: String, required: true },
|
||||||
|
defaultShow: { type: Boolean, default: false },
|
||||||
|
copyButton: { type: Boolean, default: false },
|
||||||
|
fieldId: { type: Number, default: 0 }
|
||||||
|
},
|
||||||
|
data () {
|
||||||
return {
|
return {
|
||||||
showPassword: this.defaultShow || false,
|
showPassword: this.defaultShow || false
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
labels () {
|
labels () {
|
||||||
return {
|
return {
|
||||||
title: this.$pgettext(
|
title: this.$pgettext(
|
||||||
"Content/Settings/Button.Tooltip/Verb",
|
'Content/Settings/Button.Tooltip/Verb',
|
||||||
"Show/hide password"
|
'Show/hide password'
|
||||||
),
|
),
|
||||||
copy: this.$pgettext("*/*/Button.Label/Short, Verb", "Copy"),
|
copy: this.$pgettext('*/*/Button.Label/Short, Verb', 'Copy')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
passwordInputType() {
|
passwordInputType () {
|
||||||
if (this.showPassword) {
|
if (this.showPassword) {
|
||||||
return "text";
|
return 'text'
|
||||||
}
|
}
|
||||||
return "password";
|
return 'password'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
|
||||||
Vue.component('human-date', () => import(/* webpackChunkName: "common" */ "@/components/common/HumanDate"))
|
Vue.component('HumanDate', () => import(/* webpackChunkName: "common" */ '@/components/common/HumanDate'))
|
||||||
Vue.component('human-duration', () => import(/* webpackChunkName: "common" */ "@/components/common/HumanDuration"))
|
Vue.component('HumanDuration', () => import(/* webpackChunkName: "common" */ '@/components/common/HumanDuration'))
|
||||||
Vue.component('username', () => import(/* webpackChunkName: "common" */ "@/components/common/Username"))
|
Vue.component('Username', () => import(/* webpackChunkName: "common" */ '@/components/common/Username'))
|
||||||
Vue.component('user-link', () => import(/* webpackChunkName: "common" */ "@/components/common/UserLink"))
|
Vue.component('UserLink', () => import(/* webpackChunkName: "common" */ '@/components/common/UserLink'))
|
||||||
Vue.component('actor-link', () => import(/* webpackChunkName: "common" */ "@/components/common/ActorLink"))
|
Vue.component('ActorLink', () => import(/* webpackChunkName: "common" */ '@/components/common/ActorLink'))
|
||||||
Vue.component('actor-avatar', () => import(/* webpackChunkName: "common" */ "@/components/common/ActorAvatar"))
|
Vue.component('ActorAvatar', () => import(/* webpackChunkName: "common" */ '@/components/common/ActorAvatar'))
|
||||||
Vue.component('duration', () => import(/* webpackChunkName: "common" */ "@/components/common/Duration"))
|
Vue.component('Duration', () => import(/* webpackChunkName: "common" */ '@/components/common/Duration'))
|
||||||
Vue.component('dangerous-button', () => import(/* webpackChunkName: "common" */ "@/components/common/DangerousButton"))
|
Vue.component('DangerousButton', () => import(/* webpackChunkName: "common" */ '@/components/common/DangerousButton'))
|
||||||
Vue.component('message', () => import(/* webpackChunkName: "common" */ "@/components/common/Message"))
|
Vue.component('Message', () => import(/* webpackChunkName: "common" */ '@/components/common/Message'))
|
||||||
Vue.component('copy-input', () => import(/* webpackChunkName: "common" */ "@/components/common/CopyInput"))
|
Vue.component('CopyInput', () => import(/* webpackChunkName: "common" */ '@/components/common/CopyInput'))
|
||||||
Vue.component('ajax-button', () => import(/* webpackChunkName: "common" */ "@/components/common/AjaxButton"))
|
Vue.component('AjaxButton', () => import(/* webpackChunkName: "common" */ '@/components/common/AjaxButton'))
|
||||||
Vue.component('tooltip', () => import(/* webpackChunkName: "common" */ "@/components/common/Tooltip"))
|
Vue.component('Tooltip', () => import(/* webpackChunkName: "common" */ '@/components/common/Tooltip'))
|
||||||
Vue.component('empty-state', () => import(/* webpackChunkName: "common" */ "@/components/common/EmptyState"))
|
Vue.component('EmptyState', () => import(/* webpackChunkName: "common" */ '@/components/common/EmptyState'))
|
||||||
Vue.component('expandable-div', () => import(/* webpackChunkName: "common" */ "@/components/common/ExpandableDiv"))
|
Vue.component('ExpandableDiv', () => import(/* webpackChunkName: "common" */ '@/components/common/ExpandableDiv'))
|
||||||
Vue.component('collapse-link', () => import(/* webpackChunkName: "common" */ "@/components/common/CollapseLink"))
|
Vue.component('CollapseLink', () => import(/* webpackChunkName: "common" */ '@/components/common/CollapseLink'))
|
||||||
Vue.component('action-feedback', () => import(/* webpackChunkName: "common" */ "@/components/common/ActionFeedback"))
|
Vue.component('ActionFeedback', () => import(/* webpackChunkName: "common" */ '@/components/common/ActionFeedback'))
|
||||||
Vue.component('rendered-description', () => import(/* webpackChunkName: "common" */ "@/components/common/RenderedDescription"))
|
Vue.component('RenderedDescription', () => import(/* webpackChunkName: "common" */ '@/components/common/RenderedDescription'))
|
||||||
Vue.component('content-form', () => import(/* webpackChunkName: "common" */ "@/components/common/ContentForm"))
|
Vue.component('ContentForm', () => import(/* webpackChunkName: "common" */ '@/components/common/ContentForm'))
|
||||||
Vue.component('inline-search-bar', () => import(/* webpackChunkName: "common" */ "@/components/common/InlineSearchBar"))
|
Vue.component('InlineSearchBar', () => import(/* webpackChunkName: "common" */ '@/components/common/InlineSearchBar'))
|
||||||
|
|
||||||
export default {}
|
export default {}
|
||||||
|
|
|
@ -1,39 +1,90 @@
|
||||||
<template>
|
<template>
|
||||||
<main>
|
<main>
|
||||||
<div v-if="isLoading" class="ui vertical segment" v-title="labels.title">
|
<div
|
||||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
v-if="isLoading"
|
||||||
|
v-title="labels.title"
|
||||||
|
class="ui vertical segment"
|
||||||
|
>
|
||||||
|
<div :class="['ui', 'centered', 'active', 'inline', 'loader']" />
|
||||||
</div>
|
</div>
|
||||||
<template v-if="object">
|
<template v-if="object">
|
||||||
<section class="ui vertical stripe segment channel-serie">
|
<section class="ui vertical stripe segment channel-serie">
|
||||||
<div class="ui stackable grid container">
|
<div class="ui stackable grid container">
|
||||||
<div class="ui seven wide column">
|
<div class="ui seven wide column">
|
||||||
<div v-if="isSerie" class="padded basic segment">
|
<div
|
||||||
<div class="ui two column grid" v-if="isSerie">
|
v-if="isSerie"
|
||||||
|
class="padded basic segment"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="isSerie"
|
||||||
|
class="ui two column grid"
|
||||||
|
>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="large two-images">
|
<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
|
||||||
<img alt="" class="channel-image" v-else src="../../assets/audio/default-cover.png">
|
v-if="object.cover && object.cover.urls.original"
|
||||||
<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)">
|
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">
|
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>
|
</div>
|
||||||
<div class="ui column right aligned">
|
<div class="ui column right aligned">
|
||||||
<tags-list v-if="object.tags && object.tags.length > 0" :tags="object.tags"></tags-list>
|
<tags-list
|
||||||
<div class="ui small hidden divider"></div>
|
v-if="object.tags && object.tags.length > 0"
|
||||||
<human-duration v-if="totalDuration > 0" :duration="totalDuration"></human-duration>
|
:tags="object.tags"
|
||||||
|
/>
|
||||||
|
<div class="ui small hidden divider" />
|
||||||
|
<human-duration
|
||||||
|
v-if="totalDuration > 0"
|
||||||
|
:duration="totalDuration"
|
||||||
|
/>
|
||||||
<template v-if="totalTracks > 0">
|
<template v-if="totalTracks > 0">
|
||||||
<div class="ui hidden very small divider"></div>
|
<div class="ui hidden very small divider" />
|
||||||
<translate key="1" v-if="isSerie" translate-context="Content/Channel/Paragraph"
|
<translate
|
||||||
|
v-if="isSerie"
|
||||||
|
key="1"
|
||||||
|
translate-context="Content/Channel/Paragraph"
|
||||||
translate-plural="%{ count } episodes"
|
translate-plural="%{ count } episodes"
|
||||||
:translate-n="totalTracks"
|
:translate-n="totalTracks"
|
||||||
:translate-params="{count: totalTracks}">
|
:translate-params="{count: totalTracks}"
|
||||||
|
>
|
||||||
%{ count } episode
|
%{ count } episode
|
||||||
</translate>
|
</translate>
|
||||||
<translate v-else translate-context="*/*/*" :translate-params="{count: totalTracks}" :translate-n="totalTracks" 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>
|
</template>
|
||||||
<div class="ui small hidden divider"></div>
|
<div class="ui small hidden divider" />
|
||||||
<play-button class="vibrant" :tracks="object.tracks"></play-button>
|
<play-button
|
||||||
<div class="ui hidden horizontal divider"></div>
|
class="vibrant"
|
||||||
|
:tracks="object.tracks"
|
||||||
|
/>
|
||||||
|
<div class="ui hidden horizontal divider" />
|
||||||
<album-dropdown
|
<album-dropdown
|
||||||
:object="object"
|
:object="object"
|
||||||
:public-libraries="publicLibraries"
|
:public-libraries="publicLibraries"
|
||||||
|
@ -41,42 +92,86 @@
|
||||||
:is-album="isAlbum"
|
:is-album="isAlbum"
|
||||||
:is-serie="isSerie"
|
:is-serie="isSerie"
|
||||||
:is-channel="isChannel"
|
:is-channel="isChannel"
|
||||||
:artist="artist"></album-dropdown>
|
:artist="artist"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui small hidden divider"></div>
|
<div class="ui small hidden divider" />
|
||||||
<header>
|
<header>
|
||||||
<h2 class="ui header" :title="object.title">
|
<h2
|
||||||
|
class="ui header"
|
||||||
|
:title="object.title"
|
||||||
|
>
|
||||||
{{ object.title }}
|
{{ object.title }}
|
||||||
</h2>
|
</h2>
|
||||||
<artist-label :artist="artist"></artist-label>
|
<artist-label :artist="artist" />
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="ui center aligned text padded basic segment">
|
<div
|
||||||
<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)">
|
v-else
|
||||||
<img alt="" class="channel-image" v-else src="../../assets/audio/default-cover.png">
|
class="ui center aligned text padded basic segment"
|
||||||
<div class="ui hidden divider"></div>
|
>
|
||||||
|
<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>
|
<header>
|
||||||
<h2 class="ui header" :title="object.title">
|
<h2
|
||||||
|
class="ui header"
|
||||||
|
:title="object.title"
|
||||||
|
>
|
||||||
{{ object.title }}
|
{{ object.title }}
|
||||||
</h2>
|
</h2>
|
||||||
<artist-label class="rounded" :artist="artist"></artist-label>
|
<artist-label
|
||||||
|
class="rounded"
|
||||||
|
:artist="artist"
|
||||||
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div v-if="object.release_date || (totalTracks > 0)" class="ui small hidden divider"></div>
|
<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>
|
<span v-if="object.release_date">{{ object.release_date | moment('Y') }} · </span>
|
||||||
<template v-if="totalTracks > 0">
|
<template v-if="totalTracks > 0">
|
||||||
<translate key="1" v-if="isSerie" translate-context="Content/Channel/Paragraph"
|
<translate
|
||||||
|
v-if="isSerie"
|
||||||
|
key="1"
|
||||||
|
translate-context="Content/Channel/Paragraph"
|
||||||
translate-plural="%{ count } episodes"
|
translate-plural="%{ count } episodes"
|
||||||
:translate-n="totalTracks"
|
:translate-n="totalTracks"
|
||||||
:translate-params="{count: totalTracks}">
|
:translate-params="{count: totalTracks}"
|
||||||
|
>
|
||||||
%{ count } episode
|
%{ count } episode
|
||||||
</translate>
|
</translate>
|
||||||
<translate v-else translate-context="*/*/*" :translate-params="{count: totalTracks}" :translate-n="totalTracks" 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>
|
</template>
|
||||||
<human-duration v-if="totalDuration > 0" :duration="totalDuration"></human-duration>
|
<human-duration
|
||||||
<div class="ui small hidden divider"></div>
|
v-if="totalDuration > 0"
|
||||||
<play-button class="vibrant" :album="object"></play-button>
|
:duration="totalDuration"
|
||||||
<div class="ui horizontal hidden divider"></div>
|
/>
|
||||||
|
<div class="ui small hidden divider" />
|
||||||
|
<play-button
|
||||||
|
class="vibrant"
|
||||||
|
:album="object"
|
||||||
|
/>
|
||||||
|
<div class="ui horizontal hidden divider" />
|
||||||
<album-dropdown
|
<album-dropdown
|
||||||
:object="object"
|
:object="object"
|
||||||
:public-libraries="publicLibraries"
|
:public-libraries="publicLibraries"
|
||||||
|
@ -85,40 +180,64 @@
|
||||||
:is-serie="isSerie"
|
:is-serie="isSerie"
|
||||||
:is-channel="isChannel"
|
:is-channel="isChannel"
|
||||||
:artist="artist"
|
:artist="artist"
|
||||||
></album-dropdown>
|
/>
|
||||||
<div v-if="(object.tags && object.tags.length > 0) || object.description || $store.state.auth.authenticated && object.is_local">
|
<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 small hidden divider" />
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider" />
|
||||||
<div class="ui small hidden divider"></div>
|
<div class="ui small hidden divider" />
|
||||||
<template v-if="object.tags && object.tags.length > 0" >
|
<template v-if="object.tags && object.tags.length > 0">
|
||||||
<tags-list :tags="object.tags"></tags-list>
|
<tags-list :tags="object.tags" />
|
||||||
<div class="ui small hidden divider"></div>
|
<div class="ui small hidden divider" />
|
||||||
</template>
|
</template>
|
||||||
<rendered-description
|
<rendered-description
|
||||||
v-if="object.description"
|
v-if="object.description"
|
||||||
:content="object.description"
|
:content="object.description"
|
||||||
:can-update="false"></rendered-description>
|
: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"></i>
|
<router-link
|
||||||
<translate translate-context="Content/*/Button.Label/Verb">Add a description…</translate>
|
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>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="isSerie">
|
<template v-if="isSerie">
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<rendered-description
|
<rendered-description
|
||||||
v-if="object.description"
|
v-if="object.description"
|
||||||
:content="object.description"
|
:content="object.description"
|
||||||
:can-update="false"></rendered-description>
|
: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"></i>
|
<router-link
|
||||||
<translate translate-context="Content/*/Button.Label/Verb">Add a description…</translate>
|
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>
|
</router-link>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="nine wide column">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -127,17 +246,17 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios"
|
import axios from 'axios'
|
||||||
import lodash from "@/lodash"
|
import lodash from '@/lodash'
|
||||||
import PlayButton from "@/components/audio/PlayButton"
|
import PlayButton from '@/components/audio/PlayButton'
|
||||||
import TagsList from "@/components/tags/List"
|
import TagsList from '@/components/tags/List'
|
||||||
import ArtistLabel from '@/components/audio/ArtistLabel'
|
import ArtistLabel from '@/components/audio/ArtistLabel'
|
||||||
import AlbumDropdown from './AlbumDropdown'
|
import AlbumDropdown from './AlbumDropdown'
|
||||||
|
|
||||||
function groupByDisc(initial) {
|
function groupByDisc (initial) {
|
||||||
function inner(acc, track) {
|
function inner (acc, track) {
|
||||||
var dn = track.disc_number - initial
|
const dn = track.disc_number - initial
|
||||||
if (acc[dn] == undefined) {
|
if (acc[dn] === undefined) {
|
||||||
acc.push([track])
|
acc.push([track])
|
||||||
} else {
|
} else {
|
||||||
acc[dn].push(track)
|
acc[dn].push(track)
|
||||||
|
@ -148,14 +267,14 @@ function groupByDisc(initial) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ["id"],
|
|
||||||
components: {
|
components: {
|
||||||
PlayButton,
|
PlayButton,
|
||||||
TagsList,
|
TagsList,
|
||||||
ArtistLabel,
|
ArtistLabel,
|
||||||
AlbumDropdown
|
AlbumDropdown
|
||||||
},
|
},
|
||||||
data() {
|
props: { id: { type: Number, required: true } },
|
||||||
|
data () {
|
||||||
return {
|
return {
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
object: null,
|
object: null,
|
||||||
|
@ -163,39 +282,7 @@ export default {
|
||||||
discs: [],
|
discs: [],
|
||||||
libraries: [],
|
libraries: [],
|
||||||
page: 1,
|
page: 1,
|
||||||
paginateBy: 50,
|
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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -212,7 +299,7 @@ export default {
|
||||||
return this.object.artist.content_category === 'music'
|
return this.object.artist.content_category === 'music'
|
||||||
},
|
},
|
||||||
totalDuration () {
|
totalDuration () {
|
||||||
let durations = [0]
|
const durations = [0]
|
||||||
this.object.tracks.forEach((t) => {
|
this.object.tracks.forEach((t) => {
|
||||||
if (t.uploads[0] && t.uploads[0].duration) {
|
if (t.uploads[0] && t.uploads[0].duration) {
|
||||||
durations.push(t.uploads[0].duration)
|
durations.push(t.uploads[0].duration)
|
||||||
|
@ -220,24 +307,56 @@ export default {
|
||||||
})
|
})
|
||||||
return lodash.sum(durations)
|
return lodash.sum(durations)
|
||||||
},
|
},
|
||||||
labels() {
|
labels () {
|
||||||
return {
|
return {
|
||||||
title: this.$pgettext('*/*/*', 'Album'),
|
title: this.$pgettext('*/*/*', 'Album')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
publicLibraries () {
|
publicLibraries () {
|
||||||
return this.libraries.filter(l => {
|
return this.libraries.filter(l => {
|
||||||
return l.privacy_level === 'everyone'
|
return l.privacy_level === 'everyone'
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
id() {
|
id () {
|
||||||
this.fetchData()
|
this.fetchData()
|
||||||
},
|
},
|
||||||
page() {
|
page () {
|
||||||
this.fetchData()
|
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>
|
</script>
|
||||||
|
|
|
@ -1,20 +1,44 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="object">
|
<div v-if="object">
|
||||||
<h2 class="ui header">
|
<h2 class="ui header">
|
||||||
<translate key="1" v-if="isSerie" translate-context="Content/Channels/*">Episodes</translate>
|
<translate
|
||||||
<translate key="2" v-else translate-context="*/*/*">Tracks</translate>
|
v-if="isSerie"
|
||||||
|
key="1"
|
||||||
|
translate-context="Content/Channels/*"
|
||||||
|
>
|
||||||
|
Episodes
|
||||||
|
</translate>
|
||||||
|
<translate
|
||||||
|
v-else
|
||||||
|
key="2"
|
||||||
|
translate-context="*/*/*"
|
||||||
|
>
|
||||||
|
Tracks
|
||||||
|
</translate>
|
||||||
</h2>
|
</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">
|
<template v-else-if="discs && discs.length > 1">
|
||||||
<div v-for="tracks in discs" :key="tracks.disc_number">
|
<div
|
||||||
<div class="ui hidden divider"></div>
|
v-for="tracks in discs"
|
||||||
<play-button class="right floated mini inverted vibrant" :tracks="tracks"></play-button>
|
:key="tracks.disc_number"
|
||||||
|
>
|
||||||
|
<div class="ui hidden divider" />
|
||||||
|
<play-button
|
||||||
|
class="right floated mini inverted vibrant"
|
||||||
|
:tracks="tracks"
|
||||||
|
/>
|
||||||
<translate
|
<translate
|
||||||
tag="h3"
|
tag="h3"
|
||||||
:translate-params="{number: tracks[0].disc_number}"
|
:translate-params="{number: tracks[0].disc_number}"
|
||||||
translate-context="Content/Album/"
|
translate-context="Content/Album/"
|
||||||
>Volume %{ number }</translate>
|
>
|
||||||
|
Volume %{ number }
|
||||||
|
</translate>
|
||||||
<track-table
|
<track-table
|
||||||
:is-album="true"
|
:is-album="true"
|
||||||
:tracks="object.tracks"
|
:tracks="object.tracks"
|
||||||
|
@ -26,8 +50,8 @@
|
||||||
:total="totalTracks"
|
:total="totalTracks"
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:page="page"
|
:page="page"
|
||||||
@page-changed="updatePage">
|
@page-changed="updatePage"
|
||||||
</track-table>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
@ -42,15 +66,25 @@
|
||||||
:total="totalTracks"
|
:total="totalTracks"
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:page="page"
|
:page="page"
|
||||||
@page-changed="updatePage">
|
@page-changed="updatePage"
|
||||||
</track-table>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="!artist.channel && !isSerie">
|
<template v-if="!artist.channel && !isSerie">
|
||||||
<h2>
|
<h2>
|
||||||
<translate translate-context="Content/*/Title/Noun">User libraries</translate>
|
<translate translate-context="Content/*/Title/Noun">
|
||||||
|
User libraries
|
||||||
|
</translate>
|
||||||
</h2>
|
</h2>
|
||||||
<library-widget @loaded="$emit('libraries-loaded', $event)" :url="'albums/' + object.id + '/libraries/'">
|
<library-widget
|
||||||
<translate slot="subtitle" translate-context="Content/Album/Paragraph">This album is present in the following libraries:</translate>
|
: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>
|
</library-widget>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -58,30 +92,38 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import time from "@/utils/time"
|
import time from '@/utils/time'
|
||||||
import LibraryWidget from "@/components/federation/LibraryWidget"
|
import LibraryWidget from '@/components/federation/LibraryWidget'
|
||||||
import ChannelEntries from '@/components/audio/ChannelEntries'
|
import ChannelEntries from '@/components/audio/ChannelEntries'
|
||||||
import TrackTable from '@/components/audio/track/Table'
|
import TrackTable from '@/components/audio/track/Table'
|
||||||
import PlayButton from "@/components/audio/PlayButton"
|
import PlayButton from '@/components/audio/PlayButton'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ["object", "libraries", "discs", "isSerie", "artist", "page", "paginateBy", "totalTracks"],
|
|
||||||
components: {
|
components: {
|
||||||
LibraryWidget,
|
LibraryWidget,
|
||||||
TrackTable,
|
TrackTable,
|
||||||
ChannelEntries,
|
ChannelEntries,
|
||||||
PlayButton
|
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 {
|
return {
|
||||||
time,
|
time,
|
||||||
id: this.object.id,
|
id: this.object.id
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updatePage: function(page) {
|
updatePage: function (page) {
|
||||||
this.$emit('page-changed', page)
|
this.$emit('page-changed', page)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<span>
|
<span>
|
||||||
|
|
||||||
<modal v-if="isEmbedable" :show.sync="showEmbedModal">
|
<modal
|
||||||
|
v-if="isEmbedable"
|
||||||
|
:show.sync="showEmbedModal"
|
||||||
|
>
|
||||||
<h4 class="header">
|
<h4 class="header">
|
||||||
<translate translate-context="Popup/Album/Title/Verb">Embed this album on your website</translate>
|
<translate translate-context="Popup/Album/Title/Verb">Embed this album on your website</translate>
|
||||||
</h4>
|
</h4>
|
||||||
<div class="scrolling content">
|
<div class="scrolling content">
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<embed-wizard type="album" :id="object.id" />
|
<embed-wizard
|
||||||
|
:id="object.id"
|
||||||
|
type="album"
|
||||||
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,46 +23,69 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
<button class="ui floating dropdown circular icon basic button" :title="labels.more" v-dropdown="{direction: 'downward'}">
|
<button
|
||||||
<i class="ellipsis vertical icon"></i>
|
v-dropdown="{direction: 'downward'}"
|
||||||
|
class="ui floating dropdown circular icon basic button"
|
||||||
|
:title="labels.more"
|
||||||
|
>
|
||||||
|
<i class="ellipsis vertical icon" />
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<a
|
<a
|
||||||
:href="object.fid"
|
|
||||||
v-if="domain != $store.getters['instance/domain']"
|
v-if="domain != $store.getters['instance/domain']"
|
||||||
|
:href="object.fid"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="basic item">
|
class="basic item"
|
||||||
<i class="external icon"></i>
|
>
|
||||||
<translate :translate-params="{domain: domain}" translate-context="Content/*/Button.Label/Verb">View on %{ domain }</translate>
|
<i class="external icon" />
|
||||||
|
<translate
|
||||||
|
:translate-params="{domain: domain}"
|
||||||
|
translate-context="Content/*/Button.Label/Verb"
|
||||||
|
>View on %{ domain }</translate>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
role="button"
|
|
||||||
v-if="isEmbedable"
|
v-if="isEmbedable"
|
||||||
|
role="button"
|
||||||
|
class="basic item"
|
||||||
@click="showEmbedModal = !showEmbedModal"
|
@click="showEmbedModal = !showEmbedModal"
|
||||||
class="basic item">
|
>
|
||||||
<i class="code icon"></i>
|
<i class="code icon" />
|
||||||
<translate translate-context="Content/*/Button.Label/Verb">Embed</translate>
|
<translate translate-context="Content/*/Button.Label/Verb">Embed</translate>
|
||||||
</div>
|
</div>
|
||||||
<a v-if="isAlbum && musicbrainzUrl" :href="musicbrainzUrl" target="_blank" rel="noreferrer noopener" class="basic item">
|
<a
|
||||||
<i class="external icon"></i>
|
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>
|
<translate translate-context="Content/*/*/Clickable, Verb">View on MusicBrainz</translate>
|
||||||
</a>
|
</a>
|
||||||
<a v-if="!isChannel && isAlbum" :href="discogsUrl" target="_blank" rel="noreferrer noopener" class="basic item">
|
<a
|
||||||
<i class="external icon"></i>
|
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>
|
<translate translate-context="Content/*/Button.Label/Verb">Search on Discogs</translate>
|
||||||
</a>
|
</a>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="object.is_local"
|
v-if="object.is_local"
|
||||||
:to="{name: 'library.albums.edit', params: {id: object.id }}"
|
:to="{name: 'library.albums.edit', params: {id: object.id }}"
|
||||||
class="basic item">
|
class="basic item"
|
||||||
<i class="edit icon"></i>
|
>
|
||||||
|
<i class="edit icon" />
|
||||||
<translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
|
<translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
<dangerous-button
|
<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"
|
v-if="artist && $store.state.auth.authenticated && artist.channel && artist.attributed_to.full_username === $store.state.auth.fullUsername"
|
||||||
@confirm="remove()">
|
:class="['ui', {loading: isLoading}, 'item']"
|
||||||
<i class="ui trash icon"></i>
|
@confirm="remove()"
|
||||||
|
>
|
||||||
|
<i class="ui trash icon" />
|
||||||
<translate translate-context="*/*/*/Verb">Delete…</translate>
|
<translate translate-context="*/*/*/Verb">Delete…</translate>
|
||||||
<p slot="modal-header"><translate translate-context="Popup/Channel/Title">Delete this album?</translate></p>
|
<p slot="modal-header"><translate translate-context="Popup/Channel/Title">Delete this album?</translate></p>
|
||||||
<div slot="modal-content">
|
<div slot="modal-content">
|
||||||
|
@ -64,26 +93,33 @@
|
||||||
</div>
|
</div>
|
||||||
<p slot="modal-confirm"><translate translate-context="*/*/*/Verb">Delete</translate></p>
|
<p slot="modal-confirm"><translate translate-context="*/*/*/Verb">Delete</translate></p>
|
||||||
</dangerous-button>
|
</dangerous-button>
|
||||||
<div class="divider"></div>
|
<div class="divider" />
|
||||||
<div
|
<div
|
||||||
role="button"
|
|
||||||
class="basic item"
|
|
||||||
v-for="obj in getReportableObjs({album: object, channel: artist.channel})"
|
v-for="obj in getReportableObjs({album: object, channel: artist.channel})"
|
||||||
:key="obj.target.type + obj.target.id"
|
: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 }}
|
<i class="share icon" /> {{ obj.label }}
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider" />
|
||||||
<router-link class="basic item" v-if="$store.state.auth.availablePermissions['library']" :to="{name: 'manage.library.albums.detail', params: {id: object.id}}">
|
<router-link
|
||||||
<i class="wrench icon"></i>
|
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>
|
<translate translate-context="Content/Moderation/Link">Open in moderation interface</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
<a
|
<a
|
||||||
v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser"
|
v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser"
|
||||||
class="basic item"
|
class="basic item"
|
||||||
:href="$store.getters['instance/absoluteUrl'](`/api/admin/music/album/${object.id}`)"
|
:href="$store.getters['instance/absoluteUrl'](`/api/admin/music/album/${object.id}`)"
|
||||||
target="_blank" rel="noopener noreferrer">
|
target="_blank"
|
||||||
<i class="wrench icon"></i>
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<i class="wrench icon" />
|
||||||
<translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate>
|
<translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -91,30 +127,30 @@
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import EmbedWizard from "@/components/audio/EmbedWizard"
|
import EmbedWizard from '@/components/audio/EmbedWizard'
|
||||||
import Modal from '@/components/semantic/Modal'
|
import Modal from '@/components/semantic/Modal'
|
||||||
import ReportMixin from '@/components/mixins/Report'
|
import ReportMixin from '@/components/mixins/Report'
|
||||||
|
|
||||||
import {getDomain} from '@/utils'
|
import { getDomain } from '@/utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
EmbedWizard,
|
||||||
|
Modal
|
||||||
|
},
|
||||||
mixins: [ReportMixin],
|
mixins: [ReportMixin],
|
||||||
props: {
|
props: {
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
artist: Object,
|
artist: { type: Object, required: true },
|
||||||
object: Object,
|
object: { type: Object, required: true },
|
||||||
publicLibraries: Array,
|
publicLibraries: { type: Array, required: true },
|
||||||
isAlbum: Boolean,
|
isAlbum: Boolean,
|
||||||
isChannel: Boolean,
|
isChannel: Boolean,
|
||||||
isSerie: Boolean,
|
isSerie: Boolean
|
||||||
},
|
|
||||||
components: {
|
|
||||||
EmbedWizard,
|
|
||||||
Modal,
|
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
showEmbedModal: false,
|
showEmbedModal: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -122,28 +158,30 @@ export default {
|
||||||
if (this.object) {
|
if (this.object) {
|
||||||
return getDomain(this.object.fid)
|
return getDomain(this.object.fid)
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
},
|
},
|
||||||
labels() {
|
labels () {
|
||||||
return {
|
return {
|
||||||
more: this.$pgettext('*/*/Button.Label/Noun', "More…"),
|
more: this.$pgettext('*/*/Button.Label/Noun', 'More…')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isEmbedable () {
|
isEmbedable () {
|
||||||
return (this.isChannel && this.artist.channel.actor) || this.publicLibraries.length > 0
|
return (this.isChannel && this.artist.channel.actor) || this.publicLibraries.length > 0
|
||||||
},
|
},
|
||||||
|
|
||||||
musicbrainzUrl() {
|
musicbrainzUrl () {
|
||||||
if (this.object.mbid) {
|
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 (
|
return (
|
||||||
"https://discogs.com/search/?type=release&title=" +
|
'https://discogs.com/search/?type=release&title=' +
|
||||||
encodeURI(this.object.title) + "&artist=" +
|
encodeURI(this.object.title) + '&artist=' +
|
||||||
encodeURI(this.object.artist.name)
|
encodeURI(this.object.artist.name)
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,37 +1,56 @@
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<section class="ui vertical stripe segment">
|
<section class="ui vertical stripe segment">
|
||||||
<div class="ui text container">
|
<div class="ui text container">
|
||||||
<h2>
|
<h2>
|
||||||
<translate v-if="canEdit" key="1" translate-context="Content/*/Title">Edit this album</translate>
|
<translate
|
||||||
<translate v-else key="2" translate-context="Content/*/Title">Suggest an edit on this album</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>
|
</h2>
|
||||||
<div class="ui message" v-if="!object.is_local">
|
<div
|
||||||
<translate translate-context="Content/*/Message">This object is managed by another server, you cannot edit it.</translate>
|
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>
|
</div>
|
||||||
<edit-form
|
<edit-form
|
||||||
v-else
|
v-else
|
||||||
:object-type="objectType"
|
:object-type="objectType"
|
||||||
:object="object"
|
:object="object"
|
||||||
:can-edit="canEdit"></edit-form>
|
:can-edit="canEdit"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios"
|
|
||||||
|
|
||||||
import EditForm from '@/components/library/EditForm'
|
import EditForm from '@/components/library/EditForm'
|
||||||
export default {
|
export default {
|
||||||
props: ["objectType", "object", "libraries"],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
id: this.object.id,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
EditForm
|
EditForm
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
objectType: { type: String, required: true },
|
||||||
|
object: { type: Object, required: true },
|
||||||
|
libraries: { type: Array, required: true }
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
id: this.object.id
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
canEdit () {
|
canEdit () {
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -2,78 +2,131 @@
|
||||||
<main v-title="labels.title">
|
<main v-title="labels.title">
|
||||||
<section class="ui vertical stripe segment">
|
<section class="ui vertical stripe segment">
|
||||||
<h2 class="ui header">
|
<h2 class="ui header">
|
||||||
<translate translate-context="Content/Album/Title">Browsing albums</translate>
|
<translate translate-context="Content/Album/Title">
|
||||||
|
Browsing albums
|
||||||
|
</translate>
|
||||||
</h2>
|
</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="fields">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="albums-search">
|
<label for="albums-search">
|
||||||
<translate translate-context="Content/Search/Input.Label/Noun">Search</translate>
|
<translate translate-context="Content/Search/Input.Label/Noun">Search</translate>
|
||||||
</label>
|
</label>
|
||||||
<div class="ui action input">
|
<div class="ui action input">
|
||||||
<input id="albums-search" type="text" name="search" v-model="query" :placeholder="labels.searchPlaceholder"/>
|
<input
|
||||||
<button class="ui icon button" type="submit" :aria-label="$pgettext('Content/Search/Input.Label/Noun', 'Search')">
|
id="albums-search"
|
||||||
<i class="search icon"></i>
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="tags-search"><translate translate-context="*/*/*/Noun">Tags</translate></label>
|
<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>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="album-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label>
|
<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">
|
<select
|
||||||
<option v-for="option in orderingOptions" :value="option[0]">
|
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]] }}
|
{{ sharedLabels.filters[option[1]] }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="album-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label>
|
<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">
|
<select
|
||||||
<option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option>
|
id="album-ordering-direction"
|
||||||
<option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option>
|
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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="album-results"><translate translate-context="Content/Search/Dropdown.Label/Noun">Results per page</translate></label>
|
<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">
|
<select
|
||||||
<option :value="parseInt(12)">12</option>
|
id="album-results"
|
||||||
<option :value="parseInt(25)">25</option>
|
v-model="paginateBy"
|
||||||
<option :value="parseInt(50)">50</option>
|
class="ui dropdown"
|
||||||
|
>
|
||||||
|
<option :value="parseInt(12)">
|
||||||
|
12
|
||||||
|
</option>
|
||||||
|
<option :value="parseInt(25)">
|
||||||
|
25
|
||||||
|
</option>
|
||||||
|
<option :value="parseInt(50)">
|
||||||
|
50
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<div
|
<div
|
||||||
v-if="result"
|
v-if="result"
|
||||||
transition-duration="0"
|
transition-duration="0"
|
||||||
item-selector=".column"
|
item-selector=".column"
|
||||||
percent-position="true"
|
percent-position="true"
|
||||||
stagger="0"
|
stagger="0"
|
||||||
class="">
|
class=""
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="result.results.length > 0"
|
v-if="result.results.length > 0"
|
||||||
class="ui app-cards cards">
|
class="ui app-cards cards"
|
||||||
|
>
|
||||||
<album-card
|
<album-card
|
||||||
v-for="album in result.results"
|
v-for="album in result.results"
|
||||||
:key="album.id"
|
:key="album.id"
|
||||||
:album="album"></album-card>
|
:album="album"
|
||||||
|
/>
|
||||||
</div>
|
</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">
|
<div class="ui icon header">
|
||||||
<i class="compact disc icon"></i>
|
<i class="compact disc icon" />
|
||||||
<translate translate-context="Content/Albums/Placeholder">
|
<translate translate-context="Content/Albums/Placeholder">
|
||||||
No results matching your query
|
No results matching your query
|
||||||
</translate>
|
</translate>
|
||||||
</div>
|
</div>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="$store.state.auth.authenticated"
|
v-if="$store.state.auth.authenticated"
|
||||||
:to="{name: 'content.index'}"
|
:to="{name: 'content.index'}"
|
||||||
class="ui success button labeled icon">
|
class="ui success button labeled icon"
|
||||||
<i class="upload icon"></i>
|
>
|
||||||
|
<i class="upload icon" />
|
||||||
<translate translate-context="Content/*/Verb">
|
<translate translate-context="Content/*/Verb">
|
||||||
Add some music
|
Add some music
|
||||||
</translate>
|
</translate>
|
||||||
|
@ -83,11 +136,11 @@
|
||||||
<div class="ui center aligned basic segment">
|
<div class="ui center aligned basic segment">
|
||||||
<pagination
|
<pagination
|
||||||
v-if="result && result.count > paginateBy"
|
v-if="result && result.count > paginateBy"
|
||||||
@page-changed="selectPage"
|
|
||||||
:current="page"
|
:current="page"
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:total="result.count"
|
:total="result.count"
|
||||||
></pagination>
|
@page-changed="selectPage"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
@ -95,121 +148,120 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import qs from 'qs'
|
import qs from 'qs'
|
||||||
import axios from "axios"
|
import axios from 'axios'
|
||||||
import _ from "@/lodash"
|
import $ from 'jquery'
|
||||||
import $ from "jquery"
|
|
||||||
|
|
||||||
import logger from "@/logging"
|
import logger from '@/logging'
|
||||||
|
|
||||||
import OrderingMixin from "@/components/mixins/Ordering"
|
import OrderingMixin from '@/components/mixins/Ordering'
|
||||||
import PaginationMixin from "@/components/mixins/Pagination"
|
import PaginationMixin from '@/components/mixins/Pagination'
|
||||||
import TranslationsMixin from "@/components/mixins/Translations"
|
import TranslationsMixin from '@/components/mixins/Translations'
|
||||||
import AlbumCard from "@/components/audio/album/Card"
|
import AlbumCard from '@/components/audio/album/Card'
|
||||||
import Pagination from "@/components/Pagination"
|
import Pagination from '@/components/Pagination'
|
||||||
import TagsSelector from '@/components/library/TagsSelector'
|
import TagsSelector from '@/components/library/TagsSelector'
|
||||||
|
|
||||||
const FETCH_URL = "albums/"
|
const FETCH_URL = 'albums/'
|
||||||
|
|
||||||
export default {
|
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: {
|
components: {
|
||||||
AlbumCard,
|
AlbumCard,
|
||||||
Pagination,
|
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 {
|
return {
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
result: null,
|
result: null,
|
||||||
page: parseInt(this.defaultPage),
|
page: parseInt(this.defaultPage),
|
||||||
query: this.defaultQuery,
|
query: this.defaultQuery,
|
||||||
tags: (this.defaultTags || []).filter((t) => { return t.length > 0 }),
|
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: {
|
computed: {
|
||||||
labels() {
|
labels () {
|
||||||
let searchPlaceholder = this.$pgettext('Content/Search/Input.Placeholder', "Enter album title…")
|
const searchPlaceholder = this.$pgettext('Content/Search/Input.Placeholder', 'Enter album title…')
|
||||||
let title = this.$pgettext('*/*/*', "Albums")
|
const title = this.$pgettext('*/*/*', 'Albums')
|
||||||
return {
|
return {
|
||||||
searchPlaceholder,
|
searchPlaceholder,
|
||||||
title
|
title
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
page () {
|
||||||
|
this.updateQueryString()
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
'$store.state.moderation.lastUpdate': function () {
|
||||||
|
this.fetchData()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
$('.ui.dropdown').dropdown()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateQueryString: function() {
|
updateQueryString: function () {
|
||||||
history.pushState(
|
history.pushState(
|
||||||
{},
|
{},
|
||||||
null,
|
null,
|
||||||
this.$route.path + '?' + new URLSearchParams(
|
this.$route.path + '?' + new URLSearchParams(
|
||||||
{
|
{
|
||||||
query: this.query,
|
query: this.query,
|
||||||
page: this.page,
|
page: this.page,
|
||||||
tag: this.tags,
|
tag: this.tags,
|
||||||
paginateBy: this.paginateBy,
|
paginateBy: this.paginateBy,
|
||||||
ordering: this.getOrderingAsString()
|
ordering: this.getOrderingAsString()
|
||||||
}).toString()
|
}).toString()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
fetchData: function() {
|
fetchData: function () {
|
||||||
var self = this
|
const self = this
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let url = FETCH_URL
|
const url = FETCH_URL
|
||||||
let params = {
|
const params = {
|
||||||
scope: this.scope,
|
scope: this.scope,
|
||||||
page: this.page,
|
page: this.page,
|
||||||
page_size: this.paginateBy,
|
page_size: this.paginateBy,
|
||||||
q: this.query,
|
q: this.query,
|
||||||
ordering: this.getOrderingAsString(),
|
ordering: this.getOrderingAsString(),
|
||||||
playable: "true",
|
playable: 'true',
|
||||||
tag: this.tags,
|
tag: this.tags,
|
||||||
include_channels: "true",
|
include_channels: 'true',
|
||||||
content_category: "music"
|
content_category: 'music'
|
||||||
}
|
}
|
||||||
logger.default.debug("Fetching albums")
|
logger.default.debug('Fetching albums')
|
||||||
axios.get(
|
axios.get(
|
||||||
url,
|
url,
|
||||||
{
|
{
|
||||||
params: params,
|
params: params,
|
||||||
paramsSerializer: function(params) {
|
paramsSerializer: function (params) {
|
||||||
return qs.stringify(params, { indices: false })
|
return qs.stringify(params, { indices: false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
).then(response => {
|
).then(response => {
|
||||||
self.result = response.data
|
self.result = response.data
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
}, error => {
|
}, () => {
|
||||||
self.result = null
|
self.result = null
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
selectPage: function(page) {
|
selectPage: function (page) {
|
||||||
this.page = page
|
this.page = page
|
||||||
},
|
},
|
||||||
updatePage() {
|
updatePage () {
|
||||||
this.page = this.defaultPage
|
this.page = this.defaultPage
|
||||||
}
|
}
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
page() {
|
|
||||||
this.updateQueryString()
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
"$store.state.moderation.lastUpdate": function () {
|
|
||||||
this.fetchData()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,119 +1,195 @@
|
||||||
<template>
|
<template>
|
||||||
<main v-title="labels.title">
|
<main v-title="labels.title">
|
||||||
<div v-if="isLoading" class="ui vertical segment">
|
<div
|
||||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
v-if="isLoading"
|
||||||
|
class="ui vertical segment"
|
||||||
|
>
|
||||||
|
<div :class="['ui', 'centered', 'active', 'inline', 'loader']" />
|
||||||
</div>
|
</div>
|
||||||
<template v-if="object && !isLoading">
|
<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">
|
<div class="segment-content">
|
||||||
<h2 class="ui center aligned icon header">
|
<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">
|
<div class="content">
|
||||||
{{ object.name }}
|
{{ object.name }}
|
||||||
<div class="sub header" v-if="albums">
|
<div
|
||||||
<translate translate-context="Content/Artist/Paragraph"
|
v-if="albums"
|
||||||
|
class="sub header"
|
||||||
|
>
|
||||||
|
<translate
|
||||||
|
translate-context="Content/Artist/Paragraph"
|
||||||
tag="div"
|
tag="div"
|
||||||
translate-plural="%{ count } tracks in %{ albumsCount } albums"
|
translate-plural="%{ count } tracks in %{ albumsCount } albums"
|
||||||
:translate-n="totalTracks"
|
:translate-n="totalTracks"
|
||||||
:translate-params="{count: totalTracks, albumsCount: totalAlbums}">
|
:translate-params="{count: totalTracks, albumsCount: totalAlbums}"
|
||||||
|
>
|
||||||
%{ count } track in %{ albumsCount } albums
|
%{ count } track in %{ albumsCount } albums
|
||||||
</translate>
|
</translate>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</h2>
|
</h2>
|
||||||
<tags-list v-if="object.tags && object.tags.length > 0" :tags="object.tags"></tags-list>
|
<tags-list
|
||||||
<div class="ui hidden divider"></div>
|
v-if="object.tags && object.tags.length > 0"
|
||||||
|
:tags="object.tags"
|
||||||
|
/>
|
||||||
|
<div class="ui hidden divider" />
|
||||||
<div class="header-buttons">
|
<div class="header-buttons">
|
||||||
<div class="ui 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>
|
||||||
<div class="ui buttons">
|
<div class="ui buttons">
|
||||||
<play-button :is-playable="isPlayable" class="vibrant" :artist="object">
|
<play-button
|
||||||
<translate translate-context="Content/Artist/Button.Label/Verb">Play all albums</translate>
|
:is-playable="isPlayable"
|
||||||
|
class="vibrant"
|
||||||
|
:artist="object"
|
||||||
|
>
|
||||||
|
<translate translate-context="Content/Artist/Button.Label/Verb">
|
||||||
|
Play all albums
|
||||||
|
</translate>
|
||||||
</play-button>
|
</play-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modal :show.sync="showEmbedModal" v-if="publicLibraries.length > 0">
|
<modal
|
||||||
|
v-if="publicLibraries.length > 0"
|
||||||
|
:show.sync="showEmbedModal"
|
||||||
|
>
|
||||||
<h4 class="header">
|
<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>
|
</h4>
|
||||||
<div class="scrolling content">
|
<div class="scrolling content">
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<embed-wizard type="artist" :id="object.id" />
|
<embed-wizard
|
||||||
|
:id="object.id"
|
||||||
|
type="artist"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="ui deny button">
|
<button class="ui deny button">
|
||||||
<translate translate-context="*/*/Button.Label/Verb">Cancel</translate>
|
<translate translate-context="*/*/Button.Label/Verb">
|
||||||
|
Cancel
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
<div class="ui buttons">
|
<div class="ui buttons">
|
||||||
<button class="ui button" @click="$refs.dropdown.click()">
|
<button
|
||||||
<translate translate-context="*/*/Button.Label/Noun">More…</translate>
|
class="ui button"
|
||||||
|
@click="$refs.dropdown.click()"
|
||||||
|
>
|
||||||
|
<translate translate-context="*/*/Button.Label/Noun">
|
||||||
|
More…
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
<button class="ui floating dropdown icon button" ref="dropdown" v-dropdown>
|
<button
|
||||||
<i class="dropdown icon"></i>
|
ref="dropdown"
|
||||||
|
v-dropdown
|
||||||
|
class="ui floating dropdown icon button"
|
||||||
|
>
|
||||||
|
<i class="dropdown icon" />
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<a
|
<a
|
||||||
:href="object.fid"
|
|
||||||
v-if="domain != $store.getters['instance/domain']"
|
v-if="domain != $store.getters['instance/domain']"
|
||||||
|
:href="object.fid"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="basic item">
|
class="basic item"
|
||||||
<i class="external icon"></i>
|
>
|
||||||
<translate :translate-params="{domain: domain}" translate-context="Content/*/Button.Label/Verb">View on %{ domain }</translate>
|
<i class="external icon" />
|
||||||
|
<translate
|
||||||
|
:translate-params="{domain: domain}"
|
||||||
|
translate-context="Content/*/Button.Label/Verb"
|
||||||
|
>View on %{ domain }</translate>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
role="button"
|
|
||||||
v-if="publicLibraries.length > 0"
|
v-if="publicLibraries.length > 0"
|
||||||
|
role="button"
|
||||||
|
class="basic item"
|
||||||
@click.prevent="showEmbedModal = !showEmbedModal"
|
@click.prevent="showEmbedModal = !showEmbedModal"
|
||||||
class="basic item">
|
>
|
||||||
<i class="code icon"></i>
|
<i class="code icon" />
|
||||||
<translate translate-context="Content/*/Button.Label/Verb">Embed</translate>
|
<translate translate-context="Content/*/Button.Label/Verb">
|
||||||
|
Embed
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
<a :href="wikipediaUrl" target="_blank" rel="noreferrer noopener" class="basic item">
|
<a
|
||||||
<i class="wikipedia w icon"></i>
|
: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>
|
<translate translate-context="Content/*/Button.Label/Verb">Search on Wikipedia</translate>
|
||||||
</a>
|
</a>
|
||||||
<a v-if="musicbrainzUrl" :href="musicbrainzUrl" target="_blank" rel="noreferrer noopener" class="basic item">
|
<a
|
||||||
<i class="external icon"></i>
|
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>
|
<translate translate-context="Content/*/*/Clickable, Verb">View on MusicBrainz</translate>
|
||||||
</a>
|
</a>
|
||||||
<a :href="discogsUrl" target="_blank" rel="noreferrer noopener" class="basic item">
|
<a
|
||||||
<i class="external icon"></i>
|
:href="discogsUrl"
|
||||||
<translate translate-context="Content/*/Button.Label/Verb">Search on Discogs</translate>
|
target="_blank"
|
||||||
</a>
|
rel="noreferrer noopener"
|
||||||
|
class="basic item"
|
||||||
|
>
|
||||||
|
<i class="external icon" />
|
||||||
|
<translate translate-context="Content/*/Button.Label/Verb">Search on Discogs</translate>
|
||||||
|
</a>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="object.is_local"
|
v-if="object.is_local"
|
||||||
:to="{name: 'library.artists.edit', params: {id: object.id }}"
|
: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"
|
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})"
|
v-for="obj in getReportableObjs({artist: object})"
|
||||||
:key="obj.target.type + obj.target.id"
|
: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 }}
|
<i class="share icon" /> {{ obj.label }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider" />
|
||||||
<router-link class="basic item" v-if="$store.state.auth.availablePermissions['library']" :to="{name: 'manage.library.artists.detail', params: {id: object.id}}">
|
<router-link
|
||||||
<i class="wrench icon"></i>
|
v-if="$store.state.auth.availablePermissions['library']"
|
||||||
<translate translate-context="Content/Moderation/Link">Open in moderation interface</translate>
|
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>
|
</router-link>
|
||||||
<a
|
<a
|
||||||
v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser"
|
v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser"
|
||||||
class="basic item"
|
class="basic item"
|
||||||
:href="$store.getters['instance/absoluteUrl'](`/api/admin/music/artist/${object.id}`)"
|
:href="$store.getters['instance/absoluteUrl'](`/api/admin/music/artist/${object.id}`)"
|
||||||
target="_blank" rel="noopener noreferrer">
|
target="_blank"
|
||||||
<i class="wrench icon"></i>
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<i class="wrench icon" />
|
||||||
<translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate>
|
<translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -123,44 +199,43 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<router-view
|
<router-view
|
||||||
|
:key="$route.fullPath"
|
||||||
:tracks="tracks"
|
:tracks="tracks"
|
||||||
:next-tracks-url="nextTracksUrl"
|
:next-tracks-url="nextTracksUrl"
|
||||||
:next-albums-url="nextAlbumsUrl"
|
:next-albums-url="nextAlbumsUrl"
|
||||||
:albums="albums"
|
:albums="albums"
|
||||||
:is-loading-albums="isLoadingAlbums"
|
:is-loading-albums="isLoadingAlbums"
|
||||||
|
:object="object"
|
||||||
|
object-type="artist"
|
||||||
@libraries-loaded="libraries = $event"
|
@libraries-loaded="libraries = $event"
|
||||||
:object="object" object-type="artist"
|
/>
|
||||||
:key="$route.fullPath"></router-view>
|
|
||||||
</template>
|
</template>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios"
|
import axios from 'axios'
|
||||||
import logger from "@/logging"
|
import logger from '@/logging'
|
||||||
import backend from "@/audio/backend"
|
import PlayButton from '@/components/audio/PlayButton'
|
||||||
import PlayButton from "@/components/audio/PlayButton"
|
import EmbedWizard from '@/components/audio/EmbedWizard'
|
||||||
import EmbedWizard from "@/components/audio/EmbedWizard"
|
|
||||||
import Modal from '@/components/semantic/Modal'
|
import Modal from '@/components/semantic/Modal'
|
||||||
import RadioButton from "@/components/radios/Button"
|
import RadioButton from '@/components/radios/Button'
|
||||||
import TagsList from "@/components/tags/List"
|
import TagsList from '@/components/tags/List'
|
||||||
import ReportMixin from '@/components/mixins/Report'
|
import ReportMixin from '@/components/mixins/Report'
|
||||||
|
|
||||||
import {getDomain} from '@/utils'
|
import { getDomain } from '@/utils'
|
||||||
|
|
||||||
const FETCH_URL = "albums/"
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [ReportMixin],
|
|
||||||
props: ["id"],
|
|
||||||
components: {
|
components: {
|
||||||
PlayButton,
|
PlayButton,
|
||||||
EmbedWizard,
|
EmbedWizard,
|
||||||
Modal,
|
Modal,
|
||||||
RadioButton,
|
RadioButton,
|
||||||
TagsList,
|
TagsList
|
||||||
},
|
},
|
||||||
data() {
|
mixins: [ReportMixin],
|
||||||
|
props: { id: { type: Number, required: true } },
|
||||||
|
data () {
|
||||||
return {
|
return {
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
isLoadingAlbums: true,
|
isLoadingAlbums: true,
|
||||||
|
@ -172,47 +247,7 @@ export default {
|
||||||
nextAlbumsUrl: null,
|
nextAlbumsUrl: null,
|
||||||
nextTracksUrl: null,
|
nextTracksUrl: null,
|
||||||
totalAlbums: null,
|
totalAlbums: null,
|
||||||
totalTracks: 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
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -220,37 +255,39 @@ export default {
|
||||||
if (this.object) {
|
if (this.object) {
|
||||||
return getDomain(this.object.fid)
|
return getDomain(this.object.fid)
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
},
|
},
|
||||||
isPlayable() {
|
isPlayable () {
|
||||||
return (
|
return (
|
||||||
this.object.albums.filter(a => {
|
this.object.albums.filter(a => {
|
||||||
return a.is_playable
|
return a.is_playable
|
||||||
}).length > 0
|
}).length > 0
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
labels() {
|
labels () {
|
||||||
return {
|
return {
|
||||||
title: this.$pgettext('*/*/*', 'Album')
|
title: this.$pgettext('*/*/*', 'Album')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
wikipediaUrl() {
|
wikipediaUrl () {
|
||||||
return (
|
return (
|
||||||
"https://en.wikipedia.org/w/index.php?search=" +
|
'https://en.wikipedia.org/w/index.php?search=' +
|
||||||
encodeURI(this.object.name)
|
encodeURI(this.object.name)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
musicbrainzUrl() {
|
musicbrainzUrl () {
|
||||||
if (this.object.mbid) {
|
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 (
|
return (
|
||||||
"https://discogs.com/search/?type=artist&title=" +
|
'https://discogs.com/search/?type=artist&title=' +
|
||||||
encodeURI(this.object.name)
|
encodeURI(this.object.name)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
cover() {
|
cover () {
|
||||||
if (this.object.cover && this.object.cover.urls.original) {
|
if (this.object.cover && this.object.cover.urls.original) {
|
||||||
return this.object.cover
|
return this.object.cover
|
||||||
}
|
}
|
||||||
|
@ -268,27 +305,65 @@ export default {
|
||||||
return l.privacy_level === 'everyone'
|
return l.privacy_level === 'everyone'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
headerStyle() {
|
headerStyle () {
|
||||||
if (!this.cover || !this.cover.urls.original) {
|
if (!this.cover || !this.cover.urls.original) {
|
||||||
return ""
|
return ''
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
"background-image: url(" +
|
'background-image: url(' +
|
||||||
this.$store.getters["instance/absoluteUrl"](this.cover.urls.original) +
|
this.$store.getters['instance/absoluteUrl'](this.cover.urls.original) +
|
||||||
")"
|
')'
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
contentFilter () {
|
contentFilter () {
|
||||||
let self = this
|
|
||||||
return this.$store.getters['moderation/artistFilters']().filter((e) => {
|
return this.$store.getters['moderation/artistFilters']().filter((e) => {
|
||||||
return e.target.id === this.object.id
|
return e.target.id === this.object.id
|
||||||
})[0]
|
})[0]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
id() {
|
id () {
|
||||||
this.fetchData()
|
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>
|
</script>
|
||||||
|
|
|
@ -1,69 +1,127 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="object">
|
<div v-if="object">
|
||||||
<div class="ui small text container" v-if="contentFilter">
|
<div
|
||||||
<div class="ui hidden divider"></div>
|
v-if="contentFilter"
|
||||||
|
class="ui small text container"
|
||||||
|
>
|
||||||
|
<div class="ui hidden divider" />
|
||||||
<div class="ui message">
|
<div class="ui message">
|
||||||
<p>
|
<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>
|
</p>
|
||||||
<router-link class="right floated" :to="{name: 'settings'}">
|
<router-link
|
||||||
<translate translate-context="Content/Moderation/Link">Review my filters</translate>
|
class="right floated"
|
||||||
|
:to="{name: 'settings'}"
|
||||||
|
>
|
||||||
|
<translate translate-context="Content/Moderation/Link">
|
||||||
|
Review my filters
|
||||||
|
</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
<button @click="$store.dispatch('moderation/deleteContentFilter', contentFilter.uuid)" class="ui basic tiny button">
|
<button
|
||||||
<translate translate-context="Content/Moderation/Button.Label">Remove filter</translate>
|
class="ui basic tiny button"
|
||||||
|
@click="$store.dispatch('moderation/deleteContentFilter', contentFilter.uuid)"
|
||||||
|
>
|
||||||
|
<translate translate-context="Content/Moderation/Button.Label">
|
||||||
|
Remove filter
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<section v-if="tracks.length > 0" class="ui vertical stripe segment">
|
<section
|
||||||
<track-table :is-artist="true" :show-position="false" :track-only="true" :tracks="tracks.slice(0,5)">
|
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">
|
<template slot="header">
|
||||||
<h2>
|
<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>
|
</h2>
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
</template>
|
</template>
|
||||||
</track-table>
|
</track-table>
|
||||||
</section>
|
</section>
|
||||||
<section v-if="isLoadingAlbums" class="ui vertical stripe segment">
|
<section
|
||||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
v-if="isLoadingAlbums"
|
||||||
|
class="ui vertical stripe segment"
|
||||||
|
>
|
||||||
|
<div :class="['ui', 'centered', 'active', 'inline', 'loader']" />
|
||||||
</section>
|
</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>
|
<h2>
|
||||||
<translate translate-context="Content/Artist/Title">Albums by this artist</translate>
|
<translate translate-context="Content/Artist/Title">
|
||||||
|
Albums by this artist
|
||||||
|
</translate>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="ui cards app-cards">
|
<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>
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<button :class="['ui', {loading: isLoadingMoreAlbums}, 'button']" v-if="nextAlbumsUrl && loadMoreAlbumsUrl" @click="loadMoreAlbums(loadMoreAlbumsUrl)">
|
<button
|
||||||
<translate translate-context="Content/*/Button.Label">Load more…</translate>
|
v-if="nextAlbumsUrl && loadMoreAlbumsUrl"
|
||||||
|
:class="['ui', {loading: isLoadingMoreAlbums}, 'button']"
|
||||||
|
@click="loadMoreAlbums(loadMoreAlbumsUrl)"
|
||||||
|
>
|
||||||
|
<translate translate-context="Content/*/Button.Label">
|
||||||
|
Load more…
|
||||||
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
<section class="ui vertical stripe segment">
|
<section class="ui vertical stripe segment">
|
||||||
<h2>
|
<h2>
|
||||||
<translate translate-context="Content/*/Title/Noun">User libraries</translate>
|
<translate translate-context="Content/*/Title/Noun">
|
||||||
|
User libraries
|
||||||
|
</translate>
|
||||||
</h2>
|
</h2>
|
||||||
<library-widget @loaded="$emit('libraries-loaded', $event)" :url="'artists/' + object.id + '/libraries/'">
|
<library-widget
|
||||||
<translate translate-context="Content/Artist/Paragraph" slot="subtitle">This artist is present in the following libraries:</translate>
|
: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>
|
</library-widget>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _ from "@/lodash"
|
import axios from 'axios'
|
||||||
import axios from "axios"
|
import AlbumCard from '@/components/audio/album/Card'
|
||||||
import logger from "@/logging"
|
import TrackTable from '@/components/audio/track/Table'
|
||||||
import AlbumCard from "@/components/audio/album/Card"
|
import LibraryWidget from '@/components/federation/LibraryWidget'
|
||||||
import TrackTable from "@/components/audio/track/Table"
|
|
||||||
import LibraryWidget from "@/components/federation/LibraryWidget"
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ["object", "tracks", "albums", "isLoadingAlbums", "nextTracksUrl", "nextAlbumsUrl"],
|
|
||||||
components: {
|
components: {
|
||||||
AlbumCard,
|
AlbumCard,
|
||||||
TrackTable,
|
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 () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -74,26 +132,24 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
contentFilter () {
|
contentFilter () {
|
||||||
let self = this
|
|
||||||
return this.$store.getters['moderation/artistFilters']().filter((e) => {
|
return this.$store.getters['moderation/artistFilters']().filter((e) => {
|
||||||
return e.target.id === this.object.id
|
return e.target.id === this.object.id
|
||||||
})[0]
|
})[0]
|
||||||
},
|
},
|
||||||
allAlbums () {
|
allAlbums () {
|
||||||
return this.albums.concat(this.additionalAlbums)
|
return this.albums.concat(this.additionalAlbums)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadMoreAlbums (url) {
|
loadMoreAlbums (url) {
|
||||||
let self = this
|
const self = this
|
||||||
self.isLoadingMoreAlbums = true
|
self.isLoadingMoreAlbums = true
|
||||||
axios.get(url).then((response) => {
|
axios.get(url).then((response) => {
|
||||||
self.additionalAlbums = self.additionalAlbums.concat(response.data.results)
|
self.additionalAlbums = self.additionalAlbums.concat(response.data.results)
|
||||||
self.loadMoreAlbumsUrl = response.data.next
|
self.loadMoreAlbumsUrl = response.data.next
|
||||||
self.isLoadingMoreAlbums = false
|
self.isLoadingMoreAlbums = false
|
||||||
}, (error) => {
|
}, () => {
|
||||||
self.isLoadingMoreAlbums = false
|
self.isLoadingMoreAlbums = false
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,56 @@
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<section class="ui vertical stripe segment">
|
<section class="ui vertical stripe segment">
|
||||||
<div class="ui text container">
|
<div class="ui text container">
|
||||||
<h2>
|
<h2>
|
||||||
<translate v-if="canEdit" key="1" translate-context="Content/*/Title">Edit this artist</translate>
|
<translate
|
||||||
<translate v-else key="2" translate-context="Content/*/Title">Suggest an edit on this artist</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>
|
</h2>
|
||||||
<div class="ui message" v-if="!object.is_local">
|
<div
|
||||||
<translate translate-context="Content/*/Message">This object is managed by another server, you cannot edit it.</translate>
|
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>
|
</div>
|
||||||
<edit-form
|
<edit-form
|
||||||
v-else
|
v-else
|
||||||
:object-type="objectType"
|
:object-type="objectType"
|
||||||
:object="object"
|
:object="object"
|
||||||
:can-edit="canEdit"></edit-form>
|
:can-edit="canEdit"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios"
|
|
||||||
|
|
||||||
import EditForm from '@/components/library/EditForm'
|
import EditForm from '@/components/library/EditForm'
|
||||||
export default {
|
export default {
|
||||||
props: ["objectType", "object", "libraries"],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
id: this.object.id,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
EditForm
|
EditForm
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
objectType: { type: String, required: true },
|
||||||
|
object: { type: Object, required: true },
|
||||||
|
libraries: { type: Array, required: true }
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
id: this.object.id
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
canEdit () {
|
canEdit () {
|
||||||
return true
|
return true
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue