diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a3a493e331fc680a3d1d2fc69597199380bcc517..380f359efdc1c01cad2bd4aa9f8f62f865b46765 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -136,10 +136,7 @@ eslint: - cd front - yarn install script: - # We search for all files ending with .vue or .js in src which changed in relation to develop - # and lint them. This way we focus on some errors instead of checking the hole repository - - export changedFiles=$(git diff --relative --name-only --diff-filter=d origin/develop -- src/ | grep -E "\.(vue|js)$") - - yarn run eslint --quiet -f table $(echo $changedFiles | tr '\n' ' ') + - yarn lint cache: key: "$CI_PROJECT_ID__eslint_npm_cache" paths: diff --git a/front/.eslintrc.js b/front/.eslintrc.js index c01b9875063a731dcd83272c1d546b8c2af6fdbf..c070808218f24bbd99ebaec3254ecc209e616159 100644 --- a/front/.eslintrc.js +++ b/front/.eslintrc.js @@ -20,5 +20,7 @@ module.exports = { 'vue' ], rules: { + "vue/no-v-html": "off", // TODO: tackle this properly + "vue/no-use-v-if-with-v-for": "off" } } diff --git a/front/package.json b/front/package.json index 791dd97f4c166267aea30ac39624d06e78beb3ff..75089cc7556170e741a236a9a90867ad530ab72a 100644 --- a/front/package.json +++ b/front/package.json @@ -8,7 +8,7 @@ "serve": "[ ! -d src/translations ] && npm run i18n-compile; vue-cli-service serve --port ${VUE_PORT:-8080} --host ${VUE_HOST:-0.0.0.0}", "build": "scripts/i18n-compile.sh && vue-cli-service build", "test:unit": "vue-cli-service test:unit --reporter mocha-junit-reporter", - "lint": "eslint $(git status --porcelain --untracked-files=no | grep -E '(A|M) ' | cut -d' ' -f3 | sed s_front/__ | grep -E '.(js|vue)$')", + "lint": "eslint --ext .js,.vue src", "i18n-compile": "scripts/i18n-compile.sh", "i18n-extract": "scripts/i18n-extract.sh", "fix-fomantic-css": "scripts/fix-fomantic-css.sh", diff --git a/front/src/EmbedFrame.vue b/front/src/EmbedFrame.vue index 8d0949b4fd04e91933f489b463e87d9dc288a1e3..bc31d51e40cc4a62e82e92ab03f06a783febfccd 100644 --- a/front/src/EmbedFrame.vue +++ b/front/src/EmbedFrame.vue @@ -2,57 +2,110 @@ <template> <main :class="[theme]"> <!-- SVG from https://cdn.plyr.io/3.4.7/plyr.svg --> - <svg aria-hidden="true" style="display: none" xmlns="http://www.w3.org/2000/svg"> - <symbol id="plyr-download"><path d="M9 13c.3 0 .5-.1.7-.3L15.4 7 14 5.6l-4 4V1H8v8.6l-4-4L2.6 7l5.7 5.7c.2.2.4.3.7.3zM2 15h14v2H2z"/></symbol> - <symbol id="plyr-enter-fullscreen"><path d="M10 3h3.6l-4 4L11 8.4l4-4V8h2V1h-7zM7 9.6l-4 4V10H1v7h7v-2H4.4l4-4z"/></symbol> - <symbol id="plyr-exit-fullscreen"><path d="M1 12h3.6l-4 4L2 17.4l4-4V17h2v-7H1zM16 .6l-4 4V1h-2v7h7V6h-3.6l4-4z"/></symbol> - <symbol id="plyr-fast-forward"><path d="M7.875 7.171L0 1v16l7.875-6.171V17L18 9 7.875 1z"/></symbol> - <symbol id="plyr-muted"><path d="M12.4 12.5l2.1-2.1 2.1 2.1 1.4-1.4L15.9 9 18 6.9l-1.4-1.4-2.1 2.1-2.1-2.1L11 6.9 13.1 9 11 11.1zM3.786 6.008H.714C.286 6.008 0 6.31 0 6.76v4.512c0 .452.286.752.714.752h3.072l4.071 3.858c.5.3 1.143 0 1.143-.602V2.752c0-.601-.643-.977-1.143-.601L3.786 6.008z"/></symbol> - <symbol id="plyr-pause"><path d="M6 1H3c-.6 0-1 .4-1 1v14c0 .6.4 1 1 1h3c.6 0 1-.4 1-1V2c0-.6-.4-1-1-1zM12 1c-.6 0-1 .4-1 1v14c0 .6.4 1 1 1h3c.6 0 1-.4 1-1V2c0-.6-.4-1-1-1h-3z"/></symbol> - <symbol id="plyr-pip"><path d="M13.293 3.293L7.022 9.564l1.414 1.414 6.271-6.271L17 7V1h-6z"/><path d="M13 15H3V5h5V3H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-6h-2v5z"/></symbol> - <symbol id="plyr-play"><path d="M15.562 8.1L3.87.225C3.052-.337 2 .225 2 1.125v15.75c0 .9 1.052 1.462 1.87.9L15.563 9.9c.584-.45.584-1.35 0-1.8z"/></symbol> - <symbol id="plyr-restart"><path d="M9.7 1.2l.7 6.4 2.1-2.1c1.9 1.9 1.9 5.1 0 7-.9 1-2.2 1.5-3.5 1.5-1.3 0-2.6-.5-3.5-1.5-1.9-1.9-1.9-5.1 0-7 .6-.6 1.4-1.1 2.3-1.3l-.6-1.9C6 2.6 4.9 3.2 4 4.1 1.3 6.8 1.3 11.2 4 14c1.3 1.3 3.1 2 4.9 2 1.9 0 3.6-.7 4.9-2 2.7-2.7 2.7-7.1 0-9.9L16 1.9l-6.3-.7z"/></symbol> - <symbol id="plyr-rewind"><path d="M10.125 1L0 9l10.125 8v-6.171L18 17V1l-7.875 6.171z"/></symbol> - <symbol id="plyr-settings"><path d="M16.135 7.784a2 2 0 0 1-1.23-2.969c.322-.536.225-.998-.094-1.316l-.31-.31c-.318-.318-.78-.415-1.316-.094a2 2 0 0 1-2.969-1.23C10.065 1.258 9.669 1 9.219 1h-.438c-.45 0-.845.258-.997.865a2 2 0 0 1-2.969 1.23c-.536-.322-.999-.225-1.317.093l-.31.31c-.318.318-.415.781-.093 1.317a2 2 0 0 1-1.23 2.969C1.26 7.935 1 8.33 1 8.781v.438c0 .45.258.845.865.997a2 2 0 0 1 1.23 2.969c-.322.536-.225.998.094 1.316l.31.31c.319.319.782.415 1.316.094a2 2 0 0 1 2.969 1.23c.151.607.547.865.997.865h.438c.45 0 .845-.258.997-.865a2 2 0 0 1 2.969-1.23c.535.321.997.225 1.316-.094l.31-.31c.318-.318.415-.781.094-1.316a2 2 0 0 1 1.23-2.969c.607-.151.865-.547.865-.997v-.438c0-.451-.26-.846-.865-.997zM9 12a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol> - <symbol id="plyr-volume"><path d="M15.6 3.3c-.4-.4-1-.4-1.4 0-.4.4-.4 1 0 1.4C15.4 5.9 16 7.4 16 9c0 1.6-.6 3.1-1.8 4.3-.4.4-.4 1 0 1.4.2.2.5.3.7.3.3 0 .5-.1.7-.3C17.1 13.2 18 11.2 18 9s-.9-4.2-2.4-5.7z"/><path d="M11.282 5.282a.909.909 0 0 0 0 1.316c.735.735.995 1.458.995 2.402 0 .936-.425 1.917-.995 2.487a.909.909 0 0 0 0 1.316c.145.145.636.262 1.018.156a.725.725 0 0 0 .298-.156C13.773 11.733 14.13 10.16 14.13 9c0-.17-.002-.34-.011-.51-.053-.992-.319-2.005-1.522-3.208a.909.909 0 0 0-1.316 0zM3.786 6.008H.714C.286 6.008 0 6.31 0 6.76v4.512c0 .452.286.752.714.752h3.072l4.071 3.858c.5.3 1.143 0 1.143-.602V2.752c0-.601-.643-.977-1.143-.601L3.786 6.008z"/></symbol> + <svg + aria-hidden="true" + style="display: none" + xmlns="http://www.w3.org/2000/svg" + > + <symbol id="plyr-download"><path d="M9 13c.3 0 .5-.1.7-.3L15.4 7 14 5.6l-4 4V1H8v8.6l-4-4L2.6 7l5.7 5.7c.2.2.4.3.7.3zM2 15h14v2H2z" /></symbol> + <symbol id="plyr-enter-fullscreen"><path d="M10 3h3.6l-4 4L11 8.4l4-4V8h2V1h-7zM7 9.6l-4 4V10H1v7h7v-2H4.4l4-4z" /></symbol> + <symbol id="plyr-exit-fullscreen"><path d="M1 12h3.6l-4 4L2 17.4l4-4V17h2v-7H1zM16 .6l-4 4V1h-2v7h7V6h-3.6l4-4z" /></symbol> + <symbol id="plyr-fast-forward"><path d="M7.875 7.171L0 1v16l7.875-6.171V17L18 9 7.875 1z" /></symbol> + <symbol id="plyr-muted"><path d="M12.4 12.5l2.1-2.1 2.1 2.1 1.4-1.4L15.9 9 18 6.9l-1.4-1.4-2.1 2.1-2.1-2.1L11 6.9 13.1 9 11 11.1zM3.786 6.008H.714C.286 6.008 0 6.31 0 6.76v4.512c0 .452.286.752.714.752h3.072l4.071 3.858c.5.3 1.143 0 1.143-.602V2.752c0-.601-.643-.977-1.143-.601L3.786 6.008z" /></symbol> + <symbol id="plyr-pause"><path d="M6 1H3c-.6 0-1 .4-1 1v14c0 .6.4 1 1 1h3c.6 0 1-.4 1-1V2c0-.6-.4-1-1-1zM12 1c-.6 0-1 .4-1 1v14c0 .6.4 1 1 1h3c.6 0 1-.4 1-1V2c0-.6-.4-1-1-1h-3z" /></symbol> + <symbol id="plyr-pip"><path d="M13.293 3.293L7.022 9.564l1.414 1.414 6.271-6.271L17 7V1h-6z" /><path d="M13 15H3V5h5V3H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-6h-2v5z" /></symbol> + <symbol id="plyr-play"><path d="M15.562 8.1L3.87.225C3.052-.337 2 .225 2 1.125v15.75c0 .9 1.052 1.462 1.87.9L15.563 9.9c.584-.45.584-1.35 0-1.8z" /></symbol> + <symbol id="plyr-restart"><path d="M9.7 1.2l.7 6.4 2.1-2.1c1.9 1.9 1.9 5.1 0 7-.9 1-2.2 1.5-3.5 1.5-1.3 0-2.6-.5-3.5-1.5-1.9-1.9-1.9-5.1 0-7 .6-.6 1.4-1.1 2.3-1.3l-.6-1.9C6 2.6 4.9 3.2 4 4.1 1.3 6.8 1.3 11.2 4 14c1.3 1.3 3.1 2 4.9 2 1.9 0 3.6-.7 4.9-2 2.7-2.7 2.7-7.1 0-9.9L16 1.9l-6.3-.7z" /></symbol> + <symbol id="plyr-rewind"><path d="M10.125 1L0 9l10.125 8v-6.171L18 17V1l-7.875 6.171z" /></symbol> + <symbol id="plyr-settings"><path d="M16.135 7.784a2 2 0 0 1-1.23-2.969c.322-.536.225-.998-.094-1.316l-.31-.31c-.318-.318-.78-.415-1.316-.094a2 2 0 0 1-2.969-1.23C10.065 1.258 9.669 1 9.219 1h-.438c-.45 0-.845.258-.997.865a2 2 0 0 1-2.969 1.23c-.536-.322-.999-.225-1.317.093l-.31.31c-.318.318-.415.781-.093 1.317a2 2 0 0 1-1.23 2.969C1.26 7.935 1 8.33 1 8.781v.438c0 .45.258.845.865.997a2 2 0 0 1 1.23 2.969c-.322.536-.225.998.094 1.316l.31.31c.319.319.782.415 1.316.094a2 2 0 0 1 2.969 1.23c.151.607.547.865.997.865h.438c.45 0 .845-.258.997-.865a2 2 0 0 1 2.969-1.23c.535.321.997.225 1.316-.094l.31-.31c.318-.318.415-.781.094-1.316a2 2 0 0 1 1.23-2.969c.607-.151.865-.547.865-.997v-.438c0-.451-.26-.846-.865-.997zM9 12a3 3 0 1 1 0-6 3 3 0 0 1 0 6z" /></symbol> + <symbol id="plyr-volume"><path d="M15.6 3.3c-.4-.4-1-.4-1.4 0-.4.4-.4 1 0 1.4C15.4 5.9 16 7.4 16 9c0 1.6-.6 3.1-1.8 4.3-.4.4-.4 1 0 1.4.2.2.5.3.7.3.3 0 .5-.1.7-.3C17.1 13.2 18 11.2 18 9s-.9-4.2-2.4-5.7z" /><path d="M11.282 5.282a.909.909 0 0 0 0 1.316c.735.735.995 1.458.995 2.402 0 .936-.425 1.917-.995 2.487a.909.909 0 0 0 0 1.316c.145.145.636.262 1.018.156a.725.725 0 0 0 .298-.156C13.773 11.733 14.13 10.16 14.13 9c0-.17-.002-.34-.011-.51-.053-.992-.319-2.005-1.522-3.208a.909.909 0 0 0-1.316 0zM3.786 6.008H.714C.286 6.008 0 6.31 0 6.76v4.512c0 .452.286.752.714.752h3.072l4.071 3.858c.5.3 1.143 0 1.143-.602V2.752c0-.601-.643-.977-1.143-.601L3.786 6.008z" /></symbol> <!-- those ones are from fork-awesome --> - <symbol id="plyr-step-backward"><path d="M979 141c25-25 45-16 45 19v1472c0 35-20 44-45 19L269 941c-6-6-10-12-13-19v678c0 35-29 64-64 64H64c-35 0-64-29-64-64V192c0-35 29-64 64-64h128c35 0 64 29 64 64v678c3-7 7-13 13-19z"/></symbol> - <symbol id="plyr-step-forward"><path d="M45 1651c-25 25-45 16-45-19V160c0-35 20-44 45-19l710 710c6 6 10 12 13 19V192c0-35 29-64 64-64h128c35 0 64 29 64 64v1408c0 35-29 64-64 64H832c-35 0-64-29-64-64V922c-3 7-7 13-13 19z"/></symbol> + <symbol id="plyr-step-backward"><path d="M979 141c25-25 45-16 45 19v1472c0 35-20 44-45 19L269 941c-6-6-10-12-13-19v678c0 35-29 64-64 64H64c-35 0-64-29-64-64V192c0-35 29-64 64-64h128c35 0 64 29 64 64v678c3-7 7-13 13-19z" /></symbol> + <symbol id="plyr-step-forward"><path d="M45 1651c-25 25-45 16-45-19V160c0-35 20-44 45-19l710 710c6 6 10 12 13 19V192c0-35 29-64 64-64h128c35 0 64 29 64 64v1408c0 35-29 64-64 64H832c-35 0-64-29-64-64V922c-3 7-7 13-13 19z" /></symbol> </svg> <article> - <aside class="cover main" v-if="currentTrack"> - <img height="120" v-if="currentTrack.cover" :src="currentTrack.cover" alt="Cover" /> - <img height="120" v-else src="./assets/embed/default-cover.jpeg" alt="Cover" /> + <aside + v-if="currentTrack" + class="cover main" + > + <img + v-if="currentTrack.cover" + height="120" + :src="currentTrack.cover" + alt="Cover" + > + <img + v-else + height="120" + src="./assets/embed/default-cover.jpeg" + alt="Cover" + > </aside> - <div class="content" aria-label="Track information"> + <div + class="content" + aria-label="Track information" + > <header v-if="currentTrack"> - <h3><a :href="fullUrl('/library/tracks/' + currentTrack.id)" target="_blank" rel="noopener noreferrer">{{ currentTrack.title }}</a></h3> - <a :href="fullUrl('/library/artists/' + currentTrack.artist.id)" target="_blank" rel="noopener noreferrer">{{ currentTrack.artist.name }}</a> + <h3> + <a + :href="fullUrl('/library/tracks/' + currentTrack.id)" + target="_blank" + rel="noopener noreferrer" + >{{ currentTrack.title }}</a> + </h3> + <a + :href="fullUrl('/library/artists/' + currentTrack.artist.id)" + target="_blank" + rel="noopener noreferrer" + >{{ currentTrack.artist.name }}</a> </header> - <section v-if="!isLoading" class="controls" aria-label="Audio player"> + <section + v-if="!isLoading" + class="controls" + aria-label="Audio player" + > <template v-if="currentTrack && currentTrack.sources.length > 0"> - <div class="queue-controls plyr--audio" v-if="tracks.length > 1"> + <div + v-if="tracks.length > 1" + class="queue-controls plyr--audio" + > <div class="plyr__controls"> <button + type="button" + class="plyr__control" + aria-label="Play previous track" @focus="setControlFocus($event, true)" @blur="setControlFocus($event, false)" @click="previous()" - type="button" - class="plyr__control" - aria-label="Play previous track"> - <svg class="icon--not-pressed" role="presentation" focusable="false" viewBox="0 0 1100 1650" width="80" height="80"> - <use xlink:href="#plyr-step-backward"></use> + > + <svg + class="icon--not-pressed" + role="presentation" + focusable="false" + viewBox="0 0 1100 1650" + width="80" + height="80" + > + <use xlink:href="#plyr-step-backward" /> </svg> </button> <button + type="button" + class="plyr__control" + aria-label="Play next track" @click="next()" @focus="setControlFocus($event, true)" @blur="setControlFocus($event, false)" - type="button" - class="plyr__control" - aria-label="Play next track"> - <svg class="icon--not-pressed" role="presentation" focusable="false" viewBox="0 0 1100 1650" width="80" height="80"> - <use xlink:href="#plyr-step-forward"></use> + > + <svg + class="icon--not-pressed" + role="presentation" + focusable="false" + viewBox="0 0 1100 1650" + width="80" + height="80" + > + <use xlink:href="#plyr-step-forward" /> </svg> </button> </div> @@ -62,51 +115,122 @@ :key="currentIndex" ref="player" class="player" - :options="{loadSprite: false, controls: controls, duration: currentTrack.sources[0].duration, autoplay}"> + :options="{loadSprite: false, controls: controls, duration: currentTrack.sources[0].duration, autoplay}" + > <audio preload="none"> - <source v-for="source in currentTrack.sources" :src="source.src" :type="source.type"/> + <source + v-for="(source, key) in currentTrack.sources" + :key="key" + :src="source.src" + :type="source.type" + > </audio> </vue-plyr> </template> - <div v-else class="player"> - <span v-if="error === 'invalid_type'" class="error">Widget improperly configured (bad resource type {{ type }}).</span> - <span v-else-if="error === 'invalid_id'" class="error">Widget improperly configured (missing resource id).</span> - <span v-else-if="error === 'server_not_found'" class="error">Track not found.</span> - <span v-else-if="error === 'server_requires_auth'" class="error">You need to login to access this resource.</span> - <span v-else-if="error === 'server_error'" class="error">A server error occurred.</span> - <span v-else-if="error === 'server_error'" class="error">An unknown error occurred while loading track data from server.</span> - <span v-else-if="currentTrack && currentTrack.sources.length === 0" class="error">This track is unavailable.</span> - <span v-else class="error">An unknown error occurred while loading track data.</span> + <div + v-else + class="player" + > + <span + v-if="error === 'invalid_type'" + class="error" + >Widget improperly configured (bad resource type {{ type }}).</span> + <span + v-else-if="error === 'invalid_id'" + class="error" + >Widget improperly configured (missing resource id).</span> + <span + v-else-if="error === 'server_not_found'" + class="error" + >Track not found.</span> + <span + v-else-if="error === 'server_requires_auth'" + class="error" + >You need to login to access this resource.</span> + <span + v-else-if="error === 'server_error'" + class="error" + >An unknown error occurred while loading track data from server.</span> + <span + v-else-if="currentTrack && currentTrack.sources.length === 0" + class="error" + >This track is unavailable.</span> + <span + v-else + class="error" + >An unknown error occurred while loading track data.</span> </div> - <a title="Funkwhale" href="https://funkwhale.audio" target="_blank" rel="noopener noreferrer" class="logo-wrapper"> - <logo :fill="currentTheme.textColor" class="logo"></logo> + <a + title="Funkwhale" + href="https://funkwhale.audio" + target="_blank" + rel="noopener noreferrer" + class="logo-wrapper" + > + <logo + :fill="currentTheme.textColor" + class="logo" + /> </a> </section> </div> </article> - <div v-if="tracks.length > 1" class="queue-wrapper" id="queue"> + <div + v-if="tracks.length > 1" + id="queue" + class="queue-wrapper" + > <table class="queue"> <tbody> <tr - :id="'queue-item-' + index" - role="button" + v-for="(track, index) in tracks" v-if="track.sources.length > 0" + :id="'queue-item-' + index" :key="index" + role="button" :class="[{active: index === currentIndex}]" @click="play(index)" @keyup.enter="play(index)" - v-for="(track, index) in tracks"> - <td class="position-cell" width="40"> + > + <td + class="position-cell" + width="40" + > <span class="position"> {{ index + 1 }} </span> </td> - <td class="title" :title="track.title" ><div colspan="2" class="ellipsis">{{ track.title }}</div></td> - <td class="artist" :title="track.artist.name" ><div class="ellipsis">{{ track.artist.name }}</div></td> + <td + class="title" + :title="track.title" + > + <div + colspan="2" + class="ellipsis" + > + {{ track.title }} + </div> + </td> + <td + class="artist" + :title="track.artist.name" + > + <div class="ellipsis"> + {{ track.artist.name }} + </div> + </td> <td class="album"> - <div class="ellipsis" v-if="track.album" :title="track.album.title">{{ track.album.title }}</div> + <div + v-if="track.album" + class="ellipsis" + :title="track.album.title" + > + {{ track.album.title }} + </div> + </td> + <td width="50"> + {{ time.durationFormatted(track.sources[0].duration) }} </td> - <td width="50">{{ time.durationFormatted(track.sources[0].duration) }}</td> </tr> </tbody> </table> @@ -116,26 +240,24 @@ <script> import axios from 'axios' -import Logo from "@/components/Logo" +import Logo from '@/components/Logo' import url from '@/utils/url' import time from '@/utils/time' function getURLParams () { - var urlParams - var match, - pl = /\+/g, // Regex for replacing addition symbol with a space - search = /([^&=]+)=?([^&]*)/g, - decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); }, - query = window.location.search.substring(1); + let match + const pl = /\+/g // Regex for replacing addition symbol with a space + const urlParams = {} + const search = /([^&=]+)=?([^&]*)/g + const decode = function (s) { return decodeURIComponent(s.replace(pl, ' ')) } + const query = window.location.search.substring(1) - urlParams = {}; - while (match = search.exec(query)) - urlParams[decode(match[1])] = decode(match[2]); + while (match === search.exec(query)) { urlParams[decode(match[1])] = decode(match[2]) } return urlParams } export default { - name: 'app', - components: {Logo}, + name: 'App', + components: { Logo }, data () { return { time, @@ -152,38 +274,11 @@ export default { currentIndex: -1, themes: { dark: { - textColor: 'white', + textColor: 'white' } } } }, - created () { - let params = getURLParams() - this.baseUrl = params.b || '' - this.type = params.type - if (this.supportedTypes.indexOf(this.type) === -1) { - this.error = 'invalid_type' - } - this.id = params.id - if (!this.id) { - this.error = 'invalid_id' - } - if (this.error) { - this.isLoading = false - return - } - if (!!params.instance) { - this.baseUrl = params.instance - } - - this.autoplay = params.autoplay != undefined || params.auto_play != undefined - this.fetch(this.type, this.id) - }, - mounted () { - var parser = document.createElement('a') - parser.href = this.baseUrl - this.url = parser - }, computed: { currentTrack () { if (this.tracks.length === 0) { @@ -195,12 +290,12 @@ export default { return this.themes[this.theme] }, controls () { - return [ + 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 + 'volume' // Volume control ] }, hasPrevious () { @@ -208,7 +303,54 @@ export default { }, hasNext () { return this.currentIndex < this.tracks.length - 1 + } + }, + watch: { + currentIndex (v) { + // we bind player events + const self = this + this.$nextTick(() => { + self.bindEvents() + if (self.tracks.length > 0) { + const el = document.getElementById(`queue-item-${v}`) + if (!el) { + return + } + const topPos = el.offsetTop + document.getElementById('queue').scrollTop = topPos - 10 + } + }) }, + tracks () { + this.currentIndex = 0 + } + }, + created () { + const params = getURLParams() + this.baseUrl = params.b || '' + this.type = params.type + if (this.supportedTypes.indexOf(this.type) === -1) { + this.error = 'invalid_type' + } + this.id = params.id + if (!this.id) { + this.error = 'invalid_id' + } + if (this.error) { + this.isLoading = false + return + } + if (params.instance) { + this.baseUrl = params.instance + } + + this.autoplay = params.autoplay !== undefined || params.auto_play !== undefined + this.fetch(this.type, this.id) + }, + mounted () { + const parser = document.createElement('a') + parser.href = this.baseUrl + this.url = parser }, methods: { next () { @@ -221,11 +363,11 @@ export default { this.play(this.currentIndex - 1) } }, - setControlFocus(event, enable) { + setControlFocus (event, enable) { if (enable) { - event.target.classList.add("plyr__tab-focus"); + event.target.classList.add('plyr__tab-focus') } else { - event.target.classList.remove("plyr__tab-focus"); + event.target.classList.remove('plyr__tab-focus') } }, fetch (type, id) { @@ -233,13 +375,13 @@ export default { this.fetchTrack(id) } if (type === 'album') { - this.fetchTracks({album: id, playable: true, ordering: "disc_number,position"}) + this.fetchTracks({ album: id, playable: true, ordering: 'disc_number,position' }) } if (type === 'channel') { - this.fetchTracks({channel: id, playable: true, include_channels: 'true', ordering: "-creation_date"}) + this.fetchTracks({ channel: id, playable: true, include_channels: 'true', ordering: '-creation_date' }) } if (type === 'artist') { - this.fetchTracks({artist: id, playable: true, include_channels: 'true', ordering: "-album__release_date,disc_number,position"}) + this.fetchTracks({ artist: id, playable: true, include_channels: 'true', ordering: '-album__release_date,disc_number,position' }) } if (type === 'playlist') { this.fetchTracks({}, `/api/v1/playlists/${id}/tracks/`) @@ -247,67 +389,61 @@ export default { }, play (index) { this.currentIndex = index - let self = this + const self = this this.$nextTick(() => { self.$refs.player.player.play() }) }, fetchTrack (id) { - let self = this - let url = `${this.baseUrl}/api/v1/tracks/${id}/` + const self = this + const url = `${this.baseUrl}/api/v1/tracks/${id}/` axios.get(url).then(response => { self.tracks = self.parseTracks([response.data]) - self.isLoading = false; + self.isLoading = false }).catch(error => { if (error.response) { if (error.response.status === 404) { self.error = 'server_not_found' - } - else if (error.response.status === 403) { + } else if (error.response.status === 403) { self.error = 'server_requires_auth' - } - else if (error.response.status === 500) { + } else if (error.response.status === 500) { self.error = 'server_error' - } - else { + } else { self.error = 'server_unknown_error' } } else { self.error = 'server_unknown_error' } - self.isLoading = false; + self.isLoading = false }) }, fetchTracks (filters, path) { - path = path || "/api/v1/tracks/" - filters.include_channels = "true" - let self = this - let url = `${this.baseUrl}${path}` - axios.get(url, {params: filters}).then(response => { + path = path || '/api/v1/tracks/' + filters.include_channels = 'true' + const self = this + const url = `${this.baseUrl}${path}` + axios.get(url, { params: filters }).then(response => { self.tracks = self.parseTracks(response.data.results) - self.isLoading = false; + self.isLoading = false }).catch(error => { if (error.response) { if (error.response.status === 404) { self.error = 'server_not_found' - } - else if (error.response.status === 403) { + } else if (error.response.status === 403) { self.error = 'server_requires_auth' - } - else if (error.response.status === 500) { + } else if (error.response.status === 500) { self.error = 'server_error' - } - else { + } else { self.error = 'server_unknown_error' } } else { self.error = 'server_unknown_error' } - self.isLoading = false; + self.isLoading = false }) }, parseTracks (tracks) { - let self = this + const self = this if (this.type === 'playlist') { tracks = tracks.map((t) => { return t.track @@ -325,7 +461,7 @@ export default { }) }, bindEvents () { - let self = this + const self = this this.$refs.player.player.on('ended', () => { self.next() }) @@ -336,17 +472,17 @@ export default { } return path }, - getCover(albumCover) { + getCover (albumCover) { if (albumCover) { return albumCover.urls.medium_square_crop } }, getSources (uploads) { - let self = this; - let a = document.createElement('audio') - let allowed = ['probably', 'maybe'] - let sources = uploads.filter(u => { - let canPlay = a.canPlayType(u.mimetype) + const self = this + const a = document.createElement('audio') + const allowed = ['probably', 'maybe'] + const sources = uploads.filter(u => { + const canPlay = a.canPlayType(u.mimetype) return allowed.indexOf(canPlay) > -1 }).map(u => { return { @@ -371,26 +507,6 @@ export default { } return sources } - }, - watch: { - currentIndex (v) { - // we bind player events - let self = this - this.$nextTick(() => { - self.bindEvents() - if (self.tracks.length > 0) { - let el = document.getElementById(`queue-item-${v}`); - if (!el) { - return - } - var topPos = el.offsetTop; - document.getElementById('queue').scrollTop = topPos-10; - } - }) - }, - tracks () { - this.currentIndex = 0 - } } } </script> diff --git a/front/src/audio/backend.js b/front/src/audio/backend.js index c371566758631cf2a6f8c58547d01a47c318e3dd..20032ac13dd942e3a299e1d7f11b34be2e7af41f 100644 --- a/front/src/audio/backend.js +++ b/front/src/audio/backend.js @@ -1,4 +1,4 @@ -var Album = { +const Album = { clean (album) { // we manually rebind the album and artist to each child track album.tracks = album.tracks.map((track) => { @@ -8,7 +8,7 @@ var Album = { return album } } -var Artist = { +const Artist = { clean (artist) { // clean data as given by the API artist.albums = artist.albums.map((album) => { diff --git a/front/src/audio/volume.js b/front/src/audio/volume.js index c184a9767e776e6e1f1df82877b4e51fa41ac1c5..1b7e2bc62f57a3932f746616f84a658b40f306db 100644 --- a/front/src/audio/volume.js +++ b/front/src/audio/volume.js @@ -1,25 +1,25 @@ const DYNAMIC_RANGE = 40 // dB -function toLinearVolumeScale(v) { - if (v <= 0.0) { - return 0.0 - } +function toLinearVolumeScale (v) { + if (v <= 0.0) { + return 0.0 + } - // (1.0; 0.0) -> (0; -DYNAMIC_RANGE) dB - let dB = (v-1)*DYNAMIC_RANGE + // (1.0; 0.0) -> (0; -DYNAMIC_RANGE) dB + const dB = (v - 1) * DYNAMIC_RANGE - return Math.pow(10, dB / 20) + return Math.pow(10, dB / 20) } -function toLogarithmicVolumeScale(v) { - if (v <= 0.0) { - return 0.0 - } +function toLogarithmicVolumeScale (v) { + if (v <= 0.0) { + return 0.0 + } - let dB = 20 * Math.log10(v) + const dB = 20 * Math.log10(v) - // (0; -DYNAMIC_RANGE) [dB] -> (1.0; 0.0) - return 1 - (dB / -DYNAMIC_RANGE) + // (0; -DYNAMIC_RANGE) [dB] -> (1.0; 0.0) + return 1 - (dB / -DYNAMIC_RANGE) } exports.toLinearVolumeScale = toLinearVolumeScale diff --git a/front/src/components/Footer.vue b/front/src/components/Footer.vue index 0610318708dd67795ea66bdc9f2221299ddeada8..f510e56ab0a2f6a13c869ffd62716a404261fdce 100644 --- a/front/src/components/Footer.vue +++ b/front/src/components/Footer.vue @@ -1,77 +1,208 @@ <template> - <footer id="footer" role="contentinfo" class="ui vertical footer segment" aria-labelledby="footer-label"> - <h1 id="footer-label" class="visually-hidden"> - <translate translate-context="*/*/*">Application footer</translate> + <footer + id="footer" + role="contentinfo" + class="ui vertical footer segment" + aria-labelledby="footer-label" + > + <h1 + id="footer-label" + class="visually-hidden" + > + <translate translate-context="*/*/*"> + Application footer + </translate> </h1> <div class="ui container"> <div class="ui stackable equal height stackable grid"> <section class="four wide column"> - <h4 v-if="podName" class="ui header ellipsis"> - <span v-translate="{instanceName: podName}" translate-context="Footer/About/Title">About %{instanceName}</span> + <h4 + v-if="podName" + class="ui header ellipsis" + > + <span + v-translate="{instanceName: podName}" + translate-context="Footer/About/Title" + >About %{instanceName}</span> </h4> - <h4 v-else class="ui header ellipsis"> - <span v-translate="{instanceUrl: instanceHostname}" translate-context="Footer/About/Title">About %{instanceUrl}</span> + <h4 + v-else + class="ui header ellipsis" + > + <span + v-translate="{instanceUrl: instanceHostname}" + translate-context="Footer/About/Title" + >About %{instanceUrl}</span> </h4> <div class="ui list"> - <router-link v-if="this.$route.path != '/about'" class="link item" to="/about"> - <translate translate-context="Footer/About/List item.Link">About</translate> + <router-link + v-if="$route.path != '/about'" + class="link item" + to="/about" + > + <translate translate-context="Footer/About/List item.Link"> + About + </translate> </router-link> - <router-link v-else-if="this.$route.path == '/about' && $store.state.auth.authenticated" class="link item" to="/library"> - <translate translate-context="Footer/*/List item.Link">Go to Library</translate> + <router-link + v-else-if="$route.path == '/about' && $store.state.auth.authenticated" + class="link item" + to="/library" + > + <translate translate-context="Footer/*/List item.Link"> + Go to Library + </translate> </router-link> - <router-link v-else class="link item" to="/"> - <translate translate-context="Footer/*/List item.Link">Home Page</translate> + <router-link + v-else + class="link item" + to="/" + > + <translate translate-context="Footer/*/List item.Link"> + Home Page + </translate> </router-link> - <a v-if="version" class="link item" href="https://docs.funkwhale.audio/changelog.html" target="_blank"> - <translate translate-context="Footer/*/List item" :translate-params="{version: version}" >Version %{version}</translate> - </a> - <a role="button" href="" class="link item" @click.prevent="$emit('show:set-instance-modal')" > + <a + v-if="version" + class="link item" + href="https://docs.funkwhale.audio/changelog.html" + target="_blank" + > + <translate + translate-context="Footer/*/List item" + :translate-params="{version: version}" + >Version %{version}</translate> + </a> + <a + role="button" + href="" + class="link item" + @click.prevent="$emit('show:set-instance-modal')" + > <translate translate-context="Footer/*/List item.Link">Use another instance</translate> </a> </div> <div class="ui form"> <div class="ui field"> <label for="language-select"><translate translate-context="Footer/Settings/Dropdown.Label/Short, Verb">Change language</translate></label> - <select id="language-select" class="ui dropdown" :value="$language.current" @change="$store.dispatch('ui/currentLanguage', $event.target.value)"> - <option v-for="(language, key) in $language.available" :key="key" :value="key">{{ language }}</option> + <select + id="language-select" + class="ui dropdown" + :value="$language.current" + @change="$store.dispatch('ui/currentLanguage', $event.target.value)" + > + <option + v-for="(language, key) in $language.available" + :key="key" + :value="key" + > + {{ language }} + </option> </select> </div> </div> </section> <section class="four wide column"> - <h4 v-translate class="ui header" translate-context="Footer/*/Title">Using Funkwhale</h4> + <h4 + v-translate + class="ui header" + translate-context="Footer/*/Title" + > + Using Funkwhale + </h4> <div class="ui list"> - <a href="https://docs.funkwhale.audio" class="link item" target="_blank"><translate translate-context="Footer/*/List item.Link/Short, Noun">Documentation</translate></a> - <a href="https://funkwhale.audio/apps" class="link item" target="_blank"><translate translate-context="Footer/*/List item.Link">Mobile and desktop apps</translate></a> - <a hrelf="" class="link item" @click.prevent="$emit('show:shortcuts-modal')"><translate translate-context="*/*/*/Noun">Keyboard shortcuts</translate></a> + <a + href="https://docs.funkwhale.audio" + class="link item" + target="_blank" + ><translate translate-context="Footer/*/List item.Link/Short, Noun">Documentation</translate></a> + <a + href="https://funkwhale.audio/apps" + class="link item" + target="_blank" + ><translate translate-context="Footer/*/List item.Link">Mobile and desktop apps</translate></a> + <a + hrelf="" + class="link item" + @click.prevent="$emit('show:shortcuts-modal')" + ><translate translate-context="*/*/*/Noun">Keyboard shortcuts</translate></a> </div> <div class="ui form"> <div class="ui field"> <label for="theme-select"><translate translate-context="Footer/Settings/Dropdown.Label/Short, Verb">Change theme</translate></label> - <select id="theme-select" class="ui dropdown" :value="$store.state.ui.theme" @change="$store.dispatch('ui/theme', $event.target.value)"> - <option v-for="theme in themes" :key="theme.key" :value="theme.key">{{ theme.name }}</option> + <select + id="theme-select" + class="ui dropdown" + :value="$store.state.ui.theme" + @change="$store.dispatch('ui/theme', $event.target.value)" + > + <option + v-for="theme in themes" + :key="theme.key" + :value="theme.key" + > + {{ theme.name }} + </option> </select> </div> </div> </section> <section class="four wide column"> - <h4 v-translate translate-context="Footer/*/Link" class="ui header">Getting help</h4> + <h4 + v-translate + translate-context="Footer/*/Link" + class="ui header" + > + Getting help + </h4> <div class="ui list"> - <a href="https://forum.funkwhale.audio/" class="link item" target="_blank"><translate translate-context="Footer/*/Listitem.Link">Support forum</translate></a> - <a href="https://matrix.to/#/#funkwhale-troubleshooting:matrix.org" class="link item" target="_blank"><translate translate-context="Footer/*/List item.Link">Chat room</translate></a> - <a href="https://dev.funkwhale.audio/funkwhale/funkwhale/issues" class="link item" target="_blank"><translate translate-context="Footer/*/List item.Link">Issue tracker</translate></a> + <a + href="https://forum.funkwhale.audio/" + class="link item" + target="_blank" + ><translate translate-context="Footer/*/Listitem.Link">Support forum</translate></a> + <a + href="https://matrix.to/#/#funkwhale-troubleshooting:matrix.org" + class="link item" + target="_blank" + ><translate translate-context="Footer/*/List item.Link">Chat room</translate></a> + <a + href="https://dev.funkwhale.audio/funkwhale/funkwhale/issues" + class="link item" + target="_blank" + ><translate translate-context="Footer/*/List item.Link">Issue tracker</translate></a> </div> </section> <section class="four wide column"> - <h4 v-translate class="ui header" translate-context="Footer/*/Title/Short">About Funkwhale</h4> + <h4 + v-translate + class="ui header" + translate-context="Footer/*/Title/Short" + > + About Funkwhale + </h4> <div class="ui list"> - <a href="https://funkwhale.audio" class="link item" target="_blank"><translate translate-context="Footer/*/List item.Link">Official website</translate></a> - <a href="https://contribute.funkwhale.audio" class="link item" target="_blank"><translate translate-context="Footer/*/List item.Link">Contribute</translate></a> - <a href="https://dev.funkwhale.audio/funkwhale/funkwhale" class="link item" target="_blank"><translate translate-context="Footer/*/List item.Link">Source code</translate></a> + <a + href="https://funkwhale.audio" + class="link item" + target="_blank" + ><translate translate-context="Footer/*/List item.Link">Official website</translate></a> + <a + href="https://contribute.funkwhale.audio" + class="link item" + target="_blank" + ><translate translate-context="Footer/*/List item.Link">Contribute</translate></a> + <a + href="https://dev.funkwhale.audio/funkwhale/funkwhale" + class="link item" + target="_blank" + ><translate translate-context="Footer/*/List item.Link">Source code</translate></a> </div> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <p> - <translate translate-context="Footer/*/List item.Link">The Funkwhale logo was kindly designed and provided by Francis Gading.</translate> + <translate translate-context="Footer/*/List item.Link"> + The Funkwhale logo was kindly designed and provided by Francis Gading. + </translate> </p> </section> </div> @@ -80,24 +211,22 @@ </template> <script> -import Vue from "vue" -import { mapState } from "vuex" -import axios from 'axios' +import { mapState } from 'vuex' import _ from '@/lodash' export default { - props: ["version"], + props: { version: { type: String, required: true } }, computed: { ...mapState({ messages: state => state.ui.messages, - nodeinfo: state => state.instance.nodeinfo, + nodeinfo: state => state.instance.nodeinfo }), - podName() { + podName () { return _.get(this.nodeinfo, 'metadata.nodeName') }, - instanceHostname() { - let url = this.$store.state.instance.instanceUrl - let parser = document.createElement("a") + instanceHostname () { + const url = this.$store.state.instance.instanceUrl + const parser = document.createElement('a') parser.href = url return parser.hostname }, diff --git a/front/src/components/Home.vue b/front/src/components/Home.vue index 92615c985b6fdeab5d1284c9681406a902f46de5..9b4c8194b3ed9728f3d73ec7a17ff5d0e35f8060 100644 --- a/front/src/components/Home.vue +++ b/front/src/components/Home.vue @@ -1,15 +1,25 @@ <template> - <main class="main pusher page-home" v-title="labels.title"> - <section :class="['ui', 'head', {'with-background': banner}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" :style="headerStyle"> + <main + v-title="labels.title" + class="main pusher page-home" + > + <section + :class="['ui', 'head', {'with-background': banner}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" + :style="headerStyle" + > <div class="segment-content"> <h1 class="ui center aligned large header"> <span v-translate="{podName: podName}" translate-context="Content/Home/Header" - :translate-params="{podName: podName}"> + :translate-params="{podName: podName}" + > Welcome to %{ podName }! </span> - <div v-if="shortDescription" class="sub header"> + <div + v-if="shortDescription" + class="sub header" + > {{ shortDescription }} </div> </h1> @@ -19,31 +29,61 @@ <div class="ui stackable grid"> <div class="ten wide column"> <h2 class="header"> - <translate translate-context="Content/Home/Header">About this Funkwhale pod</translate> + <translate translate-context="Content/Home/Header"> + About this Funkwhale pod + </translate> </h2> - <div class="ui raised segment" id="pod"> + <div + id="pod" + class="ui raised segment" + > <div class="ui stackable grid"> <div class="eight wide column"> <p v-if="!truncatedDescription"> - <translate translate-context="Content/Home/Paragraph">No description available.</translate> + <translate translate-context="Content/Home/Paragraph"> + No description available. + </translate> </p> <template v-if="truncatedDescription || rules"> - <div v-if="truncatedDescription" v-html="truncatedDescription"></div> - <div v-if="truncatedDescription" class="ui hidden divider"></div> + <div + v-if="truncatedDescription" + v-html="truncatedDescription" + /> + <div + v-if="truncatedDescription" + class="ui hidden divider" + /> <div class="ui relaxed list"> - <div class="item" v-if="truncatedDescription"> - <i class="arrow right icon"></i> + <div + v-if="truncatedDescription" + class="item" + > + <i class="arrow right icon" /> <div class="content"> - <router-link class="ui link" :to="{name: 'about'}"> - <translate translate-context="Content/Home/Link">Learn more</translate> + <router-link + class="ui link" + :to="{name: 'about'}" + > + <translate translate-context="Content/Home/Link"> + Learn more + </translate> </router-link> </div> </div> - <div class="item" v-if="rules"> - <i class="book open icon"></i> + <div + v-if="rules" + class="item" + > + <i class="book open icon" /> <div class="content"> - <router-link class="ui link" v-if="rules" :to="{name: 'about', hash: '#rules'}"> - <translate translate-context="Content/Home/Link">Server rules</translate> + <router-link + v-if="rules" + class="ui link" + :to="{name: 'about', hash: '#rules'}" + > + <translate translate-context="Content/Home/Link"> + Server rules + </translate> </router-link> </div> </div> @@ -53,71 +93,130 @@ <div class="eight wide column"> <template v-if="stats"> <h3 class="sub header"> - <translate translate-context="Content/Home/Header">Statistics</translate> + <translate translate-context="Content/Home/Header"> + Statistics + </translate> </h3> <p> - <i class="user icon"></i><translate translate-context="Content/Home/Stat" :translate-params="{count: stats.users.toLocaleString($store.state.ui.momentLocale) }" :translate-n="stats.users" translate-plural="%{ count } active users">%{ count } active user</translate> + <i class="user icon" /><translate + translate-context="Content/Home/Stat" + :translate-params="{count: stats.users.toLocaleString($store.state.ui.momentLocale) }" + :translate-n="stats.users" + translate-plural="%{ count } active users" + > + %{ count } active user + </translate> </p> <p> - <i class="music icon"></i><translate translate-context="Content/Home/Stat" :translate-params="{count: parseInt(stats.hours).toLocaleString($store.state.ui.momentLocale)}" :translate-n="parseInt(stats.hours)" translate-plural="%{ count } hours of music">%{ count } hour of music</translate> + <i class="music icon" /><translate + translate-context="Content/Home/Stat" + :translate-params="{count: parseInt(stats.hours).toLocaleString($store.state.ui.momentLocale)}" + :translate-n="parseInt(stats.hours)" + translate-plural="%{ count } hours of music" + > + %{ count } hour of music + </translate> </p> - </template> <template v-if="contactEmail"> <h3 class="sub header"> - <translate translate-context="Content/Home/Header/Name">Contact</translate> + <translate translate-context="Content/Home/Header/Name"> + Contact + </translate> </h3> - <i class="at icon"></i> + <i class="at icon" /> <a :href="`mailto:${contactEmail}`">{{ contactEmail }}</a> </template> - </div> </div> </div> </div> <div class="six wide column"> - <img class="ui image" src="../assets/network.png" alt=""/> + <img + class="ui image" + src="../assets/network.png" + alt="" + > </div> </div> - <div class="ui hidden divider"></div> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> + <div class="ui hidden divider" /> <div class="ui stackable grid"> <div class="four wide column"> <h3 class="header"> - <translate translate-context="Footer/*/Title/Short">About Funkwhale</translate> + <translate translate-context="Footer/*/Title/Short"> + About Funkwhale + </translate> </h3> - <p v-translate translate-context="Content/Home/Paragraph">This pod runs Funkwhale, a community-driven project that lets you listen and share music and audio within a decentralized, open network.</p> - <p v-translate translate-context="Content/Home/Paragraph">Funkwhale is free and developed by a friendly community of volunteers.</p> - <a target="_blank" rel="noopener" href="https://funkwhale.audio"> - <i class="external alternate icon"></i> + <p + v-translate + translate-context="Content/Home/Paragraph" + > + This pod runs Funkwhale, a community-driven project that lets you listen and share music and audio within a decentralized, open network. + </p> + <p + v-translate + translate-context="Content/Home/Paragraph" + > + Funkwhale is free and developed by a friendly community of volunteers. + </p> + <a + target="_blank" + rel="noopener" + href="https://funkwhale.audio" + > + <i class="external alternate icon" /> <translate translate-context="Content/Home/Link">Visit funkwhale.audio</translate> </a> </div> <div class="four wide column"> <h3 class="header"> - <translate translate-context="Head/Login/Title">Log In</translate> + <translate translate-context="Head/Login/Title"> + Log In + </translate> </h3> - <login-form button-classes="success" :show-signup="false"></login-form> - <div class="ui hidden clearing divider"></div> + <login-form + button-classes="success" + :show-signup="false" + /> + <div class="ui hidden clearing divider" /> </div> <div class="four wide column"> <h3 class="header"> - <translate translate-context="*/Signup/Title">Sign up</translate> + <translate translate-context="*/Signup/Title"> + Sign up + </translate> </h3> <template v-if="openRegistrations"> <p> - <translate translate-context="Content/Home/Paragraph">Sign up now to keep track of your favorites, create playlists, discover new content and much more!</translate> + <translate translate-context="Content/Home/Paragraph"> + Sign up now to keep track of your favorites, create playlists, discover new content and much more! + </translate> </p> <p v-if="defaultUploadQuota"> - <translate translate-context="Content/Home/Paragraph" :translate-params="{quota: humanSize(defaultUploadQuota * 1000 * 1000)}">Users on this pod also get %{ quota } of free storage to upload their own content!</translate> + <translate + translate-context="Content/Home/Paragraph" + :translate-params="{quota: humanSize(defaultUploadQuota * 1000 * 1000)}" + > + Users on this pod also get %{ quota } of free storage to upload their own content! + </translate> </p> - <signup-form button-classes="success" :show-login="false"></signup-form> + <signup-form + button-classes="success" + :show-login="false" + /> </template> <div v-else> - <p translate-context="Content/Home/Paragraph">Registrations are closed on this pod. You can signup on another pod using the link below.</p> - <a target="_blank" rel="noopener" href="https://funkwhale.audio/#get-started"> - <i class="external alternate icon"></i> + <p translate-context="Content/Home/Paragraph"> + Registrations are closed on this pod. You can signup on another pod using the link below. + </p> + <a + target="_blank" + rel="noopener" + href="https://funkwhale.audio/#get-started" + > + <i class="external alternate icon" /> <translate translate-context="Content/Home/Link">Find another pod</translate> </a> </div> @@ -125,39 +224,63 @@ <div class="four wide column"> <h3 class="header"> - <translate translate-context="Content/Home/Header">Useful links</translate> + <translate translate-context="Content/Home/Header"> + Useful links + </translate> </h3> <div class="ui relaxed list"> <div class="item"> - <i class="headphones icon"></i> + <i class="headphones icon" /> <div class="content"> - <router-link v-if="anonymousCanListen" class="header" to="/library"> - <translate translate-context="Content/Home/Link">Browse public content</translate> + <router-link + v-if="anonymousCanListen" + class="header" + to="/library" + > + <translate translate-context="Content/Home/Link"> + Browse public content + </translate> </router-link> <div class="description"> - <translate translate-context="Content/Home/Link">Listen to public albums and playlists shared on this pod</translate> + <translate translate-context="Content/Home/Link"> + Listen to public albums and playlists shared on this pod + </translate> </div> </div> </div> <div class="item"> - <i class="mobile alternate icon"></i> + <i class="mobile alternate icon" /> <div class="content"> - <a class="header" href="https://funkwhale.audio/apps" target="_blank" rel="noopener"> + <a + class="header" + href="https://funkwhale.audio/apps" + target="_blank" + rel="noopener" + > <translate translate-context="Content/Home/Link">Mobile apps</translate> </a> <div class="description"> - <translate translate-context="Content/Home/Link">Use Funkwhale on other devices with our apps</translate> + <translate translate-context="Content/Home/Link"> + Use Funkwhale on other devices with our apps + </translate> </div> </div> </div> <div class="item"> - <i class="book icon"></i> + <i class="book icon" /> <div class="content"> - <a class="header" href="https://docs.funkwhale.audio/users/index.html" target="_blank" rel="noopener"> + <a + class="header" + href="https://docs.funkwhale.audio/users/index.html" + target="_blank" + rel="noopener" + > <translate translate-context="Content/Home/Link">User guides</translate> </a> <div class="description"> - <translate translate-context="Content/Home/Link">Discover everything you need to know about Funkwhale and its features</translate> + <translate translate-context="Content/Home/Link"> + Discover everything you need to know about Funkwhale and its features + </translate> </div> </div> </div> @@ -165,20 +288,37 @@ </div> </div> </section> - <section v-if="anonymousCanListen" class="ui vertical stripe segment"> - <album-widget :filters="{playable: true, ordering: '-creation_date'}" :limit="10"> - <template slot="title"><translate translate-context="Content/Home/Title">Recently added albums</translate></template> + <section + v-if="anonymousCanListen" + class="ui vertical stripe segment" + > + <album-widget + :filters="{playable: true, ordering: '-creation_date'}" + :limit="10" + > + <template slot="title"> + <translate translate-context="Content/Home/Title"> + Recently added albums + </translate> + </template> <router-link to="/library"> - <translate translate-context="Content/Home/Link">View more…</translate> - <div class="ui hidden divider"></div> + <translate translate-context="Content/Home/Link"> + View more… + </translate> + <div class="ui hidden divider" /> </router-link> </album-widget> - <div class="ui hidden section divider"></div> - <h3 class="ui header" > - <translate translate-context="*/*/*">New channels</translate> + <div class="ui hidden section divider" /> + <h3 class="ui header"> + <translate translate-context="*/*/*"> + New channels + </translate> </h3> - <channels-widget :show-modification-date="true" :limit="10" :filters="{ordering: '-creation_date', external: 'false'}"></channels-widget> - + <channels-widget + :show-modification-date="true" + :limit="10" + :filters="{ordering: '-creation_date', external: 'false'}" + /> </section> </main> </template> @@ -186,20 +326,20 @@ <script> import $ from 'jquery' import _ from '@/lodash' -import {mapState} from 'vuex' +import { mapState } from 'vuex' import showdown from 'showdown' -import AlbumWidget from "@/components/audio/album/Widget" -import ChannelsWidget from "@/components/audio/ChannelsWidget" -import LoginForm from "@/components/auth/LoginForm" -import SignupForm from "@/components/auth/SignupForm" -import {humanSize } from '@/filters' +import AlbumWidget from '@/components/audio/album/Widget' +import ChannelsWidget from '@/components/audio/ChannelsWidget' +import LoginForm from '@/components/auth/LoginForm' +import SignupForm from '@/components/auth/SignupForm' +import { humanSize } from '@/filters' export default { components: { AlbumWidget, ChannelsWidget, LoginForm, - SignupForm, + SignupForm }, data () { return { @@ -210,15 +350,15 @@ export default { }, computed: { ...mapState({ - nodeinfo: state => state.instance.nodeinfo, + nodeinfo: state => state.instance.nodeinfo }), - labels() { + labels () { return { - title: this.$pgettext('Head/Home/Title', "Home") + title: this.$pgettext('Head/Home/Title', 'Home') } }, - podName() { - return _.get(this.nodeinfo, 'metadata.nodeName') || "Funkwhale" + podName () { + return _.get(this.nodeinfo, 'metadata.nodeName') || 'Funkwhale' }, banner () { return _.get(this.nodeinfo, 'metadata.banner') @@ -236,12 +376,12 @@ export default { if (!this.longDescription) { return } - let doc = this.markdown.makeHtml(this.longDescription) - let nodes = $.parseHTML(doc) - let excerptParts = [] + const doc = this.markdown.makeHtml(this.longDescription) + const nodes = $.parseHTML(doc) + const excerptParts = [] let handled = 0 nodes.forEach((n) => { - let content = n.innerHTML || n.nodeValue + const content = n.innerHTML || n.nodeValue if (handled < this.excerptLength && content.trim()) { excerptParts.push(n) handled += 1 @@ -250,9 +390,9 @@ export default { return excerptParts.map((p) => { return p.outerHTML }).join('') }, stats () { - let data = { + const data = { users: _.get(this.nodeinfo, 'usage.users.activeMonth', null), - hours: _.get(this.nodeinfo, 'metadata.library.music.hours', null), + hours: _.get(this.nodeinfo, 'metadata.library.music.hours', null) } if (data.users === null || data.artists === null) { return @@ -271,16 +411,16 @@ export default { openRegistrations () { return _.get(this.nodeinfo, 'openRegistrations') }, - headerStyle() { + headerStyle () { if (!this.banner) { - return "" + return '' } return ( - "background-image: url(" + - this.$store.getters["instance/absoluteUrl"](this.banner) + - ")" + 'background-image: url(' + + this.$store.getters['instance/absoluteUrl'](this.banner) + + ')' ) - }, + } }, watch: { '$store.state.auth.authenticated': { diff --git a/front/src/components/Logo.vue b/front/src/components/Logo.vue index 9785437c975b8e19669eb8da3296553a2fb58224..073edb958fca45357030b53a885bd34b6d25b679 100644 --- a/front/src/components/Logo.vue +++ b/front/src/components/Logo.vue @@ -1,31 +1,50 @@ <template> - <svg version="1.1" id="layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - viewBox="0 0 141.7 141.7" enable-background="new 0 0 141.7 141.7" xml:space="preserve"> - <g> + <svg + id="layer_1" + version="1.1" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + x="0px" + y="0px" + viewBox="0 0 141.7 141.7" + enable-background="new 0 0 141.7 141.7" + xml:space="preserve" + > <g> - <path :fill="fill" d="M70.9,86.1c11.7,0,21.2-9.5,21.2-21.2c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1c0,6-4.9,11-11,11 - c-6,0-11-4.9-11-11c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1C49.7,76.6,59.2,86.1,70.9,86.1z"/> - <path :fill="fill" d="M70.9,106.1c22.7,0,41.2-18.5,41.2-41.2c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1 + <g> + <path + :fill="fill" + d="M70.9,86.1c11.7,0,21.2-9.5,21.2-21.2c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1c0,6-4.9,11-11,11 + c-6,0-11-4.9-11-11c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1C49.7,76.6,59.2,86.1,70.9,86.1z" + /> + <path + :fill="fill" + d="M70.9,106.1c22.7,0,41.2-18.5,41.2-41.2c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1 c0,17.1-13.9,31-31,31c-17.1,0-31-13.9-31-31c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1C29.6,87.6,48.1,106.1,70.9,106.1z" /> - <path :fill="fill" d="M131.1,63.8h-8c-0.6,0-1.1,0.5-1.1,1.1C122,93.1,99,116,70.9,116c-28.2,0-51.1-22.9-51.1-51.1 + <path + :fill="fill" + d="M131.1,63.8h-8c-0.6,0-1.1,0.5-1.1,1.1C122,93.1,99,116,70.9,116c-28.2,0-51.1-22.9-51.1-51.1 c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1c0,33.8,27.5,61.3,61.3,61.3c33.8,0,61.3-27.5,61.3-61.3 - C132.2,64.3,131.7,63.8,131.1,63.8z"/> - </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 + 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 c0,0,2,5.2,6.4,5.2c4.7,0,6.4-5.2,6.4-5.2l0.3-5.2c0,0-0.1-4.7,2.4-8.5c1.6-2.5,3.2-4.3,5.8-5.7c4-2.3,8.4-2.7,12.5-4.8 c4.1-2.1,8.1-4.8,10.8-8.6c2.7-3.8,4-8.8,2.5-13.2c-7.8-0.4-16.8,0.5-23.7,4.2c-9.6,5.1-15.4,3.3-17.1,10.9h-0.1 - c-1.7-7.7-7.5-5.8-17.1-10.9c-6.9-3.7-15.9-4.6-23.7-4.2c-1.5,4.4-0.2,9.4,2.5,13.2C35.2,32.5,39.2,35.2,43.3,37.3z"/> - </g> + c-1.7-7.7-7.5-5.8-17.1-10.9c-6.9-3.7-15.9-4.6-23.7-4.2c-1.5,4.4-0.2,9.4,2.5,13.2C35.2,32.5,39.2,35.2,43.3,37.3z" + /> + </g> </svg> - </template> <script> export default { props: { - fill: {type: String, default: '#222222'} + fill: { type: String, default: '#222222' } } } </script> diff --git a/front/src/components/LogoText.vue b/front/src/components/LogoText.vue index f60b3f32611df2e41efd183538930ef9e98b9a30..6e9cc3fc136b8c58833bd44bb304378b5cee2f54 100644 --- a/front/src/components/LogoText.vue +++ b/front/src/components/LogoText.vue @@ -1,5 +1,8 @@ <template> - <svg viewBox="0 0 271.66678 53.49133" version="1.1"> + <svg + viewBox="0 0 271.66678 53.49133" + version="1.1" + > <g transform="translate(34.65295 -109.48195)"> <g> <g transform="matrix(.3191 0 0 .3191 -45.91741 93.47184)"> @@ -14,7 +17,11 @@ </g> </g> </g> - <g transform="translate(-.75595 -.75595)" :fill="text" stroke-width=".74383"> + <g + transform="translate(-.75595 -.75595)" + :fill="text" + stroke-width=".74383" + > <path d="M32.84591 132.89252c0-6.69443 2.6034-9.29781 10.41356-9.29781 1.63641 0 3.71912.14876 4.83486.37191.59506.14876 1.11574.59506 1.11574 1.11574v2.00832c0 .59506-.4463 1.11574-1.11574 1.11574h-.66944c-.8182 0-1.48765-.29753-2.529-.29753-4.83487 0-5.80184.96698-5.80184 4.98363v.29753h6.62004c.59506 0 1.11574.4463 1.11574 1.11574v2.15709c0 .66945-.4463 1.11574-1.11574 1.11574h-6.62004v11.30614c0 .59506-.4463 1.11574-1.11574 1.11574h-4.01666c-.59506 0-1.11574-.52068-1.11574-1.11574z" /> <path d="M57.02023 141.59528c0 3.04968 1.41327 4.31418 3.49598 4.31418 1.78518 0 3.49598-1.2645 4.83487-2.60339v-12.12435c0-.59506.52068-1.11573 1.11574-1.11573h4.09103c.59506 0 1.11574.52067 1.11574 1.11573v17.70304c0 .59506-.4463 1.11574-1.11574 1.11574h-4.09104c-.59505 0-1.11573-.52068-1.11573-1.11574v-1.19012c-1.7108 1.48765-3.57036 2.67777-6.32252 2.67777-4.83486 0-8.25646-2.529-8.25646-8.70275v-10.41355c0-.59506.4463-1.11574 1.11574-1.11574h4.09104c.59506 0 1.11574.52068 1.11574 1.11574v10.33917z" /> <path d="M90.71552 138.47121c0-3.04968-1.41327-4.31419-3.49598-4.31419-1.78518 0-3.57036 1.26451-4.90925 2.60339v12.19874c0 .59506-.4463 1.11573-1.11573 1.11573h-4.09104c-.66945 0-1.11574-.52067-1.11574-1.11573v-17.77743c0-.59506.4463-1.11573 1.11574-1.11573h4.16542c.59506 0 1.11574.52067 1.11574 1.11573v1.19012c1.7108-1.48765 3.57036-2.67777 6.3969-2.67777 4.83486 0 8.25645 2.52901 8.25645 8.70276v10.41355c0 .59506-.4463 1.11574-1.11573 1.11574h-4.09104c-.59506 0-1.11574-.52068-1.11574-1.11574z" /> @@ -33,9 +40,9 @@ <script> export default { props: { - primary: {type: String, default: '#009fe3'}, - secondary: {type: String, default: 'var(--text-color)'}, - text: {type: String, default: 'var(--text-color)'}, + primary: { type: String, default: '#009fe3' }, + secondary: { type: String, default: 'var(--text-color)' }, + text: { type: String, default: 'var(--text-color)' } } } </script> diff --git a/front/src/components/PageNotFound.vue b/front/src/components/PageNotFound.vue index 274a5798c34ba3176227be17711e95a7c21a31a9..57d7c335029bcf823b254abbbc67937953677c9b 100644 --- a/front/src/components/PageNotFound.vue +++ b/front/src/components/PageNotFound.vue @@ -1,19 +1,33 @@ <template> - <main class="main pusher" :v-title="labels.title"> + <main + class="main pusher" + :v-title="labels.title" + > <section class="ui vertical stripe segment"> <div class="ui text container"> <h1 class="ui huge header"> - <i class="warning icon"></i> + <i class="warning icon" /> <div class="content"> - <translate translate-context="Content/*/Title">Page not found!</translate> + <translate translate-context="Content/*/Title"> + Page not found! + </translate> </div> </h1> - <p><translate translate-context="Content/*/Paragraph">Sorry, the page you asked for does not exist:</translate></p> + <p> + <translate translate-context="Content/*/Paragraph"> + Sorry, the page you asked for does not exist: + </translate> + </p> <a :href="path">{{ path }}</a> - <div class="ui hidden divider"></div> - <router-link class="ui icon labeled right button" to="/"> - <translate translate-context="Content/*/Button.Label/Verb">Go to home page</translate> - <i class="right arrow icon"></i> + <div class="ui hidden divider" /> + <router-link + class="ui icon labeled right button" + to="/" + > + <translate translate-context="Content/*/Button.Label/Verb"> + Go to home page + </translate> + <i class="right arrow icon" /> </router-link> </div> </section> @@ -22,15 +36,15 @@ <script> export default { - data: function() { + data: function () { return { path: window.location.href } }, computed: { - labels() { + labels () { return { - title: this.$pgettext('Head/*/Title', "Page Not Found") + title: this.$pgettext('Head/*/Title', 'Page Not Found') } } } diff --git a/front/src/components/Pagination.vue b/front/src/components/Pagination.vue index d25e72b15d8a2166578eb717a2de77e7a83216e2..4f6353b71001ad723b8579ae158c2aee6a448cfe 100644 --- a/front/src/components/Pagination.vue +++ b/front/src/components/Pagination.vue @@ -1,57 +1,68 @@ <template> - <div v-if='maxPage > 1' class="ui pagination menu component-pagination" role="navigation" :aria-label="labels.pagination"> - <a href + <div + v-if="maxPage > 1" + class="ui pagination menu component-pagination" + role="navigation" + :aria-label="labels.pagination" + > + <a + href :disabled="current - 1 < 1" role="button" :aria-label="labels.previousPage" + :class="[{'disabled': current - 1 < 1}, 'item']" @click.prevent.stop="selectPage(current - 1)" - :class="[{'disabled': current - 1 < 1}, 'item']"><i class="angle left icon"></i></a> + ><i class="angle left icon" /></a> <template v-if="!compact"> - <a href + <a v-for="page in pages" :key="page" + href + :class="[{'active': page === current}, {'disabled': page === 'skip'}, 'item']" @click.prevent.stop="selectPage(page)" - :class="[{'active': page === current}, {'disabled': page === 'skip'}, 'item']"> + > <span v-if="page !== 'skip'">{{ page }}</span> <span v-else>…</span> </a> </template> - <a href + <a + href :disabled="current + 1 > maxPage" role="button" :aria-label="labels.nextPage" + :class="[{'disabled': current + 1 > maxPage}, 'item']" @click.prevent.stop="selectPage(current + 1)" - :class="[{'disabled': current + 1 > maxPage}, 'item']"><i class="angle right icon"></i></a> + ><i class="angle right icon" /></a> </div> </template> <script> -import _ from "@/lodash" +import _ from '@/lodash' export default { props: { current: { type: Number, default: 1 }, paginateBy: { type: Number, default: 25 }, - total: { type: Number }, + total: { type: Number, required: true }, compact: { type: Boolean, default: false } }, computed: { - labels() { + labels () { return { - pagination: this.$pgettext('Content/*/Hidden text/Noun', "Pagination"), - previousPage: this.$pgettext('Content/*/Link', "Previous Page"), - nextPage: this.$pgettext('Content/*/Link', "Next Page") + pagination: this.$pgettext('Content/*/Hidden text/Noun', 'Pagination'), + previousPage: this.$pgettext('Content/*/Link', 'Previous Page'), + nextPage: this.$pgettext('Content/*/Link', 'Next Page') } }, - pages: function() { - let range = 2 - let current = this.current - let beginning = _.range(1, Math.min(this.maxPage, 1 + range)) - let middle = _.range( + pages: function () { + const range = 2 + const current = this.current + const beginning = _.range(1, Math.min(this.maxPage, 1 + range)) + const middle = _.range( Math.max(1, current - range + 1), Math.min(this.maxPage, current + range) ) - let end = _.range(this.maxPage, Math.max(1, this.maxPage - range)) + const end = _.range(this.maxPage, Math.max(1, this.maxPage - range)) let allowed = beginning.concat(middle, end) allowed = _.uniq(allowed) allowed = _.sortBy(allowed, [ @@ -59,11 +70,11 @@ export default { return e } ]) - let final = [] + const final = [] allowed.forEach(p => { - let last = final.slice(-1)[0] + const last = final.slice(-1)[0] let consecutive = true - if (last === "skip") { + if (last === 'skip') { consecutive = false } else { if (!last) { @@ -75,25 +86,25 @@ export default { if (consecutive) { final.push(p) } else { - if (p !== "skip") { - final.push("skip") + if (p !== 'skip') { + final.push('skip') final.push(p) } } }) return final }, - maxPage: function() { + maxPage: function () { return Math.ceil(this.total / this.paginateBy) } }, methods: { - selectPage: function(page) { + selectPage: function (page) { if (page > this.maxPage || page < 1) { return } if (this.current !== page) { - this.$emit("page-changed", page) + this.$emit('page-changed', page) } } } diff --git a/front/src/components/Queue.vue b/front/src/components/Queue.vue index 232346bb14cb337cd50403730bc9e9f5c9e29fbd..019c8280382d6040e647d6468596c4c2f0e495ef 100644 --- a/front/src/components/Queue.vue +++ b/front/src/components/Queue.vue @@ -1,77 +1,148 @@ <template> - <section class="main with-background component-queue" :aria-label="labels.queue"> + <section + class="main with-background component-queue" + :aria-label="labels.queue" + > <div :class="['ui vertical stripe queue segment', playerFocused ? 'player-focused' : '']"> <div class="ui fluid container"> - <div class="ui stackable grid" id="queue-grid"> + <div + id="queue-grid" + class="ui stackable grid" + > <div class="ui six wide column current-track"> - <div class="ui basic segment" id="player"> + <div + id="player" + class="ui basic segment" + > <template v-if="currentTrack"> - <img ref="cover" alt="" v-if="currentTrack.cover && currentTrack.cover.urls.large_square_crop" :src="$store.getters['instance/absoluteUrl'](currentTrack.cover.urls.large_square_crop)"> - <img ref="cover" alt="" v-else-if="currentTrack.album && currentTrack.album.cover && currentTrack.album.cover.urls.large_square_crop" :src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.urls.large_square_crop)"> - <img class="ui image" alt="" v-else src="../assets/audio/default-cover.png"> + <img + v-if="currentTrack.cover && currentTrack.cover.urls.large_square_crop" + ref="cover" + alt="" + :src="$store.getters['instance/absoluteUrl'](currentTrack.cover.urls.large_square_crop)" + > + <img + v-else-if="currentTrack.album && currentTrack.album.cover && currentTrack.album.cover.urls.large_square_crop" + ref="cover" + alt="" + :src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.urls.large_square_crop)" + > + <img + v-else + class="ui image" + alt="" + src="../assets/audio/default-cover.png" + > <h1 class="ui header"> <div class="content ellipsis"> - <router-link class="small header discrete link track" :to="{name: 'library.tracks.detail', params: {id: currentTrack.id }}"> + <router-link + class="small header discrete link track" + :to="{name: 'library.tracks.detail', params: {id: currentTrack.id }}" + > {{ currentTrack.title }} </router-link> <div class="sub header ellipsis"> - <router-link class="discrete link artist" :to="{name: 'library.artists.detail', params: {id: currentTrack.artist.id }}">{{ currentTrack.artist.name }}</router-link> - <template v-if="currentTrack.album"> / - <router-link class="discrete link album" :to="{name: 'library.albums.detail', params: {id: currentTrack.album.id }}">{{ currentTrack.album.title }}</router-link> + <router-link + class="discrete link artist" + :to="{name: 'library.artists.detail', params: {id: currentTrack.artist.id }}" + > + {{ currentTrack.artist.name }} + </router-link> + <template v-if="currentTrack.album"> + / + <router-link + class="discrete link album" + :to="{name: 'library.albums.detail', params: {id: currentTrack.album.id }}" + > + {{ currentTrack.album.title }} + </router-link> </template> </div> </div> </h1> - <div class="ui small warning message" v-if="currentTrack && errored"> + <div + v-if="currentTrack && errored" + class="ui small warning message" + > <h3 class="header"> - <translate translate-context="Sidebar/Player/Error message.Title">The track cannot be loaded</translate> + <translate translate-context="Sidebar/Player/Error message.Title"> + The track cannot be loaded + </translate> </h3> <p v-if="hasNext && playing && $store.state.player.errorCount < $store.state.player.maxConsecutiveErrors"> - <translate translate-context="Sidebar/Player/Error message.Paragraph">The next track will play automatically in a few seconds…</translate> - <i class="loading spinner icon"></i> + <translate translate-context="Sidebar/Player/Error message.Paragraph"> + The next track will play automatically in a few seconds… + </translate> + <i class="loading spinner icon" /> </p> <p> - <translate translate-context="Sidebar/Player/Error message.Paragraph">You may have a connectivity issue.</translate> + <translate translate-context="Sidebar/Player/Error message.Paragraph"> + You may have a connectivity issue. + </translate> </p> </div> <div class="additional-controls tablet-and-below"> <track-favorite-icon v-if="$store.state.auth.authenticated" - :track="currentTrack"></track-favorite-icon> + :track="currentTrack" + /> <track-playlist-icon v-if="$store.state.auth.authenticated" - :track="currentTrack"></track-playlist-icon> + :track="currentTrack" + /> <button v-if="$store.state.auth.authenticated" - @click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})" :class="['ui', 'really', 'basic', 'circular', 'icon', 'button']" :aria-label="labels.addArtistContentFilter" - :title="labels.addArtistContentFilter"> - <i :class="['eye slash outline', 'basic', 'icon']"></i> + :title="labels.addArtistContentFilter" + @click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})" + > + <i :class="['eye slash outline', 'basic', 'icon']" /> </button> </div> <div class="progress-wrapper"> - <div class="progress-area" v-if="currentTrack && !errored"> + <div + v-if="currentTrack && !errored" + class="progress-area" + > <div ref="progress" :class="['ui', 'small', 'vibrant', {'indicating': isLoadingAudio}, 'progress']" - @click="touchProgress"> - <div class="buffer bar" :data-percent="bufferProgress" :style="{ 'width': bufferProgress + '%' }"></div> - <div class="position bar" :data-percent="progress" :style="{ 'width': progress + '%' }"></div> + @click="touchProgress" + > + <div + class="buffer bar" + :data-percent="bufferProgress" + :style="{ 'width': bufferProgress + '%' }" + /> + <div + class="position bar" + :data-percent="progress" + :style="{ 'width': progress + '%' }" + /> </div> </div> - <div class="progress-area" v-else> + <div + v-else + class="progress-area" + > <div ref="progress" - :class="['ui', 'small', 'vibrant', 'progress']"> - <div class="buffer bar"></div> - <div class="position bar"></div> + :class="['ui', 'small', 'vibrant', 'progress']" + > + <div class="buffer bar" /> + <div class="position bar" /> </div> </div> <div class="progress"> <template v-if="!isLoadingAudio"> - <a href="" :aria-label="labels.restart" class="left floated timer discrete start" @click.prevent="setCurrentTime(0)">{{currentTimeFormatted}}</a> - <span class="right floated timer total">{{durationFormatted}}</span> + <a + href="" + :aria-label="labels.restart" + class="left floated timer discrete start" + @click.prevent="setCurrentTime(0)" + >{{ currentTimeFormatted }}</a> + <span class="right floated timer total">{{ durationFormatted }}</span> </template> <template v-else> <span class="left floated timer">00:00</span> @@ -80,45 +151,47 @@ </div> </div> <div class="player-controls tablet-and-below"> - <template> - <span - role="button" - :title="labels.previousTrack" - :aria-label="labels.previousTrack" - class="control" - @click.prevent.stop="$store.dispatch('queue/previous')" - :disabled="emptyQueue"> - <i :class="['ui', 'backward step', {'disabled': emptyQueue}, 'icon']"></i> - </span> + <span + role="button" + :title="labels.previousTrack" + :aria-label="labels.previousTrack" + class="control" + :disabled="emptyQueue" + @click.prevent.stop="$store.dispatch('queue/previous')" + > + <i :class="['ui', 'backward step', {'disabled': emptyQueue}, 'icon']" /> + </span> - <span - role="button" - v-if="!playing" - :title="labels.play" - :aria-label="labels.play" - @click.prevent.stop="resumePlayback" - class="control"> - <i :class="['ui', 'play', {'disabled': !currentTrack}, 'icon']"></i> - </span> - <span - role="button" - v-else - :title="labels.pause" - :aria-label="labels.pause" - @click.prevent.stop="pausePlayback" - class="control"> - <i :class="['ui', 'pause', {'disabled': !currentTrack}, 'icon']"></i> - </span> - <span - role="button" - :title="labels.next" - :aria-label="labels.next" - class="control" - @click.prevent.stop="$store.dispatch('queue/next')" - :disabled="!hasNext"> - <i :class="['ui', {'disabled': !hasNext}, 'forward step', 'icon']" ></i> - </span> - </template> + <span + v-if="!playing" + role="button" + :title="labels.play" + :aria-label="labels.play" + class="control" + @click.prevent.stop="resumePlayback" + > + <i :class="['ui', 'play', {'disabled': !currentTrack}, 'icon']" /> + </span> + <span + v-else + role="button" + :title="labels.pause" + :aria-label="labels.pause" + class="control" + @click.prevent.stop="pausePlayback" + > + <i :class="['ui', 'pause', {'disabled': !currentTrack}, 'icon']" /> + </span> + <span + role="button" + :title="labels.next" + :aria-label="labels.next" + class="control" + :disabled="!hasNext" + @click.prevent.stop="$store.dispatch('queue/next')" + > + <i :class="['ui', {'disabled': !hasNext}, 'forward step', 'icon']" /> + </span> </div> </template> </div> @@ -129,20 +202,30 @@ <div class="content"> <button class="ui right floated basic button" - @click="$store.commit('ui/queueFocused', null)"> - <translate translate-context="*/Queue/*/Verb">Close</translate> + @click="$store.commit('ui/queueFocused', null)" + > + <translate translate-context="*/Queue/*/Verb"> + Close + </translate> </button> <button class="ui right floated basic button danger" - @click="$store.dispatch('queue/clean')"> - <translate translate-context="*/Queue/*/Verb">Clear</translate> + @click="$store.dispatch('queue/clean')" + > + <translate translate-context="*/Queue/*/Verb"> + Clear + </translate> </button> {{ labels.queue }} <div class="sub header"> <div> - <translate translate-context="Sidebar/Queue/Text" :translate-params="{index: queue.currentIndex + 1, length: queue.tracks.length}"> + <translate + translate-context="Sidebar/Queue/Text" + :translate-params="{index: queue.currentIndex + 1, length: queue.tracks.length}" + > Track %{ index } of %{ length } - </translate><template v-if="!$store.state.radios.running"> - + </translate><template v-if="!$store.state.radios.running"> + - <span :title="labels.duration"> {{ timeLeft }} </span> @@ -153,22 +236,53 @@ </h2> </div> <table class="ui compact very basic fixed single line selectable unstackable table"> - <draggable v-model="tracks" tag="tbody" @update="reorder" handle=".handle"> + <draggable + v-model="tracks" + tag="tbody" + handle=".handle" + @update="reorder" + > <tr v-for="(track, index) in tracks" :key="index" - :class="['queue-item', {'active': index === queue.currentIndex}]"> + :class="['queue-item', {'active': index === queue.currentIndex}]" + > <td class="handle"> - <i class="grip lines icon"></i> + <i class="grip lines icon" /> </td> - <td class="image-cell" @click="$store.dispatch('queue/currentIndex', index)"> - <img class="ui mini image" alt="" v-if="track.cover && track.cover.urls.original" :src="$store.getters['instance/absoluteUrl'](track.cover.urls.medium_square_crop)"> - <img class="ui mini image" alt="" v-else-if="track.album && track.album.cover && track.album.cover.urls.original" :src="$store.getters['instance/absoluteUrl'](track.album.cover.urls.medium_square_crop)"> - <img class="ui mini image" alt="" v-else src="../assets/audio/default-cover.png"> + <td + class="image-cell" + @click="$store.dispatch('queue/currentIndex', index)" + > + <img + v-if="track.cover && track.cover.urls.original" + class="ui mini image" + alt="" + :src="$store.getters['instance/absoluteUrl'](track.cover.urls.medium_square_crop)" + > + <img + v-else-if="track.album && track.album.cover && track.album.cover.urls.original" + class="ui mini image" + alt="" + :src="$store.getters['instance/absoluteUrl'](track.album.cover.urls.medium_square_crop)" + > + <img + v-else + class="ui mini image" + alt="" + src="../assets/audio/default-cover.png" + > </td> - <td colspan="3" @click="$store.dispatch('queue/currentIndex', index)"> - <button class="title reset ellipsis" :title="track.title" :aria-label="labels.selectTrack"> - <strong>{{ track.title }}</strong><br /> + <td + colspan="3" + @click="$store.dispatch('queue/currentIndex', index)" + > + <button + class="title reset ellipsis" + :title="track.title" + :aria-label="labels.selectTrack" + > + <strong>{{ track.title }}</strong><br> <span> {{ track.artist.name }} </span> @@ -181,23 +295,44 @@ </td> <td class="controls"> <template v-if="$store.getters['favorites/isFavorite'](track.id)"> - <i class="pink heart icon"></i> + <i class="pink heart icon" /> </template> - <button :aria-label="labels.removeFromQueue" :title="labels.removeFromQueue" @click.stop="cleanTrack(index)" :class="['ui', 'really', 'tiny', 'basic', 'circular', 'icon', 'button']"> - <i class="x icon"></i> + <button + :aria-label="labels.removeFromQueue" + :title="labels.removeFromQueue" + :class="['ui', 'really', 'tiny', 'basic', 'circular', 'icon', 'button']" + @click.stop="cleanTrack(index)" + > + <i class="x icon" /> </button> </td> </tr> </draggable> </table> - <div v-if="$store.state.radios.running" class="ui info message"> + <div + v-if="$store.state.radios.running" + class="ui info message" + > <div class="content"> <h3 class="header"> - <i class="feed icon"></i> <translate translate-context="Sidebar/Player/Title">You have a radio playing</translate> + <i class="feed icon" /> <translate translate-context="Sidebar/Player/Title"> + You have a radio playing + </translate> </h3> - <p><translate translate-context="Sidebar/Player/Paragraph">New tracks will be appended here automatically.</translate></p> - <button @click="$store.dispatch('radios/stop')" class="ui basic primary button"><translate translate-context="*/Player/Button.Label/Short, Verb">Stop radio</translate></button> + <p> + <translate translate-context="Sidebar/Player/Paragraph"> + New tracks will be appended here automatically. + </translate> + </p> + <button + class="ui basic primary button" + @click="$store.dispatch('radios/stop')" + > + <translate translate-context="*/Player/Button.Label/Short, Verb"> + Stop radio + </translate> + </button> </div> </div> </div> @@ -229,16 +364,6 @@ export default { time } }, - mounted () { - this.focusTrap = createFocusTrap(this.$el, { allowOutsideClick: () => { return true } }) - this.focusTrap.activate() - this.$nextTick(() => { - setTimeout(() => { - this.scrollToCurrent() - // delay is to let transition work - }, 400) - }) - }, computed: { ...mapState({ currentIndex: state => state.queue.currentIndex, @@ -298,6 +423,46 @@ export default { return this.$store.state.ui.queueFocused === 'player' } }, + watch: { + '$store.state.ui.queueFocused': { + handler (v) { + if (v === 'queue') { + this.$nextTick(() => { + this.scrollToCurrent() + }) + } + }, + immediate: true + }, + '$store.state.queue.currentIndex': { + handler () { + this.$nextTick(() => { + this.scrollToCurrent() + }) + } + }, + '$store.state.queue.tracks': { + handler (v) { + if (!v || v.length === 0) { + this.$store.commit('ui/queueFocused', null) + } + }, + immediate: true + }, + '$route.fullPath' () { + this.$store.commit('ui/queueFocused', null) + } + }, + mounted () { + this.focusTrap = createFocusTrap(this.$el, { allowOutsideClick: () => { return true } }) + this.focusTrap.activate() + this.$nextTick(() => { + setTimeout(() => { + this.scrollToCurrent() + // delay is to let transition work + }, 400) + }) + }, methods: { ...mapActions({ cleanTrack: 'queue/cleanTrack', @@ -348,36 +513,6 @@ export default { }) }, 100) } - }, - watch: { - '$store.state.ui.queueFocused': { - handler (v) { - if (v === 'queue') { - this.$nextTick(() => { - this.scrollToCurrent() - }) - } - }, - immediate: true - }, - '$store.state.queue.currentIndex': { - handler () { - this.$nextTick(() => { - this.scrollToCurrent() - }) - } - }, - '$store.state.queue.tracks': { - handler (v) { - if (!v || v.length === 0) { - this.$store.commit('ui/queueFocused', null) - } - }, - immediate: true - }, - '$route.fullPath' () { - this.$store.commit('ui/queueFocused', null) - } } } </script> diff --git a/front/src/components/RemoteSearchForm.vue b/front/src/components/RemoteSearchForm.vue index 880a49c555904167fb7b662e04b81a16dcf59c7a..22482771aba2cebffd94db6e5135d238306908be 100644 --- a/front/src/components/RemoteSearchForm.vue +++ b/front/src/components/RemoteSearchForm.vue @@ -1,19 +1,51 @@ <template> - <div v-if="type === 'both' || type === undefined" class="two ui buttons"> - <button class="ui left floated labeled icon button" @click.prevent="changeType('rss')"><i class="feed icon"></i> - <translate translate-context="Content/Search/Input.Label/Noun">RSS</translate> + <div + v-if="type === 'both' || type === undefined" + class="two ui buttons" + > + <button + class="ui left floated labeled icon button" + @click.prevent="changeType('rss')" + > + <i class="feed icon" /> + <translate translate-context="Content/Search/Input.Label/Noun"> + RSS + </translate> </button> - <div class="or"></div> - <button class="ui right floated right labeled icon button" @click.prevent="changeType('artists')"><i class="globe icon"></i> - <translate translate-context="Content/Search/Input.Label/Noun">Fediverse</translate> + <div class="or" /> + <button + class="ui right floated right labeled icon button" + @click.prevent="changeType('artists')" + > + <i class="globe icon" /> + <translate translate-context="Content/Search/Input.Label/Noun"> + Fediverse + </translate> </button> </div> <div v-else> - <form id="remote-search" :class="['ui', {loading: isLoading}, 'form']" @submit.stop.prevent="submit"> - <div v-if="errors.length > 0" role="alert" class="ui negative message"> - <h3 class="header"><translate translate-context="Content/*/Error message.Title">Error while fetching object</translate></h3> + <form + id="remote-search" + :class="['ui', {loading: isLoading}, 'form']" + @submit.stop.prevent="submit" + > + <div + v-if="errors.length > 0" + role="alert" + class="ui negative message" + > + <h3 class="header"> + <translate translate-context="Content/*/Error message.Title"> + Error while fetching object + </translate> + </h3> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> <div class="ui required field"> @@ -21,19 +53,45 @@ {{ labels.fieldLabel }} </label> <p v-if="type === 'rss'"> - <translate translate-context="Content/Fetch/Paragraph">Use this form to subscribe to an RSS feed from its URL.</translate> + <translate translate-context="Content/Fetch/Paragraph"> + Use this form to subscribe to an RSS feed from its URL. + </translate> </p> <p v-else-if="type === 'artists'"> - <translate translate-context="Content/Fetch/Paragraph">Use this form to subscribe to a channel hosted somewhere else on the Fediverse.</translate> + <translate translate-context="Content/Fetch/Paragraph"> + Use this form to subscribe to a channel hosted somewhere else on the Fediverse. + </translate> </p> - <input type="text" name="object-id" id="object-id" :placeholder="labels.fieldPlaceholder" v-model="id" required> + <input + id="object-id" + v-model="id" + type="text" + name="object-id" + :placeholder="labels.fieldPlaceholder" + required + > </div> - <button v-if="showSubmit" type="submit" :class="['ui', 'primary', {loading: isLoading}, 'button']" :disabled="isLoading || !id || id.length === 0"> - <translate translate-context="Content/Search/Input.Label/Noun">Search</translate> + <button + v-if="showSubmit" + type="submit" + :class="['ui', 'primary', {loading: isLoading}, 'button']" + :disabled="isLoading || !id || id.length === 0" + > + <translate translate-context="Content/Search/Input.Label/Noun"> + Search + </translate> </button> </form> - <div v-if="!isLoading && fetch && fetch.status === 'finished' && !redirectRoute" role="alert" class="ui warning message"> - <p><translate translate-context="Content/*/Error message.Title">This kind of object isn't supported yet</translate></p> + <div + v-if="!isLoading && fetch && fetch.status === 'finished' && !redirectRoute" + role="alert" + class="ui warning message" + > + <p> + <translate translate-context="Content/*/Error message.Title"> + This kind of object isn't supported yet + </translate> + </p> </div> </div> </template> @@ -42,85 +100,101 @@ import axios from 'axios' export default { props: { - initialId: { type: String, required: false}, - type: { type: String, required: false}, - redirect: { type: Boolean, default: true}, - showSubmit: { type: Boolean, default: true}, - standalone: { type: Boolean, default: true}, + initialId: { type: String, required: false, default: '' }, + initialType: { type: String, required: false, default: '' }, + redirect: { type: Boolean, default: true }, + showSubmit: { type: Boolean, default: true }, + standalone: { type: Boolean, default: true } }, data () { return { + type: this.initialType, id: this.initialId, fetch: null, obj: null, isLoading: false, - errors: [], - } - }, - created () { - if (this.id) { - if (this.type === 'rss') { - this.rssSubscribe() - } else if (this.type === 'artists') { - this.createFetch() - } + 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") + 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") + 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, + 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': - let [username, domain] = this.objInfo.full_username.split('@') - return {name: 'profile.full', params: {username, domain}} + 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}} + return { name: 'library.detail', params: { id: this.objInfo.uuid } } case 'artist': - return {name: 'library.artists.detail', params: {id: this.objInfo.id}} + return { name: 'library.artists.detail', params: { id: this.objInfo.id } } case 'album': - return {name: 'library.albums.detail', params: {id: this.objInfo.id}} + return { name: 'library.albums.detail', params: { id: this.objInfo.id } } case 'track': - return {name: 'library.tracks.detail', params: {id: this.objInfo.id}} + return { name: 'library.tracks.detail', params: { id: this.objInfo.id } } case 'upload': - return {name: 'library.uploads.detail', params: {id: this.objInfo.uuid}} + return { name: 'library.uploads.detail', params: { id: this.objInfo.uuid } } case 'channel': - return {name: 'channels.detail', params: {id: this.objInfo.uuid}} + return { name: 'channels.detail', params: { id: this.objInfo.uuid } } default: - break; + break + } + return null + } + }, + + watch: { + initialId (v) { + this.id = v + this.createFetch() + }, + redirectRoute (v) { + if (v && this.redirect) { + this.$router.push(v) + } + } + }, + created () { + if (this.id) { + if (this.type === 'rss') { + this.rssSubscribe() + } else if (this.type === 'artists') { + this.createFetch() } } }, methods: { - changeType(newType) { + changeType (newType) { this.type = newType }, submit () { @@ -135,13 +209,13 @@ export default { return } if (this.standalone) { - this.$router.replace({name: "search", query: {id: this.id}}) + this.$router.replace({ name: 'search', query: { id: this.id } }) } this.fetch = null - let self = this + const self = this self.errors = [] self.isLoading = true - let payload = { + const payload = { object: this.id } @@ -150,7 +224,7 @@ export default { self.fetch = response.data if (self.fetch.status === 'errored' || self.fetch.status === 'skipped') { self.errors.push( - self.$pgettext("Content/*/Error message.Title", "This object cannot be retrieved") + self.$pgettext('Content/*/Error message.Title', 'This object cannot be retrieved') ) } }, error => { @@ -163,40 +237,27 @@ export default { return } if (this.standalone) { - this.$router.replace({name: "search", query: {id: this.id, type: 'rss'}}) + this.$router.replace({ name: 'search', query: { id: this.id, type: 'rss' } }) } this.fetch = null - let self = this + const self = this self.errors = [] self.isLoading = true - let payload = { + const payload = { url: this.id } axios.post('channels/rss-subscribe/', payload).then((response) => { self.isLoading = false - self.$store.commit('channels/subscriptions', {uuid: response.data.channel.uuid, value: true}) + self.$store.commit('channels/subscriptions', { uuid: response.data.channel.uuid, value: true }) self.$emit('subscribed', response.data) if (self.redirect) { - self.$router.push({name: 'channels.detail', params: {id: response.data.channel.uuid}}) + self.$router.push({ name: 'channels.detail', params: { id: response.data.channel.uuid } }) } - }, error => { self.isLoading = false self.errors = error.backendErrors }) - }, - }, - - watch: { - initialId (v) { - this.id = v - this.createFetch() - }, - redirectRoute (v) { - if (v && this.redirect) { - this.$router.push(v) - } } } } diff --git a/front/src/components/ServiceMessages.vue b/front/src/components/ServiceMessages.vue index a7fad93a57fb25ecb60e366b9c5c998b970ef374..a7f990888493e999e874b1726aaf81886f40389c 100644 --- a/front/src/components/ServiceMessages.vue +++ b/front/src/components/ServiceMessages.vue @@ -1,7 +1,11 @@ <template> <div class="ui toast-container"> - <message v-for="message in $store.state.ui.messages" :message="message" :key="message.key"></message> - <slot></slot> + <message + v-for="message in $store.state.ui.messages" + :key="message.key" + :message="message" + /> + <slot /> </div> </template> diff --git a/front/src/components/SetInstanceModal.vue b/front/src/components/SetInstanceModal.vue index d0d7b27f1e9236045fc4e154e1a3dfbd571ffe11..dc1b17f22461174d780b02b0a4d4c8bec217d727 100644 --- a/front/src/components/SetInstanceModal.vue +++ b/front/src/components/SetInstanceModal.vue @@ -1,41 +1,105 @@ <template> - <modal @update:show="$emit('update:show', $event); isError = false" :show="show"> - <h3 class="header"><translate translate-context="Popup/Instance/Title">Choose your instance</translate></h3> + <modal + :show="show" + @update:show="$emit('update:show', $event); isError = false" + > + <h3 class="header"> + <translate translate-context="Popup/Instance/Title"> + Choose your instance + </translate> + </h3> <div class="scrolling content"> - <div v-if="isError" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Popup/Instance/Error message.Title">It is not possible to connect to the given URL</translate></h4> + <div + v-if="isError" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Popup/Instance/Error message.Title"> + It is not possible to connect to the given URL + </translate> + </h4> <ul class="list"> - <li><translate translate-context="Popup/Instance/Error message.List item">The server might be down</translate></li> - <li><translate translate-context="Popup/Instance/Error message.List item">The given address is not a Funkwhale server</translate></li> + <li> + <translate translate-context="Popup/Instance/Error message.List item"> + The server might be down + </translate> + </li> + <li> + <translate translate-context="Popup/Instance/Error message.List item"> + The given address is not a Funkwhale server + </translate> + </li> </ul> </div> - <form class="ui form" @submit.prevent="checkAndSwitch(instanceUrl)"> - <p v-if="$store.state.instance.instanceUrl" class="description" translate-context="Popup/Login/Paragraph" v-translate="{url: $store.state.instance.instanceUrl, hostname: instanceHostname }"> - You are currently connected to <a href="%{ url }" target="_blank">%{ hostname } <i class="external icon"></i></a>. If you continue, you will be disconnected from your current instance and all your local data will be deleted. + <form + class="ui form" + @submit.prevent="checkAndSwitch(instanceUrl)" + > + <p + v-if="$store.state.instance.instanceUrl" + v-translate="{url: $store.state.instance.instanceUrl, hostname: instanceHostname }" + class="description" + translate-context="Popup/Login/Paragraph" + > + You are currently connected to <a + href="%{ url }" + target="_blank" + >%{ hostname } <i class="external icon" /></a>. If you continue, you will be disconnected from your current instance and all your local data will be deleted. </p> <p v-else> - <translate translate-context="Popup/Instance/Paragraph">To continue, please select the Funkwhale instance you want to connect to. Enter the address directly, or select one of the suggested choices.</translate> + <translate translate-context="Popup/Instance/Paragraph"> + To continue, please select the Funkwhale instance you want to connect to. Enter the address directly, or select one of the suggested choices. + </translate> </p> <div class="field"> <label for="instance-picker"><translate translate-context="Popup/Instance/Input.Label/Noun">Instance URL</translate></label> <div class="ui action input"> - <input id ="instance-picker" type="text" v-model="instanceUrl" placeholder="https://funkwhale.server"> - <button type="submit" :class="['ui', 'icon', {loading: isLoading}, 'button']"> - <translate translate-context="*/*/Button.Label/Verb">Submit</translate> + <input + id="instance-picker" + v-model="instanceUrl" + type="text" + placeholder="https://funkwhale.server" + > + <button + type="submit" + :class="['ui', 'icon', {loading: isLoading}, 'button']" + > + <translate translate-context="*/*/Button.Label/Verb"> + Submit + </translate> </button> </div> </div> </form> - <div class="ui hidden divider"></div> - <form class="ui form" @submit.prevent=""> + <div class="ui hidden divider" /> + <form + class="ui form" + @submit.prevent="" + > <div class="field"> - <h4><translate translate-context="Popup/Instance/List.Label">Suggested choices</translate></h4> - <button v-for="url in suggestedInstances" @click="checkAndSwitch(url)" class="ui basic button">{{ url }}</button> + <h4> + <translate translate-context="Popup/Instance/List.Label"> + Suggested choices + </translate> + </h4> + <button + v-for="(url, key) in suggestedInstances" + :key="key" + class="ui basic button" + @click="checkAndSwitch(url)" + > + {{ url }} + </button> </div> </form> </div> <div class="actions"> - <button class="ui basic cancel button"><translate translate-context="*/*/Button.Label/Verb">Cancel</translate></button> + <button class="ui basic cancel button"> + <translate translate-context="*/*/Button.Label/Verb"> + Cancel + </translate> + </button> </div> </modal> </template> @@ -43,25 +107,52 @@ <script> import Modal from '@/components/semantic/Modal' import axios from 'axios' -import _ from "@/lodash" +import _ from '@/lodash' export default { - props: ['show'], components: { - Modal, + Modal }, - data() { + props: { show: { type: Boolean, required: true } }, + data () { return { instanceUrl: null, nodeinfo: null, isError: false, isLoading: false, - path: 'api/v1/instance/nodeinfo/2.0/', + path: 'api/v1/instance/nodeinfo/2.0/' + } + }, + computed: { + suggestedInstances () { + const instances = this.$store.state.instance.knownInstances.slice(0) + if (this.$store.state.instance.frontSettings.defaultServerUrl) { + let serverUrl = this.$store.state.instance.frontSettings.defaultServerUrl + if (!serverUrl.endsWith('/')) { + serverUrl = serverUrl + '/' + } + instances.push(serverUrl) + } + const self = this + instances.push(this.$store.getters['instance/defaultUrl'](), 'https://demo.funkwhale.audio/') + return _.uniq(instances.filter((e) => { return e !== self.$store.state.instance.instanceUrl })) + }, + instanceHostname () { + const url = this.$store.state.instance.instanceUrl + const parser = document.createElement('a') + parser.href = url + return parser.hostname + } + }, + watch: { + '$store.state.instance.instanceUrl' () { + this.$store.dispatch('instance/fetchSettings') + this.fetchNodeInfo() } }, methods: { fetchNodeInfo () { - let self = this + const self = this axios.get('instance/nodeinfo/2.0/').then(response => { self.nodeinfo = response.data }) @@ -71,7 +162,7 @@ export default { if (!urlFetch.endsWith('/')) { urlFetch = `${urlFetch}/${this.path}` } else { - urlFetch = `${urlFetch}${this.path}` + urlFetch = `${urlFetch}${this.path}` } if (!urlFetch.startsWith('https://') && !urlFetch.startsWith('http://')) { urlFetch = `https://${urlFetch}` @@ -79,14 +170,14 @@ export default { return urlFetch }, requestDistantNodeInfo (url) { - var self = this + const self = this axios.get(this.fetchUrl(url)).then(function (response) { self.isLoading = false - if(!url.startsWith('https://') && !url.startsWith('http://')) { + if (!url.startsWith('https://') && !url.startsWith('http://')) { url = `https://${url}` } self.switchInstance(url) - }).catch(function (error) { + }).catch(function () { self.isLoading = false self.isError = true }) @@ -95,12 +186,12 @@ export default { // Here we disconnect from the current instance and reconnect to the new one. No check is performed… this.$emit('update:show', false) this.isError = false - let msg = this.$pgettext('*/Instance/Message', 'You are now using the Funkwhale instance at %{ url }') + const msg = this.$pgettext('*/Instance/Message', 'You are now using the Funkwhale instance at %{ url }') this.$store.commit('ui/addMessage', { - content: this.$gettextInterpolate(msg, {url: url}), + content: this.$gettextInterpolate(msg, { url: url }), date: new Date() }) - let self = this + const self = this this.$nextTick(() => { self.$store.commit('instance/instanceUrl', null) self.$store.dispatch('instance/setUrl', url) @@ -111,34 +202,7 @@ export default { this.isError = false // Clear error message if any… this.isLoading = true this.requestDistantNodeInfo(url) - }, - }, - computed: { - suggestedInstances () { - let instances = this.$store.state.instance.knownInstances.slice(0) - if (this.$store.state.instance.frontSettings.defaultServerUrl) { - let serverUrl = this.$store.state.instance.frontSettings.defaultServerUrl - if (!serverUrl.endsWith('/')) { - serverUrl = serverUrl + '/' - } - instances.push(serverUrl) - } - let self = this - instances.push(this.$store.getters['instance/defaultUrl'](), 'https://demo.funkwhale.audio/') - return _.uniq(instances.filter((e) => {return e != self.$store.state.instance.instanceUrl})) - }, - instanceHostname() { - let url = this.$store.state.instance.instanceUrl - let parser = document.createElement("a") - parser.href = url - return parser.hostname - }, - }, - watch: { - '$store.state.instance.instanceUrl' () { - this.$store.dispatch('instance/fetchSettings') - this.fetchNodeInfo() - }, - }, + } + } } </script> diff --git a/front/src/components/ShortcutsModal.vue b/front/src/components/ShortcutsModal.vue index 2090f8a9571f5dbe5564f1ae91e4ba0b68eb4322..cba1a5128d3fd25d7cf1c2d1508ae1c9db4d48cd 100644 --- a/front/src/components/ShortcutsModal.vue +++ b/front/src/components/ShortcutsModal.vue @@ -1,42 +1,59 @@ <template> - <modal @update:show="$emit('update:show', $event)" :show="show"> + <modal + :show="show" + @update:show="$emit('update:show', $event)" + > <header class="header"> - <translate translate-context="*/*/*/Noun">Keyboard shortcuts</translate> + <translate translate-context="*/*/*/Noun"> + Keyboard shortcuts + </translate> </header> <section class="scrolling content"> <div class="ui stackable two column grid"> <div class="column"> <table - class="ui compact basic table" v-for="section in player" - :key="section.title"> - <caption>{{ section.title }}</caption> - <tbody> - <tr v-for="shortcut in section.shortcuts" :key="shortcut.summary"> - <td>{{ shortcut.summary }}</td> - <td><span class="ui label">{{ shortcut.key }}</span></td> - </tr> - </tbody> + :key="section.title" + class="ui compact basic table" + > + <caption>{{ section.title }}</caption> + <tbody> + <tr + v-for="shortcut in section.shortcuts" + :key="shortcut.summary" + > + <td>{{ shortcut.summary }}</td> + <td><span class="ui label">{{ shortcut.key }}</span></td> + </tr> + </tbody> </table> </div> <div class="column"> <table - class="ui compact basic table" v-for="section in general" - :key="section.title"> - <caption>{{ section.title }}</caption> - <tbody> - <tr v-for="shortcut in section.shortcuts" :key="shortcut.summary"> - <td>{{ shortcut.summary }}</td> - <td><span class="ui label">{{ shortcut.key }}</span></td> - </tr> - </tbody> + :key="section.title" + class="ui compact basic table" + > + <caption>{{ section.title }}</caption> + <tbody> + <tr + v-for="shortcut in section.shortcuts" + :key="shortcut.summary" + > + <td>{{ shortcut.summary }}</td> + <td><span class="ui label">{{ shortcut.key }}</span></td> + </tr> + </tbody> </table> </div> </div> </section> <footer class="actions"> - <button class="ui basic cancel button"><translate translate-context="*/*/Button.Label/Verb">Close</translate></button> + <button class="ui basic cancel button"> + <translate translate-context="*/*/Button.Label/Verb"> + Close + </translate> + </button> </footer> </modal> </template> @@ -44,10 +61,10 @@ <script> export default { - props: ['show'], components: { - Modal: () => import(/* webpackChunkName: "modal" */ "@/components/semantic/Modal"), + Modal: () => import(/* webpackChunkName: "modal" */ '@/components/semantic/Modal') }, + props: { show: { type: Boolean, required: true } }, computed: { general () { return [ @@ -65,9 +82,9 @@ export default { { key: 'esc', summary: this.$pgettext('Popup/Keyboard shortcuts/Table.Label/Verb', 'Unfocus searchbar') - }, + } ] - }, + } ] }, @@ -135,7 +152,7 @@ export default { { key: 'f', summary: this.$pgettext('Popup/Keyboard shortcuts/Table.Label/Verb', 'Toggle favorite') - }, + } ] } ] diff --git a/front/src/components/admin/SettingsGroup.vue b/front/src/components/admin/SettingsGroup.vue index 332e35e6aea0bd52387e132a6040c2e41a669109..94e7620e87311425a411d7c3099afd55a7f737c5 100644 --- a/front/src/components/admin/SettingsGroup.vue +++ b/front/src/components/admin/SettingsGroup.vue @@ -1,86 +1,156 @@ <template> - <form :id="group.id" class="ui form component-settings-group" @submit.prevent="save"> + <form + :id="group.id" + class="ui form component-settings-group" + @submit.prevent="save" + > <div class="ui divider" /> - <h3 class="ui header">{{ group.label }}</h3> - <div v-if="errors.length > 0" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Content/*/Error message.Title">Error while saving settings</translate></h4> + <h3 class="ui header"> + {{ group.label }} + </h3> + <div + v-if="errors.length > 0" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Content/*/Error message.Title"> + Error while saving settings + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> - <div v-if="result" class="ui positive message"> - <translate translate-context="Content/Settings/Paragraph">Settings updated successfully.</translate> + <div + v-if="result" + class="ui positive message" + > + <translate translate-context="Content/Settings/Paragraph"> + Settings updated successfully. + </translate> </div> - <p v-if="group.help">{{ group.help }}</p> - <div v-for="setting in settings" class="ui field"> + <p v-if="group.help"> + {{ group.help }} + </p> + <div + v-for="(setting, key) in settings" + :key="key" + class="ui field" + > <template v-if="setting.field.widget.class !== 'CheckboxInput'"> <label :for="setting.identifier">{{ setting.verbose_name }}</label> - <p v-if="setting.help_text">{{ setting.help_text }}</p> + <p v-if="setting.help_text"> + {{ setting.help_text }} + </p> </template> - <content-form v-if="setting.fieldType === 'markdown'" v-model="values[setting.identifier]" v-bind="setting.fieldParams" /> + <content-form + v-if="setting.fieldType === 'markdown'" + v-model="values[setting.identifier]" + v-bind="setting.fieldParams" + /> <signup-form-builder v-else-if="setting.fieldType === 'formBuilder'" :value="values[setting.identifier]" :signup-approval-enabled="values.moderation__signup_approval_enabled" - @input="set(setting.identifier, $event)" /> + @input="set(setting.identifier, $event)" + /> <input + v-else-if="setting.field.widget.class === 'PasswordInput'" :id="setting.identifier" + v-model="values[setting.identifier]" :name="setting.identifier" - v-else-if="setting.field.widget.class === 'PasswordInput'" type="password" class="ui input" - v-model="values[setting.identifier]" /> + > <input + v-else-if="setting.field.widget.class === 'TextInput'" :id="setting.identifier" + v-model="values[setting.identifier]" :name="setting.identifier" - v-else-if="setting.field.widget.class === 'TextInput'" type="text" class="ui input" - v-model="values[setting.identifier]" /> + > <input + v-else-if="setting.field.class === 'IntegerField'" :id="setting.identifier" + v-model.number="values[setting.identifier]" :name="setting.identifier" - v-else-if="setting.field.class === 'IntegerField'" type="number" class="ui input" - v-model.number="values[setting.identifier]" /> + > <textarea + v-else-if="setting.field.widget.class === 'Textarea'" :id="setting.identifier" + v-model="values[setting.identifier]" :name="setting.identifier" - v-else-if="setting.field.widget.class === 'Textarea'" type="text" class="ui input" - v-model="values[setting.identifier]" /> - <div v-else-if="setting.field.widget.class === 'CheckboxInput'" class="ui toggle checkbox"> + /> + <div + v-else-if="setting.field.widget.class === 'CheckboxInput'" + class="ui toggle checkbox" + > <input :id="setting.identifier" - :name="setting.identifier" v-model="values[setting.identifier]" - type="checkbox" /> + :name="setting.identifier" + type="checkbox" + > <label :for="setting.identifier">{{ setting.verbose_name }}</label> - <p v-if="setting.help_text">{{ setting.help_text }}</p> + <p v-if="setting.help_text"> + {{ setting.help_text }} + </p> </div> <select - :id="setting.identifier" v-else-if="setting.field.class === 'MultipleChoiceField'" + :id="setting.identifier" v-model="values[setting.identifier]" multiple - class="ui search selection dropdown"> - <option v-for="v in setting.additional_data.choices" :value="v[0]">{{ v[1] }}</option> + class="ui search selection dropdown" + > + <option + v-for="(v, index) in setting.additional_data.choices" + :key="index" + :value="v[0]" + > + {{ v[1] }} + </option> </select> <div v-else-if="setting.field.widget.class === 'ImageWidget'"> - <input :id="setting.identifier" type="file" :ref="setting.identifier"> + <input + :id="setting.identifier" + :ref="setting.identifier" + type="file" + > <div v-if="values[setting.identifier]"> - <div class="ui hidden divider"></div> - <h3 class="ui header"><translate translate-context="Content/Settings/Title/Noun">Current image</translate></h3> - <img class="ui image" alt="" v-if="values[setting.identifier]" :src="$store.getters['instance/absoluteUrl'](values[setting.identifier])" /> + <div class="ui hidden divider" /> + <h3 class="ui header"> + <translate translate-context="Content/Settings/Title/Noun"> + Current image + </translate> + </h3> + <img + v-if="values[setting.identifier]" + class="ui image" + alt="" + :src="$store.getters['instance/absoluteUrl'](values[setting.identifier])" + > </div> </div> </div> <button type="submit" - :class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']"> - <translate translate-context="Content/*/Button.Label/Verb">Save</translate> + :class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']" + > + <translate translate-context="Content/*/Button.Label/Verb"> + Save + </translate> </button> </form> </template> @@ -91,12 +161,12 @@ import axios from 'axios' import lodash from '@/lodash' export default { - props: { - group: {type: Object, required: true}, - settingsData: {type: Array, required: true} - }, components: { - SignupFormBuilder: () => import(/* webpackChunkName: "signup-form-builder" */ "@/components/admin/SignupFormBuilder"), + SignupFormBuilder: () => import(/* webpackChunkName: "signup-form-builder" */ '@/components/admin/SignupFormBuilder') + }, + props: { + group: { type: Object, required: true }, + settingsData: { type: Array, required: true } }, data () { return { @@ -106,28 +176,44 @@ export default { isLoading: false } }, + computed: { + settings () { + const byIdentifier = {} + this.settingsData.forEach(e => { + byIdentifier[e.identifier] = e + }) + return this.group.settings.map(e => { + return { ...byIdentifier[e.name], fieldType: e.fieldType, fieldParams: e.fieldParams || {} } + }) + }, + fileSettings () { + return this.settings.filter((s) => { + return s.field.widget.class === 'ImageWidget' + }) + } + }, created () { - let self = this + const self = this this.settings.forEach(e => { self.values[e.identifier] = e.value }) }, methods: { save () { - let self = this + const self = this this.isLoading = true self.errors = [] self.result = null let postData = self.values let contentType = 'application/json' - let fileSettingsIDs = this.fileSettings.map((s) => {return s.identifier}) + const fileSettingsIDs = this.fileSettings.map((s) => { return s.identifier }) if (fileSettingsIDs.length > 0) { contentType = 'multipart/form-data' postData = new FormData() this.settings.forEach((s) => { if (fileSettingsIDs.indexOf(s.identifier) > -1) { - let input = self.$refs[s.identifier][0] - let files = input.files + const input = self.$refs[s.identifier][0] + const files = input.files console.log('ref', input, files) if (files && files.length > 0) { postData.append(s.identifier, files[0]) @@ -139,8 +225,8 @@ export default { } axios.post('instance/admin/settings/bulk/', postData, { headers: { - 'Content-Type': contentType, - }, + 'Content-Type': contentType + } }).then((response) => { self.result = true response.data.forEach((s) => { @@ -158,22 +244,6 @@ export default { this.values = lodash.cloneDeep(this.values) this.$set(this.values, key, value) } - }, - computed: { - settings () { - let byIdentifier = {} - this.settingsData.forEach(e => { - byIdentifier[e.identifier] = e - }) - return this.group.settings.map(e => { - return {...byIdentifier[e.name], fieldType: e.fieldType, fieldParams: e.fieldParams || {}} - }) - }, - fileSettings () { - return this.settings.filter((s) => { - return s.field.widget.class === 'ImageWidget' - }) - } } } </script> diff --git a/front/src/components/admin/SignupFormBuilder.vue b/front/src/components/admin/SignupFormBuilder.vue index 3724b883ecd43b3ff5350786feb970b5ce027875..1eb29f632fed68c535878d288c17dab16159e3ec 100644 --- a/front/src/components/admin/SignupFormBuilder.vue +++ b/front/src/components/admin/SignupFormBuilder.vue @@ -1,116 +1,173 @@ <template> <div> - <div class="ui top attached tabular menu"> - <button :class="[{active: !isPreviewing}, 'item']" @click.stop.prevent="isPreviewing = false"> - <translate translate-context="Content/*/Button.Label/Verb">Edit form</translate> + <button + :class="[{active: !isPreviewing}, 'item']" + @click.stop.prevent="isPreviewing = false" + > + <translate translate-context="Content/*/Button.Label/Verb"> + Edit form + </translate> </button> - <button :class="[{active: isPreviewing}, 'item']" @click.stop.prevent="isPreviewing = true"> - <translate translate-context="*/Form/Menu.item">Preview form</translate> + <button + :class="[{active: isPreviewing}, 'item']" + @click.stop.prevent="isPreviewing = true" + > + <translate translate-context="*/Form/Menu.item"> + Preview form + </translate> </button> </div> - <div v-if="isPreviewing" class="ui bottom attached segment"> + <div + v-if="isPreviewing" + class="ui bottom attached segment" + > <signup-form :customization="local" :signup-approval-enabled="signupApprovalEnabled" - :fetch-description-html="true"></signup-form> - <div class="ui clearing hidden divider"></div> + :fetch-description-html="true" + /> + <div class="ui clearing hidden divider" /> </div> - <div v-else class="ui bottom attached segment"> + <div + v-else + class="ui bottom attached segment" + > <div class="field"> <label for="help-text"> <translate translate-context="*/*/Label">Help text</translate> </label> <p> - <translate translate-context="*/*/Help">An optional text to be displayed at the start of the sign-up form.</translate> + <translate translate-context="*/*/Help"> + An optional text to be displayed at the start of the sign-up form. + </translate> </p> <content-form field-id="help-text" :permissive="true" :value="(local.help_text || {}).text" - @input="update('help_text.text', $event)"></content-form> + @input="update('help_text.text', $event)" + /> </div> <div class="field"> <label> <translate translate-context="*/*/Label">Additional fields</translate> </label> <p> - <translate translate-context="*/*/Help">Additional form fields to be displayed in the form. Only shown if manual sign-up validation is enabled.</translate> + <translate translate-context="*/*/Help"> + Additional form fields to be displayed in the form. Only shown if manual sign-up validation is enabled. + </translate> </p> <table v-if="local.fields.length > 0"> <thead> <tr> <th> - <translate translate-context="*/*/Form-builder,Help">Field label</translate> + <translate translate-context="*/*/Form-builder,Help"> + Field label + </translate> </th> <th> - <translate translate-context="*/*/Form-builder,Help">Field type</translate> + <translate translate-context="*/*/Form-builder,Help"> + Field type + </translate> </th> <th> - <translate translate-context="*/*/Form-builder,Help">Required</translate> + <translate translate-context="*/*/Form-builder,Help"> + Required + </translate> </th> <th><span class="visually-hidden"><translate translate-context="*/*/Form-builder,Help">Actions</translate></span></th> </tr> </thead> <tbody> - <tr v-for="(field, idx) in local.fields"> + <tr + v-for="(field, idx) in local.fields" + :key="idx" + > <td> - <input type="text" v-model="field.label" required> + <input + v-model="field.label" + type="text" + required + > </td> <td> <select v-model="field.input_type"> <option value="short_text"> - <translate translate-context="*/*/Form-builder">Short text</translate> + <translate translate-context="*/*/Form-builder"> + Short text + </translate> </option> <option value="long_text"> - <translate translate-context="*/*/Form-builder">Long text</translate> + <translate translate-context="*/*/Form-builder"> + Long text + </translate> </option> </select> </td> <td> <select v-model="field.required"> <option :value="true"> - <translate translate-context="*/*/*">Yes</translate> + <translate translate-context="*/*/*"> + Yes + </translate> </option> <option :value="false"> - <translate translate-context="*/*/*">No</translate> + <translate translate-context="*/*/*"> + No + </translate> </option> </select> </td> <td> <i :disabled="idx === 0" - @click="move(idx, -1)" role="button" + role="button" :title="labels.up" - :class="['up', 'arrow', {disabled: idx === 0}, 'icon']"></i> + :class="['up', 'arrow', {disabled: idx === 0}, 'icon']" + @click="move(idx, -1)" + /> <i :disabled="idx >= local.fields.length - 1" - @click="move(idx, 1)" role="button" + role="button" :title="labels.down" - :class="['down', 'arrow', {disabled: idx >= local.fields.length - 1}, 'icon']"></i> - <i @click="remove(idx)" role="button" :title="labels.delete" class="x icon"></i> + :class="['down', 'arrow', {disabled: idx >= local.fields.length - 1}, 'icon']" + @click="move(idx, 1)" + /> + <i + role="button" + :title="labels.delete" + class="x icon" + @click="remove(idx)" + /> </td> </tr> </tbody> </table> - <div class="ui hidden divider"></div> - <button v-if="local.fields.length < maxFields" class="ui basic button" @click.stop.prevent="addField"> - <translate translate-context="*/*/Form-builder">Add a new field</translate> + <div class="ui hidden divider" /> + <button + v-if="local.fields.length < maxFields" + class="ui basic button" + @click.stop.prevent="addField" + > + <translate translate-context="*/*/Form-builder"> + Add a new field + </translate> </button> </div> </div> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> </div> </template> <script> import lodash from '@/lodash' -import SignupForm from "@/components/auth/SignupForm" +import SignupForm from '@/components/auth/SignupForm' -function arrayMove(arr, oldIndex, newIndex) { +function arrayMove (arr, oldIndex, newIndex) { if (newIndex >= arr.length) { - var k = newIndex - arr.length + 1 + let k = newIndex - arr.length + 1 while (k--) { arr.push(undefined) } @@ -122,40 +179,40 @@ function arrayMove(arr, oldIndex, newIndex) { // v-model with objects is complex, cf // https://simonkollross.de/posts/vuejs-using-v-model-with-objects-for-custom-components export default { - props: { - value: {type: Object}, - signupApprovalEnabled: {type: Boolean}, - }, components: { SignupForm }, + props: { + value: { type: Object, required: true }, + signupApprovalEnabled: { type: Boolean } + }, data () { return { maxFields: 10, isPreviewing: false } }, - created () { - this.$emit('input', this.local) - }, computed: { labels () { return { delete: this.$pgettext('*/*/*', 'Delete'), up: this.$pgettext('*/*/*', 'Move up'), - down: this.$pgettext('*/*/*', 'Move down'), + down: this.$pgettext('*/*/*', 'Move down') } }, - local() { - return (this.value && this.value.fields) ? this.value : { help_text: {text: null, content_type: "text/markdown"}, fields: [] } - }, + local () { + return (this.value && this.value.fields) ? this.value : { help_text: { text: null, content_type: 'text/markdown' }, fields: [] } + } + }, + created () { + this.$emit('input', this.local) }, methods: { addField () { - let newValue = lodash.tap(lodash.cloneDeep(this.local), v => v.fields.push({ + const newValue = lodash.tap(lodash.cloneDeep(this.local), v => v.fields.push({ label: this.$pgettext('*/*/Form-builder', 'Additional field') + ' ' + (this.local.fields.length + 1), required: true, - input_type: 'short_text', + input_type: 'short_text' })) this.$emit('input', newValue) }, @@ -169,10 +226,10 @@ export default { if (idx + incr >= this.local.fields.length) { return } - let newFields = arrayMove(lodash.cloneDeep(this.local).fields, idx, idx + incr) + const newFields = arrayMove(lodash.cloneDeep(this.local).fields, idx, idx + incr) this.update('fields', newFields) }, - update(key, value) { + update (key, value) { if (key === 'help_text.text') { key = 'help_text' if (!value || value.length === 0) { @@ -180,12 +237,12 @@ export default { } else { value = { text: value, - content_type: "text/markdown" + content_type: 'text/markdown' } } } this.$emit('input', lodash.tap(lodash.cloneDeep(this.local), v => lodash.set(v, key, value))) - }, - }, + } + } } </script> diff --git a/front/src/components/audio/ArtistLabel.vue b/front/src/components/audio/ArtistLabel.vue index 659d0884f689b4abf4448c3cb2faa14cd38820ec..bbcc502934f4c2ba6033441ed3164a30a56bb5ca 100644 --- a/front/src/components/audio/ArtistLabel.vue +++ b/front/src/components/audio/ArtistLabel.vue @@ -1,25 +1,34 @@ <template> - <router-link class="artist-label ui image label" :to="route"> - <img alt="" :class="[{circular: artist.content_category != 'podcast'}]" v-if="artist.cover && artist.cover.urls.original" v-lazy="$store.getters['instance/absoluteUrl'](artist.cover.urls.medium_square_crop)" /> - <i :class="[artist.content_category != 'podcast' ? 'circular' : 'bordered', 'inverted violet users icon']" v-else /> + <router-link + class="artist-label ui image label" + :to="route" + > + <img + v-if="artist.cover && artist.cover.urls.original" + v-lazy="$store.getters['instance/absoluteUrl'](artist.cover.urls.medium_square_crop)" + alt="" + :class="[{circular: artist.content_category != 'podcast'}]" + > + <i + v-else + :class="[artist.content_category != 'podcast' ? 'circular' : 'bordered', 'inverted violet users icon']" + /> {{ artist.name }} </router-link> </template> <script> -import {momentFormat} from '@/filters' - export default { props: { - artist: Object, + artist: { type: Object, required: true } }, computed: { route () { if (this.artist.channel) { - return {name: 'channels.detail', params: {id: this.artist.channel.uuid}} + return { name: 'channels.detail', params: { id: this.artist.channel.uuid } } } - return {name: 'library.artists.detail', params: {id: this.artist.id}} + return { name: 'library.artists.detail', params: { id: this.artist.id } } } } } diff --git a/front/src/components/audio/ChannelCard.vue b/front/src/components/audio/ChannelCard.vue index 78cc14c443cf8d696702ee96adb45af94f8578c6..8578e69ffb47a01fa0e95fddb873bd168edf7fe5 100644 --- a/front/src/components/audio/ChannelCard.vue +++ b/front/src/components/audio/ChannelCard.vue @@ -1,67 +1,100 @@ <template> <div class="card app-card"> <div + v-lazy:background-image="imageUrl" + :class="['ui', 'head-image', {'circular': object.artist.content_category != 'podcast'}, {'padded': object.artist.content_category === 'podcast'}, 'image', {'default-cover': !object.artist.cover}]" @click="$router.push({name: 'channels.detail', params: {id: urlId}})" - :class="['ui', 'head-image', {'circular': object.artist.content_category != 'podcast'}, {'padded': object.artist.content_category === 'podcast'}, 'image', {'default-cover': !object.artist.cover}]" v-lazy:background-image="imageUrl"> - <play-button :icon-only="true" :is-playable="true" :button-classes="['ui', 'circular', 'large', 'vibrant', 'icon', 'button']" :artist="object.artist"></play-button> + > + <play-button + :icon-only="true" + :is-playable="true" + :button-classes="['ui', 'circular', 'large', 'vibrant', 'icon', 'button']" + :artist="object.artist" + /> </div> <div class="content"> <strong> - <router-link class="discrete link" :to="{name: 'channels.detail', params: {id: urlId}}"> + <router-link + class="discrete link" + :to="{name: 'channels.detail', params: {id: urlId}}" + > {{ object.artist.name }} </router-link> </strong> <div class="description"> - <translate class="meta ellipsis" translate-context="Content/Channel/Paragraph" - key="1" + <translate v-if="object.artist.content_category === 'podcast'" + key="1" + class="meta ellipsis" + translate-context="Content/Channel/Paragraph" translate-plural="%{ count } episodes" :translate-n="object.artist.tracks_count" - :translate-params="{count: object.artist.tracks_count}"> + :translate-params="{count: object.artist.tracks_count}" + > %{ count } episode </translate> - <translate key="2" v-else translate-context="*/*/*" :translate-params="{count: object.artist.tracks_count}" :translate-n="object.artist.tracks_count" translate-plural="%{ count } tracks">%{ count } track</translate> - <tags-list label-classes="tiny" :truncate-size="20" :limit="2" :show-more="false" :tags="object.artist.tags"></tags-list> + <translate + v-else + key="2" + translate-context="*/*/*" + :translate-params="{count: object.artist.tracks_count}" + :translate-n="object.artist.tracks_count" + translate-plural="%{ count } tracks" + > + %{ count } track + </translate> + <tags-list + label-classes="tiny" + :truncate-size="20" + :limit="2" + :show-more="false" + :tags="object.artist.tags" + /> </div> - </div> <div class="extra content"> <time v-translate class="meta ellipsis" :datetime="object.artist.modification_date" - :title="updatedTitle"> + :title="updatedTitle" + > %{ updatedAgo } </time> <play-button class="right floated basic icon" :dropdown-only="true" :is-playable="true" - :dropdown-icon-classes="['ellipsis', 'horizontal', 'large really discrete']" :artist="object.artist" :channel="object" :account="object.attributed_to"></play-button> + :dropdown-icon-classes="['ellipsis', 'horizontal', 'large really discrete']" + :artist="object.artist" + :channel="object" + :account="object.attributed_to" + /> </div> </div> </template> <script> import PlayButton from '@/components/audio/PlayButton' -import TagsList from "@/components/tags/List" +import TagsList from '@/components/tags/List' -import {momentFormat} from '@/filters' -import moment from "moment" +import { momentFormat } from '@/filters' +import moment from 'moment' export default { - props: { - object: {type: Object}, - }, components: { PlayButton, TagsList }, + props: { + object: { type: Object, required: true } + }, computed: { imageUrl () { if (this.object.artist.cover) { return this.$store.getters['instance/absoluteUrl'](this.object.artist.cover.urls.medium_square_crop) } + return null }, urlId () { if (this.object.actor && this.object.actor.is_local) { @@ -73,9 +106,9 @@ export default { } }, updatedTitle () { - let d = momentFormat(this.object.artist.modification_date) - let message = this.$pgettext('*/*/*', 'Updated on %{ date }') - return this.$gettextInterpolate(message, {date: d}) + const d = momentFormat(this.object.artist.modification_date) + const message = this.$pgettext('*/*/*', 'Updated on %{ date }') + return this.$gettextInterpolate(message, { date: d }) }, updatedAgo () { return moment(this.object.artist.modification_date).fromNow() diff --git a/front/src/components/audio/ChannelEntries.vue b/front/src/components/audio/ChannelEntries.vue index f067b5a7540ba089999d55645d30c4e679994528..21c4dc1c85f0571233e2abbd7d1236b4180b8836 100644 --- a/front/src/components/audio/ChannelEntries.vue +++ b/front/src/components/audio/ChannelEntries.vue @@ -1,9 +1,12 @@ <template> <div> - <slot></slot> - <div class="ui hidden divider"></div> - <div v-if="isLoading" class="ui inverted active dimmer"> - <div class="ui loader"></div> + <slot /> + <div class="ui hidden divider" /> + <div + v-if="isLoading" + class="ui inverted active dimmer" + > + <div class="ui loader" /> </div> <podcast-table v-if="isPodcast" @@ -16,9 +19,10 @@ :show-album="false" :paginate-results="true" :total="count" - @page-changed="updatePage" :page="page" - :paginate-by="limit"></podcast-table> + :paginate-by="limit" + @page-changed="updatePage" + /> <track-table v-else :default-cover="defaultCover" @@ -30,13 +34,19 @@ :show-album="false" :paginate-results="true" :total="count" - @page-changed="updatePage" :page="page" - :paginate-by="limit"></track-table> + :paginate-by="limit" + @page-changed="updatePage" + /> <template v-if="!isLoading && objects.length === 0"> - <empty-state @refresh="fetchData('tracks/')" :refresh="true"> + <empty-state + :refresh="true" + @refresh="fetchData('tracks/')" + > <p> - <translate translate-context="Content/Channels/*">You may need to subscribe to this channel to see its content.</translate> + <translate translate-context="Content/Channels/*"> + You may need to subscribe to this channel to see its content. + </translate> </p> </empty-state> </template> @@ -50,26 +60,31 @@ import PodcastTable from '@/components/audio/podcast/Table' import TrackTable from '@/components/audio/track/Table' export default { - props: { - filters: {type: Object, required: true}, - limit: {type: Number, default: 10}, - defaultCover: {type: Object}, - isPodcast: {type: Boolean, required: true}, - }, components: { PodcastTable, - TrackTable, + TrackTable + }, + props: { + filters: { type: Object, required: true }, + limit: { type: Number, default: 10 }, + defaultCover: { type: Object, required: true }, + isPodcast: { type: Boolean, required: true } }, data () { return { objects: [], count: 0, isLoading: false, - errors: null, + errors: [], nextPage: null, page: 1 } }, + watch: { + page () { + this.fetchData('tracks/') + } + }, created () { this.fetchData('tracks/') }, @@ -79,31 +94,26 @@ export default { return } this.isLoading = true - let self = this - let params = _.clone(this.filters) + const self = this + const params = _.clone(this.filters) params.page_size = this.limit params.page = this.page params.include_channels = true try { - let channelsPromise = await axios.get(url, {params: params}) - self.nextPage = channelsPromise.data.next - self.objects = channelsPromise.data.results - self.count = channelsPromise.data.count - self.$emit('fetched', channelsPromise.data) - self.isLoading = false - } catch(e) { + const channelsPromise = await axios.get(url, { params: params }) + self.nextPage = channelsPromise.data.next + self.objects = channelsPromise.data.results + self.count = channelsPromise.data.count + self.$emit('fetched', channelsPromise.data) + self.isLoading = false + } catch (e) { self.isLoading = false - self.errors = error.backendErrors + self.errors = e.backendErrors } }, - updatePage: function(page) { + updatePage: function (page) { this.page = page } - }, - watch: { - page() { - this.fetchData('tracks/') - } } } </script> diff --git a/front/src/components/audio/ChannelEntryCard.vue b/front/src/components/audio/ChannelEntryCard.vue index 79ca6baf080b8aa4b869b601e31632ca0f714912..2ca1f93e0aca7bba744d4aade7b546b555d82de3 100644 --- a/front/src/components/audio/ChannelEntryCard.vue +++ b/front/src/components/audio/ChannelEntryCard.vue @@ -1,48 +1,77 @@ <template> <div :class="[{active: currentTrack && isPlaying && entry.id === currentTrack.id}, 'channel-entry-card']"> <div class="controls"> - <play-button class="basic circular icon" :discrete="true" :icon-only="true" :is-playable="true" :button-classes="['ui', 'circular', 'inverted vibrant', 'icon', 'button']" :track="entry"></play-button> + <play-button + class="basic circular icon" + :discrete="true" + :icon-only="true" + :is-playable="true" + :button-classes="['ui', 'circular', 'inverted vibrant', 'icon', 'button']" + :track="entry" + /> </div> <img - @click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})" + v-if="cover && cover.urls.original" + v-lazy="$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)" alt="" class="channel-image image" - v-if="cover && cover.urls.original" - v-lazy="$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)"> - <img @click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})" - class="channel-image image" - v-else-if="entry.artist.content_category === 'podcast' && defaultCover != undefined" - v-lazy="$store.getters['instance/absoluteUrl'](defaultCover.urls.medium_square_crop)"> + > <img + v-else-if="entry.artist.content_category === 'podcast' && defaultCover != undefined" + v-lazy="$store.getters['instance/absoluteUrl'](defaultCover.urls.medium_square_crop)" + class="channel-image image" @click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})" + > + <img + v-else-if="entry.album && entry.album.cover && entry.album.cover.urls.original" + v-lazy="$store.getters['instance/absoluteUrl'](entry.album.cover.urls.medium_square_crop)" alt="" class="channel-image image" - v-else-if="entry.album && entry.album.cover && entry.album.cover.urls.original" - v-lazy="$store.getters['instance/absoluteUrl'](entry.album.cover.urls.medium_square_crop)"> - <img @click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})" + > + <img + v-else alt="" class="channel-image image" - v-else - src="../../assets/audio/default-cover.png"> + src="../../assets/audio/default-cover.png" + @click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})" + > <div class="ellipsis content"> <strong> - <router-link class="discrete link" :to="{name: 'library.tracks.detail', params: {id: entry.id}}"> + <router-link + class="discrete link" + :to="{name: 'library.tracks.detail', params: {id: entry.id}}" + > {{ entry.title }} </router-link> </strong> <br> - <human-date class="really discrete" :date="entry.creation_date"></human-date> + <human-date + class="really discrete" + :date="entry.creation_date" + /> </div> <div class="meta"> - <template v-if="$store.state.auth.authenticated && $store.getters['favorites/isFavorite'](entry.id)"> - <track-favorite-icon class="tiny" :track="entry"></track-favorite-icon> - </template> - <human-duration v-if="duration" :duration="duration"></human-duration> + <template v-if="$store.state.auth.authenticated && $store.getters['favorites/isFavorite'](entry.id)"> + <track-favorite-icon + class="tiny" + :track="entry" + /> + </template> + <human-duration + v-if="duration" + :duration="duration" + /> </div> <div class="controls"> - <play-button class="play-button basic icon" :dropdown-only="true" :is-playable="entry.is_playable" :dropdown-icon-classes="['ellipsis', 'vertical', 'large really discrete']" :track="entry"></play-button> + <play-button + class="play-button basic icon" + :dropdown-only="true" + :is-playable="entry.is_playable" + :dropdown-icon-classes="['ellipsis', 'vertical', 'large really discrete']" + :track="entry" + /> </div> </div> </template> @@ -50,19 +79,21 @@ <script> import PlayButton from '@/components/audio/PlayButton' import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon' -import { mapGetters } from "vuex" - +import { mapGetters } from 'vuex' export default { - props: ['entry', 'defaultCover'], components: { PlayButton, - TrackFavoriteIcon, + TrackFavoriteIcon + }, + props: { + entry: { type: Object, required: true }, + defaultCover: { type: Object, required: true } }, computed: { ...mapGetters({ - currentTrack: "queue/currentTrack", + currentTrack: 'queue/currentTrack' }), isPlaying () { @@ -72,14 +103,16 @@ export default { if (this.entry.cover) { return this.entry.cover } + return null }, duration () { - let uploads = this.entry.uploads.filter((e) => { + const uploads = this.entry.uploads.filter((e) => { return e.duration }) if (uploads.length > 0) { return uploads[0].duration } + return null } } } diff --git a/front/src/components/audio/ChannelForm.vue b/front/src/components/audio/ChannelForm.vue index fbdb8280a87d1ddd18afd7a07a6eb3daeda8e9ff..3c96a9319706453342770f0634a0f5f0537399f4 100644 --- a/front/src/components/audio/ChannelForm.vue +++ b/front/src/components/audio/ChannelForm.vue @@ -1,24 +1,55 @@ <template> - <form class="ui form" @submit.prevent.stop="submit"> - <div v-if="errors.length > 0" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Content/*/Error message.Title">Error while saving channel</translate></h4> + <form + class="ui form" + @submit.prevent.stop="submit" + > + <div + v-if="errors.length > 0" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Content/*/Error message.Title"> + Error while saving channel + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> <template v-if="metadataChoices"> - <fieldset v-if="creating && step === 1" class="ui grouped channel-type required field"> + <fieldset + v-if="creating && step === 1" + class="ui grouped channel-type required field" + > <legend> - <translate translate-context="Content/Channel/Paragraph">What will this channel be used for?</translate> + <translate translate-context="Content/Channel/Paragraph"> + What will this channel be used for? + </translate> </legend> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <div class="field"> - <div :class="['ui', 'radio', 'checkbox', {selected: choice.value == newValues.content_category}]" v-for="choice in categoryChoices"> - <input type="radio" name="channel-category" :id="`category-${choice.value}`" :value="choice.value" v-model="newValues.content_category"> + <div + v-for="(choice, key) in categoryChoices" + :key="key" + :class="['ui', 'radio', 'checkbox', {selected: choice.value == newValues.content_category}]" + > + <input + :id="`category-${choice.value}`" + v-model="newValues.content_category" + type="radio" + name="channel-category" + :value="choice.value" + > <label :for="`category-${choice.value}`"> - <span :class="['right floated', 'placeholder', 'image', {circular: choice.value === 'music'}]"></span> + <span :class="['right floated', 'placeholder', 'image', {circular: choice.value === 'music'}]" /> <strong>{{ choice.label }}</strong> - <div class="ui small hidden divider"></div> + <div class="ui small hidden divider" /> {{ choice.helpText }} </label> </div> @@ -29,20 +60,35 @@ <label for="channel-name"> <translate translate-context="Content/Channel/*">Name</translate> </label> - <input type="text" required v-model="newValues.name" :placeholder="labels.namePlaceholder"> + <input + v-model="newValues.name" + type="text" + required + :placeholder="labels.namePlaceholder" + > </div> <div class="ui required field"> <label for="channel-username"> <translate translate-context="Content/Channel/*">Fediverse handle</translate> </label> <div class="ui left labeled input"> - <div class="ui basic label">@</div> - <input type="text" :required="creating" :disabled="!creating" :placeholder="labels.usernamePlaceholder" v-model="newValues.username"> + <div class="ui basic label"> + @ + </div> + <input + v-model="newValues.username" + type="text" + :required="creating" + :disabled="!creating" + :placeholder="labels.usernamePlaceholder" + > </div> <template v-if="creating"> - <div class="ui small hidden divider"></div> + <div class="ui small hidden divider" /> <p> - <translate translate-context="Content/Channels/Paragraph">Used in URLs and to follow this channel in the Fediverse. It cannot be changed later.</translate> + <translate translate-context="Content/Channels/Paragraph"> + Used in URLs and to follow this channel in the Fediverse. It cannot be changed later. + </translate> </p> </template> </div> @@ -51,12 +97,17 @@ v-model="newValues.cover" :required="false" :image-class="newValues.content_category === 'podcast' ? '' : 'circular'" - @delete="newValues.cover = null"> - <translate translate-context="Content/Channel/*" slot="label">Channel Picture</translate> + @delete="newValues.cover = null" + > + <translate + slot="label" + translate-context="Content/Channel/*" + > + Channel Picture + </translate> </attachment-input> - </div> - <div class="ui small hidden divider"></div> + <div class="ui small hidden divider" /> <div class="ui stackable grid row"> <div class="ten wide column"> <div class="ui field"> @@ -64,46 +115,67 @@ <translate translate-context="*/*/*">Tags</translate> </label> <tags-selector - v-model="newValues.tags" id="channel-tags" - :required="false"></tags-selector> + v-model="newValues.tags" + :required="false" + /> </div> </div> - <div class="six wide column" v-if="newValues.content_category === 'podcast'"> + <div + v-if="newValues.content_category === 'podcast'" + class="six wide column" + > <div class="ui required field"> <label for="channel-language"> <translate translate-context="*/*/*">Language</translate> </label> <select - name="channel-language" id="channel-language" v-model="newValues.metadata.language" + name="channel-language" required - class="ui search selection dropdown"> - <option v-for="v in metadataChoices.language" :value="v.value">{{ v.label }}</option> + class="ui search selection dropdown" + > + <option + v-for="(v, key) in metadataChoices.language" + :key="key" + :value="v.value" + > + {{ v.label }} + </option> </select> </div> </div> </div> - <div class="ui small hidden divider"></div> + <div class="ui small hidden divider" /> <div class="ui field"> <label for="channel-name"> <translate translate-context="*/*/*">Description</translate> </label> - <content-form v-model="newValues.description"></content-form> + <content-form v-model="newValues.description" /> </div> - <div class="ui two fields" v-if="newValues.content_category === 'podcast'"> + <div + v-if="newValues.content_category === 'podcast'" + class="ui two fields" + > <div class="ui required field"> <label for="channel-itunes-category"> <translate translate-context="*/*/*">Category</translate> </label> <select - name="itunes-category" id="itunes-category" v-model="newValues.metadata.itunes_category" + name="itunes-category" required - class="ui dropdown"> - <option v-for="v in metadataChoices.itunes_category" :value="v.value">{{ v.label }}</option> + class="ui dropdown" + > + <option + v-for="(v, key) in metadataChoices.itunes_category" + :key="key" + :value="v.value" + > + {{ v.label }} + </option> </select> </div> <div class="ui field"> @@ -111,45 +183,64 @@ <translate translate-context="*/*/*">Subcategory</translate> </label> <select - name="itunes-category" id="itunes-category" v-model="newValues.metadata.itunes_subcategory" + name="itunes-category" :disabled="!newValues.metadata.itunes_category" - class="ui dropdown"> - <option v-for="v in itunesSubcategories" :value="v">{{ v }}</option> + class="ui dropdown" + > + <option + v-for="(v, key) in itunesSubcategories" + :key="key" + :value="v" + > + {{ v }} + </option> </select> </div> </div> - <div class="ui two fields" v-if="newValues.content_category === 'podcast'"> + <div + v-if="newValues.content_category === 'podcast'" + class="ui two fields" + > <div class="ui field"> <label for="channel-itunes-email"> <translate translate-context="*/*/*">Owner e-mail address</translate> </label> <input - name="channel-itunes-email" id="channel-itunes-email" + v-model="newValues.metadata.owner_email" + name="channel-itunes-email" type="email" - v-model="newValues.metadata.owner_email"> + > </div> <div class="ui field"> <label for="channel-itunes-name"> <translate translate-context="*/*/*">Owner name</translate> </label> <input - name="channel-itunes-name" id="channel-itunes-name" + v-model="newValues.metadata.owner_name" + name="channel-itunes-name" maxlength="255" - v-model="newValues.metadata.owner_name"> + > </div> </div> <p> - <translate translate-context="*/*/*">Used for the itunes:email and itunes:name field required by certain platforms such as Spotify or iTunes.</translate> + <translate translate-context="*/*/*"> + Used for the itunes:email and itunes:name field required by certain platforms such as Spotify or iTunes. + </translate> </p> </template> </template> - <div v-else class="ui active inverted dimmer"> + <div + v-else + class="ui active inverted dimmer" + > <div class="ui text loader"> - <translate translate-context="*/*/*">Loading</translate> + <translate translate-context="*/*/*"> + Loading + </translate> </div> </div> </form> @@ -161,29 +252,25 @@ import axios from 'axios' import AttachmentInput from '@/components/common/AttachmentInput' import TagsSelector from '@/components/library/TagsSelector' -function slugify(text) { +function slugify (text) { return text.toString().toLowerCase() - .replace(/\s+/g, '') // Remove spaces - .replace(/[^\w]+/g, '') // Remove all non-word chars + .replace(/\s+/g, '') // Remove spaces + .replace(/[^\w]+/g, '') // Remove all non-word chars } export default { - props: { - object: {type: Object, required: false, default: null}, - step: {type: Number, required: false, default: 1}, - }, components: { AttachmentInput, TagsSelector }, - - created () { - this.fetchMetadataChoices() + props: { + object: { type: Object, required: false, default: null }, + step: { type: Number, required: false, default: 1 } }, data () { - let oldValues = {} + const oldValues = {} if (this.object) { - oldValues.metadata = {...(this.object.metadata || {})} + oldValues.metadata = { ...(this.object.metadata || {}) } oldValues.name = this.object.artist.name oldValues.description = this.object.artist.description oldValues.cover = this.object.artist.cover @@ -196,13 +283,13 @@ export default { errors: [], metadataChoices: null, newValues: { - name: oldValues.name || "", - username: oldValues.username || "", + name: oldValues.name || '', + username: oldValues.username || '', tags: oldValues.tags || [], - description: (oldValues.description || {}).text || "", + description: (oldValues.description || {}).text || '', cover: (oldValues.cover || {}).uuid || null, - content_category: oldValues.content_category || "podcast", - metadata: oldValues.metadata || {}, + content_category: oldValues.content_category || 'podcast', + metadata: oldValues.metadata || {} } } }, @@ -213,20 +300,20 @@ export default { categoryChoices () { return [ { - value: "podcast", - label: this.$pgettext('*/*/*', "Podcasts"), - helpText: this.$pgettext('Content/Channels/Help', "Host your episodes and keep your community updated."), + value: 'podcast', + label: this.$pgettext('*/*/*', 'Podcasts'), + helpText: this.$pgettext('Content/Channels/Help', 'Host your episodes and keep your community updated.') }, { - value: "music", - label: this.$pgettext('*/*/*', "Artist discography"), - helpText: this.$pgettext('Content/Channels/Help', "Publish music you make as a nice discography of albums and singles."), + value: 'music', + label: this.$pgettext('*/*/*', 'Artist discography'), + helpText: this.$pgettext('Content/Channels/Help', 'Publish music you make as a nice discography of albums and singles.') } ] }, itunesSubcategories () { for (let index = 0; index < this.metadataChoices.itunes_category.length; index++) { - const element = this.metadataChoices.itunes_category[index]; + const element = this.metadataChoices.itunes_category[index] if (element.value === this.newValues.metadata.itunes_category) { return element.children || [] } @@ -235,8 +322,8 @@ export default { }, labels () { return { - namePlaceholder: this.$pgettext('Content/Channel/Form.Field.Placeholder', "Awesome channel name"), - usernamePlaceholder: this.$pgettext('Content/Channel/Form.Field.Placeholder', "awesomechannelname"), + namePlaceholder: this.$pgettext('Content/Channel/Form.Field.Placeholder', 'Awesome channel name'), + usernamePlaceholder: this.$pgettext('Content/Channel/Form.Field.Placeholder', 'awesomechannelname') } }, submittable () { @@ -247,9 +334,41 @@ export default { return !!v } }, + watch: { + 'newValues.name' (v) { + if (this.creating) { + this.newValues.username = slugify(v) + } + }, + 'newValues.metadata.itunes_category' (v) { + this.newValues.metadata.itunes_subcategory = null + }, + 'newValues.content_category': { + handler (v) { + this.$emit('category', v) + }, + immediate: true + }, + isLoading: { + handler (v) { + this.$emit('loading', v) + }, + immediate: true + }, + submittable: { + handler (v) { + this.$emit('submittable', v) + }, + immediate: true + } + }, + + created () { + this.fetchMetadataChoices() + }, methods: { fetchMetadataChoices () { - let self = this + const self = this axios.get('channels/metadata-choices').then((response) => { self.metadataChoices = response.data }, error => { @@ -258,21 +377,21 @@ export default { }, submit () { this.isLoading = true - let self = this - let handler = this.creating ? axios.post : axios.patch - let url = this.creating ? `channels/` : `channels/${this.object.uuid}` - let payload = { + const self = this + const handler = this.creating ? axios.post : axios.patch + const url = this.creating ? 'channels/' : `channels/${this.object.uuid}` + const payload = { name: this.newValues.name, username: this.newValues.username, tags: this.newValues.tags, content_category: this.newValues.content_category, cover: this.newValues.cover, - metadata: this.newValues.metadata, + metadata: this.newValues.metadata } if (this.newValues.description) { payload.description = { content_type: 'text/markdown', - text: this.newValues.description, + text: this.newValues.description } } else { payload.description = null @@ -291,34 +410,6 @@ export default { self.$emit('errored', self.errors) }) } - }, - watch: { - "newValues.name" (v) { - if (this.creating) { - this.newValues.username = slugify(v) - } - }, - "newValues.metadata.itunes_category" (v) { - this.newValues.metadata.itunes_subcategory = null - }, - "newValues.content_category": { - handler (v) { - this.$emit("category", v) - }, - immediate: true - }, - isLoading: { - handler (v) { - this.$emit("loading", v) - }, - immediate: true - }, - submittable: { - handler (v) { - this.$emit("submittable", v) - }, - immediate: true - }, } } </script> diff --git a/front/src/components/audio/ChannelSerieCard.vue b/front/src/components/audio/ChannelSerieCard.vue index 84f737c52dabc814e7e14076de1f8ab5dd944954..ab5542f7529aae693c23f0363c3412453d15441b 100644 --- a/front/src/components/audio/ChannelSerieCard.vue +++ b/front/src/components/audio/ChannelSerieCard.vue @@ -1,28 +1,62 @@ <template> <div class="channel-serie-card"> <div class="two-images"> - <img alt="" @click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})" class="channel-image" v-if="cover && cover.urls.original" v-lazy="$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)"> - <img alt="" @click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})" class="channel-image" v-else src="../../assets/audio/default-cover.png"> - <img alt="" @click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})" class="channel-image" v-if="cover && cover.urls.original" v-lazy="$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)"> - <img alt="" @click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})" class="channel-image" v-else src="../../assets/audio/default-cover.png"> + <img + v-if="cover && cover.urls.original" + v-lazy="$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)" + alt="" + class="channel-image" + @click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})" + > + <img + v-else + alt="" + class="channel-image" + src="../../assets/audio/default-cover.png" + @click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})" + > + <img + v-if="cover && cover.urls.original" + v-lazy="$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)" + alt="" + class="channel-image" + @click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})" + > + <img + v-else + alt="" + class="channel-image" + src="../../assets/audio/default-cover.png" + @click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})" + > </div> <div class="content ellipsis"> <strong> - <router-link class="discrete link" :to="{name: 'library.albums.detail', params: {id: serie.id}}"> + <router-link + class="discrete link" + :to="{name: 'library.albums.detail', params: {id: serie.id}}" + > {{ serie.title }} </router-link> </strong> <div class="description"> - <translate translate-context="Content/Channel/Paragraph" + <translate + translate-context="Content/Channel/Paragraph" translate-plural="%{ count } episodes" :translate-n="serie.tracks_count" - :translate-params="{count: serie.tracks_count}"> + :translate-params="{count: serie.tracks_count}" + > %{ count } episode </translate> </div> </div> <div class="controls"> - <play-button :icon-only="true" :is-playable="true" :button-classes="['ui', 'circular', 'vibrant', 'icon', 'button']" :album="serie"></play-button> + <play-button + :icon-only="true" + :is-playable="true" + :button-classes="['ui', 'circular', 'vibrant', 'icon', 'button']" + :album="serie" + /> </div> </div> </template> @@ -31,18 +65,19 @@ import PlayButton from '@/components/audio/PlayButton' export default { - props: ['serie'], components: { - PlayButton, + PlayButton }, + props: { serie: { type: Object, required: true } }, computed: { cover () { if (this.serie.cover) { return this.serie.cover } + return null }, duration () { - let uploads = this.serie.uploads.filter((e) => { + const uploads = this.serie.uploads.filter((e) => { return e.duration }) return uploads[0].duration diff --git a/front/src/components/audio/ChannelSeries.vue b/front/src/components/audio/ChannelSeries.vue index 69e73598724fcb3cf032887c932816440fea83b2..26b0aa0beee205025781d7ed37ba241edc040e7d 100644 --- a/front/src/components/audio/ChannelSeries.vue +++ b/front/src/components/audio/ChannelSeries.vue @@ -1,26 +1,51 @@ <template> <div> - <slot></slot> - <div class="ui hidden divider"></div> - <div v-if="isLoading" class="ui inverted active dimmer"> - <div class="ui loader"></div> + <slot /> + <div class="ui hidden divider" /> + <div + v-if="isLoading" + class="ui inverted active dimmer" + > + <div class="ui loader" /> </div> <template v-if="isPodcast"> - <channel-serie-card v-for="serie in objects" :serie="serie" :key="serie.id" /> + <channel-serie-card + v-for="serie in objects" + :key="serie.id" + :serie="serie" + /> </template> - <div v-else class="ui app-cards cards"> - <album-card v-for="album in objects" :album="album" :key="album.id" /> + <div + v-else + class="ui app-cards cards" + > + <album-card + v-for="album in objects" + :key="album.id" + :album="album" + /> </div> <template v-if="nextPage"> - <div class="ui hidden divider"></div> - <button v-if="nextPage" @click="fetchData(nextPage)" :class="['ui', 'basic', 'button']"> - <translate translate-context="*/*/Button,Label">Show more</translate> + <div class="ui hidden divider" /> + <button + v-if="nextPage" + :class="['ui', 'basic', 'button']" + @click="fetchData(nextPage)" + > + <translate translate-context="*/*/Button,Label"> + Show more + </translate> </button> </template> <template v-if="!isLoading && objects.length === 0"> - <empty-state @refresh="fetchData('albums/')" :refresh="true"> + <empty-state + :refresh="true" + @refresh="fetchData('albums/')" + > <p> - <translate translate-context="Content/Channels/*">You may need to subscribe to this channel to see its contents.</translate> + <translate translate-context="Content/Channels/*"> + You may need to subscribe to this channel to see its contents. + </translate> </p> </empty-state> </template> @@ -33,16 +58,15 @@ import axios from 'axios' import ChannelSerieCard from '@/components/audio/ChannelSerieCard' import AlbumCard from '@/components/audio/album/Card' - export default { - props: { - filters: {type: Object, required: true}, - isPodcast: {type: Boolean, default: true}, - limit: {type: Number, default: 5}, - }, components: { ChannelSerieCard, - AlbumCard, + AlbumCard + }, + props: { + filters: { type: Object, required: true }, + isPodcast: { type: Boolean, default: true }, + limit: { type: Number, default: 5 } }, data () { return { @@ -62,11 +86,11 @@ export default { return } this.isLoading = true - let self = this - let params = _.clone(this.filters) + const self = this + const params = _.clone(this.filters) params.page_size = this.limit params.include_channels = true - axios.get(url, {params: params}).then((response) => { + axios.get(url, { params: params }).then((response) => { self.nextPage = response.data.next self.isLoading = false self.objects = self.objects.concat(response.data.results) @@ -75,7 +99,7 @@ export default { self.isLoading = false self.errors = error.backendErrors }) - }, + } } } </script> diff --git a/front/src/components/audio/ChannelsWidget.vue b/front/src/components/audio/ChannelsWidget.vue index f6957a4b691c5d4fb675880af1e383d7ea1edd37..68b26feaa06d38e0451241c379b407edc9faf2c6 100644 --- a/front/src/components/audio/ChannelsWidget.vue +++ b/front/src/components/audio/ChannelsWidget.vue @@ -1,21 +1,37 @@ <template> <div> - <slot></slot> - <div class="ui hidden divider"></div> + <slot /> + <div class="ui hidden divider" /> <div class="ui app-cards cards"> - <div v-if="isLoading" class="ui inverted active dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui inverted active dimmer" + > + <div class="ui loader" /> </div> - <channel-card v-for="object in objects" :object="object" :key="object.uuid" /> + <channel-card + v-for="object in objects" + :key="object.uuid" + :object="object" + /> </div> <template v-if="nextPage"> - <div class="ui hidden divider"></div> - <button v-if="nextPage" @click="fetchData(nextPage)" :class="['ui', 'basic', 'button']"> - <translate translate-context="*/*/Button,Label">Show more</translate> + <div class="ui hidden divider" /> + <button + v-if="nextPage" + :class="['ui', 'basic', 'button']" + @click="fetchData(nextPage)" + > + <translate translate-context="*/*/Button,Label"> + Show more + </translate> </button> </template> <template v-if="!isLoading && objects.length === 0"> - <empty-state @refresh="fetchData('channels/')" :refresh="true"></empty-state> + <empty-state + :refresh="true" + @refresh="fetchData('channels/')" + /> </template> </div> </template> @@ -26,13 +42,13 @@ import axios from 'axios' import ChannelCard from '@/components/audio/ChannelCard' export default { - props: { - filters: {type: Object, required: true}, - limit: {type: Number, default: 5}, - }, components: { ChannelCard }, + props: { + filters: { type: Object, required: true }, + limit: { type: Number, default: 5 } + }, data () { return { objects: [], @@ -51,11 +67,11 @@ export default { return } this.isLoading = true - let self = this - let params = _.clone(this.filters) + const self = this + const params = _.clone(this.filters) params.page_size = this.limit params.include_channels = true - axios.get(url, {params: params}).then((response) => { + axios.get(url, { params: params }).then((response) => { self.nextPage = response.data.next self.isLoading = false self.objects = self.objects.concat(response.data.results) @@ -65,7 +81,7 @@ export default { self.isLoading = false self.errors = error.backendErrors }) - }, + } } } </script> diff --git a/front/src/components/audio/EmbedWizard.vue b/front/src/components/audio/EmbedWizard.vue index 81103879d7fde8124deac1a4168056beb4ba8061..2dc136afa29f640bc66071276688fa314e12d649 100644 --- a/front/src/components/audio/EmbedWizard.vue +++ b/front/src/components/audio/EmbedWizard.vue @@ -1,13 +1,19 @@ <template> <div> - <div role="alert" class="ui warning message" v-if="!anonymousCanListen"> + <div + v-if="!anonymousCanListen" + role="alert" + class="ui warning message" + > <p> <strong> <translate translate-context="Content/Embed/Message">Sharing will not work because this pod doesn't allow anonymous users to access content.</translate> </strong> </p> <p> - <translate translate-context="Content/Embed/Message">Please contact your admins and ask them to update the corresponding setting.</translate> + <translate translate-context="Content/Embed/Message"> + Please contact your admins and ask them to update the corresponding setting. + </translate> </p> </div> <div class="ui form"> @@ -15,49 +21,100 @@ <div class="field"> <div class="field"> <label for="embed-width"><translate translate-context="Popup/Embed/Input.Label">Widget width</translate></label> - <p><translate translate-context="Popup/Embed/Paragraph">Leave empty for a responsive widget</translate></p> - <input id="embed-width" type="number" v-model.number="width" min="0" step="10" /> + <p> + <translate translate-context="Popup/Embed/Paragraph"> + Leave empty for a responsive widget + </translate> + </p> + <input + id="embed-width" + v-model.number="width" + type="number" + min="0" + step="10" + > </div> <template v-if="type != 'track'"> <br> <div class="field"> <label for="embed-height"><translate translate-context="Popup/Embed/Input.Label">Widget height</translate></label> - <input id="embed-height" type="number" v-model="height" :min="minHeight" max="1000" step="10" /> + <input + id="embed-height" + v-model="height" + type="number" + :min="minHeight" + max="1000" + step="10" + > </div> </template> </div> <div class="field"> - <button @click="copy" class="ui right accent labeled icon floated button"><i class="copy icon"></i><translate translate-context="*/*/Button.Label/Short, Verb">Copy</translate></button> + <button + class="ui right accent labeled icon floated button" + @click="copy" + > + <i class="copy icon" /><translate translate-context="*/*/Button.Label/Short, Verb"> + Copy + </translate> + </button> <label for="embed-width"><translate translate-context="Popup/Embed/Input.Label/Noun">Embed code</translate></label> - <p><translate translate-context="Popup/Embed/Paragraph">Copy/paste this code in your website HTML</translate></p> - <textarea ref="textarea" :value="embedCode" rows="5" readonly> - </textarea> + <p> + <translate translate-context="Popup/Embed/Paragraph"> + Copy/paste this code in your website HTML + </translate> + </p> + <textarea + ref="textarea" + :value="embedCode" + rows="5" + readonly + /> <div class="ui right"> - <p class="message" v-if=copied><translate translate-context="Content/*/Paragraph">Text copied to clipboard!</translate></p> + <p + v-if="copied" + class="message" + > + <translate translate-context="Content/*/Paragraph"> + Text copied to clipboard! + </translate> + </p> </div> </div> </div> </div> <div class="preview"> <h3> - <a :href="iframeSrc" target="_blank"> + <a + :href="iframeSrc" + target="_blank" + > <translate translate-context="Popup/Embed/Title/Noun">Preview</translate> </a> </h3> - <iframe :width="frameWidth" :height="height" scrolling="no" frameborder="no" :src="iframeSrc"></iframe> + <iframe + :width="frameWidth" + :height="height" + scrolling="no" + frameborder="no" + :src="iframeSrc" + /> </div> </div> </template> <script> -import { mapState } from "vuex" +import { mapState } from 'vuex' import _ from '@/lodash' export default { - props: ['type', 'id'], + props: { + type: { type: String, required: true }, + id: { type: Number, required: true } + }, data () { - let d = { + const d = { width: null, height: 150, minHeight: 100, @@ -71,7 +128,7 @@ export default { }, computed: { ...mapState({ - nodeinfo: state => state.instance.nodeinfo, + nodeinfo: state => state.instance.nodeinfo }), anonymousCanListen () { return _.get(this.nodeinfo, 'metadata.library.anonymousCanListen', false) @@ -82,7 +139,7 @@ export default { // include hostname/protocol too so that the iframe link is absolute base = `${window.location.protocol}//${window.location.host}${base}` } - let instanceUrl = this.$store.state.instance.instanceUrl + const instanceUrl = this.$store.state.instance.instanceUrl let b = '' if (!window.location.href.startsWith(instanceUrl)) { // the frontend is running on a separate domain, so we need to provide @@ -98,15 +155,15 @@ export default { return '100%' }, embedCode () { - let src = this.iframeSrc.replace(/&/g, '&') + const src = this.iframeSrc.replace(/&/g, '&') return `<iframe width="${this.frameWidth}" height="${this.height}" scrolling="no" frameborder="no" src="${src}"></iframe>` } }, methods: { copy () { this.$refs.textarea.select() - document.execCommand("Copy") - let self = this + document.execCommand('Copy') + const self = this self.copied = true this.timeout = setTimeout(() => { self.copied = false diff --git a/front/src/components/audio/LibraryFollowButton.vue b/front/src/components/audio/LibraryFollowButton.vue index 8aa0a8fe30c25eb192767edcfbc3085f80642b5c..8190cca6ebdf6351b2f89c9d6e9032b3a9979b6e 100644 --- a/front/src/components/audio/LibraryFollowButton.vue +++ b/front/src/components/audio/LibraryFollowButton.vue @@ -1,16 +1,34 @@ - <template> - <button @click.stop="toggle" :class="['ui', 'pink', {'inverted': isApproved || isPending}, {'favorited': isApproved}, 'icon', 'labeled', 'button']"> - <i class="heart icon"></i> - <translate v-if="isApproved" translate-context="Content/Library/Card.Button.Label/Verb">Unfollow</translate> - <translate v-else-if="isPending" translate-context="Content/Library/Card.Button.Label/Verb">Cancel follow request</translate> - <translate v-else translate-context="Content/Library/Card.Button.Label/Verb">Follow</translate> +<template> + <button + :class="['ui', 'pink', {'inverted': isApproved || isPending}, {'favorited': isApproved}, 'icon', 'labeled', 'button']" + @click.stop="toggle" + > + <i class="heart icon" /> + <translate + v-if="isApproved" + translate-context="Content/Library/Card.Button.Label/Verb" + > + Unfollow + </translate> + <translate + v-else-if="isPending" + translate-context="Content/Library/Card.Button.Label/Verb" + > + Cancel follow request + </translate> + <translate + v-else + translate-context="Content/Library/Card.Button.Label/Verb" + > + Follow + </translate> </button> </template> <script> export default { props: { - library: {type: Object}, + library: { type: Object, required: true } }, computed: { isPending () { @@ -34,6 +52,5 @@ export default { } } - } </script> diff --git a/front/src/components/audio/PlayButton.vue b/front/src/components/audio/PlayButton.vue index dd0e651324a4f1d46200bc81f738f94c3408861a..aba5a346d04b40787297dbc92b30132322f83209 100644 --- a/front/src/components/audio/PlayButton.vue +++ b/front/src/components/audio/PlayButton.vue @@ -1,52 +1,121 @@ <template> - <span :title="title" :class="['ui', {'tiny': discrete}, {'icon': !discrete}, {'buttons': !dropdownOnly && !iconOnly}, 'play-button component-play-button']"> + <span + :title="title" + :class="['ui', {'tiny': discrete}, {'icon': !discrete}, {'buttons': !dropdownOnly && !iconOnly}, 'play-button component-play-button']" + > <button v-if="!dropdownOnly" - @click.stop.prevent="replacePlay" :disabled="!playable" :aria-label="labels.replacePlay" - :class="buttonClasses.concat(['ui', {loading: isLoading}, {'mini': discrete}, {disabled: !playable}])"> - <i v-if="playing" class="pause icon"></i> - <i v-else :class="[playIconClass, 'icon']"></i> + :class="buttonClasses.concat(['ui', {loading: isLoading}, {'mini': discrete}, {disabled: !playable}])" + @click.stop.prevent="replacePlay" + > + <i + v-if="playing" + class="pause icon" + /> + <i + v-else + :class="[playIconClass, 'icon']" + /> <template v-if="!discrete && !iconOnly"> <slot><translate translate-context="*/Queue/Button.Label/Short, Verb">Play</translate></slot></template> </button> <button v-if="!discrete && !iconOnly" + :class="['ui', {disabled: !playable && !filterableArtist}, 'floating', 'dropdown', {'icon': !dropdownOnly}, {'button': !dropdownOnly}]" @click.stop.prevent="clicked = true" - :class="['ui', {disabled: !playable && !filterableArtist}, 'floating', 'dropdown', {'icon': !dropdownOnly}, {'button': !dropdownOnly}]"> - <i :class="dropdownIconClasses.concat(['icon'])" :title="title" ></i> - <div class="menu" v-if="clicked"> - <button class="item basic" ref="add" data-ref="add" :disabled="!playable" @click.stop.prevent="add" :title="labels.addToQueue"> - <i class="plus icon"></i><translate translate-context="*/Queue/Dropdown/Button/Label/Short">Add to queue</translate> + > + <i + :class="dropdownIconClasses.concat(['icon'])" + :title="title" + /> + <div + v-if="clicked" + class="menu" + > + <button + ref="add" + class="item basic" + data-ref="add" + :disabled="!playable" + :title="labels.addToQueue" + @click.stop.prevent="add" + > + <i class="plus icon" /><translate translate-context="*/Queue/Dropdown/Button/Label/Short">Add to queue</translate> </button> - <button class="item basic" ref="addNext" data-ref="addNext" :disabled="!playable" @click.stop.prevent="addNext()" :title="labels.playNext"> - <i class="step forward icon"></i>{{ labels.playNext }} + <button + ref="addNext" + class="item basic" + data-ref="addNext" + :disabled="!playable" + :title="labels.playNext" + @click.stop.prevent="addNext()" + > + <i class="step forward icon" />{{ labels.playNext }} </button> - <button class="item basic" ref="playNow" data-ref="playNow" :disabled="!playable" @click.stop.prevent="addNext(true)" :title="labels.playNow"> - <i class="play icon"></i>{{ labels.playNow }} + <button + ref="playNow" + class="item basic" + data-ref="playNow" + :disabled="!playable" + :title="labels.playNow" + @click.stop.prevent="addNext(true)" + > + <i class="play icon" />{{ labels.playNow }} </button> - <button v-if="track" class="item basic" :disabled="!playable" @click.stop.prevent="$store.dispatch('radios/start', {type: 'similar', objectId: track.id})" :title="labels.startRadio"> - <i class="feed icon"></i><translate translate-context="*/Queue/Button.Label/Short, Verb">Play radio</translate> + <button + v-if="track" + class="item basic" + :disabled="!playable" + :title="labels.startRadio" + @click.stop.prevent="$store.dispatch('radios/start', {type: 'similar', objectId: track.id})" + > + <i class="feed icon" /><translate translate-context="*/Queue/Button.Label/Short, Verb">Play radio</translate> </button> - <button v-if="track" class="item basic" :disabled="!playable" @click.stop="$store.commit('playlists/chooseTrack', track)"> - <i class="list icon"></i> + <button + v-if="track" + class="item basic" + :disabled="!playable" + @click.stop="$store.commit('playlists/chooseTrack', track)" + > + <i class="list icon" /> <translate translate-context="Sidebar/Player/Icon.Tooltip/Verb">Add to playlist…</translate> </button> - <button v-if="track" class="item basic" @click.stop.prevent="$router.push(`/library/tracks/${track.id}/`)"> - <i class="info icon"></i> - <translate v-if="track.artist.content_category === 'podcast'" translate-context="*/Queue/Dropdown/Button/Label/Short">Episode details</translate> - <translate v-else translate-context="*/Queue/Dropdown/Button/Label/Short">Track details</translate> + <button + v-if="track" + class="item basic" + @click.stop.prevent="$router.push(`/library/tracks/${track.id}/`)" + > + <i class="info icon" /> + <translate + v-if="track.artist.content_category === 'podcast'" + translate-context="*/Queue/Dropdown/Button/Label/Short" + >Episode details</translate> + <translate + v-else + translate-context="*/Queue/Dropdown/Button/Label/Short" + >Track details</translate> </button> - <div class="divider"></div> - <button v-if="filterableArtist" ref="filterArtist" data-ref="filterArtist" class="item basic" :disabled="!filterableArtist" @click.stop.prevent="filterArtist" :title="labels.hideArtist"> - <i class="eye slash outline icon"></i><translate translate-context="*/Queue/Dropdown/Button/Label/Short">Hide content from this artist</translate> + <div class="divider" /> + <button + v-if="filterableArtist" + ref="filterArtist" + data-ref="filterArtist" + class="item basic" + :disabled="!filterableArtist" + :title="labels.hideArtist" + @click.stop.prevent="filterArtist" + > + <i class="eye slash outline icon" /><translate translate-context="*/Queue/Dropdown/Button/Label/Short">Hide content from this artist</translate> </button> <button v-for="obj in getReportableObjs({track, album, artist, playlist, account, channel})" :key="obj.target.type + obj.target.id" + :ref="`report${obj.target.type}${obj.target.id}`" class="item basic" - :ref="`report${obj.target.type}${obj.target.id}`" :data-ref="`report${obj.target.type}${obj.target.id}`" - @click.stop.prevent="$store.dispatch('moderation/report', obj.target)"> + :data-ref="`report${obj.target.type}${obj.target.id}`" + @click.stop.prevent="$store.dispatch('moderation/report', obj.target)" + > <i class="share icon" /> {{ obj.label }} </button> </div> @@ -55,7 +124,6 @@ </template> <script> -import axios from 'axios' import jQuery from 'jquery' import ReportMixin from '@/components/mixins/Report' @@ -65,23 +133,23 @@ export default { mixins: [ReportMixin, PlayOptionsMixin], props: { // we can either have a single or multiple tracks to play when clicked - tracks: {type: Array, required: false}, - track: {type: Object, required: false}, - account: {type: Object, required: false}, - dropdownIconClasses: {type: Array, required: false, default: () => { return ['dropdown'] }}, - playIconClass: {type: String, required: false, default: 'play icon'}, - buttonClasses: {type: Array, required: false, default: () => { return ['button'] }}, - playlist: {type: Object, required: false}, - discrete: {type: Boolean, default: false}, - dropdownOnly: {type: Boolean, default: false}, - iconOnly: {type: Boolean, default: false}, - artist: {type: Object, required: false}, - album: {type: Object, required: false}, - library: {type: Object, required: false}, - channel: {type: Object, required: false}, - isPlayable: {type: Boolean, required: false, default: null}, - playing: {type: Boolean, required: false, default: false}, - paused: {type: Boolean, required: false, default: false} + tracks: { type: Array, required: false, default: () => { return [] } }, + track: { type: Object, required: false, default: () => { return {} } }, + account: { type: Object, required: false, default: () => { return {} } }, + dropdownIconClasses: { type: Array, required: false, default: () => { return ['dropdown'] } }, + playIconClass: { type: String, required: false, default: 'play icon' }, + buttonClasses: { type: Array, required: false, default: () => { return ['button'] } }, + playlist: { type: Object, required: false, default: () => { return {} } }, + discrete: { type: Boolean, default: false }, + dropdownOnly: { type: Boolean, default: false }, + iconOnly: { type: Boolean, default: false }, + artist: { type: Object, required: false, default: () => { return {} } }, + album: { type: Object, required: false, default: () => { return {} } }, + library: { type: Object, required: false, default: () => { return {} } }, + channel: { type: Object, required: false, default: () => { return {} } }, + isPlayable: { type: Boolean, required: false, default: null }, + playing: { type: Boolean, required: false, default: false }, + paused: { type: Boolean, required: false, default: false } }, data () { return { @@ -111,7 +179,7 @@ export default { startRadio: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play similar songs'), report: this.$pgettext('*/Moderation/*/Button/Label,Verb', 'Report…'), addToPlaylist: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Add to playlist…'), - replacePlay, + replacePlay } }, title () { @@ -122,43 +190,42 @@ export default { return this.$pgettext('*/Queue/Button/Title', 'This track is not available in any library you have access to') } } - }, + return null + } }, watch: { clicked () { - let self = this + const self = this this.$nextTick(() => { jQuery(this.$el).find('.ui.dropdown').dropdown({ selectOnKeydown: false, action: function (text, value, $el) { // used to ensure focusing the dropdown and clicking via keyboard // works as expected - let button = self.$refs[$el.data('ref')] + const button = self.$refs[$el.data('ref')] if (Array.isArray(button)) { button[0].click() } else { button.click() } jQuery(self.$el).find('.ui.dropdown').dropdown('hide') - }, + } }) jQuery(this.$el).find('.ui.dropdown').dropdown('show', function () { // little magic to ensure the menu is always visible in the viewport // By default, try to diplay it on the right if there is enough room - let menu = jQuery(self.$el).find('.ui.dropdown').find(".menu") - let viewportOffset = menu.get(0).getBoundingClientRect(); - let left = viewportOffset.left; - let viewportWidth = document.documentElement.clientWidth - let rightOverflow = viewportOffset.right - viewportWidth - let leftOverflow = -viewportOffset.left + const menu = jQuery(self.$el).find('.ui.dropdown').find('.menu') + const viewportOffset = menu.get(0).getBoundingClientRect() + const viewportWidth = document.documentElement.clientWidth + const rightOverflow = viewportOffset.right - viewportWidth + const leftOverflow = -viewportOffset.left let offset = 0 if (rightOverflow > 0) { offset = -rightOverflow - 5 - menu.css({cssText: `left: ${offset}px !important;`}); - } - else if (leftOverflow > 0) { - offset = leftOverflow + 5 - menu.css({cssText: `right: -${offset}px !important;`}); + menu.css({ cssText: `left: ${offset}px !important;` }) + } else if (leftOverflow > 0) { + offset = leftOverflow + 5 + menu.css({ cssText: `right: -${offset}px !important;` }) } }) }) diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue index fce05c8d7b6f9594a1bc0c9e7d15d58556eb1663..d4a9d658f39fc40857ff73c94a7b1ac7dfa405ef 100644 --- a/front/src/components/audio/Player.vue +++ b/front/src/components/audio/Player.vue @@ -1,64 +1,144 @@ <template> - <section role="complementary" v-if="currentTrack" class="player-wrapper ui bottom-player component-player" aria-labelledby="player-label"> - <h1 id="player-label" class="visually-hidden"> - <translate translate-context="*/*/*">Audio player and controls</translate> + <section + v-if="currentTrack" + role="complementary" + class="player-wrapper ui bottom-player component-player" + aria-labelledby="player-label" + > + <h1 + id="player-label" + class="visually-hidden" + > + <translate translate-context="*/*/*"> + Audio player and controls + </translate> </h1> - <div class="ui inverted segment fixed-controls" @click.prevent.stop="toggleMobilePlayer"> + <div + class="ui inverted segment fixed-controls" + @click.prevent.stop="toggleMobilePlayer" + > <div - :class="['ui', 'top attached', 'small', 'inverted', {'indicating': isLoadingAudio}, 'progress']"> - <div class="buffer bar" :data-percent="bufferProgress" :style="{ 'width': bufferProgress + '%' }"></div> - <div class="position bar" :data-percent="progress" :style="{ 'width': progress + '%' }"></div> + :class="['ui', 'top attached', 'small', 'inverted', {'indicating': isLoadingAudio}, 'progress']" + > + <div + class="buffer bar" + :data-percent="bufferProgress" + :style="{ 'width': bufferProgress + '%' }" + /> + <div + class="position bar" + :data-percent="progress" + :style="{ 'width': progress + '%' }" + /> </div> <div class="controls-row"> - <div class="controls track-controls queue-not-focused desktop-and-up"> - <div class="ui tiny image" @click.stop.prevent="$router.push({name: 'library.tracks.detail', params: {id: currentTrack.id }})"> - <img alt="" ref="cover" v-if="currentTrack.cover && currentTrack.cover.urls.original" :src="$store.getters['instance/absoluteUrl'](currentTrack.cover.urls.medium_square_crop)"> - <img alt="" ref="cover" v-else-if="currentTrack.album && currentTrack.album.cover && currentTrack.album.cover.urls && currentTrack.album.cover.urls.original" :src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.urls.medium_square_crop)"> - <img alt="" v-else src="../../assets/audio/default-cover.png"> + <div + class="ui tiny image" + @click.stop.prevent="$router.push({name: 'library.tracks.detail', params: {id: currentTrack.id }})" + > + <img + v-if="currentTrack.cover && currentTrack.cover.urls.original" + ref="cover" + alt="" + :src="$store.getters['instance/absoluteUrl'](currentTrack.cover.urls.medium_square_crop)" + > + <img + v-else-if="currentTrack.album && currentTrack.album.cover && currentTrack.album.cover.urls && currentTrack.album.cover.urls.original" + ref="cover" + alt="" + :src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.urls.medium_square_crop)" + > + <img + v-else + alt="" + src="../../assets/audio/default-cover.png" + > </div> - <div @click.stop.prevent="" class="middle aligned content ellipsis"> + <div + class="middle aligned content ellipsis" + @click.stop.prevent="" + > <strong> - <router-link @click.stop.prevent="" class="small header discrete link track" :to="{name: 'library.tracks.detail', params: {id: currentTrack.id }}"> + <router-link + class="small header discrete link track" + :to="{name: 'library.tracks.detail', params: {id: currentTrack.id }}" + @click.stop.prevent="" + > {{ currentTrack.title }} </router-link> </strong> <div class="meta"> - <router-link @click.stop.prevent="" class="discrete link" :to="{name: 'library.artists.detail', params: {id: currentTrack.artist.id }}">{{ currentTrack.artist.name }}</router-link> - <template v-if="currentTrack.album"> / - <router-link @click.stop.prevent="" class="discrete link" :to="{name: 'library.albums.detail', params: {id: currentTrack.album.id }}">{{ currentTrack.album.title }}</router-link> + <router-link + class="discrete link" + :to="{name: 'library.artists.detail', params: {id: currentTrack.artist.id }}" + @click.stop.prevent="" + > + {{ currentTrack.artist.name }} + </router-link> + <template v-if="currentTrack.album"> + / + <router-link + class="discrete link" + :to="{name: 'library.albums.detail', params: {id: currentTrack.album.id }}" + @click.stop.prevent="" + > + {{ currentTrack.album.title }} + </router-link> </template> </div> </div> </div> <div class="controls track-controls queue-not-focused tablet-and-below"> <div class="ui tiny image"> - <img alt="" ref="cover" v-if="currentTrack.cover && currentTrack.cover.urls.original" :src="$store.getters['instance/absoluteUrl'](currentTrack.cover.urls.medium_square_crop)"> - <img alt="" ref="cover" v-else-if="currentTrack.album && currentTrack.album.cover && currentTrack.album.cover.urls.original" :src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.urls.medium_square_crop)"> - <img alt="" v-else src="../../assets/audio/default-cover.png"> + <img + v-if="currentTrack.cover && currentTrack.cover.urls.original" + ref="cover" + alt="" + :src="$store.getters['instance/absoluteUrl'](currentTrack.cover.urls.medium_square_crop)" + > + <img + v-else-if="currentTrack.album && currentTrack.album.cover && currentTrack.album.cover.urls.original" + ref="cover" + alt="" + :src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.urls.medium_square_crop)" + > + <img + v-else + alt="" + src="../../assets/audio/default-cover.png" + > </div> <div class="middle aligned content ellipsis"> <strong> {{ currentTrack.title }} </strong> <div class="meta"> - {{ currentTrack.artist.name }}<template v-if="currentTrack.album"> / {{ currentTrack.album.title }}</template> + {{ currentTrack.artist.name }}<template v-if="currentTrack.album"> + / {{ currentTrack.album.title }} + </template> </div> </div> </div> - <div class="controls desktop-and-up fluid align-right" v-if="$store.state.auth.authenticated"> + <div + v-if="$store.state.auth.authenticated" + class="controls desktop-and-up fluid align-right" + > <track-favorite-icon class="control white" - :track="currentTrack"></track-favorite-icon> + :track="currentTrack" + /> <track-playlist-icon class="control white" - :track="currentTrack"></track-playlist-icon> + :track="currentTrack" + /> <button - @click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})" :class="['ui', 'really', 'basic', 'circular', 'icon', 'button', 'control']" :aria-label="labels.addArtistContentFilter" - :title="labels.addArtistContentFilter"> - <i :class="['eye slash outline', 'basic', 'icon']"></i> + :title="labels.addArtistContentFilter" + @click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})" + > + <i :class="['eye slash outline', 'basic', 'icon']" /> </button> </div> <div class="player-controls controls queue-not-focused"> @@ -66,41 +146,48 @@ :title="labels.previous" :aria-label="labels.previous" class="circular button control tablet-and-up" + :disabled="!hasPrevious" @click.prevent.stop="$store.dispatch('queue/previous')" - :disabled="!hasPrevious"> - <i :class="['ui', 'large', {'disabled': !hasPrevious}, 'backward step', 'icon']" ></i> + > + <i :class="['ui', 'large', {'disabled': !hasPrevious}, 'backward step', 'icon']" /> </button> <button v-if="!playing" :title="labels.play" :aria-label="labels.play" + class="circular button control" @click.prevent.stop="resumePlayback" - class="circular button control"> - <i :class="['ui', 'big', 'play', {'disabled': !currentTrack}, 'icon']"></i> + > + <i :class="['ui', 'big', 'play', {'disabled': !currentTrack}, 'icon']" /> </button> <button v-else :title="labels.pause" :aria-label="labels.pause" + class="circular button control" @click.prevent.stop="pausePlayback" - class="circular button control"> - <i :class="['ui', 'big', 'pause', {'disabled': !currentTrack}, 'icon']"></i> + > + <i :class="['ui', 'big', 'pause', {'disabled': !currentTrack}, 'icon']" /> </button> <button :title="labels.next" :aria-label="labels.next" class="circular button control" + :disabled="!hasNext" @click.prevent.stop="$store.dispatch('queue/next')" - :disabled="!hasNext"> - <i :class="['ui', 'large', {'disabled': !hasNext}, 'forward step', 'icon']" ></i> + > + <i :class="['ui', 'large', {'disabled': !hasNext}, 'forward step', 'icon']" /> </button> </div> <div class="controls progress-controls queue-not-focused tablet-and-up small align-left"> <div class="timer"> <template v-if="!isLoadingAudio"> - <span class="start" @click.stop.prevent="setCurrentTime(0)">{{currentTimeFormatted}}</span> - | <span class="total">{{durationFormatted}}</span> + <span + class="start" + @click.stop.prevent="setCurrentTime(0)" + >{{ currentTimeFormatted }}</span> + | <span class="total">{{ durationFormatted }}</span> </template> <template v-else> 00:00 | 00:00 @@ -111,35 +198,40 @@ <div class="group"> <volume-control class="expandable" /> <button - class="circular control button" v-if="looping === 0" + class="circular control button" :title="labels.loopingDisabled" :aria-label="labels.loopingDisabled" + :disabled="!currentTrack" @click.prevent.stop="$store.commit('player/looping', 1)" - :disabled="!currentTrack"> - <i :class="['ui', {'disabled': !currentTrack}, 'step', 'repeat', 'icon']"></i> + > + <i :class="['ui', {'disabled': !currentTrack}, 'step', 'repeat', 'icon']" /> </button> <button + v-if="looping === 1" class="looping circular control button" - @click.prevent.stop="$store.commit('player/looping', 2)" :title="labels.loopingSingle" :aria-label="labels.loopingSingle" - v-if="looping === 1" - :disabled="!currentTrack"> + :disabled="!currentTrack" + @click.prevent.stop="$store.commit('player/looping', 2)" + > <i - class="repeat icon"> + class="repeat icon" + > <span class="ui circular tiny vibrant label">1</span> </i> </button> <button + v-if="looping === 2" class="looping circular control button" :title="labels.loopingWhole" :aria-label="labels.loopingWhole" - v-if="looping === 2" :disabled="!currentTrack" - @click.prevent.stop="$store.commit('player/looping', 0)"> + @click.prevent.stop="$store.commit('player/looping', 0)" + > <i - class="repeat icon"> + class="repeat icon" + > <span class="ui circular tiny vibrant label">∞</span> </i> </button> @@ -148,55 +240,80 @@ :disabled="queue.tracks.length === 0" :title="labels.shuffle" :aria-label="labels.shuffle" - @click.prevent.stop="shuffle()"> - <div v-if="isShuffling" class="ui inline shuffling inverted tiny active loader"></div> - <i v-else :class="['ui', 'random', {'disabled': queue.tracks.length === 0}, 'icon']" ></i> + @click.prevent.stop="shuffle()" + > + <div + v-if="isShuffling" + class="ui inline shuffling inverted tiny active loader" + /> + <i + v-else + :class="['ui', 'random', {'disabled': queue.tracks.length === 0}, 'icon']" + /> </button> </div> <div class="group"> <div class="fake-dropdown"> - <button class="position circular control button desktop-and-up" @click.stop="toggleMobilePlayer" aria-expanded="true"> - <i class="stream icon"></i> - <translate translate-context="Sidebar/Queue/Text" :translate-params="{index: queue.currentIndex + 1, length: queue.tracks.length}"> + <button + class="position circular control button desktop-and-up" + aria-expanded="true" + @click.stop="toggleMobilePlayer" + > + <i class="stream icon" /> + <translate + translate-context="Sidebar/Queue/Text" + :translate-params="{index: queue.currentIndex + 1, length: queue.tracks.length}" + > %{ index } of %{ length } </translate> </button> - <button class="position circular control button tablet-and-below" @click.stop="switchTab"> - <i class="stream icon"></i> - <translate translate-context="Sidebar/Queue/Text" :translate-params="{index: queue.currentIndex + 1, length: queue.tracks.length}"> + <button + class="position circular control button tablet-and-below" + @click.stop="switchTab" + > + <i class="stream icon" /> + <translate + translate-context="Sidebar/Queue/Text" + :translate-params="{index: queue.currentIndex + 1, length: queue.tracks.length}" + > %{ index } of %{ length } </translate> </button> <button - class="circular control button close-control desktop-and-up" v-if="$store.state.ui.queueFocused" - @click.stop="toggleMobilePlayer"> - <i class="large down angle icon"></i> + class="circular control button close-control desktop-and-up" + @click.stop="toggleMobilePlayer" + > + <i class="large down angle icon" /> </button> <button - class="circular control button desktop-and-up" v-else - @click.stop="toggleMobilePlayer"> - <i class="large up angle icon"></i> + class="circular control button desktop-and-up" + @click.stop="toggleMobilePlayer" + > + <i class="large up angle icon" /> </button> <button - class="circular control button close-control tablet-and-below" v-if="$store.state.ui.queueFocused === 'player'" - @click.stop="switchTab"> - <i class="large up angle icon"></i> + class="circular control button close-control tablet-and-below" + @click.stop="switchTab" + > + <i class="large up angle icon" /> </button> <button - class="circular control button tablet-and-below" v-if="$store.state.ui.queueFocused === 'queue'" - @click.stop="switchTab"> - <i class="large down angle icon"></i> + class="circular control button tablet-and-below" + @click.stop="switchTab" + > + <i class="large down angle icon" /> </button> </div> <button class="circular control button close-control tablet-and-below" - @click.stop="$store.commit('ui/queueFocused', null)"> - <i class="x icon"></i> + @click.stop="$store.commit('ui/queueFocused', null)" + > + <i class="x icon" /> </button> </div> </div> @@ -219,7 +336,7 @@ @keydown.f.exact="$store.dispatch('favorites/toggle', currentTrack.id)" @keydown.q.exact="clean" @keydown.e.exact="toggleMobilePlayer" - /> + /> </section> </template> @@ -259,6 +376,124 @@ export default { nextTrackPreloaded: false } }, + computed: { + ...mapState({ + currentIndex: state => state.queue.currentIndex, + playing: state => state.player.playing, + isLoadingAudio: state => state.player.isLoadingAudio, + volume: state => state.player.volume, + looping: state => state.player.looping, + duration: state => state.player.duration, + bufferProgress: state => state.player.bufferProgress, + errored: state => state.player.errored, + currentTime: state => state.player.currentTime, + queue: state => state.queue + }), + ...mapGetters({ + currentTrack: 'queue/currentTrack', + hasNext: 'queue/hasNext', + hasPrevious: 'queue/hasPrevious', + emptyQueue: 'queue/isEmpty', + durationFormatted: 'player/durationFormatted', + currentTimeFormatted: 'player/currentTimeFormatted', + progress: 'player/progress' + }), + updateProgressThrottled () { + return _.throttle(this.updateProgress, 50) + }, + labels () { + const audioPlayer = this.$pgettext('Sidebar/Player/Hidden text', 'Media player') + const previous = this.$pgettext('Sidebar/Player/Icon.Tooltip', 'Previous track') + const play = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Play') + const pause = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Pause') + const next = this.$pgettext('Sidebar/Player/Icon.Tooltip', 'Next track') + const unmute = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Unmute') + const mute = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Mute') + const expandQueue = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Expand queue') + const loopingDisabled = this.$pgettext('Sidebar/Player/Icon.Tooltip', + 'Looping disabled. Click to switch to single-track looping.' + ) + const loopingSingle = this.$pgettext('Sidebar/Player/Icon.Tooltip', + 'Looping on a single track. Click to switch to whole queue looping.' + ) + const loopingWhole = this.$pgettext('Sidebar/Player/Icon.Tooltip', + 'Looping on whole queue. Click to disable looping.' + ) + const shuffle = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Shuffle your queue') + const clear = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Clear your queue') + const addArtistContentFilter = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Hide content from this artist…') + return { + audioPlayer, + previous, + play, + pause, + next, + unmute, + mute, + loopingDisabled, + loopingSingle, + loopingWhole, + shuffle, + clear, + expandQueue, + addArtistContentFilter + } + } + }, + watch: { + currentTrack: { + async handler (newValue, oldValue) { + if (newValue === oldValue) { + return + } + this.nextTrackPreloaded = false + clearTimeout(this.playTimeout) + if (this.currentSound) { + this.currentSound.pause() + } + this.$store.commit('player/isLoadingAudio', true) + this.playTimeout = setTimeout(async () => { + await this.loadSound(newValue, oldValue) + }, 100) + this.updateMetadata() + }, + immediate: false + }, + volume: { + immediate: true, + handler (newValue) { + this.sliderVolume = newValue + Howler.volume(toLinearVolumeScale(newValue)) + } + }, + sliderVolume (newValue) { + this.$store.commit('player/volume', newValue) + }, + playing: async function (newValue) { + if (this.currentSound) { + if (newValue === true) { + this.soundId = this.currentSound.play(this.soundId) + } else { + this.currentSound.pause(this.soundId) + } + } else { + await this.loadSound(this.currentTrack, null) + } + + this.observeProgress(newValue) + }, + currentTime (newValue) { + if (!this.isUpdatingTime) { + this.setCurrentTime(newValue) + } + this.isUpdatingTime = false + }, + emptyQueue (newValue) { + if (newValue) { + Howler.unload() + } + } + }, mounted () { this.$store.dispatch('player/updateProgress', 0) this.$store.commit('player/playing', false) @@ -661,124 +896,6 @@ export default { navigator.mediaSession.metadata = new window.MediaMetadata(metadata) } } - }, - computed: { - ...mapState({ - currentIndex: state => state.queue.currentIndex, - playing: state => state.player.playing, - isLoadingAudio: state => state.player.isLoadingAudio, - volume: state => state.player.volume, - looping: state => state.player.looping, - duration: state => state.player.duration, - bufferProgress: state => state.player.bufferProgress, - errored: state => state.player.errored, - currentTime: state => state.player.currentTime, - queue: state => state.queue - }), - ...mapGetters({ - currentTrack: 'queue/currentTrack', - hasNext: 'queue/hasNext', - hasPrevious: 'queue/hasPrevious', - emptyQueue: 'queue/isEmpty', - durationFormatted: 'player/durationFormatted', - currentTimeFormatted: 'player/currentTimeFormatted', - progress: 'player/progress' - }), - updateProgressThrottled () { - return _.throttle(this.updateProgress, 50) - }, - labels () { - const audioPlayer = this.$pgettext('Sidebar/Player/Hidden text', 'Media player') - const previous = this.$pgettext('Sidebar/Player/Icon.Tooltip', 'Previous track') - const play = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Play') - const pause = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Pause') - const next = this.$pgettext('Sidebar/Player/Icon.Tooltip', 'Next track') - const unmute = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Unmute') - const mute = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Mute') - const expandQueue = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Expand queue') - const loopingDisabled = this.$pgettext('Sidebar/Player/Icon.Tooltip', - 'Looping disabled. Click to switch to single-track looping.' - ) - const loopingSingle = this.$pgettext('Sidebar/Player/Icon.Tooltip', - 'Looping on a single track. Click to switch to whole queue looping.' - ) - const loopingWhole = this.$pgettext('Sidebar/Player/Icon.Tooltip', - 'Looping on whole queue. Click to disable looping.' - ) - const shuffle = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Shuffle your queue') - const clear = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Clear your queue') - const addArtistContentFilter = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Hide content from this artist…') - return { - audioPlayer, - previous, - play, - pause, - next, - unmute, - mute, - loopingDisabled, - loopingSingle, - loopingWhole, - shuffle, - clear, - expandQueue, - addArtistContentFilter - } - } - }, - watch: { - currentTrack: { - async handler (newValue, oldValue) { - if (newValue === oldValue) { - return - } - this.nextTrackPreloaded = false - clearTimeout(this.playTimeout) - if (this.currentSound) { - this.currentSound.pause() - } - this.$store.commit('player/isLoadingAudio', true) - this.playTimeout = setTimeout(async () => { - await this.loadSound(newValue, oldValue) - }, 100) - this.updateMetadata() - }, - immediate: false - }, - volume: { - immediate: true, - handler (newValue) { - this.sliderVolume = newValue - Howler.volume(toLinearVolumeScale(newValue)) - } - }, - sliderVolume (newValue) { - this.$store.commit('player/volume', newValue) - }, - playing: async function (newValue) { - if (this.currentSound) { - if (newValue === true) { - this.soundId = this.currentSound.play(this.soundId) - } else { - this.currentSound.pause(this.soundId) - } - } else { - await this.loadSound(this.currentTrack, null) - } - - this.observeProgress(newValue) - }, - currentTime (newValue) { - if (!this.isUpdatingTime) { - this.setCurrentTime(newValue) - } - this.isUpdatingTime = false - }, - emptyQueue (newValue) { - if (newValue) { - Howler.unload() - } - } } } </script> diff --git a/front/src/components/audio/Search.vue b/front/src/components/audio/Search.vue index 85960eb383d5b723ac7fbfb5fc65dbdcb4efe90b..a27018da5e47f73cc47988fe27d7fdf8b9a8b39f 100644 --- a/front/src/components/audio/Search.vue +++ b/front/src/components/audio/Search.vue @@ -1,29 +1,69 @@ <template> <div> - <h2><translate translate-context="Content/Search/Title">Search for some music</translate></h2> + <h2> + <translate translate-context="Content/Search/Title"> + Search for some music + </translate> + </h2> <div :class="['ui', {'loading': isLoading }, 'search']"> <div class="ui icon big input"> - <i class="search icon"></i> - <input ref="search" class="prompt" :placeholder="labels.searchPlaceholder" v-model.trim="query" type="text" /> + <i class="search icon" /> + <input + ref="search" + v-model.trim="query" + class="prompt" + :placeholder="labels.searchPlaceholder" + type="text" + > </div> </div> <template v-if="query.length > 0"> - <h3 class="ui title"><translate translate-context="*/*/*/Noun">Artists</translate></h3> + <h3 class="ui title"> + <translate translate-context="*/*/*/Noun"> + Artists + </translate> + </h3> <div v-if="results.artists.length > 0"> <div class="ui cards"> - <artist-card :key="artist.id" v-for="artist in results.artists" :artist="artist" ></artist-card> + <artist-card + v-for="artist in results.artists" + :key="artist.id" + :artist="artist" + /> </div> </div> - <p v-else><translate translate-context="Content/Search/Paragraph">No artist matched your query</translate></p> + <p v-else> + <translate translate-context="Content/Search/Paragraph"> + No artist matched your query + </translate> + </p> </template> <template v-if="query.length > 0"> - <h3 class="ui title"><translate translate-context="*/*/*">Albums</translate></h3> - <div v-if="results.albums.length > 0" class="ui stackable three column grid"> - <div class="column" :key="album.id" v-for="album in results.albums"> - <album-card class="fluid" :album="album" ></album-card> + <h3 class="ui title"> + <translate translate-context="*/*/*"> + Albums + </translate> + </h3> + <div + v-if="results.albums.length > 0" + class="ui stackable three column grid" + > + <div + v-for="album in results.albums" + :key="album.id" + class="column" + > + <album-card + class="fluid" + :album="album" + /> </div> </div> - <p v-else><translate translate-context="Content/Search/Paragraph">No album matched your query</translate></p> + <p v-else> + <translate translate-context="Content/Search/Paragraph"> + No album matched your query + </translate> + </p> </template> </div> </template> @@ -41,7 +81,7 @@ export default { ArtistCard }, props: { - autofocus: {type: Boolean, default: false} + autofocus: { type: Boolean, default: false } }, data () { return { @@ -53,12 +93,6 @@ export default { isLoading: false } }, - mounted () { - if (this.autofocus) { - this.$refs.search.focus() - } - this.search() - }, computed: { labels () { return { @@ -66,15 +100,26 @@ export default { } } }, + watch: { + query () { + this.search() + } + }, + mounted () { + if (this.autofocus) { + this.$refs.search.focus() + } + this.search() + }, methods: { search: _.debounce(function () { if (this.query.length < 1) { return } - var self = this + const self = this self.isLoading = true logger.default.debug('Searching track matching "' + this.query + '"') - let params = { + const params = { query: this.query } axios.get('search', { @@ -90,11 +135,6 @@ export default { artists: results.artists } } - }, - watch: { - query () { - this.search() - } } } </script> diff --git a/front/src/components/audio/SearchBar.vue b/front/src/components/audio/SearchBar.vue index d254ac645c1c919233ca428f2f21af6262ac46a2..5690aa742c516f613081394165c8e62cf053ae06 100644 --- a/front/src/components/audio/SearchBar.vue +++ b/front/src/components/audio/SearchBar.vue @@ -1,11 +1,19 @@ <template> <div class="ui fluid category search"> - <slot></slot><div class="ui icon input"> - <input :aria-label="labels.searchContent" ref="search" type="search" class="prompt" name="search" :placeholder="labels.placeholder" @keydown.esc="$event.target.blur()"> - <i class="search icon"></i> + <slot /><div class="ui icon input"> + <input + ref="search" + :aria-label="labels.searchContent" + type="search" + class="prompt" + name="search" + :placeholder="labels.placeholder" + @keydown.esc="$event.target.blur()" + > + <i class="search icon" /> </div> - <div class="results"></div> - <slot name="after"></slot> + <div class="results" /> + <slot name="after" /> <GlobalEvents @keydown.shift.f.prevent.exact="focusSearch" /> @@ -16,11 +24,11 @@ import jQuery from 'jquery' import router from '@/router' import lodash from '@/lodash' -import GlobalEvents from "@/components/utils/global-events" +import GlobalEvents from '@/components/utils/global-events' export default { components: { - GlobalEvents, + GlobalEvents }, computed: { labels () { @@ -31,22 +39,21 @@ export default { } }, mounted () { - let artistLabel = this.$pgettext('*/*/*/Noun', 'Artist') - let albumLabel = this.$pgettext('*/*/*', 'Album') - let trackLabel = this.$pgettext('*/*/*/Noun', 'Track') - let tagLabel = this.$pgettext('*/*/*/Noun', 'Tag') - let self = this - var searchQuery; + const artistLabel = this.$pgettext('*/*/*/Noun', 'Artist') + const albumLabel = this.$pgettext('*/*/*', 'Album') + const trackLabel = this.$pgettext('*/*/*/Noun', 'Track') + const tagLabel = this.$pgettext('*/*/*/Noun', 'Tag') + const self = this + let searchQuery - jQuery(this.$el).keypress(function(e) { - if(e.which == 13) { + jQuery(this.$el).keypress(function (e) { + if (e.which === 13) { // Cancel any API search request to backend… - jQuery(this.$el).search('cancel query'); + jQuery(this.$el).search('cancel query') // Go direct to the artist page… - router.push(`/search?q=${searchQuery}&type=artists`); - } - }); - + router.push(`/search?q=${searchQuery}&type=artists`) + } + }) jQuery(this.$el).search({ type: 'category', @@ -57,9 +64,9 @@ export default { noResults: this.$pgettext('Sidebar/Search/Error.Label', 'Sorry, there are no results for this search') }, onSelect (result, response) { - jQuery(self.$el).search("set value", searchQuery) + jQuery(self.$el).search('set value', searchQuery) router.push(result.routerUrl) - jQuery(self.$el).search("hide results") + jQuery(self.$el).search('hide results') return false }, onSearchQuery (query) { @@ -78,17 +85,17 @@ export default { return xhrObject }, onResponse: function (initialResponse) { - let objId = self.extractObjId(searchQuery) - let results = {} + const objId = self.extractObjId(searchQuery) + const results = {} let isEmptyResults = true - let categories = [ + const categories = [ { code: 'federation', - name: self.$pgettext('*/*/*', 'Federation'), + name: self.$pgettext('*/*/*', 'Federation') }, { code: 'podcasts', - name: self.$pgettext('*/*/*', 'Podcasts'), + name: self.$pgettext('*/*/*', 'Podcasts') }, { code: 'artists', @@ -148,12 +155,12 @@ export default { }, getId (t) { return t.name - }, + } }, { code: 'more', - name: '', - }, + name: '' + } ] categories.forEach(category => { results[category.code] = { @@ -161,29 +168,27 @@ export default { results: [] } if (category.code === 'federation') { - if (objId) { isEmptyResults = false - let searchMessage = self.$pgettext('Search/*/*', 'Search on the fediverse') - results['federation'] = { + const searchMessage = self.$pgettext('Search/*/*', 'Search on the fediverse') + results.federation = { name: self.$pgettext('*/*/*', 'Federation'), results: [{ title: searchMessage, routerUrl: { name: 'search', query: { - id: objId, + id: objId } } }] } } - } - else if (category.code === 'podcasts') { + } else if (category.code === 'podcasts') { if (objId) { isEmptyResults = false - let searchMessage = self.$pgettext('Search/*/*', 'Subscribe to podcast via RSS') - results['podcasts'] = { + const searchMessage = self.$pgettext('Search/*/*', 'Subscribe to podcast via RSS') + results.podcasts = { name: self.$pgettext('*/*/*', 'Podcasts'), results: [{ title: searchMessage, @@ -191,33 +196,31 @@ export default { name: 'search', query: { id: objId, - type: "rss" + type: 'rss' } } }] } } - } - else if (category.code === 'more') { - let searchMessage = self.$pgettext('Search/*/*', 'More results 🡒') - results['more'] = { + } else if (category.code === 'more') { + const searchMessage = self.$pgettext('Search/*/*', 'More results 🡒') + results.more = { name: '', results: [{ title: searchMessage, routerUrl: { name: 'search', query: { - type: "artists", + type: 'artists', q: searchQuery } } }] } - } - else { + } else { initialResponse[category.code].forEach(result => { isEmptyResults = false - let id = category.getId(result) + const id = category.getId(result) results[category.code].results.push({ title: category.getTitle(result), id, diff --git a/front/src/components/audio/VolumeControl.vue b/front/src/components/audio/VolumeControl.vue index 06110a4e2ed71cff57f21ffcac0b8cbf550dcad5..aa61c89b83b8c1968b4838d6fbd3752a68de5b69 100644 --- a/front/src/components/audio/VolumeControl.vue +++ b/front/src/components/audio/VolumeControl.vue @@ -1,74 +1,87 @@ <template> - <button class="circular control button" :class="['component-volume-control', {'expanded': expanded}]" @click.prevent.stop="" @mouseover="handleOver" @mouseleave="handleLeave"> + <button + class="circular control button" + :class="['component-volume-control', {'expanded': expanded}]" + @click.prevent.stop="" + @mouseover="handleOver" + @mouseleave="handleLeave" + > <span - role="button" v-if="sliderVolume === 0" + role="button" :title="labels.unmute" :aria-label="labels.unmute" - @click.prevent.stop="unmute"> - <i class="volume off icon"></i> + @click.prevent.stop="unmute" + > + <i class="volume off icon" /> </span> <span - role="button" v-else-if="sliderVolume < 0.5" + role="button" :title="labels.mute" :aria-label="labels.mute" - @click.prevent.stop="mute"> - <i class="volume down icon"></i> + @click.prevent.stop="mute" + > + <i class="volume down icon" /> </span> <span - role="button" v-else + role="button" :title="labels.mute" :aria-label="labels.mute" - @click.prevent.stop="mute"> - <i class="volume up icon"></i> + @click.prevent.stop="mute" + > + <i class="volume up icon" /> </span> <div class="popup"> - <label for="volume-slider" class="visually-hidden">{{ labels.slider }}</label> + <label + for="volume-slider" + class="visually-hidden" + >{{ labels.slider }}</label> <input id="volume-slider" + v-model="sliderVolume" type="range" step="any" min="0" - v-bind:max="volumeSteps" - v-model="sliderVolume" /> + :max="volumeSteps" + > </div> </button> </template> <script> -import { mapState, mapGetters, mapActions } from "vuex" +import mapActions from 'vuex' export default { data () { return { expanded: false, timeout: null, - volumeSteps: 100, + volumeSteps: 100 } }, computed: { sliderVolume: { get () { - return this.$store.state.player.volume * this.volumeSteps; + return this.$store.state.player.volume * this.volumeSteps }, set (v) { - this.$store.commit("player/volume", v / this.volumeSteps) + this.$store.commit('player/volume', v / this.volumeSteps) } }, labels () { return { - unmute: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Unmute"), - mute: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Mute"), - slider: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Adjust volume") + unmute: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Unmute'), + mute: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Mute'), + slider: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Adjust volume') } } }, methods: { ...mapActions({ - mute: "player/mute", - unmute: "player/unmute", - toggleMute: "player/toggleMute", + mute: 'player/mute', + unmute: 'player/unmute', + toggleMute: 'player/toggleMute' }), handleOver () { if (this.timeout) { @@ -80,7 +93,7 @@ export default { if (this.timeout) { clearTimeout(this.timeout) } - this.timeout = setTimeout(() => {this.expanded = false}, 500) + this.timeout = setTimeout(() => { this.expanded = false }, 500) } } } diff --git a/front/src/components/audio/album/Card.vue b/front/src/components/audio/album/Card.vue index cba58c9dd0aed8f12d969b47c0d5cf078d40d190..7f5afac6352df7dab3d5f018d6e98e82cda43f25 100644 --- a/front/src/components/audio/album/Card.vue +++ b/front/src/components/audio/album/Card.vue @@ -1,19 +1,32 @@ <template> <div class="card app-card component-album-card"> <div + v-lazy:background-image="imageUrl" + :class="['ui', 'head-image', 'image', {'default-cover': !album.cover || !album.cover.urls.original}]" @click="$router.push({name: 'library.albums.detail', params: {id: album.id}})" - :class="['ui', 'head-image', 'image', {'default-cover': !album.cover || !album.cover.urls.original}]" v-lazy:background-image="imageUrl"> - <play-button :icon-only="true" :is-playable="album.is_playable" :button-classes="['ui', 'circular', 'large', 'vibrant', 'icon', 'button']" :album="album"></play-button> + > + <play-button + :icon-only="true" + :is-playable="album.is_playable" + :button-classes="['ui', 'circular', 'large', 'vibrant', 'icon', 'button']" + :album="album" + /> </div> <div class="content"> <strong> - <router-link class="discrete link" :to="{name: 'library.albums.detail', params: {id: album.id}}"> + <router-link + class="discrete link" + :to="{name: 'library.albums.detail', params: {id: album.id}}" + > {{ album.title }} </router-link> </strong> <div class="description"> <span> - <router-link class="discrete link" :to="{name: 'library.artists.detail', params: {id: album.artist.id}}"> + <router-link + class="discrete link" + :to="{name: 'library.artists.detail', params: {id: album.artist.id}}" + > {{ album.artist.name }} </router-link> </span> @@ -21,8 +34,21 @@ </div> <div class="extra content"> <span v-if="album.release_date">{{ album.release_date | moment('Y') }} · </span> - <translate translate-context="*/*/*" :translate-params="{count: album.tracks_count}" :translate-n="album.tracks_count" translate-plural="%{ count } tracks">%{ count } track</translate> - <play-button class="right floated basic icon" :dropdown-only="true" :is-playable="album.is_playable" :dropdown-icon-classes="['ellipsis', 'horizontal', 'large really discrete']" :album="album"></play-button> + <translate + translate-context="*/*/*" + :translate-params="{count: album.tracks_count}" + :translate-n="album.tracks_count" + translate-plural="%{ count } tracks" + > + %{ count } track + </translate> + <play-button + class="right floated basic icon" + :dropdown-only="true" + :is-playable="album.is_playable" + :dropdown-icon-classes="['ellipsis', 'horizontal', 'large really discrete']" + :album="album" + /> </div> </div> </template> @@ -31,17 +57,18 @@ import PlayButton from '@/components/audio/PlayButton' export default { - props: { - album: {type: Object}, - }, components: { PlayButton }, + props: { + album: { type: Object, required: true } + }, computed: { imageUrl () { if (this.album.cover && this.album.cover.urls.original) { return this.$store.getters['instance/absoluteUrl'](this.album.cover.urls.medium_square_crop) } + return null } } } diff --git a/front/src/components/audio/album/Widget.vue b/front/src/components/audio/album/Widget.vue index 6323ab87b40f9841e525b6bdbb4d48d64b0ca39c..44a59473c7c6fd75465f5c496d50cacda96c1b48 100644 --- a/front/src/components/audio/album/Widget.vue +++ b/front/src/components/audio/album/Widget.vue @@ -1,25 +1,54 @@ <template> <div class="wrapper"> - <h3 v-if="!!this.$slots.title" class="ui header"> - <slot name="title"></slot> - <span v-if="showCount" class="ui tiny circular label">{{ count }}</span> + <h3 + v-if="!!$slots.title" + class="ui header" + > + <slot name="title" /> + <span + v-if="showCount" + class="ui tiny circular label" + >{{ count }}</span> </h3> - <slot></slot> - <inline-search-bar v-model="query" v-if="search" @search="albums = []; fetchData()"></inline-search-bar> - <div class="ui hidden divider"></div> + <slot /> + <inline-search-bar + v-if="search" + v-model="query" + @search="albums = []; fetchData()" + /> + <div class="ui hidden divider" /> <div class="ui app-cards cards"> - <div v-if="isLoading" class="ui inverted active dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui inverted active dimmer" + > + <div class="ui loader" /> </div> - <album-card v-for="album in albums" :album="album" :key="album.id" /> + <album-card + v-for="album in albums" + :key="album.id" + :album="album" + /> </div> - <slot v-if="!isLoading && albums.length === 0" name="empty-state"> - <empty-state @refresh="fetchData" :refresh="true"></empty-state> + <slot + v-if="!isLoading && albums.length === 0" + name="empty-state" + > + <empty-state + :refresh="true" + @refresh="fetchData" + /> </slot> <template v-if="nextPage"> - <div class="ui hidden divider"></div> - <button v-if="nextPage" @click="fetchData(nextPage)" :class="['ui', 'basic', 'button']"> - <translate translate-context="*/*/Button,Label">Show more</translate> + <div class="ui hidden divider" /> + <button + v-if="nextPage" + :class="['ui', 'basic', 'button']" + @click="fetchData(nextPage)" + > + <translate translate-context="*/*/Button,Label"> + Show more + </translate> </button> </template> </div> @@ -30,16 +59,16 @@ import axios from 'axios' import AlbumCard from '@/components/audio/album/Card' export default { - props: { - filters: {type: Object, required: true}, - controls: {type: Boolean, default: true}, - showCount: {type: Boolean, default: false}, - search: {type: Boolean, default: false}, - limit: {type: Number, default: 12}, - }, components: { AlbumCard }, + props: { + filters: { type: Object, required: true }, + controls: { type: Boolean, default: true }, + showCount: { type: Boolean, default: false }, + search: { type: Boolean, default: false }, + limit: { type: Number, default: 12 } + }, data () { return { albums: [], @@ -48,7 +77,15 @@ export default { errors: null, previousPage: null, nextPage: null, - query: '', + query: '' + } + }, + watch: { + offset () { + this.fetchData() + }, + '$store.state.moderation.lastUpdate': function () { + this.fetchData() } }, created () { @@ -58,11 +95,11 @@ export default { fetchData (url) { url = url || 'albums/' this.isLoading = true - let self = this - let params = {q: this.query, ...this.filters} + const self = this + const params = { q: this.query, ...this.filters } params.page_size = this.limit params.offset = this.offset - axios.get(url, {params: params}).then((response) => { + axios.get(url, { params: params }).then((response) => { self.previousPage = response.data.previous self.nextPage = response.data.next self.isLoading = false @@ -79,14 +116,6 @@ export default { } else { this.offset = Math.max(this.offset - this.limit, 0) } - }, - }, - watch: { - offset () { - this.fetchData() - }, - "$store.state.moderation.lastUpdate": function () { - this.fetchData() } } } diff --git a/front/src/components/audio/artist/Card.vue b/front/src/components/audio/artist/Card.vue index f51c48bd6a007d58d01c827a5b24601dc52969fd..c02d825f76b8ea72a15e149edcdbcd86ef024aea 100644 --- a/front/src/components/audio/artist/Card.vue +++ b/front/src/components/audio/artist/Card.vue @@ -1,49 +1,88 @@ <template> <div class="app-card card"> <div + v-lazy:background-image="imageUrl" + :class="['ui', 'head-image', 'circular', 'image', {'default-cover': !cover || !cover.urls.original}]" @click="$router.push({name: 'library.artists.detail', params: {id: artist.id}})" - :class="['ui', 'head-image', 'circular', 'image', {'default-cover': !cover || !cover.urls.original}]" v-lazy:background-image="imageUrl"> - <play-button :icon-only="true" :is-playable="artist.is_playable" :button-classes="['ui', 'circular', 'large', 'vibrant', 'icon', 'button']" :artist="artist"></play-button> + > + <play-button + :icon-only="true" + :is-playable="artist.is_playable" + :button-classes="['ui', 'circular', 'large', 'vibrant', 'icon', 'button']" + :artist="artist" + /> </div> <div class="content"> <strong> - <router-link class="discrete link" :to="{name: 'library.artists.detail', params: {id: artist.id}}"> + <router-link + class="discrete link" + :to="{name: 'library.artists.detail', params: {id: artist.id}}" + > {{ artist.name|truncate(30) }} </router-link> </strong> - <tags-list label-classes="tiny" :truncate-size="20" :limit="2" :show-more="false" :tags="artist.tags"></tags-list> + <tags-list + label-classes="tiny" + :truncate-size="20" + :limit="2" + :show-more="false" + :tags="artist.tags" + /> </div> <div class="extra content"> - <translate v-if="artist.content_category === 'music'" translate-context="*/*/*" :translate-params="{count: artist.tracks_count}" :translate-n="artist.tracks_count" translate-plural="%{ count } tracks">%{ count } track</translate> - <translate v-else translate-context="*/*/*" :translate-params="{count: artist.tracks_count}" :translate-n="artist.tracks_count" translate-plural="%{ count } episodes">%{ count } episode</translate> - <play-button class="right floated basic icon" :dropdown-only="true" :is-playable="artist.is_playable" :dropdown-icon-classes="['ellipsis', 'horizontal', 'large really discrete']" :artist="artist"></play-button> + <translate + v-if="artist.content_category === 'music'" + translate-context="*/*/*" + :translate-params="{count: artist.tracks_count}" + :translate-n="artist.tracks_count" + translate-plural="%{ count } tracks" + > + %{ count } track + </translate> + <translate + v-else + translate-context="*/*/*" + :translate-params="{count: artist.tracks_count}" + :translate-n="artist.tracks_count" + translate-plural="%{ count } episodes" + > + %{ count } episode + </translate> + <play-button + class="right floated basic icon" + :dropdown-only="true" + :is-playable="artist.is_playable" + :dropdown-icon-classes="['ellipsis', 'horizontal', 'large really discrete']" + :artist="artist" + /> </div> </div> </template> <script> import PlayButton from '@/components/audio/PlayButton' -import TagsList from "@/components/tags/List" +import TagsList from '@/components/tags/List' export default { - props: ['artist'], components: { PlayButton, TagsList }, + props: { artist: { type: Object, required: true } }, data () { return { initialAlbums: 30, - showAllAlbums: true, + showAllAlbums: true } }, computed: { imageUrl () { - let cover = this.cover + const cover = this.cover if (cover && cover.urls.original) { return this.$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop) } + return null }, cover () { if (this.artist.cover && this.artist.cover.urls.original) { @@ -54,7 +93,7 @@ export default { }).filter((c) => { return c && c.urls.original })[0] - }, + } } } </script> diff --git a/front/src/components/audio/artist/Widget.vue b/front/src/components/audio/artist/Widget.vue index 5ea2e160798325659c4b740a8f4b8d47db28fb58..9cf3ae0cc582b9843a6b7dccd17ca5099ca1b5e8 100644 --- a/front/src/components/audio/artist/Widget.vue +++ b/front/src/components/audio/artist/Widget.vue @@ -1,24 +1,50 @@ <template> <div class="wrapper"> - <h3 v-if="header" class="ui header"> - <slot name="title"></slot> + <h3 + v-if="header" + class="ui header" + > + <slot name="title" /> <span class="ui tiny circular label">{{ count }}</span> </h3> - <inline-search-bar v-model="query" v-if="search" @search="objects = []; fetchData()"></inline-search-bar> - <div class="ui hidden divider"></div> + <inline-search-bar + v-if="search" + v-model="query" + @search="objects = []; fetchData()" + /> + <div class="ui hidden divider" /> <div class="ui five app-cards cards"> - <div v-if="isLoading" class="ui inverted active dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui inverted active dimmer" + > + <div class="ui loader" /> </div> - <artist-card :artist="artist" v-for="artist in objects" :key="artist.id"></artist-card> + <artist-card + v-for="artist in objects" + :key="artist.id" + :artist="artist" + /> </div> - <slot v-if="!isLoading && objects.length === 0" name="empty-state"> - <empty-state @refresh="fetchData" :refresh="true"></empty-state> + <slot + v-if="!isLoading && objects.length === 0" + name="empty-state" + > + <empty-state + :refresh="true" + @refresh="fetchData" + /> </slot> <template v-if="nextPage"> - <div class="ui hidden divider"></div> - <button v-if="nextPage" @click="fetchData(nextPage)" :class="['ui', 'basic', 'button']"> - <translate translate-context="*/*/Button,Label">Show more</translate> + <div class="ui hidden divider" /> + <button + v-if="nextPage" + :class="['ui', 'basic', 'button']" + @click="fetchData(nextPage)" + > + <translate translate-context="*/*/Button,Label"> + Show more + </translate> </button> </template> </div> @@ -26,17 +52,17 @@ <script> import axios from 'axios' -import ArtistCard from "@/components/audio/artist/Card" +import ArtistCard from '@/components/audio/artist/Card' export default { - props: { - filters: {type: Object, required: true}, - controls: {type: Boolean, default: true}, - header: {type: Boolean, default: true}, - search: {type: Boolean, default: false}, - }, components: { - ArtistCard, + ArtistCard + }, + props: { + filters: { type: Object, required: true }, + controls: { type: Boolean, default: true }, + header: { type: Boolean, default: true }, + search: { type: Boolean, default: false } }, data () { return { @@ -47,7 +73,15 @@ export default { errors: null, previousPage: null, nextPage: null, - query: '', + query: '' + } + }, + watch: { + offset () { + this.fetchData() + }, + '$store.state.moderation.lastUpdate': function () { + this.fetchData() } }, created () { @@ -57,11 +91,11 @@ export default { fetchData (url) { url = url || 'artists/' this.isLoading = true - let self = this - let params = {q: this.query, ...this.filters} + const self = this + const params = { q: this.query, ...this.filters } params.page_size = this.limit params.offset = this.offset - axios.get(url, {params: params}).then((response) => { + axios.get(url, { params: params }).then((response) => { self.previousPage = response.data.previous self.nextPage = response.data.next self.isLoading = false @@ -78,14 +112,6 @@ export default { } else { this.offset = Math.max(this.offset - this.limit, 0) } - }, - }, - watch: { - offset () { - this.fetchData() - }, - "$store.state.moderation.lastUpdate": function () { - this.fetchData() } } } diff --git a/front/src/components/audio/podcast/MobileRow.vue b/front/src/components/audio/podcast/MobileRow.vue index 9e7236e7af14addd4aab55106e70b185bad0a6e4..e4f0db9805617d22c403c68cba7e9d6d97ac2bdb 100644 --- a/front/src/components/audio/podcast/MobileRow.vue +++ b/front/src/components/audio/podcast/MobileRow.vue @@ -7,12 +7,10 @@ > <div v-if="showArt" - @click.prevent.exact="activateTrack(track, index)" class="image left floated column" + @click.prevent.exact="activateTrack(track, index)" > <img - alt="" - class="ui artist-track mini image" v-if=" track.album && track.album.cover && track.album.cover.urls.original " @@ -21,10 +19,10 @@ track.album.cover.urls.medium_square_crop ) " - /> - <img alt="" class="ui artist-track mini image" + > + <img v-else-if=" track.cover " @@ -33,10 +31,10 @@ track.cover.urls.medium_square_crop ) " - /> - <img alt="" class="ui artist-track mini image" + > + <img v-else-if=" track.artist.cover " @@ -45,19 +43,21 @@ track.artist.cover.urls.medium_square_crop ) " - /> - <img alt="" class="ui artist-track mini image" + > + <img v-else + alt="" + class="ui artist-track mini image" src="../../../assets/audio/default-cover.png" - /> + > </div> <div - tabindex=0 - @click="activateTrack(track, index)" + tabindex="0" role="button" class="content ellipsis left floated column" + @click="activateTrack(track, index)" > <p :class="[ @@ -68,24 +68,33 @@ > {{ track.title }} </p> - <p v-if="track.artist.content_category === 'podcast'" class="track-meta mobile"> - <human-date class="really discrete" :date="track.creation_date"></human-date> + <p + v-if="track.artist.content_category === 'podcast'" + class="track-meta mobile" + > + <human-date + class="really discrete" + :date="track.creation_date" + /> <span>·</span> <human-duration v-if="track.uploads[0] && track.uploads[0].duration" :duration="track.uploads[0].duration" - ></human-duration> + /> </p> - <p v-else class="track-meta mobile"> + <p + v-else + class="track-meta mobile" + > {{ track.artist.name }} <span>·</span> <human-duration v-if="track.uploads[0] && track.uploads[0].duration" :duration="track.uploads[0].duration" - ></human-duration> + /> </p> </div> <div - v-if="$store.state.auth.authenticated && this.track.artist.content_category !== 'podcast'" + v-if="$store.state.auth.authenticated && track.artist.content_category !== 'podcast'" :class="[ 'meta', 'right', @@ -100,12 +109,11 @@ class="tiny" :border="false" :track="track" - ></track-favorite-icon> + /> </div> <div role="button" :aria-label="actionsButtonLabel" - @click.prevent.exact="showTrackModal = !showTrackModal" :class="[ 'modal-button', 'right', @@ -114,36 +122,36 @@ 'mobile', { 'with-art': showArt }, ]" + @click.prevent.exact="showTrackModal = !showTrackModal" > <i class="ellipsis large vertical icon" /> </div> <track-modal - @update:show="showTrackModal = $event;" :show="showTrackModal" :track="track" :index="index" :is-artist="isArtist" :is-album="isAlbum" - ></track-modal> + @update:show="showTrackModal = $event;" + /> </div> </template> <script> -import PlayIndicator from "@/components/audio/track/PlayIndicator"; -import { mapActions, mapGetters } from "vuex"; -import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon"; -import TrackModal from "@/components/audio/track/Modal"; -import PlayOptionsMixin from "@/components/mixins/PlayOptions" +import { mapActions, mapGetters } from 'vuex' +import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon' +import TrackModal from '@/components/audio/track/Modal' +import PlayOptionsMixin from '@/components/mixins/PlayOptions' export default { - mixins: [PlayOptionsMixin], - data() { - return { - showTrackModal: false, - } + + components: { + TrackFavoriteIcon, + TrackModal }, + mixins: [PlayOptionsMixin], props: { - tracks: Array, + tracks: { type: Array, required: true }, showAlbum: { type: Boolean, required: false, default: true }, showArtist: { type: Boolean, required: false, default: true }, showPosition: { type: Boolean, required: false, default: false }, @@ -155,41 +163,40 @@ export default { showDuration: { type: Boolean, required: false, default: true }, index: { type: Number, required: true }, track: { type: Object, required: true }, - isArtist: {type: Boolean, required: false, default: false}, - isAlbum: {type: Boolean, required: false, default: false}, + isArtist: { type: Boolean, required: false, default: false }, + isAlbum: { type: Boolean, required: false, default: false } }, - - components: { - PlayIndicator, - TrackFavoriteIcon, - TrackModal, + data () { + return { + showTrackModal: false + } }, computed: { ...mapGetters({ - currentTrack: "queue/currentTrack", + currentTrack: 'queue/currentTrack' }), - isPlaying() { - return this.$store.state.player.playing; + isPlaying () { + return this.$store.state.player.playing }, actionsButtonLabel () { - return this.$pgettext('Content/Track/Icon.Tooltip/Verb', 'Show track actions') - }, + return this.$pgettext('Content/Track/Icon.Tooltip/Verb', 'Show track actions') + } }, methods: { - prettyPosition(position, size) { - var s = String(position); + prettyPosition (position, size) { + let s = String(position) while (s.length < (size || 2)) { - s = "0" + s; + s = '0' + s } - return s; + return s }, ...mapActions({ - resumePlayback: "player/resumePlayback", - pausePlayback: "player/pausePlayback", - }), - }, -}; + resumePlayback: 'player/resumePlayback', + pausePlayback: 'player/pausePlayback' + }) + } +} </script> diff --git a/front/src/components/audio/podcast/Row.vue b/front/src/components/audio/podcast/Row.vue index 4339166f5b211d2e0258e717f866028bb875e7ca..17fcb30a225490b68accc385297c5f09cbe71a97 100644 --- a/front/src/components/audio/podcast/Row.vue +++ b/front/src/components/audio/podcast/Row.vue @@ -15,8 +15,6 @@ @click.prevent.exact="activateTrack(track, index)" > <img - alt="" - class="ui artist-track mini image" v-if=" track.cover && track.cover.urls.original " @@ -25,10 +23,10 @@ track.cover.urls.medium_square_crop ) " - /> - <img alt="" class="ui artist-track mini image" + > + <img v-else-if=" defaultCover " @@ -37,21 +35,32 @@ defaultCover.cover.urls.medium_square_crop ) " - /> - <img alt="" class="ui artist-track mini image" + > + <img v-else + alt="" + class="ui artist-track mini image" src="../../../assets/audio/default-cover.png" - /> + > </div> - <div tabindex=0 class="content left floated column"> + <div + tabindex="0" + class="content left floated column" + > <a class="podcast-episode-title ellipsis" - @click.prevent.exact="activateTrack(track, index)">{{ track.title }}</a> - <p class="podcast-episode-meta">{{ description.text }}</p> + @click.prevent.exact="activateTrack(track, index)" + >{{ track.title }}</a> + <p class="podcast-episode-meta"> + {{ description.text }} + </p> </div> - <div v-if="displayActions" class="meta right floated column"> + <div + v-if="displayActions" + class="meta right floated column" + > <play-button id="playmenu" class="play-button basic icon" @@ -63,22 +72,25 @@ 'large really discrete', ]" :track="track" - ></play-button> + /> </div> </div> </template> <script> import axios from 'axios' -import PlayIndicator from "@/components/audio/track/PlayIndicator"; -import { mapActions, mapGetters } from "vuex"; -import PlayButton from "@/components/audio/PlayButton"; -import PlayOptions from "@/components/mixins/PlayOptions"; +import { mapActions, mapGetters } from 'vuex' +import PlayButton from '@/components/audio/PlayButton' +import PlayOptions from '@/components/mixins/PlayOptions' export default { + + components: { + PlayButton + }, mixins: [PlayOptions], props: { - tracks: Array, + tracks: { type: Array, required: true }, showAlbum: { type: Boolean, required: false, default: true }, showArtist: { type: Boolean, required: false, default: true }, showPosition: { type: Boolean, required: false, default: false }, @@ -90,34 +102,29 @@ export default { showDuration: { type: Boolean, required: false, default: true }, index: { type: Number, required: true }, track: { type: Object, required: true }, - defaultCover: { type: Object, required: false }, + defaultCover: { type: Object, required: false, default: () => { return {} } } }, - data() { + data () { return { hover: null, errors: null, - description: null, + description: null } }, - created () { - this.fetchData('tracks/' + this.track.id + '/' ) - }, - - components: { - PlayIndicator, - PlayButton, - }, - computed: { ...mapGetters({ - currentTrack: "queue/currentTrack", + currentTrack: 'queue/currentTrack' }), - isPlaying() { - return this.$store.state.player.playing; - }, + isPlaying () { + return this.$store.state.player.playing + } + }, + + created () { + this.fetchData('tracks/' + this.track.id + '/') }, methods: { @@ -126,29 +133,29 @@ export default { return } this.isLoading = true - let self = this + const self = this try { - let channelsPromise = await axios.get(url) + const channelsPromise = await axios.get(url) self.description = channelsPromise.data.description self.isLoading = false - } catch(e) { + } catch (e) { self.isLoading = false - self.errors = error.backendErrors + self.errors = e.backendErrors } }, - prettyPosition(position, size) { - var s = String(position); + prettyPosition (position, size) { + let s = String(position) while (s.length < (size || 2)) { - s = "0" + s; + s = '0' + s } - return s; + return s }, ...mapActions({ - resumePlayback: "player/resumePlayback", - pausePlayback: "player/pausePlayback", - }), - }, -}; + resumePlayback: 'player/resumePlayback', + pausePlayback: 'player/pausePlayback' + }) + } +} </script> diff --git a/front/src/components/audio/podcast/Table.vue b/front/src/components/audio/podcast/Table.vue index 44f01c8206c2e394ed08281d1b45a9c904a8dc8c..2f03f1f97e6d5f3529dc2aa50d48583f5c17bc76 100644 --- a/front/src/components/audio/podcast/Table.vue +++ b/front/src/components/audio/podcast/Table.vue @@ -1,10 +1,10 @@ <template> <div> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <!-- Add a header if needed --> - <slot name="header"></slot> + <slot name="header" /> <div> <div @@ -13,38 +13,44 @@ <!-- For each item, build a row --> <podcast-row v-for="(track, index) in tracks" - :track="track" :key="track.id" + :track="track" :index="index" :tracks="tracks" :display-actions="displayActions" :show-duration="showDuration" :is-podcast="isPodcast" - ></podcast-row> + /> </div> - <div v-if="paginateResults" class="ui center aligned basic segment desktop-and-up"> + <div + v-if="paginateResults" + class="ui center aligned basic segment desktop-and-up" + > <pagination :total="total" :current="page" :paginate-by="paginateBy" - v-on="$listeners"> - </pagination> + v-on="$listeners" + /> </div> </div> <div :class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-below']" > - <div v-if="isLoading" class="ui inverted active dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui inverted active dimmer" + > + <div class="ui loader" /> </div> <!-- For each item, build a row --> <track-mobile-row v-for="(track, index) in tracks" - :track="track" :key="track.id" + :track="track" :index="index" :tracks="tracks" :show-position="showPosition" @@ -53,36 +59,37 @@ :is-artist="isArtist" :is-album="isAlbum" :is-podcast="isPodcast" - ></track-mobile-row> - <div v-if="paginateResults" class="ui center aligned basic segment tablet-and-below"> + /> + <div + v-if="paginateResults" + class="ui center aligned basic segment tablet-and-below" + > <pagination v-if="paginateResults" :total="total" :current="page" :compact="true" - v-on="$listeners"></pagination> + v-on="$listeners" + /> </div> </div> </div> </template> <script> -import _ from "@/lodash"; -import TrackRow from "@/components/audio/track/Row"; -import PodcastRow from "@/components/audio/podcast/Row"; -import TrackMobileRow from "@/components/audio/track/MobileRow"; -import Pagination from "@/components/Pagination"; +import PodcastRow from '@/components/audio/podcast/Row' +import TrackMobileRow from '@/components/audio/track/MobileRow' +import Pagination from '@/components/Pagination' export default { components: { - TrackRow, TrackMobileRow, Pagination, - PodcastRow, + PodcastRow }, props: { - tracks: Array, + tracks: { type: Array, required: true }, showAlbum: { type: Boolean, required: false, default: true }, showArtist: { type: Boolean, required: false, default: true }, showPosition: { type: Boolean, required: false, default: false }, @@ -94,33 +101,33 @@ export default { showDuration: { type: Boolean, required: false, default: true }, isArtist: { type: Boolean, required: false, default: false }, isAlbum: { type: Boolean, required: false, default: false }, - paginateResults: { type: Boolean, required: false, default: true}, - total: { type: Number, required: false}, - page: {type: Number, required: false, default: 1}, - paginateBy: {type: Number, required: false, default: 25}, - isPodcast: {type: Boolean, required: true}, - defaultCover: {type: Object, required: false}, + paginateResults: { type: Boolean, required: false, default: true }, + total: { type: Number, required: false, default: 0 }, + page: { type: Number, required: false, default: 1 }, + paginateBy: { type: Number, required: false, default: 25 }, + isPodcast: { type: Boolean, required: true }, + defaultCover: { type: Object, required: false, default: () => { return {} } } }, - data() { + data () { return { - isLoading: false, - }; + isLoading: false + } }, computed: { - labels() { + labels () { return { - title: this.$pgettext("*/*/*/Noun", "Title"), - album: this.$pgettext("*/*/*/Noun", "Album"), - artist: this.$pgettext("*/*/*/Noun", "Artist"), - }; - }, + title: this.$pgettext('*/*/*/Noun', 'Title'), + album: this.$pgettext('*/*/*/Noun', 'Album'), + artist: this.$pgettext('*/*/*/Noun', 'Artist') + } + } }, methods: { - updatePage: function(page) { + updatePage: function (page) { this.$emit('page-changed', page) } - }, -}; + } +} </script> diff --git a/front/src/components/audio/track/MobileRow.vue b/front/src/components/audio/track/MobileRow.vue index da2b1f3d28033397692f3f7d9da6307d64a5b3ee..98005ff9d1d980c14ff046a81614dcfdb2df77c7 100644 --- a/front/src/components/audio/track/MobileRow.vue +++ b/front/src/components/audio/track/MobileRow.vue @@ -7,12 +7,10 @@ > <div v-if="showArt" - @click.prevent.exact="activateTrack(track, index)" class="image left floated column" + @click.prevent.exact="activateTrack(track, index)" > <img - alt="" - class="ui artist-track mini image" v-if=" track.album && track.album.cover && track.album.cover.urls.original " @@ -21,10 +19,10 @@ track.album.cover.urls.medium_square_crop ) " - /> - <img alt="" class="ui artist-track mini image" + > + <img v-else-if=" track.cover " @@ -33,10 +31,10 @@ track.cover.urls.medium_square_crop ) " - /> - <img alt="" class="ui artist-track mini image" + > + <img v-else-if=" track.artist.cover " @@ -45,19 +43,21 @@ track.artist.cover.urls.medium_square_crop ) " - /> - <img alt="" class="ui artist-track mini image" + > + <img v-else + alt="" + class="ui artist-track mini image" src="../../../assets/audio/default-cover.png" - /> + > </div> <div - tabindex=0 - @click="activateTrack(track, index)" + tabindex="0" role="button" class="content ellipsis left floated column" + @click="activateTrack(track, index)" > <p :class="[ @@ -73,7 +73,7 @@ <human-duration v-if="track.uploads[0] && track.uploads[0].duration" :duration="track.uploads[0].duration" - ></human-duration> + /> </p> </div> <div @@ -92,12 +92,11 @@ class="tiny" :border="false" :track="track" - ></track-favorite-icon> + /> </div> <div role="button" :aria-label="actionsButtonLabel" - @click.prevent.exact="showTrackModal = !showTrackModal" :class="[ 'modal-button', 'right', @@ -106,36 +105,36 @@ 'mobile', { 'with-art': showArt }, ]" + @click.prevent.exact="showTrackModal = !showTrackModal" > <i class="ellipsis large vertical icon" /> </div> <track-modal - @update:show="showTrackModal = $event;" :show="showTrackModal" :track="track" :index="index" :is-artist="isArtist" :is-album="isAlbum" - ></track-modal> + @update:show="showTrackModal = $event;" + /> </div> </template> <script> -import PlayIndicator from "@/components/audio/track/PlayIndicator"; -import { mapActions, mapGetters } from "vuex"; -import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon"; -import TrackModal from "@/components/audio/track/Modal"; -import PlayOptionsMixin from "@/components/mixins/PlayOptions" +import { mapActions, mapGetters } from 'vuex' +import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon' +import TrackModal from '@/components/audio/track/Modal' +import PlayOptionsMixin from '@/components/mixins/PlayOptions' export default { - mixins: [PlayOptionsMixin], - data() { - return { - showTrackModal: false, - } + + components: { + TrackFavoriteIcon, + TrackModal }, + mixins: [PlayOptionsMixin], props: { - tracks: Array, + tracks: { type: Array, required: true }, showAlbum: { type: Boolean, required: false, default: true }, showArtist: { type: Boolean, required: false, default: true }, showPosition: { type: Boolean, required: false, default: false }, @@ -147,41 +146,40 @@ export default { showDuration: { type: Boolean, required: false, default: true }, index: { type: Number, required: true }, track: { type: Object, required: true }, - isArtist: {type: Boolean, required: false, default: false}, - isAlbum: {type: Boolean, required: false, default: false}, + isArtist: { type: Boolean, required: false, default: false }, + isAlbum: { type: Boolean, required: false, default: false } }, - - components: { - PlayIndicator, - TrackFavoriteIcon, - TrackModal, + data () { + return { + showTrackModal: false + } }, computed: { ...mapGetters({ - currentTrack: "queue/currentTrack", + currentTrack: 'queue/currentTrack' }), - isPlaying() { - return this.$store.state.player.playing; + isPlaying () { + return this.$store.state.player.playing }, actionsButtonLabel () { - return this.$pgettext('Content/Track/Icon.Tooltip/Verb', 'Show track actions') - }, + return this.$pgettext('Content/Track/Icon.Tooltip/Verb', 'Show track actions') + } }, methods: { - prettyPosition(position, size) { - var s = String(position); + prettyPosition (position, size) { + let s = String(position) while (s.length < (size || 2)) { - s = "0" + s; + s = '0' + s } - return s; + return s }, ...mapActions({ - resumePlayback: "player/resumePlayback", - pausePlayback: "player/pausePlayback", - }), - }, -}; + resumePlayback: 'player/resumePlayback', + pausePlayback: 'player/pausePlayback' + }) + } +} </script> diff --git a/front/src/components/audio/track/PlayIndicator.vue b/front/src/components/audio/track/PlayIndicator.vue index c97b3c1224b35b92383403d9ca36cc864887f3dc..131a9f388bb3d8c035782c2a35543fa2e45abb2d 100644 --- a/front/src/components/audio/track/PlayIndicator.vue +++ b/front/src/components/audio/track/PlayIndicator.vue @@ -1,8 +1,8 @@ <template> <div id="audio-bars"> - <div class="audio-bar"></div> - <div class="audio-bar"></div> - <div class="audio-bar"></div> - <div class="audio-bar"></div> + <div class="audio-bar" /> + <div class="audio-bar" /> + <div class="audio-bar" /> + <div class="audio-bar" /> </div> -</template> \ No newline at end of file +</template> diff --git a/front/src/components/audio/track/Row.vue b/front/src/components/audio/track/Row.vue index b2c7fd025cde756a67bfa15abda97e08154285e0..12c69f955e6a1978649b2847b93d7528eabc6ac7 100644 --- a/front/src/components/audio/track/Row.vue +++ b/front/src/components/audio/track/Row.vue @@ -16,19 +16,18 @@ <play-indicator v-if=" !$store.state.player.isLoadingAudio && - currentTrack && - isPlaying && - track.id === currentTrack.id && - !(track.id == hover) + currentTrack && + isPlaying && + track.id === currentTrack.id && + !(track.id == hover) " - > - </play-indicator> + /> <button v-else-if=" currentTrack && - !isPlaying && - track.id === currentTrack.id && - !track.id == hover + !isPlaying && + track.id === currentTrack.id && + !track.id == hover " class="ui really tiny basic icon button play-button paused" > @@ -37,9 +36,9 @@ <button v-else-if=" currentTrack && - isPlaying && - track.id === currentTrack.id && - track.id == hover + isPlaying && + track.id === currentTrack.id && + track.id == hover " class="ui really tiny basic icon button play-button" > @@ -51,7 +50,10 @@ > <i class="play icon" /> </button> - <span class="track-position" v-else-if="showPosition"> + <span + v-else-if="showPosition" + class="track-position" + > {{ prettyPosition(track.position) }} </span> </div> @@ -62,8 +64,6 @@ @click.prevent.exact="activateTrack(track, index)" > <img - alt="" - class="ui artist-track mini image" v-if=" track.album && track.album.cover && track.album.cover.urls.original " @@ -72,10 +72,10 @@ track.album.cover.urls.medium_square_crop ) " - /> - <img alt="" class="ui artist-track mini image" + > + <img v-else-if=" track.cover && track.cover.urls.original " @@ -84,10 +84,10 @@ track.cover.urls.medium_square_crop ) " - /> - <img alt="" class="ui artist-track mini image" + > + <img v-else-if=" track.artist && track.artist.cover && track.album.cover.urls.original " @@ -96,36 +96,49 @@ track.cover.urls.medium_square_crop ) " - /> - <img alt="" class="ui artist-track mini image" + > + <img v-else + alt="" + class="ui artist-track mini image" src="../../../assets/audio/default-cover.png" - /> + > </div> - <div tabindex=0 class="content ellipsis left floated column"> + <div + tabindex="0" + class="content ellipsis left floated column" + > <a @click="activateTrack(track, index)" > {{ track.title }} </a> </div> - <div v-if="showAlbum" class="content ellipsis left floated column"> + <div + v-if="showAlbum" + class="content ellipsis left floated column" + > <router-link :to="{ name: 'library.albums.detail', params: { id: track.album.id } }" - >{{ track.album.title }}</router-link > + {{ track.album.title }} + </router-link> </div> - <div v-if="showArtist" class="content ellipsis left floated column"> + <div + v-if="showArtist" + class="content ellipsis left floated column" + > <router-link class="artist link" :to="{ name: 'library.artists.detail', params: { id: track.artist.id }, }" - >{{ track.artist.name }}</router-link > + {{ track.artist.name }} + </router-link> </div> <div v-if="$store.state.auth.authenticated" @@ -135,15 +148,21 @@ class="tiny" :border="false" :track="track" - ></track-favorite-icon> + /> </div> - <div v-if="showDuration" class="meta right floated column"> + <div + v-if="showDuration" + class="meta right floated column" + > <human-duration v-if="track.uploads[0] && track.uploads[0].duration" :duration="track.uploads[0].duration" - ></human-duration> + /> </div> - <div v-if="displayActions" class="meta right floated column"> + <div + v-if="displayActions" + class="meta right floated column" + > <play-button id="playmenu" class="play-button basic icon" @@ -155,22 +174,28 @@ 'large really discrete', ]" :track="track" - ></play-button> + /> </div> </div> </template> <script> -import PlayIndicator from "@/components/audio/track/PlayIndicator"; -import { mapActions, mapGetters } from "vuex"; -import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon"; -import PlayButton from "@/components/audio/PlayButton"; -import PlayOptions from "@/components/mixins/PlayOptions"; +import PlayIndicator from '@/components/audio/track/PlayIndicator' +import { mapActions, mapGetters } from 'vuex' +import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon' +import PlayButton from '@/components/audio/PlayButton' +import PlayOptions from '@/components/mixins/PlayOptions' export default { + + components: { + PlayIndicator, + TrackFavoriteIcon, + PlayButton + }, mixins: [PlayOptions], props: { - tracks: Array, + tracks: { type: Array, required: true }, showAlbum: { type: Boolean, required: false, default: true }, showArtist: { type: Boolean, required: false, default: true }, showPosition: { type: Boolean, required: false, default: false }, @@ -181,45 +206,39 @@ export default { displayActions: { type: Boolean, required: false, default: true }, showDuration: { type: Boolean, required: false, default: true }, index: { type: Number, required: true }, - track: { type: Object, required: true }, + track: { type: Object, required: true } }, - data() { + data () { return { - hover: null, + hover: null } }, - components: { - PlayIndicator, - TrackFavoriteIcon, - PlayButton, - }, - computed: { ...mapGetters({ - currentTrack: "queue/currentTrack", + currentTrack: 'queue/currentTrack' }), - isPlaying() { - return this.$store.state.player.playing; - }, + isPlaying () { + return this.$store.state.player.playing + } }, methods: { - prettyPosition(position, size) { - var s = String(position); + prettyPosition (position, size) { + let s = String(position) while (s.length < (size || 2)) { - s = "0" + s; + s = '0' + s } - return s; + return s }, - + ...mapActions({ - resumePlayback: "player/resumePlayback", - pausePlayback: "player/pausePlayback", - }), - }, -}; + resumePlayback: 'player/resumePlayback', + pausePlayback: 'player/pausePlayback' + }) + } +} </script> diff --git a/front/src/components/audio/track/Table.vue b/front/src/components/audio/track/Table.vue index ee21cfd4394aa190435cf0858cc85b4e95126f3f..1634de335464a3df8e1629b640bddc4dc498dc60 100644 --- a/front/src/components/audio/track/Table.vue +++ b/front/src/components/audio/track/Table.vue @@ -2,65 +2,95 @@ <div> <!-- Show the search bar if search is true --> <inline-search-bar - v-model="query" v-if="search" + v-model="query" @search=" additionalTracks = []; fetchData(); " - ></inline-search-bar> - <div class="ui hidden divider"></div> + /> + <div class="ui hidden divider" /> <!-- Add a header if needed --> - <slot name="header"></slot> + <slot name="header" /> <!-- Show a message if no tracks are available --> - <slot v-if="!isLoading && allTracks.length === 0" name="empty-state"> + <slot + v-if="!isLoading && allTracks.length === 0" + name="empty-state" + > <empty-state - @refresh="fetchData('tracks/')" :refresh="true" - ></empty-state> + @refresh="fetchData('tracks/')" + /> </slot> <div v-else> <div :class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-up']" > - <div v-if="isLoading" class="ui inverted active dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui inverted active dimmer" + > + <div class="ui loader" /> </div> <div class="track-table row"> - <div v-if="showPosition" class="actions left floated column"> - <i class="hashtag icon"></i> + <div + v-if="showPosition" + class="actions left floated column" + > + <i class="hashtag icon" /> </div> - <div v-else class="actions left floated column"></div> - <div v-if="showArt" class="image left floated column"></div> + <div + v-else + class="actions left floated column" + /> + <div + v-if="showArt" + class="image left floated column" + /> <div class="content ellipsis left floated column"> <b>{{ labels.title }}</b> </div> - <div v-if="showAlbum" class="content ellipsisleft floated column"> + <div + v-if="showAlbum" + class="content ellipsisleft floated column" + > <b>{{ labels.album }}</b> </div> - <div v-if="showArtist" class="content ellipsis left floated column"> + <div + v-if="showArtist" + class="content ellipsis left floated column" + > <b>{{ labels.artist }}</b> </div> <div v-if="$store.state.auth.authenticated" class="meta right floated column" - ></div> - <div v-if="showDuration" class="meta right floated column"> - <i class="clock outline icon" style="padding: 0.5rem" /> + /> + <div + v-if="showDuration" + class="meta right floated column" + > + <i + class="clock outline icon" + style="padding: 0.5rem" + /> </div> - <div v-if="displayActions" class="meta right floated column"></div> + <div + v-if="displayActions" + class="meta right floated column" + /> </div> <!-- For each item, build a row --> <track-row v-for="(track, index) in allTracks" - :track="track" :key="track.id" + :track="track" :index="index" :tracks="allTracks" :show-album="showAlbum" @@ -70,31 +100,37 @@ :display-actions="displayActions" :show-duration="showDuration" :is-podcast="isPodcast" - ></track-row> + /> </div> - <div v-if="paginateResults" class="ui center aligned basic segment desktop-and-up"> + <div + v-if="paginateResults" + class="ui center aligned basic segment desktop-and-up" + > <pagination :total="total" :current="page" :paginate-by="paginateBy" - v-on="$listeners"> - </pagination> + v-on="$listeners" + /> </div> </div> <div :class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-below']" > - <div v-if="isLoading" class="ui inverted active dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui inverted active dimmer" + > + <div class="ui loader" /> </div> <!-- For each item, build a row --> <track-mobile-row v-for="(track, index) in allTracks" - :track="track" :key="track.id" + :track="track" :index="index" :tracks="allTracks" :show-position="showPosition" @@ -103,35 +139,39 @@ :is-artist="isArtist" :is-album="isAlbum" :is-podcast="isPodcast" - ></track-mobile-row> - <div v-if="paginateResults" class="ui center aligned basic segment tablet-and-below"> + /> + <div + v-if="paginateResults" + class="ui center aligned basic segment tablet-and-below" + > <pagination v-if="paginateResults" :total="total" :current="page" :compact="true" - v-on="$listeners"></pagination> + v-on="$listeners" + /> </div> </div> </div> </template> <script> -import _ from "@/lodash"; -import axios from "axios"; -import TrackRow from "@/components/audio/track/Row"; -import TrackMobileRow from "@/components/audio/track/MobileRow"; -import Pagination from "@/components/Pagination"; +import _ from '@/lodash' +import axios from 'axios' +import TrackRow from '@/components/audio/track/Row' +import TrackMobileRow from '@/components/audio/track/MobileRow' +import Pagination from '@/components/Pagination' export default { components: { TrackRow, TrackMobileRow, - Pagination, + Pagination }, props: { - tracks: Array, + tracks: { type: Array, default: () => { return [] } }, showAlbum: { type: Boolean, required: false, default: true }, showArtist: { type: Boolean, required: false, default: true }, showPosition: { type: Boolean, required: false, default: false }, @@ -144,66 +184,66 @@ export default { isArtist: { type: Boolean, required: false, default: false }, isAlbum: { type: Boolean, required: false, default: false }, isPodcast: { type: Boolean, required: false, default: false }, - paginateResults: { type: Boolean, required: false, default: true}, - total: { type: Number, required: false}, - page: {type: Number, required: false, default: 1}, - paginateBy: {type: Number, required: false, default: 25} + paginateResults: { type: Boolean, required: false, default: true }, + total: { type: Number, required: false, default: 0 }, + page: { type: Number, required: false, default: 1 }, + paginateBy: { type: Number, required: false, default: 25 } }, - data() { + data () { return { fetchDataUrl: this.nextUrl, isLoading: false, additionalTracks: [], - query: "", - }; + query: '' + } }, computed: { - allTracks() { - return (this.tracks || []).concat(this.additionalTracks); + allTracks () { + return (this.tracks || []).concat(this.additionalTracks) }, - labels() { + labels () { return { - title: this.$pgettext("*/*/*/Noun", "Title"), - album: this.$pgettext("*/*/*/Noun", "Album"), - artist: this.$pgettext("*/*/*/Noun", "Artist"), - }; - }, + title: this.$pgettext('*/*/*/Noun', 'Title'), + album: this.$pgettext('*/*/*/Noun', 'Album'), + artist: this.$pgettext('*/*/*/Noun', 'Artist') + } + } + }, + created () { + if (!this.tracks) { + this.fetchData('tracks/') + } }, methods: { - async fetchData(url) { + async fetchData (url) { if (!url) { - return; + return } - this.isLoading = true; - let self = this; - let params = _.clone(this.filters); - let tracksPromise = axios.get(url, { params: params }) - params.page_size = this.limit; - params.page = this.page; - params.include_channels = true; + this.isLoading = true + const self = this + const params = _.clone(this.filters) + const tracksPromise = axios.get(url, { params: params }) + params.page_size = this.limit + params.page = this.page + params.include_channels = true try { await tracksPromise - self.nextPage = tracksPromise.data.next; - self.objects = tracksPromise.data.results; - self.count = tracksPromise.data.count; - self.$emit("fetched", tracksPromise.data); - self.isLoading = false; - } catch(e) { - self.isLoading = false; - self.errors = error.backendErrors; + self.nextPage = tracksPromise.data.next + self.objects = tracksPromise.data.results + self.count = tracksPromise.data.count + self.$emit('fetched', tracksPromise.data) + self.isLoading = false + } catch (e) { + self.isLoading = false + self.errors = e.backendErrors } }, - updatePage: function(page) { + updatePage: function (page) { this.$emit('page-changed', page) } - }, - created() { - if (!this.tracks) { - this.fetchData("tracks/"); - } - }, -}; + } +} </script> diff --git a/front/src/components/audio/track/Widget.vue b/front/src/components/audio/track/Widget.vue index 68d6988a9a5e4a80e53cdb6416fcdf5bea521079..85b7c6104cf1aea53f3c9b6383be64ab0822ad97 100644 --- a/front/src/components/audio/track/Widget.vue +++ b/front/src/components/audio/track/Widget.vue @@ -1,17 +1,48 @@ <template> <div class="component-track-widget"> - <h3 v-if="!!this.$slots.title"> - <slot name="title"></slot> - <span v-if="showCount" class="ui tiny circular label">{{ count }}</span> + <h3 v-if="!!$slots.title"> + <slot name="title" /> + <span + v-if="showCount" + class="ui tiny circular label" + >{{ count }}</span> </h3> - <div v-if="count > 0" class="ui divided unstackable items"> - <div :class="['item', itemClasses]" v-for="object in objects" :key="object.id"> + <div + v-if="count > 0" + class="ui divided unstackable items" + > + <div + v-for="object in objects" + :key="object.id" + :class="['item', itemClasses]" + > <div class="ui tiny image"> - <img alt="" v-if="object.track.album && object.track.album.cover" v-lazy="$store.getters['instance/absoluteUrl'](object.track.album.cover.urls.medium_square_crop)"> - <img alt="" v-else-if="object.track.cover" v-lazy="$store.getters['instance/absoluteUrl'](object.track.cover.urls.medium_square_crop)"/> - <img alt="" v-else-if="object.track.artist.cover" v-lazy="$store.getters['instance/absoluteUrl'](object.track.artist.cover.urls.medium_square_crop)"/> - <img alt="" v-else src="../../../assets/audio/default-cover.png"> - <play-button class="play-overlay" :icon-only="true" :button-classes="['ui', 'circular', 'tiny', 'vibrant', 'icon', 'button']" :track="object.track"></play-button> + <img + v-if="object.track.album && object.track.album.cover" + v-lazy="$store.getters['instance/absoluteUrl'](object.track.album.cover.urls.medium_square_crop)" + alt="" + > + <img + v-else-if="object.track.cover" + v-lazy="$store.getters['instance/absoluteUrl'](object.track.cover.urls.medium_square_crop)" + alt="" + > + <img + v-else-if="object.track.artist.cover" + v-lazy="$store.getters['instance/absoluteUrl'](object.track.artist.cover.urls.medium_square_crop)" + alt="" + > + <img + v-else + alt="" + src="../../../assets/audio/default-cover.png" + > + <play-button + class="play-overlay" + :icon-only="true" + :button-classes="['ui', 'circular', 'tiny', 'vibrant', 'icon', 'button']" + :track="object.track" + /> </div> <div class="middle aligned content"> <div class="ui unstackable grid"> @@ -23,15 +54,32 @@ </div> <div class="meta ellipsis"> <span> - <router-link class="discrete link" :to="{name: 'library.artists.detail', params: {id: object.track.artist.id}}"> + <router-link + class="discrete link" + :to="{name: 'library.artists.detail', params: {id: object.track.artist.id}}" + > {{ object.track.artist.name }} </router-link> </span> </div> - <tags-list label-classes="tiny" :truncate-size="20" :limit="2" :show-more="false" :tags="object.track.tags"></tags-list> + <tags-list + label-classes="tiny" + :truncate-size="20" + :limit="2" + :show-more="false" + :tags="object.track.tags" + /> - <div class="extra" v-if="isActivity"> - <router-link class="left floated" :to="{name: 'profile.overview', params: {username: object.user.username}}">@{{ object.user.username }}</router-link> + <div + v-if="isActivity" + class="extra" + > + <router-link + class="left floated" + :to="{name: 'profile.overview', params: {username: object.user.username}}" + > + @{{ object.user.username }} + </router-link> <span class="right floated"><human-date :date="object.creation_date" /></span> </div> </div> @@ -41,30 +89,46 @@ :account="object.actor" :dropdown-only="true" :dropdown-icon-classes="['ellipsis', 'vertical', 'large really discrete']" - :track="object.track"></play-button> + :track="object.track" + /> </div> </div> </div> </div> - <div v-if="isLoading" class="ui inverted active dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui inverted active dimmer" + > + <div class="ui loader" /> </div> </div> - <div v-else class="ui placeholder segment"> + <div + v-else + class="ui placeholder segment" + > <div class="ui icon header"> - <i class="music icon"></i> + <i class="music icon" /> <translate translate-context="Content/Home/Placeholder"> Nothing found </translate> </div> - <div v-if="isLoading" class="ui inverted active dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui inverted active dimmer" + > + <div class="ui loader" /> </div> </div> <template v-if="nextPage"> - <div class="ui hidden divider"></div> - <button v-if="nextPage" @click="fetchData(nextPage)" :class="['ui', 'basic', 'button']"> - <translate translate-context="*/*/Button,Label">Show more</translate> + <div class="ui hidden divider" /> + <button + v-if="nextPage" + :class="['ui', 'basic', 'button']" + @click="fetchData(nextPage)" + > + <translate translate-context="*/*/Button,Label"> + Show more + </translate> </button> </template> </div> @@ -74,21 +138,21 @@ import _ from '@/lodash' import axios from 'axios' import PlayButton from '@/components/audio/PlayButton' -import TagsList from "@/components/tags/List" +import TagsList from '@/components/tags/List' export default { - props: { - filters: {type: Object, required: true}, - url: {type: String, required: true}, - isActivity: {type: Boolean, default: true}, - showCount: {type: Boolean, default: false}, - limit: {type: Number, default: 5}, - itemClasses: {type: String, default: ''}, - }, components: { PlayButton, TagsList }, + props: { + filters: { type: Object, required: true }, + url: { type: String, required: true }, + isActivity: { type: Boolean, default: true }, + showCount: { type: Boolean, default: false }, + limit: { type: Number, default: 5 }, + itemClasses: { type: String, default: '' } + }, data () { return { objects: [], @@ -99,6 +163,17 @@ export default { nextPage: null } }, + watch: { + offset () { + this.fetchData() + }, + '$store.state.moderation.lastUpdate': function () { + this.fetchData(this.url) + }, + count (v) { + this.$emit('count', v) + } + }, created () { this.fetchData(this.url) }, @@ -108,11 +183,11 @@ export default { return } this.isLoading = true - let self = this - let params = _.clone(this.filters) + const self = this + const params = _.clone(this.filters) params.page_size = this.limit params.offset = this.offset - axios.get(url, {params: params}).then((response) => { + axios.get(url, { params: params }).then((response) => { self.previousPage = response.data.previous self.nextPage = response.data.next self.isLoading = false @@ -123,7 +198,7 @@ export default { newObjects = response.data.results } else { newObjects = response.data.results.map((r) => { - return {track: r} + return { track: r } }) } self.objects = [...self.objects, ...newObjects] @@ -139,17 +214,6 @@ export default { this.offset = Math.max(this.offset - this.limit, 0) } } - }, - watch: { - offset () { - this.fetchData() - }, - "$store.state.moderation.lastUpdate": function () { - this.fetchData(this.url) - }, - count (v) { - this.$emit('count', v) - } } } </script> diff --git a/front/src/components/auth/ApplicationEdit.vue b/front/src/components/auth/ApplicationEdit.vue index b4a0b37356b77fb46e4d6934110260cc7599733c..4d04f9cdbe8c4ee73a9c37ffb21ba191a4900df0 100644 --- a/front/src/components/auth/ApplicationEdit.vue +++ b/front/src/components/auth/ApplicationEdit.vue @@ -1,16 +1,26 @@ <template> - <main class="main pusher" v-title="labels.title"> + <main + v-title="labels.title" + class="main pusher" + > <div class="ui vertical stripe segment"> <section class="ui text container"> - <div v-if="isLoading" class="ui inverted active dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui inverted active dimmer" + > + <div class="ui loader" /> </div> <template v-else> <router-link :to="{name: 'settings'}"> - <translate translate-context="Content/Applications/Link">Back to settings</translate> + <translate translate-context="Content/Applications/Link"> + Back to settings + </translate> </router-link> <h2 class="ui header"> - <translate translate-context="Content/Applications/Title">Application details</translate> + <translate translate-context="Content/Applications/Title"> + Application details + </translate> </h2> <div class="ui form"> <p> @@ -20,25 +30,45 @@ </p> <div class="field"> <label for="copy-id"><translate translate-context="Content/Applications/Label">Application ID</translate></label> - <copy-input id="copy-id" :value="application.client_id" /> + <copy-input + id="copy-id" + :value="application.client_id" + /> </div> <div class="field"> <label for="copy-secret"><translate translate-context="Content/Applications/Label">Application secret</translate></label> - <copy-input id="copy-secret" :value="application.client_secret" /> + <copy-input + id="copy-secret" + :value="application.client_secret" + /> </div> - <div class="field" v-if="application.token != undefined"> + <div + v-if="application.token != undefined" + class="field" + > <label for="copy-secret"><translate translate-context="Content/Applications/Label">Access token</translate></label> - <copy-input id="copy-secret" :value="application.token" /> - <a href="" @click.prevent="refreshToken"> - <i class="refresh icon"></i> + <copy-input + id="copy-secret" + :value="application.token" + /> + <a + href="" + @click.prevent="refreshToken" + > + <i class="refresh icon" /> <translate translate-context="Content/Applications/Label">Regenerate token</translate> </a> </div> </div> <h2 class="ui header"> - <translate translate-context="Content/Applications/Title">Edit application</translate> + <translate translate-context="Content/Applications/Title"> + Edit application + </translate> </h2> - <application-form @updated="application = $event" :app="application" /> + <application-form + :app="application" + @updated="application = $event" + /> </template> </section> </div> @@ -46,19 +76,26 @@ </template> <script> -import axios from "axios" +import axios from 'axios' -import ApplicationForm from "@/components/auth/ApplicationForm" +import ApplicationForm from '@/components/auth/ApplicationForm' export default { - props: ['id'], components: { ApplicationForm }, - data() { + props: { id: { type: Number, required: true } }, + data () { return { application: null, - isLoading: false, + isLoading: false + } + }, + computed: { + labels () { + return { + title: this.$pgettext('Content/Applications/Title', 'Edit application') + } } }, created () { @@ -67,7 +104,7 @@ export default { methods: { fetchApplication () { this.isLoading = true - let self = this + const self = this axios.get(`oauth/apps/${this.id}/`).then((response) => { self.isLoading = false self.application = response.data @@ -78,17 +115,10 @@ export default { }, async refreshToken () { self.isLoading = true - let response = await axios.post(`oauth/apps/${this.id}/refresh-token`) + const response = await axios.post(`oauth/apps/${this.id}/refresh-token`) this.application = response.data self.isLoading = false } - }, - computed: { - labels() { - return { - title: this.$pgettext('Content/Applications/Title', "Edit application") - } - }, } } </script> diff --git a/front/src/components/auth/ApplicationForm.vue b/front/src/components/auth/ApplicationForm.vue index 4ddd5bc1a394756f9bc87f04b17540f70132ca3a..bfbcd580843909d268520cfeb2d488e5b1f3274f 100644 --- a/front/src/components/auth/ApplicationForm.vue +++ b/front/src/components/auth/ApplicationForm.vue @@ -1,19 +1,45 @@ <template> - - <form class="ui form component-form" role="alert" @submit.prevent="submit()"> - <div v-if="errors.length > 0" class="ui negative message"> - <h4 class="header"><translate translate-context="Content/*/Error message.Title">We cannot save your changes</translate></h4> + <form + class="ui form component-form" + role="alert" + @submit.prevent="submit()" + > + <div + v-if="errors.length > 0" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Content/*/Error message.Title"> + We cannot save your changes + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> <div class="ui field"> <label for="application-name"><translate translate-context="*/*/*/Noun">Name</translate></label> - <input id="application-name" name="name" required type="text" v-model="fields.name" /> + <input + id="application-name" + v-model="fields.name" + name="name" + required + type="text" + > </div> <div class="ui field"> <label for="redirect-uris"><translate translate-context="Content/Applications/Input.Label/Noun">Redirect URI</translate></label> - <input id="redirect-uris" name="redirect_uris" type="text" v-model="fields.redirect_uris" /> + <input + id="redirect-uris" + v-model="fields.redirect_uris" + name="redirect_uris" + type="text" + > <p class="help"> <translate translate-context="Content/Applications/Help Text"> Use "urn:ietf:wg:oauth:2.0:oob" as a redirect URI if your application is not served on the web. @@ -28,13 +54,18 @@ </translate> </p> <div class="ui stackable two column grid"> - <div v-for="parent in allScopes" class="column"> + <div + v-for="(parent, key) in allScopes" + :key="key" + class="column" + > <div class="ui parent checkbox"> <input + :id="parent.id" v-model="scopeArray" :value="parent.id" - :id="parent.id" - type="checkbox"> + type="checkbox" + > <label :for="parent.id"> {{ parent.label }} <p class="help"> @@ -43,13 +74,17 @@ </label> </div> - <div v-for="child in parent.children"> + <div + v-for="(child, index) in parent.children" + :key="index" + > <div class="ui child checkbox"> <input + :id="child.id" v-model="scopeArray" :value="child.id" - :id="child.id" - type="checkbox"> + type="checkbox" + > <label :for="child.id"> {{ child.id }} <p class="help"> @@ -60,29 +95,43 @@ </div> </div> </div> - - </div> - <button :class="['ui', {'loading': isLoading}, 'success', 'button']" type="submit"> - <translate v-if="updating" key="2" translate-context="Content/Applications/Button.Label/Verb">Update application</translate> - <translate v-else key="3" translate-context="Content/Applications/Button.Label/Verb">Create application</translate> + </div> + <button + :class="['ui', {'loading': isLoading}, 'success', 'button']" + type="submit" + > + <translate + v-if="updating" + key="2" + translate-context="Content/Applications/Button.Label/Verb" + > + Update application + </translate> + <translate + v-else + key="3" + translate-context="Content/Applications/Button.Label/Verb" + > + Create application + </translate> </button> </form> </template> <script> -import _ from "@/lodash" -import axios from "axios" -import TranslationsMixin from "@/components/mixins/Translations" +import _ from '@/lodash' +import axios from 'axios' +import TranslationsMixin from '@/components/mixins/Translations' export default { mixins: [TranslationsMixin], props: { - app: {type: Object, required: false}, - defaults: {type: Object, required: false} + app: { type: Object, required: false, default: () => { return {} } }, + defaults: { type: Object, required: false, default: () => { return {} } } }, - data() { - let app = this.app || {} - let defaults = this.defaults || {} + data () { + const app = this.app || {} + const defaults = this.defaults || {} return { isLoading: false, errors: [], @@ -92,45 +141,19 @@ export default { scopes: app.scopes || defaults.scopes || 'read' }, scopes: [ - {id: "profile", icon: 'user'}, - {id: "libraries", icon: 'book'}, - {id: "favorites", icon: 'heart'}, - {id: "listenings", icon: 'music'}, - {id: "follows", icon: 'users'}, - {id: "playlists", icon: 'list'}, - {id: "radios", icon: 'rss'}, - {id: "filters", icon: 'eye slash'}, - {id: "notifications", icon: 'bell'}, - {id: "edits", icon: 'pencil alternate'}, + { id: 'profile', icon: 'user' }, + { id: 'libraries', icon: 'book' }, + { id: 'favorites', icon: 'heart' }, + { id: 'listenings', icon: 'music' }, + { id: 'follows', icon: 'users' }, + { id: 'playlists', icon: 'list' }, + { id: 'radios', icon: 'rss' }, + { id: 'filters', icon: 'eye slash' }, + { id: 'notifications', icon: 'bell' }, + { id: 'edits', icon: 'pencil alternate' } ] } }, - methods: { - submit () { - this.errors = [] - let self = this - self.isLoading = true - let payload = this.fields - let event, promise, message - if (this.updating) { - event = 'updated' - promise = axios.patch(`oauth/apps/${this.app.client_id}/`, payload) - } else { - event = 'created' - promise = axios.post(`oauth/apps/`, payload) - } - return promise.then( - response => { - self.isLoading = false - self.$emit(event, response.data) - }, - error => { - self.isLoading = false - self.errors = error.backendErrors - } - ) - }, - }, computed: { updating () { return this.app @@ -144,8 +167,8 @@ export default { } }, allScopes () { - let self = this - let parents = [ + const self = this + const parents = [ { id: 'read', label: this.$pgettext('Content/OAuth Scopes/Label/Verb', 'Read'), @@ -157,19 +180,45 @@ export default { label: this.$pgettext('Content/OAuth Scopes/Label/Verb', 'Write'), description: this.$pgettext('Content/OAuth Scopes/Help Text', 'Write-only access to user data'), value: this.scopeArray.indexOf('write') > -1 - }, + } ] parents.forEach((p) => { p.children = self.scopes.map(s => { - let id = `${p.id}:${s.id}` + const id = `${p.id}:${s.id}` return { id, - value: this.scopeArray.indexOf(id) > -1, + value: this.scopeArray.indexOf(id) > -1 } }) }) return parents } + }, + methods: { + submit () { + this.errors = [] + const self = this + self.isLoading = true + const payload = this.fields + let event, promise + if (this.updating) { + event = 'updated' + promise = axios.patch(`oauth/apps/${this.app.client_id}/`, payload) + } else { + event = 'created' + promise = axios.post('oauth/apps/', payload) + } + return promise.then( + response => { + self.isLoading = false + self.$emit(event, response.data) + }, + error => { + self.isLoading = false + self.errors = error.backendErrors + } + ) + } } } </script> diff --git a/front/src/components/auth/ApplicationNew.vue b/front/src/components/auth/ApplicationNew.vue index 4c7bb903bbd9587179e9ba6ab1ff65efbdac068a..8ffa13fe3a3d24a62f0a662bd141ee490980623e 100644 --- a/front/src/components/auth/ApplicationNew.vue +++ b/front/src/components/auth/ApplicationNew.vue @@ -1,46 +1,58 @@ <template> - <main class="main pusher" v-title="labels.title"> + <main + v-title="labels.title" + class="main pusher" + > <div class="ui vertical stripe segment"> <section class="ui text container"> <router-link :to="{name: 'settings'}"> - <translate translate-context="Content/Applications/Link">Back to settings</translate> + <translate translate-context="Content/Applications/Link"> + Back to settings + </translate> </router-link> <h2 class="ui header"> - <translate translate-context="Content/Settings/Button.Label">Create a new application</translate> + <translate translate-context="Content/Settings/Button.Label"> + Create a new application + </translate> </h2> <application-form :defaults="defaults" - @created="$router.push({name: 'settings.applications.edit', params: {id: $event.client_id}})" /> + @created="$router.push({name: 'settings.applications.edit', params: {id: $event.client_id}})" + /> </section> </div> </main> </template> <script> -import ApplicationForm from "@/components/auth/ApplicationForm" +import ApplicationForm from '@/components/auth/ApplicationForm' export default { - props: ['name', 'redirect_uris', 'scopes'], components: { ApplicationForm }, - data() { + props: { + name: { type: String, required: true }, + redirectUris: { type: String, required: true }, + scopes: { type: Array, required: true } + }, + data () { return { application: null, isLoading: false, defaults: { name: this.name, - redirect_uris: this.redirect_uris, - scopes: this.scopes, + redirectUris: this.redirectUris, + scopes: this.scopes } } }, computed: { - labels() { + labels () { return { - title: this.$pgettext('Content/Settings/Button.Label', "Create a new application") + title: this.$pgettext('Content/Settings/Button.Label', 'Create a new application') } - }, + } } } </script> diff --git a/front/src/components/auth/Authorize.vue b/front/src/components/auth/Authorize.vue index 2b7c823c2979d46753f4c23232942e93e6bd2099..d86252009972f9dfea6d99c44c4d570feb8bc7c1 100644 --- a/front/src/components/auth/Authorize.vue +++ b/front/src/components/auth/Authorize.vue @@ -1,34 +1,91 @@ <template> - <main class="main pusher" v-title="labels.title"> + <main + v-title="labels.title" + class="main pusher" + > <section class="ui vertical stripe segment"> <div class="ui small text container"> - <h2><i class="lock open icon"></i><translate translate-context="Content/Auth/Title/Verb">Authorize third-party app</translate></h2> - <div v-if="errors.length > 0" role="alert" class="ui negative message"> - <h4 v-if="application" class="header"><translate translate-context="Popup/Moderation/Error message">Error while authorizing application</translate></h4> - <h4 v-else class="header"><translate translate-context="Popup/Moderation/Error message">Error while fetching application data</translate></h4> + <h2> + <i class="lock open icon" /><translate translate-context="Content/Auth/Title/Verb"> + Authorize third-party app + </translate> + </h2> + <div + v-if="errors.length > 0" + role="alert" + class="ui negative message" + > + <h4 + v-if="application" + class="header" + > + <translate translate-context="Popup/Moderation/Error message"> + Error while authorizing application + </translate> + </h4> + <h4 + v-else + class="header" + > + <translate translate-context="Popup/Moderation/Error message"> + Error while fetching application data + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> - <div v-if="isLoading" class="ui inverted active dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui inverted active dimmer" + > + <div class="ui loader" /> </div> - <form v-else-if="application && !code" :class="['ui', {loading: isLoading}, 'form']" @submit.prevent="submit"> - <h3><translate translate-context="Content/Auth/Title" :translate-params="{app: application.name}">%{ app } wants to access your Funkwhale account</translate></h3> + <form + v-else-if="application && !code" + :class="['ui', {loading: isLoading}, 'form']" + @submit.prevent="submit" + > + <h3> + <translate + translate-context="Content/Auth/Title" + :translate-params="{app: application.name}" + > + %{ app } wants to access your Funkwhale account + </translate> + </h3> - <h4 v-for="topic in topicScopes" class="ui header vertical-align"> - <span v-if="topic.write && !topic.read" :class="['ui', 'basic', 'right floated', 'tiny', 'vertically-spaced component-label label']"> - <i class="pencil icon"></i> + <h4 + v-for="(topic, key) in topicScopes" + :key="key" + class="ui header vertical-align" + > + <span + v-if="topic.write && !topic.read" + :class="['ui', 'basic', 'right floated', 'tiny', 'vertically-spaced component-label label']" + > + <i class="pencil icon" /> <translate translate-context="Content/Auth/Label/Noun">Write-only</translate> </span> - <span v-else-if="!topic.write && topic.read" :class="['ui', 'basic', 'right floated', 'tiny', 'vertically-spaced component-label label']"> + <span + v-else-if="!topic.write && topic.read" + :class="['ui', 'basic', 'right floated', 'tiny', 'vertically-spaced component-label label']" + > <translate translate-context="Content/Auth/Label/Noun">Read-only</translate> </span> - <span v-else-if="topic.write && topic.read" :class="['ui', 'basic', 'right floated', 'tiny', 'vertically-spaced component-label label']"> - <i class="pencil icon"></i> + <span + v-else-if="topic.write && topic.read" + :class="['ui', 'basic', 'right floated', 'tiny', 'vertically-spaced component-label label']" + > + <i class="pencil icon" /> <translate translate-context="Content/Auth/Label/Noun">Full access</translate> </span> - <i :class="[topic.icon, 'icon']"></i> + <i :class="[topic.icon, 'icon']" /> <div class="content"> {{ topic.label }} <div class="sub header"> @@ -38,23 +95,46 @@ </h4> <div v-if="unknownRequestedScopes.length > 0"> <p><strong><translate translate-context="Content/Auth/Paragraph">The application is also requesting the following unknown permissions:</translate></strong></p> - <ul v-for="scope in unknownRequestedScopes"> - <li>{{ scope }}</li> + <ul + v-for="(unknownscope, key) in unknownRequestedScopes" + :key="key" + > + <li>{{ unknownscope }}</li> </ul> - </div> - <button class="ui success labeled icon button" type="submit"> - <i class="lock open icon"></i> - <translate translate-context="Content/Signup/Button.Label/Verb" :translate-params="{app: application.name}">Authorize %{ app }</translate> + <button + class="ui success labeled icon button" + type="submit" + > + <i class="lock open icon" /> + <translate + translate-context="Content/Signup/Button.Label/Verb" + :translate-params="{app: application.name}" + > + Authorize %{ app } + </translate> </button> - <p v-if="redirectUri === 'urn:ietf:wg:oauth:2.0:oob'" key="1" v-translate translate-context="Content/Auth/Paragraph"> - You will be shown a code to copy-paste in the application.</p> - <p v-else key="2" v-translate="{url: redirectUri}" translate-context="Content/Auth/Paragraph" :translate-params="{url: redirectUri}">You will be redirected to <strong>%{ url }</strong></p> - + <p + v-if="redirectUri === 'urn:ietf:wg:oauth:2.0:oob'" + key="1" + v-translate + translate-context="Content/Auth/Paragraph" + > + You will be shown a code to copy-paste in the application. + </p> + <p + v-else + key="2" + v-translate="{url: redirectUri}" + translate-context="Content/Auth/Paragraph" + :translate-params="{url: redirectUri}" + > + You will be redirected to <strong>%{ url }</strong> + </p> </form> <div v-else-if="code"> <p><strong><translate translate-context="Content/Auth/Paragraph">Copy-paste the following code in the application:</translate></strong></p> - <copy-input :value="code"></copy-input> + <copy-input :value="code" /> </div> </div> </section> @@ -62,60 +142,54 @@ </template> <script> -import TranslationsMixin from "@/components/mixins/Translations" +import TranslationsMixin from '@/components/mixins/Translations' import axios from 'axios' -import {checkRedirectToLogin} from '@/utils' +import { checkRedirectToLogin } from '@/utils' export default { mixins: [TranslationsMixin], - props: [ - 'clientId', - 'redirectUri', - 'scope', - 'responseType', - 'nonce', - 'state', - ], - data() { + props: { + clientId: { type: String, required: true }, + redirectUri: { type: String, required: true }, + scope: { type: String, required: true }, + responseType: { type: String, required: true }, + nonce: { type: String, required: true }, + state: { type: String, required: true } + }, + data () { return { application: null, isLoading: false, errors: [], code: null, knownScopes: [ - {id: "profile", icon: 'user'}, - {id: "libraries", icon: 'book'}, - {id: "favorites", icon: 'heart'}, - {id: "listenings", icon: 'music'}, - {id: "follows", icon: 'users'}, - {id: "playlists", icon: 'list'}, - {id: "radios", icon: 'rss'}, - {id: "filters", icon: 'eye slash'}, - {id: "notifications", icon: 'bell'}, - {id: "edits", icon: 'pencil alternate'}, - {id: "security", icon: 'lock'}, - {id: "reports", icon: 'warning sign'}, + { id: 'profile', icon: 'user' }, + { id: 'libraries', icon: 'book' }, + { id: 'favorites', icon: 'heart' }, + { id: 'listenings', icon: 'music' }, + { id: 'follows', icon: 'users' }, + { id: 'playlists', icon: 'list' }, + { id: 'radios', icon: 'rss' }, + { id: 'filters', icon: 'eye slash' }, + { id: 'notifications', icon: 'bell' }, + { id: 'edits', icon: 'pencil alternate' }, + { id: 'security', icon: 'lock' }, + { id: 'reports', icon: 'warning sign' } ] } }, - created () { - checkRedirectToLogin(this.$store, this.$router) - if (this.clientId) { - this.fetchApplication() - } - }, computed: { labels () { return { - title: this.$pgettext('Head/Authorize/Title', "Allow application") + title: this.$pgettext('Head/Authorize/Title', 'Allow application') } }, requestedScopes () { return (this.scope || '').split(' ') }, supportedScopes () { - let supported = ['read', 'write'] + const supported = ['read', 'write'] this.knownScopes.forEach(s => { supported.push(`read:${s.id}`) supported.push(`write:${s.id}`) @@ -123,14 +197,14 @@ export default { return supported }, unknownRequestedScopes () { - let self = this + const self = this return this.requestedScopes.filter(s => { return self.supportedScopes.indexOf(s) < 0 }) }, topicScopes () { - let self = this - let requested = this.requestedScopes + const self = this + const requested = this.requestedScopes let write = false let read = false if (requested.indexOf('read') > -1) { @@ -141,24 +215,30 @@ export default { } return this.knownScopes.map(s => { - let id = s.id + const id = s.id return { id: id, icon: s.icon, label: self.sharedLabels.scopes[s.id].label, description: self.sharedLabels.scopes[s.id].description, read: read || requested.indexOf(`read:${id}`) > -1, - write: write || requested.indexOf(`write:${id}`) > -1, + write: write || requested.indexOf(`write:${id}`) > -1 } }).filter(c => { return c.read || c.write }) } }, + created () { + checkRedirectToLogin(this.$store, this.$router) + if (this.clientId) { + this.fetchApplication() + } + }, methods: { fetchApplication () { this.isLoading = true - let self = this + const self = this axios.get(`oauth/apps/${this.clientId}/`).then((response) => { self.isLoading = false self.application = response.data @@ -169,8 +249,8 @@ export default { }, submit () { this.isLoading = true - let self = this - let data = new FormData(); + const self = this + const data = new FormData() data.set('redirect_uri', this.redirectUri) data.set('scope', this.scope) data.set('allow', true) @@ -178,7 +258,7 @@ export default { data.set('response_type', this.responseType) data.set('state', this.state) data.set('nonce', this.nonce) - axios.post(`oauth/authorize/`, data, {headers: {'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest'}}).then((response) => { + axios.post('oauth/authorize/', data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest' } }).then((response) => { if (self.redirectUri === 'urn:ietf:wg:oauth:2.0:oob') { self.isLoading = false self.code = response.data.code diff --git a/front/src/components/auth/LoginForm.vue b/front/src/components/auth/LoginForm.vue index b7515c4277fbd2f752b65e7bfeeb9a41c1e8c532..cecade905b43cf00fb78d748215e59cec2baa47e 100644 --- a/front/src/components/auth/LoginForm.vue +++ b/front/src/components/auth/LoginForm.vue @@ -1,18 +1,35 @@ <template> - <form class="ui form" @submit.prevent="submit()"> - <div v-if="error" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Content/Login/Error message.Title">We cannot log you in</translate></h4> + <form + class="ui form" + @submit.prevent="submit()" + > + <div + v-if="error" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Content/Login/Error message.Title"> + We cannot log you in + </translate> + </h4> <ul class="list"> <li v-if="error == 'invalid_credentials' && $store.state.instance.settings.moderation.signup_approval_enabled.value"> - <translate translate-context="Content/Login/Error message.List item/Call to action">If you signed-up recently, you may need to wait before our moderation team review your account, or verify your e-mail address.</translate> + <translate translate-context="Content/Login/Error message.List item/Call to action"> + If you signed-up recently, you may need to wait before our moderation team review your account, or verify your e-mail address. + </translate> </li> <li v-else-if="error == 'invalid_credentials'"> - <translate translate-context="Content/Login/Error message.List item/Call to action">Please double-check that your username and password combination is correct and make sure you verified your e-mail address.</translate> + <translate translate-context="Content/Login/Error message.List item/Call to action"> + Please double-check that your username and password combination is correct and make sure you verified your e-mail address. + </translate> + </li> + <li v-else> + {{ error }} </li> - <li v-else>{{ error }}</li> </ul> </div> - <template v-if="$store.getters['instance/appDomain'] === $store.getters['instance/domain']" > + <template v-if="$store.getters['instance/appDomain'] === $store.getters['instance/domain']"> <div class="field"> <label for="username-field"> <translate translate-context="Content/Login/Input.Label/Noun">Username or e-mail address</translate> @@ -24,14 +41,14 @@ </template> </label> <input - ref="username" - required - name="username" - type="text" - id="username-field" - autofocus - :placeholder="labels.usernamePlaceholder" - v-model="credentials.username" + id="username-field" + ref="username" + v-model="credentials.username" + required + name="username" + type="text" + autofocus + :placeholder="labels.usernamePlaceholder" > </div> <div class="field"> @@ -41,65 +58,78 @@ <translate translate-context="*/Login/*/Verb">Reset your password</translate> </router-link> </label> - <password-input field-id="password-field" required v-model="credentials.password" /> - + <password-input + v-model="credentials.password" + field-id="password-field" + required + /> </div> </template> <template v-else> <p> - <translate translate-context="Contant/Auth/Paragraph" :translate-params="{domain: $store.getters['instance/domain']}">You will be redirected to %{ domain } to authenticate.</translate> + <translate + translate-context="Contant/Auth/Paragraph" + :translate-params="{domain: $store.getters['instance/domain']}" + > + You will be redirected to %{ domain } to authenticate. + </translate> </p> </template> - <button :class="['ui', {'loading': isLoading}, 'right', 'floated', buttonClasses, 'button']" type="submit"> - <translate translate-context="*/Login/*/Verb">Login</translate> + <button + :class="['ui', {'loading': isLoading}, 'right', 'floated', buttonClasses, 'button']" + type="submit" + > + <translate translate-context="*/Login/*/Verb"> + Login + </translate> </button> </form> </template> <script> -import PasswordInput from "@/components/forms/PasswordInput" +import PasswordInput from '@/components/forms/PasswordInput' export default { - props: { - next: { type: String, default: "/library" }, - buttonClasses: { type: String, default: "success" }, - showSignup: { type: Boolean, default: true}, - }, components: { PasswordInput }, - data() { + props: { + next: { type: String, default: '/library' }, + buttonClasses: { type: String, default: 'success' }, + showSignup: { type: Boolean, default: true } + }, + data () { return { // We need to initialize the component with any // properties that will be used in it credentials: { - username: "", - password: "" + username: '', + password: '' }, - error: "", + error: '', isLoading: false } }, + computed: { + labels () { + const usernamePlaceholder = this.$pgettext('Content/Login/Input.Placeholder', 'Enter your username or e-mail address') + return { + usernamePlaceholder + } + } + }, created () { if (this.$store.state.auth.authenticated) { this.$router.push(this.next) } }, - mounted() { + mounted () { if (this.$refs.username) { this.$refs.username.focus() } }, - computed: { - labels() { - let usernamePlaceholder = this.$pgettext('Content/Login/Input.Placeholder', "Enter your username or e-mail address") - return { - usernamePlaceholder, - } - } - }, methods: { - async submit() { + async submit () { if (this.$store.getters['instance/appDomain'] === this.$store.getters['instance/domain']) { return await this.submitSession() } else { @@ -107,21 +137,21 @@ export default { await this.$store.dispatch('auth/oauthLogin', this.next) } }, - async submitSession() { - var self = this + async submitSession () { + const self = this self.isLoading = true - this.error = "" - var credentials = { + this.error = '' + const credentials = { username: this.credentials.username, password: this.credentials.password } this.$store - .dispatch("auth/login", { + .dispatch('auth/login', { credentials, next: this.next, onError: error => { if (error.response.status === 400) { - self.error = "invalid_credentials" + self.error = 'invalid_credentials' } else { self.error = error.backendErrors[0] } diff --git a/front/src/components/auth/Logout.vue b/front/src/components/auth/Logout.vue index cee056b7f56ddf241a623cff06b191d3e9881ba4..aa1a98041d8cc104f604858fdcea93a77fc4bb14 100644 --- a/front/src/components/auth/Logout.vue +++ b/front/src/components/auth/Logout.vue @@ -1,18 +1,50 @@ <template> - <main class="main pusher" v-title="labels.title"> + <main + v-title="labels.title" + class="main pusher" + > <section class="ui vertical stripe segment"> - <div v-if="$store.state.auth.authenticated" class="ui small text container"> + <div + v-if="$store.state.auth.authenticated" + class="ui small text container" + > <h2> - <translate translate-context="Content/Login/Title">Are you sure you want to log out?</translate> + <translate translate-context="Content/Login/Title"> + Are you sure you want to log out? + </translate> </h2> - <p v-translate="{username: $store.state.auth.username}" translate-context="Content/Login/Paragraph">You are currently logged in as %{ username }</p> - <button class="ui button" @click="$store.dispatch('auth/logout')"><translate translate-context="Content/Login/Button.Label">Yes, log me out!</translate></button> + <p + v-translate="{username: $store.state.auth.username}" + translate-context="Content/Login/Paragraph" + > + You are currently logged in as %{ username } + </p> + <button + class="ui button" + @click="$store.dispatch('auth/logout')" + > + <translate translate-context="Content/Login/Button.Label"> + Yes, log me out! + </translate> + </button> </div> - <div v-else class="ui small text container"> + <div + v-else + class="ui small text container" + > <h2> - <translate translate-context="Content/Login/Title">You aren't currently logged in</translate> + <translate translate-context="Content/Login/Title"> + You aren't currently logged in + </translate> </h2> - <router-link to='/login' class="ui button"><translate translate-context="Content/Login/Button.Label">Log in!</translate></router-link> + <router-link + to="/login" + class="ui button" + > + <translate translate-context="Content/Login/Button.Label"> + Log in! + </translate> + </router-link> </div> </section> </main> @@ -21,9 +53,9 @@ <script> export default { computed: { - labels() { + labels () { return { - title: this.$pgettext('Head/Login/Title', "Log Out") + title: this.$pgettext('Head/Login/Title', 'Log Out') } } } diff --git a/front/src/components/auth/Plugin.vue b/front/src/components/auth/Plugin.vue index 571cd7f558110c3a443b543684ff6d24c5c434a6..5967f013bf3b8c7ed3daceb7a81b4af2e7c50db1 100644 --- a/front/src/components/auth/Plugin.vue +++ b/front/src/components/auth/Plugin.vue @@ -1,96 +1,193 @@ <template> - <form :class="['ui segment form', {loading: isLoading}]" @submit.prevent="submit"> + <form + :class="['ui segment form', {loading: isLoading}]" + @submit.prevent="submit" + > <h3>{{ plugin.label }}</h3> - <div v-if="plugin.description" v-html="markdown.makeHtml(plugin.description)"></div> - <template v-if="plugin.homepage" > - <div class="ui small hidden divider"></div> - <a :href="plugin.homepage" target="_blank"> - <i class="external icon"></i> + <div + v-if="plugin.description" + v-html="markdown.makeHtml(plugin.description)" + /> + <template v-if="plugin.homepage"> + <div class="ui small hidden divider" /> + <a + :href="plugin.homepage" + target="_blank" + > + <i class="external icon" /> <translate translate-context="Footer/*/List item.Link/Short, Noun">Documentation</translate> </a> </template> - <div class="ui clearing hidden divider"></div> - <div v-if="errors.length > 0" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Content/*/Error message.Title">Error while saving plugin</translate></h4> + <div class="ui clearing hidden divider" /> + <div + v-if="errors.length > 0" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Content/*/Error message.Title"> + Error while saving plugin + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> <div class="field"> <div class="ui toggle checkbox"> - <input :id="`${plugin.name}-enabled`" type="checkbox" v-model="enabled" /> + <input + :id="`${plugin.name}-enabled`" + v-model="enabled" + type="checkbox" + > <label :for="`${plugin.name}-enabled`"><translate translate-context="*/*/*">Enabled</translate></label> </div> </div> - <div class="ui clearing hidden divider"></div> - <div v-if="plugin.source" class="field"> + <div class="ui clearing hidden divider" /> + <div + v-if="plugin.source" + class="field" + > <label for="plugin-library"><translate translate-context="*/*/*/Noun">Library</translate></label> - <select id="plugin-library" v-model="values['library']"> - <option :value="l.uuid" v-for="l in libraries" :key="l.uuid">{{ l.name }}</option> + <select + id="plugin-library" + v-model="values['library']" + > + <option + v-for="l in libraries" + :key="l.uuid" + :value="l.uuid" + > + {{ l.name }} + </option> </select> <div> - <translate translate-context="*/*/Paragraph/Noun">Library where files should be imported.</translate> + <translate translate-context="*/*/Paragraph/Noun"> + Library where files should be imported. + </translate> </div> </div> - <template v-if="plugin.conf && plugin.conf.length > 0" v-for="field in plugin.conf"> - <div v-if="field.type === 'text'" class="field"> + <template + v-for="(field, key) in plugin.conf" + v-if="plugin.conf && plugin.conf.length > 0" + > + <div + v-if="field.type === 'text'" + :key="key" + class="field" + > <label :for="`plugin-${field.name}`">{{ field.label || field.name }}</label> - <input :id="`plugin-${field.name}`" type="text" v-model="values[field.name]"> - <div v-if="field.help" v-html="markdown.makeHtml(field.help)"></div> + <input + :id="`plugin-${field.name}`" + v-model="values[field.name]" + type="text" + > + <div + v-if="field.help" + v-html="markdown.makeHtml(field.help)" + /> </div> - <div v-if="field.type === 'long_text'" class="field"> + <div + v-if="field.type === 'long_text'" + :key="key" + class="field" + > <label :for="`plugin-${field.name}`">{{ field.label || field.name }}</label> - <textarea :id="`plugin-${field.name}`" type="text" v-model="values[field.name]" rows="5" /> - <div v-if="field.help" v-html="markdown.makeHtml(field.help)"></div> + <textarea + :id="`plugin-${field.name}`" + v-model="values[field.name]" + type="text" + rows="5" + /> + <div + v-if="field.help" + v-html="markdown.makeHtml(field.help)" + /> </div> - <div v-if="field.type === 'url'" class="field"> + <div + v-if="field.type === 'url'" + :key="key" + class="field" + > <label :for="`plugin-${field.name}`">{{ field.label || field.name }}</label> - <input :id="`plugin-${field.name}`" type="url" v-model="values[field.name]"> - <div v-if="field.help" v-html="markdown.makeHtml(field.help)"></div> + <input + :id="`plugin-${field.name}`" + v-model="values[field.name]" + type="url" + > + <div + v-if="field.help" + v-html="markdown.makeHtml(field.help)" + /> </div> - <div v-if="field.type === 'password'" class="field"> + <div + v-if="field.type === 'password'" + :key="key" + class="field" + > <label :for="`plugin-${field.name}`">{{ field.label || field.name }}</label> - <input :id="`plugin-${field.name}`" type="password" v-model="values[field.name]"> - <div v-if="field.help" v-html="markdown.makeHtml(field.help)"></div> + <input + :id="`plugin-${field.name}`" + v-model="values[field.name]" + type="password" + > + <div + v-if="field.help" + v-html="markdown.makeHtml(field.help)" + /> </div> </template> <button type="submit" - :class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']"> - <translate translate-context="Content/*/Button.Label/Verb">Save</translate> + :class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']" + > + <translate translate-context="Content/*/Button.Label/Verb"> + Save + </translate> </button> <button - type="scan" v-if="plugin.source" + type="scan" + :class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']" @click.prevent="submitAndScan" - :class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']"> - <translate translate-context="Content/*/Button.Label/Verb">Scan</translate> + > + <translate translate-context="Content/*/Button.Label/Verb"> + Scan + </translate> </button> - <div class="ui clearing hidden divider"></div> + <div class="ui clearing hidden divider" /> </form> </template> <script> -import axios from "axios" +import axios from 'axios' import lodash from '@/lodash' import showdown from 'showdown' export default { - props: ['plugin', "libraries"], + props: { + plugin: { type: Object, required: true }, + libraries: { type: Array, required: true } + }, data () { return { markdown: new showdown.Converter(), isLoading: false, enabled: this.plugin.enabled, values: lodash.clone(this.plugin.values || {}), - errors: [], + errors: [] } }, methods: { async submit () { this.isLoading = true this.errors = [] - let url = `plugins/${this.plugin.name}` - let enableUrl = this.enabled ? `${url}/enable` : `${url}/disable` + const url = `plugins/${this.plugin.name}` + const enableUrl = this.enabled ? `${url}/enable` : `${url}/disable` await axios.post(enableUrl) try { await axios.post(url, this.values) @@ -101,19 +198,19 @@ export default { }, async scan () { this.isLoading = true - this.errors = [] - let url = `plugins/${this.plugin.name}/scan` - try { - await axios.post(url, this.values) - } catch (e) { - this.errors = e.backendErrors - } - this.isLoading = false + this.errors = [] + const url = `plugins/${this.plugin.name}/scan` + try { + await axios.post(url, this.values) + } catch (e) { + this.errors = e.backendErrors + } + this.isLoading = false }, async submitAndScan () { await this.submit() await this.scan() } - }, + } } </script> diff --git a/front/src/components/auth/SignupForm.vue b/front/src/components/auth/SignupForm.vue index 35bb8df7b172457a857eee57d69b3558250917b4..0711178ae761b1b785074bea43d905fd986cf930 100644 --- a/front/src/components/auth/SignupForm.vue +++ b/front/src/components/auth/SignupForm.vue @@ -2,140 +2,197 @@ <div v-if="submitted"> <div class="ui success message"> <p v-if="signupRequiresApproval"> - <translate translate-context="Content/Signup/Form/Paragraph">Your account request was successfully submitted. You will be notified by e-mail when our moderation team has reviewed your request.</translate> + <translate translate-context="Content/Signup/Form/Paragraph"> + Your account request was successfully submitted. You will be notified by e-mail when our moderation team has reviewed your request. + </translate> </p> <p v-else> - <translate translate-context="Content/Signup/Form/Paragraph">Your account was successfully created. Please verify your e-mail address before trying to login.</translate> + <translate translate-context="Content/Signup/Form/Paragraph"> + Your account was successfully created. Please verify your e-mail address before trying to login. + </translate> </p> </div> - <h2><translate translate-context="Content/Login/Title/Verb">Log in to your Funkwhale account</translate></h2> - <login-form button-classes="basic success" :show-signup="false"></login-form> + <h2> + <translate translate-context="Content/Login/Title/Verb"> + Log in to your Funkwhale account + </translate> + </h2> + <login-form + button-classes="basic success" + :show-signup="false" + /> </div> <form v-else :class="['ui', {'loading': isLoadingInstanceSetting}, 'form']" - @submit.prevent="submit()"> - <p class="ui message" v-if="!$store.state.instance.settings.users.registration_enabled.value"> - <translate translate-context="Content/Signup/Form/Paragraph">Public registrations are not possible on this instance. You will need an invitation code to sign up.</translate> + @submit.prevent="submit()" + > + <p + v-if="!$store.state.instance.settings.users.registration_enabled.value" + class="ui message" + > + <translate translate-context="Content/Signup/Form/Paragraph"> + Public registrations are not possible on this instance. You will need an invitation code to sign up. + </translate> </p> - <p class="ui message" v-else-if="signupRequiresApproval"> - <translate translate-context="Content/Signup/Form/Paragraph">Registrations on this pod are open, but reviewed by moderators before approval.</translate> + <p + v-else-if="signupRequiresApproval" + class="ui message" + > + <translate translate-context="Content/Signup/Form/Paragraph"> + Registrations on this pod are open, but reviewed by moderators before approval. + </translate> </p> <template v-if="formCustomization && formCustomization.help_text"> - <rendered-description :content="formCustomization.help_text" :fetch-html="fetchDescriptionHtml" :permissive="true"></rendered-description> - <div class="ui hidden divider"></div> + <rendered-description + :content="formCustomization.help_text" + :fetch-html="fetchDescriptionHtml" + :permissive="true" + /> + <div class="ui hidden divider" /> </template> - <div v-if="errors.length > 0" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Content/Signup/Form/Paragraph">Your account cannot be created.</translate></h4> + <div + v-if="errors.length > 0" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Content/Signup/Form/Paragraph"> + Your account cannot be created. + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> <div class="required field"> <label for="username-field"><translate translate-context="Content/*/*">Username</translate></label> <input - ref="username" - name="username" - required - id="username-field" - type="text" - autofocus - :placeholder="labels.usernamePlaceholder" - v-model="username"> + id="username-field" + ref="username" + v-model="username" + name="username" + required + type="text" + autofocus + :placeholder="labels.usernamePlaceholder" + > </div> <div class="required field"> <label for="email-field"><translate translate-context="Content/*/*/Noun">E-mail address</translate></label> <input - id="email-field" - ref="email" - name="email" - required - type="email" - :placeholder="labels.emailPlaceholder" - v-model="email"> + id="email-field" + ref="email" + v-model="email" + name="email" + required + type="email" + :placeholder="labels.emailPlaceholder" + > </div> <div class="required field"> <label for="password-field"><translate translate-context="*/*/*">Password</translate></label> - <password-input field-id="password-field" v-model="password" /> + <password-input + v-model="password" + field-id="password-field" + /> </div> - <div class="required field" v-if="!$store.state.instance.settings.users.registration_enabled.value"> + <div + v-if="!$store.state.instance.settings.users.registration_enabled.value" + class="required field" + > <label for="invitation-code"><translate translate-context="Content/*/Input.Label">Invitation code</translate></label> <input - id="invitation-code" - required - type="text" - name="invitation" - :placeholder="labels.placeholder" - v-model="invitation"> + id="invitation-code" + v-model="invitation" + required + type="text" + name="invitation" + :placeholder="labels.placeholder" + > </div> <template v-if="signupRequiresApproval && formCustomization && formCustomization.fields && formCustomization.fields.length > 0"> - <div :class="[{required: field.required}, 'field']" v-for="(field, idx) in formCustomization.fields" :key="idx"> + <div + v-for="(field, idx) in formCustomization.fields" + :key="idx" + :class="[{required: field.required}, 'field']" + > <label :for="`custom-field-${idx}`">{{ field.label }}</label> <textarea v-if="field.input_type === 'long_text'" :id="`custom-field-${idx}`" :value="customFields[field.label]" :required="field.required" - @input="$set(customFields, field.label, $event.target.value)" rows="5"></textarea> - <input v-else :id="`custom-field-${idx}`" type="text" :value="customFields[field.label]" :required="field.required" @input="$set(customFields, field.label, $event.target.value)"> + rows="5" + @input="$set(customFields, field.label, $event.target.value)" + /> + <input + v-else + :id="`custom-field-${idx}`" + type="text" + :value="customFields[field.label]" + :required="field.required" + @input="$set(customFields, field.label, $event.target.value)" + > </div> </template> - <button :class="['ui', buttonClasses, {'loading': isLoading}, ' right floated button']" type="submit"> - <translate translate-context="Content/Signup/Button.Label">Create my account</translate> + <button + :class="['ui', buttonClasses, {'loading': isLoading}, ' right floated button']" + type="submit" + > + <translate translate-context="Content/Signup/Button.Label"> + Create my account + </translate> </button> </form> </template> <script> -import axios from "axios" -import logger from "@/logging" +import axios from 'axios' +import logger from '@/logging' -import LoginForm from "@/components/auth/LoginForm" -import PasswordInput from "@/components/forms/PasswordInput" +import LoginForm from '@/components/auth/LoginForm' +import PasswordInput from '@/components/forms/PasswordInput' export default { - props: { - defaultInvitation: { type: String, required: false, default: null }, - next: { type: String, default: "/" }, - buttonClasses: { type: String, default: "success" }, - customization: { type: Object, default: null}, - fetchDescriptionHtml: { type: Boolean, default: false}, - fetchDescriptionHtml: { type: Boolean, default: false}, - signupApprovalEnabled: {type: Boolean, default: null, required: false}, - }, components: { LoginForm, - PasswordInput, + PasswordInput + }, + 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() { + data () { return { - username: "", - email: "", - password: "", + username: '', + email: '', + password: '', isLoadingInstanceSetting: true, errors: [], isLoading: false, invitation: this.defaultInvitation, customFields: {}, - submitted: false, + submitted: false } }, - created() { - let self = this - this.$store.dispatch("instance/fetchSettings", { - callback: function() { - self.isLoadingInstanceSetting = false - } - }) - }, computed: { - labels() { - let placeholder = this.$pgettext( - "Content/Signup/Form/Placeholder", - "Enter your invitation code (case insensitive)" + labels () { + const placeholder = this.$pgettext( + 'Content/Signup/Form/Placeholder', + 'Enter your invitation code (case insensitive)' ) - let usernamePlaceholder = this.$pgettext("Content/Signup/Form/Placeholder", "Enter your username") - let emailPlaceholder = this.$pgettext("Content/Signup/Form/Placeholder", "Enter your e-mail address") + const usernamePlaceholder = this.$pgettext('Content/Signup/Form/Placeholder', 'Enter your username') + const emailPlaceholder = this.$pgettext('Content/Signup/Form/Placeholder', 'Enter your e-mail address') return { usernamePlaceholder, emailPlaceholder, @@ -152,22 +209,30 @@ export default { return this.signupApprovalEnabled } }, + created () { + const self = this + this.$store.dispatch('instance/fetchSettings', { + callback: function () { + self.isLoadingInstanceSetting = false + } + }) + }, methods: { - submit() { - var self = this + submit () { + const self = this self.isLoading = true this.errors = [] - var payload = { + const payload = { username: this.username, password1: this.password, password2: this.password, email: this.email, invitation: this.invitation, - request_fields: this.customFields, + request_fields: this.customFields } - return axios.post("auth/registration/", payload).then( + return axios.post('auth/registration/', payload).then( response => { - logger.default.info("Successfully created account") + logger.default.info('Successfully created account') self.submitted = true self.isLoading = false }, diff --git a/front/src/components/auth/SubsonicTokenForm.vue b/front/src/components/auth/SubsonicTokenForm.vue index 4659bfac770842a5a1a0fa0e7641b8faec04b4bc..fce863249491609091c5023485b4d8e22b091fbd 100644 --- a/front/src/components/auth/SubsonicTokenForm.vue +++ b/front/src/components/auth/SubsonicTokenForm.vue @@ -1,61 +1,144 @@ <template> - <form class="ui form" @submit.prevent="requestNewToken()"> - <h2><translate translate-context="Content/Settings/Title">Subsonic API password</translate></h2> - <p class="ui message" v-if="!subsonicEnabled"> - <translate translate-context="Content/Settings/Paragraph">The Subsonic API is not available on this Funkwhale instance.</translate> + <form + class="ui form" + @submit.prevent="requestNewToken()" + > + <h2> + <translate translate-context="Content/Settings/Title"> + Subsonic API password + </translate> + </h2> + <p + v-if="!subsonicEnabled" + class="ui message" + > + <translate translate-context="Content/Settings/Paragraph"> + The Subsonic API is not available on this Funkwhale instance. + </translate> </p> <p> - <translate translate-context="Content/Settings/Paragraph'">Funkwhale is compatible with other music players that support the Subsonic API.</translate> <translate translate-context="Content/Settings/Paragraph">You can use those to enjoy your playlist and music in offline mode, on your smartphone or tablet, for instance.</translate> + <translate translate-context="Content/Settings/Paragraph'"> + Funkwhale is compatible with other music players that support the Subsonic API. + </translate> <translate translate-context="Content/Settings/Paragraph"> + You can use those to enjoy your playlist and music in offline mode, on your smartphone or tablet, for instance. + </translate> </p> <p> - <translate translate-context="Content/Settings/Paragraph">However, accessing Funkwhale from those clients requires a separate password you can set below.</translate> + <translate translate-context="Content/Settings/Paragraph"> + However, accessing Funkwhale from those clients requires a separate password you can set below. + </translate> </p> - <p><a href="https://docs.funkwhale.audio/users/apps.html#subsonic-compatible-clients" target="_blank"> - <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> + <p> + <a + href="https://docs.funkwhale.audio/users/apps.html#subsonic-compatible-clients" + target="_blank" + > + <translate translate-context="Content/Settings/Link">Discover how to use Funkwhale from other apps</translate> + </a> + </p> + <div + v-if="success" + class="ui positive message" + > + <h4 class="header"> + {{ successMessage }} + </h4> </div> - <div v-if="subsonicEnabled && errors.length > 0" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Content/*/Error message.Title">Error</translate></h4> + <div + v-if="subsonicEnabled && errors.length > 0" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Content/*/Error message.Title"> + Error + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> <template v-if="subsonicEnabled"> - <div v-if="token" class="field"> - <label for="subsonic-password" class="visually-hidden">{{ labels.subsonicField }}</label> + <div + v-if="token" + class="field" + > + <label + for="subsonic-password" + class="visually-hidden" + >{{ labels.subsonicField }}</label> <password-input - field-id="subsonic-password" ref="passwordInput" - v-model="token" :key="token" + v-model="token" + field-id="subsonic-password" :copy-button="true" - :default-show="showToken"/> + :default-show="showToken" + /> </div> <dangerous-button v-if="token" :class="['ui', {'loading': isLoading}, 'button']" - :action="requestNewToken"> - <translate translate-context="*/Settings/Button.Label/Verb">Request a new password</translate> - <p slot="modal-header"><translate translate-context="Popup/Settings/Title">Request a new Subsonic API password?</translate></p> - <p slot="modal-content"><translate translate-context="Popup/Settings/Paragraph">This will log you out from existing devices that use the current password.</translate></p> - <div slot="modal-confirm"><translate translate-context="*/Settings/Button.Label/Verb">Request a new password</translate></div> + :action="requestNewToken" + > + <translate translate-context="*/Settings/Button.Label/Verb"> + Request a new password + </translate> + <p slot="modal-header"> + <translate translate-context="Popup/Settings/Title"> + Request a new Subsonic API password? + </translate> + </p> + <p slot="modal-content"> + <translate translate-context="Popup/Settings/Paragraph"> + This will log you out from existing devices that use the current password. + </translate> + </p> + <div slot="modal-confirm"> + <translate translate-context="*/Settings/Button.Label/Verb"> + Request a new password + </translate> + </div> </dangerous-button> <button v-else color="" :class="['ui', {'loading': isLoading}, 'button']" - @click="requestNewToken"><translate translate-context="Content/Settings/Button.Label/Verb">Request a password</translate></button> - <dangerous-button - v-if="token" - :class="['ui', {'loading': isLoading}, 'warning', 'button']" - :action="disable"> - <translate translate-context="Content/Settings/Button.Label/Verb">Disable Subsonic access</translate> - <p slot="modal-header"><translate translate-context="Popup/Settings/Title">Disable Subsonic API access?</translate></p> - <p slot="modal-content"><translate translate-context="Popup/Settings/Paragraph">This will completely disable access to the Subsonic API using from account.</translate></p> - <div slot="modal-confirm"><translate translate-context="Popup/Settings/Button.Label">Disable access</translate></div> - </dangerous-button> + @click="requestNewToken" + > + <translate translate-context="Content/Settings/Button.Label/Verb"> + Request a password + </translate> + </button> + <dangerous-button + v-if="token" + :class="['ui', {'loading': isLoading}, 'warning', 'button']" + :action="disable" + > + <translate translate-context="Content/Settings/Button.Label/Verb"> + Disable Subsonic access + </translate> + <p slot="modal-header"> + <translate translate-context="Popup/Settings/Title"> + Disable Subsonic API access? + </translate> + </p> + <p slot="modal-content"> + <translate translate-context="Popup/Settings/Paragraph"> + This will completely disable access to the Subsonic API using from account. + </translate> + </p> + <div slot="modal-confirm"> + <translate translate-context="Popup/Settings/Button.Label"> + Disable access + </translate> + </div> + </dangerous-button> </template> </form> </template> @@ -78,6 +161,16 @@ export default { showToken: false } }, + computed: { + subsonicEnabled () { + return this.$store.state.instance.settings.subsonic.enabled.value + }, + labels () { + return { + subsonicField: this.$pgettext('Content/Password/Input.label', 'Your subsonic API password') + } + } + }, created () { this.fetchToken() }, @@ -86,10 +179,10 @@ export default { this.success = false this.errors = [] this.isLoading = true - let self = this - let url = `users/${this.$store.state.auth.username}/subsonic-token/` + const self = this + const url = `users/${this.$store.state.auth.username}/subsonic-token/` return axios.get(url).then(response => { - self.token = response.data['subsonic_api_token'] + self.token = response.data.subsonic_api_token self.isLoading = false }, error => { self.isLoading = false @@ -101,11 +194,11 @@ export default { this.success = false this.errors = [] this.isLoading = true - let self = this - let url = `users/${this.$store.state.auth.username}/subsonic-token/` + const self = this + const url = `users/${this.$store.state.auth.username}/subsonic-token/` return axios.post(url, {}).then(response => { self.showToken = true - self.token = response.data['subsonic_api_token'] + self.token = response.data.subsonic_api_token self.isLoading = false self.success = true }, error => { @@ -118,8 +211,8 @@ export default { this.success = false this.errors = [] this.isLoading = true - let self = this - let url = `users/${this.$store.state.auth.username}/subsonic-token/` + const self = this + const url = `users/${this.$store.state.auth.username}/subsonic-token/` return axios.delete(url).then(response => { self.isLoading = false self.token = null @@ -129,16 +222,6 @@ export default { self.errors = error.backendErrors }) } - }, - computed: { - subsonicEnabled () { - return this.$store.state.instance.settings.subsonic.enabled.value - }, - labels () { - return { - subsonicField: this.$pgettext("Content/Password/Input.label", "Your subsonic API password") - } - } } } </script> diff --git a/front/src/components/channels/AlbumForm.vue b/front/src/components/channels/AlbumForm.vue index 79044de7808fe8ccb9efb97ad3fadb6365621258..1e042b5cd15ec354d36299877258058ea0c8f7de 100644 --- a/front/src/components/channels/AlbumForm.vue +++ b/front/src/components/channels/AlbumForm.vue @@ -1,16 +1,35 @@ <template> - <form @submit.stop.prevent :class="['ui', {loading: isLoading}, 'form']"> - <div v-if="errors.length > 0" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Content/*/Error message.Title">Error while creating</translate></h4> + <form + :class="['ui', {loading: isLoading}, 'form']" + @submit.stop.prevent + > + <div + v-if="errors.length > 0" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Content/*/Error message.Title"> + Error while creating + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> <div class="ui required field"> <label for="album-title"> <translate translate-context="*/*/*/Noun">Title</translate> </label> - <input type="text" v-model="values.title"> + <input + v-model="values.title" + type="text" + > </div> </form> </template> @@ -18,17 +37,17 @@ import axios from 'axios' export default { + components: {}, props: { - channel: {type: Object, required: true}, + channel: { type: Object, required: true } }, - components: {}, data () { return { errors: [], isLoading: false, values: { - title: "", - }, + title: '' + } } }, computed: { @@ -36,20 +55,28 @@ export default { return this.values.title.length > 0 } }, + watch: { + submittable (v) { + this.$emit('submittable', v) + }, + isLoading (v) { + this.$emit('loading', v) + } + }, methods: { submit () { - let self = this + const self = this self.isLoading = true self.errors = [] - let payload = { + const payload = { ...this.values, - artist: this.channel.artist.id, + artist: this.channel.artist.id } return axios.post('albums/', payload).then( response => { self.isLoading = false - self.$emit("created") + self.$emit('created') }, error => { self.errors = error.backendErrors @@ -57,14 +84,6 @@ export default { } ) } - }, - watch: { - submittable (v) { - this.$emit("submittable", v) - }, - isLoading (v) { - this.$emit("loading", v) - } } } </script> diff --git a/front/src/components/channels/AlbumModal.vue b/front/src/components/channels/AlbumModal.vue index 5dae29156c673b8acbc02a123902d3e5213db8fb..cf50cc0314508f2c48e04893b31c7193e17977f3 100644 --- a/front/src/components/channels/AlbumModal.vue +++ b/front/src/components/channels/AlbumModal.vue @@ -1,21 +1,47 @@ <template> - <modal class="small" :show.sync="show"> + <modal + class="small" + :show.sync="show" + > <h4 class="header"> - <translate key="1" v-if="channel.content_category === 'podcasts'" translate-context="Popup/Channels/Title/Verb">New series</translate> - <translate key="2" v-else translate-context="Popup/Channels/Title">New album</translate> + <translate + v-if="channel.content_category === 'podcasts'" + key="1" + translate-context="Popup/Channels/Title/Verb" + > + New series + </translate> + <translate + v-else + key="2" + translate-context="Popup/Channels/Title" + > + New album + </translate> </h4> <div class="scrolling content"> <channel-album-form ref="albumForm" + :channel="channel" @loading="isLoading = $event" @submittable="submittable = $event" @created="$emit('created', $event)" - :channel="channel"></channel-album-form> + /> </div> <div class="actions"> - <button class="ui basic cancel button"><translate translate-context="*/*/Button.Label/Verb">Cancel</translate></button> - <button :class="['ui', 'primary', {loading: isLoading}, 'button']" :disabled="!submittable" @click.stop.prevent="$refs.albumForm.submit()"> - <translate translate-context="*/*/Button.Label">Create</translate> + <button class="ui basic cancel button"> + <translate translate-context="*/*/Button.Label/Verb"> + Cancel + </translate> + </button> + <button + :class="['ui', 'primary', {loading: isLoading}, 'button']" + :disabled="!submittable" + @click.stop.prevent="$refs.albumForm.submit()" + > + <translate translate-context="*/*/Button.Label"> + Create + </translate> </button> </div> </modal> @@ -26,16 +52,16 @@ import Modal from '@/components/semantic/Modal' import ChannelAlbumForm from '@/components/channels/AlbumForm' export default { - props: ['channel'], components: { Modal, ChannelAlbumForm }, + props: { channel: { type: Object, required: true } }, data () { return { isLoading: false, submittable: false, - show: false, + show: false } }, watch: { diff --git a/front/src/components/channels/AlbumSelect.vue b/front/src/components/channels/AlbumSelect.vue index 7b37704693f0e26fac21d54b09ac9edaad3c59c7..a5fdf91da0fddc75e8af3c494fcc65aceb55e6ce 100644 --- a/front/src/components/channels/AlbumSelect.vue +++ b/front/src/components/channels/AlbumSelect.vue @@ -1,15 +1,41 @@ <template> <div> <label for="album-dropdown"> - <translate v-if="channel && channel.artist.content_category === 'podcast'" key="1" translate-context="*/*/*">Series</translate> - <translate v-else key="2" translate-context="*/*/*">Album</translate> + <translate + v-if="channel && channel.artist.content_category === 'podcast'" + key="1" + translate-context="*/*/*" + >Series</translate> + <translate + v-else + key="2" + translate-context="*/*/*" + >Album</translate> </label> - <select id="album-dropdown" :value="value" @input="$emit('input', $event.target.value)" class="ui search normal dropdown"> + <select + id="album-dropdown" + :value="value" + class="ui search normal dropdown" + @input="$emit('input', $event.target.value)" + > <option value=""> - <translate translate-context="*/*/*">None</translate> + <translate translate-context="*/*/*"> + None + </translate> </option> - <option v-for="album in albums" :key="album.id" :value="album.id"> - {{ album.title }} (<translate translate-context="*/*/*" :translate-params="{count: album.tracks_count}" :translate-n="album.tracks_count" translate-plural="%{ count } tracks">%{ count } track</translate>) + <option + v-for="album in albums" + :key="album.id" + :value="album.id" + > + {{ album.title }} (<translate + translate-context="*/*/*" + :translate-params="{count: album.tracks_count}" + :translate-n="album.tracks_count" + translate-plural="%{ count } tracks" + > + %{ count } track + </translate>) </option> </select> </div> @@ -18,11 +44,19 @@ import axios from 'axios' export default { - props: ['value', 'channel'], + props: { + value: { type: String, required: true }, + channel: { type: Object, required: true } + }, data () { return { albums: [], - isLoading: false, + isLoading: false + } + }, + watch: { + async channel () { + await this.fetchData() } }, async created () { @@ -35,14 +69,9 @@ export default { return } this.isLoading = true - let response = await axios.get('albums/', {params: {artist: this.channel.artist.id, include_channels: 'true'}}) + const response = await axios.get('albums/', { params: { artist: this.channel.artist.id, include_channels: 'true' } }) this.albums = response.data.results this.isLoading = false - }, - }, - watch: { - async channel () { - await this.fetchData() } } } diff --git a/front/src/components/channels/LicenseSelect.vue b/front/src/components/channels/LicenseSelect.vue index 98317b16c49db614a7ffee215adfac0af2103019..f5704af43f3188df1b4941ba7931b258c3f0b287 100644 --- a/front/src/components/channels/LicenseSelect.vue +++ b/front/src/components/channels/LicenseSelect.vue @@ -3,15 +3,36 @@ <label for="license-dropdown"> <translate translate-context="Content/*/*/Noun">License</translate> </label> - <select id="license-dropdown" :value="value" @input="$emit('input', $event.target.value)" class="ui search normal dropdown"> + <select + id="license-dropdown" + :value="value" + class="ui search normal dropdown" + @input="$emit('input', $event.target.value)" + > <option value=""> - <translate translate-context="*/*/*">None</translate> + <translate translate-context="*/*/*"> + None + </translate> + </option> + <option + v-for="l in featuredLicenses" + :key="l.code" + :value="l.code" + > + {{ l.name }} </option> - <option v-for="l in featuredLicenses" :key="l.code" :value="l.code">{{ l.name }}</option> </select> - <p class="help" v-if="value"> - <div class="ui very small hidden divider"></div> - <a :href="currentLicense.url" v-if="value" target="_blank" rel="noreferrer noopener"> + <div class="ui very small hidden divider" /> + <p + v-if="value" + class="help" + > + <a + v-if="value" + :href="currentLicense.url" + target="_blank" + rel="noreferrer noopener" + > <translate translate-context="Content/*/*">About this license</translate> </a> </p> @@ -21,7 +42,7 @@ import axios from 'axios' export default { - props: ['value'], + props: { value: { type: String, required: true } }, data () { return { availableLicenses: [], @@ -32,38 +53,38 @@ export default { 'cc-by-nc-4.0', 'cc-by-nc-sa-4.0', 'cc-by-nc-nd-4.0', - 'cc-by-nd-4.0', + 'cc-by-nd-4.0' ], - isLoading: false, + isLoading: false } }, - async created () { - await this.fetchLicenses() - }, computed: { featuredLicenses () { - let self = this + const self = this return this.availableLicenses.filter((l) => { return self.featuredLicensesIds.indexOf(l.code) > -1 }) }, currentLicense () { - let self = this + const self = this if (this.value) { return this.availableLicenses.filter((l) => { return l.code === self.value })[0] - } + return null } }, + async created () { + await this.fetchLicenses() + }, methods: { async fetchLicenses () { this.isLoading = true - let response = await axios.get('licenses/') + const response = await axios.get('licenses/') this.availableLicenses = response.data.results this.isLoading = false - }, - }, + } + } } </script> diff --git a/front/src/components/channels/SubscribeButton.vue b/front/src/components/channels/SubscribeButton.vue index 3fb06be29c9eb3e6f9232bf09e085f4e8e2e23b7..3d95305d8a74e8c47ac1737ae212d1e90bdd4564 100644 --- a/front/src/components/channels/SubscribeButton.vue +++ b/front/src/components/channels/SubscribeButton.vue @@ -1,20 +1,40 @@ - <template> - <button v-if="$store.state.auth.authenticated" @click.stop="toggle" :class="['ui', 'pink', {'inverted': isSubscribed}, {'favorited': isSubscribed}, 'icon', 'labeled', 'button']"> - <i class="heart icon"></i> - <translate v-if="isSubscribed" translate-context="Content/Track/Button.Message">Unsubscribe</translate> - <translate v-else translate-context="Content/Track/*/Verb">Subscribe</translate> +<template> + <button + v-if="$store.state.auth.authenticated" + :class="['ui', 'pink', {'inverted': isSubscribed}, {'favorited': isSubscribed}, 'icon', 'labeled', 'button']" + @click.stop="toggle" + > + <i class="heart icon" /> + <translate + v-if="isSubscribed" + translate-context="Content/Track/Button.Message" + > + Unsubscribe + </translate> + <translate + v-else + translate-context="Content/Track/*/Verb" + > + Subscribe + </translate> </button> - <button @click="$refs.loginModal.show = true" v-else :class="['ui', 'pink', 'icon', 'labeled', 'button']"> - <i class="heart icon"></i> - <translate translate-context="Content/Track/*/Verb">Subscribe</translate> + <button + v-else + :class="['ui', 'pink', 'icon', 'labeled', 'button']" + @click="$refs.loginModal.show = true" + > + <i class="heart icon" /> + <translate translate-context="Content/Track/*/Verb"> + Subscribe + </translate> <login-modal ref="loginModal" class="small" - :nextRoute='this.$route.fullPath' - :message='this.message.authMessage' - :cover='this.channel.artist.cover' - @created="$refs.loginModal.show = false;"> - </login-modal> + :next-route="$route.fullPath" + :message="message.authMessage" + :cover="channel.artist.cover" + @created="$refs.loginModal.show = false;" + /> </button> </template> @@ -22,12 +42,12 @@ import LoginModal from '@/components/common/LoginModal' export default { - props: { - channel: {type: Object}, - }, components: { LoginModal }, + props: { + channel: { type: Object, required: true } + }, computed: { title () { if (this.isSubscribed) { @@ -40,10 +60,10 @@ export default { return this.$store.getters['channels/isSubscribed'](this.channel.uuid) }, message () { - return { + return { authMessage: this.$pgettext('Popup/Message/Paragraph', 'You need to be logged in to subscribe to this channel') } - }, + } }, methods: { toggle () { @@ -56,6 +76,5 @@ export default { } } - } </script> diff --git a/front/src/components/channels/UploadForm.vue b/front/src/components/channels/UploadForm.vue index f40fe11791f88c676331d96b1158eeeb67882335..712e9627ba7503b4d205400c3540b778b3c60866 100644 --- a/front/src/components/channels/UploadForm.vue +++ b/front/src/components/channels/UploadForm.vue @@ -1,70 +1,132 @@ <template> - <form @submit.stop.prevent :class="['ui', {loading: isLoadingStep1}, 'form component-file-upload']"> - <div v-if="errors.length > 0" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Content/*/Error message.Title">Error while publishing</translate></h4> + <form + :class="['ui', {loading: isLoadingStep1}, 'form component-file-upload']" + @submit.stop.prevent + > + <div + v-if="errors.length > 0" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Content/*/Error message.Title"> + Error while publishing + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> <div :class="['ui', 'required', {hidden: step > 1}, 'field']"> <label for="channel-dropdown"> <translate translate-context="*/*/*">Channel</translate> </label> - <div id="channel-dropdown" class="ui search normal selection dropdown"> - <div class="text"></div> - <i class="dropdown icon"></i> + <div + id="channel-dropdown" + class="ui search normal selection dropdown" + > + <div class="text" /> + <i class="dropdown icon" /> </div> </div> - <album-select v-model.number="values.album" :channel="selectedChannel" :class="['ui', {hidden: step > 1}, 'field']"></album-select> - <license-select v-model="values.license" :class="['ui', {hidden: step > 1}, 'field']"></license-select> + <album-select + v-model.number="values.album" + :channel="selectedChannel" + :class="['ui', {hidden: step > 1}, 'field']" + /> + <license-select + v-model="values.license" + :class="['ui', {hidden: step > 1}, 'field']" + /> <div :class="['ui', {hidden: step > 1}, 'message']"> <div class="content"> <p> - <i class="copyright icon"></i> - <translate translate-context="Content/Channels/Popup.Paragraph">Add a license to your upload to ensure some freedoms to your public.</translate> + <i class="copyright icon" /> + <translate translate-context="Content/Channels/Popup.Paragraph"> + Add a license to your upload to ensure some freedoms to your public. + </translate> </p> </div> </div> <template v-if="step >= 2 && step < 4"> - <div role="alert" class="ui warning message" v-if="remainingSpace === 0"> + <div + v-if="remainingSpace === 0" + role="alert" + class="ui warning message" + > <div class="content"> <p> - <i class="warning icon"></i> - <translate translate-context="Content/Library/Paragraph">You don't have any space left to upload your files. Please contact the moderators.</translate> + <i class="warning icon" /> + <translate translate-context="Content/Library/Paragraph"> + You don't have any space left to upload your files. Please contact the moderators. + </translate> </p> </div> </div> <template v-else> - <div class="ui visible info message" v-if="step === 2 && draftUploads && draftUploads.length > 0 && includeDraftUploads === null"> + <div + v-if="step === 2 && draftUploads && draftUploads.length > 0 && includeDraftUploads === null" + class="ui visible info message" + > <p> - <i class="redo icon"></i> - <translate translate-context="Popup/Channels/Paragraph">You have some draft uploads pending publication.</translate> + <i class="redo icon" /> + <translate translate-context="Popup/Channels/Paragraph"> + You have some draft uploads pending publication. + </translate> </p> - <button @click.stop.prevent="includeDraftUploads = false" class="ui basic button"> - <translate translate-context="*/*/*">Ignore</translate> + <button + class="ui basic button" + @click.stop.prevent="includeDraftUploads = false" + > + <translate translate-context="*/*/*"> + Ignore + </translate> </button> - <button @click.stop.prevent="includeDraftUploads = true" class="ui basic button"> - <translate translate-context="*/*/*">Resume</translate> + <button + class="ui basic button" + @click.stop.prevent="includeDraftUploads = true" + > + <translate translate-context="*/*/*"> + Resume + </translate> </button> </div> - <div v-if="uploadedFiles.length > 0" :class="[{hidden: step === 3}]"> - <div class="channel-file" v-for="(file, idx) in uploadedFiles"> + <div + v-if="uploadedFiles.length > 0" + :class="[{hidden: step === 3}]" + > + <div + v-for="(file, idx) in uploadedFiles" + :key="idx" + class="channel-file" + > <div class="content"> - <div role="button" + <div v-if="file.response.uuid" - @click.stop.prevent="selectedUploadId = file.response.uuid" + role="button" class="ui basic icon button" - :title="labels.editTitle"> - <i class="pencil icon"></i> + :title="labels.editTitle" + @click.stop.prevent="selectedUploadId = file.response.uuid" + > + <i class="pencil icon" /> </div> <div v-if="file.error" - @click.stop.prevent="selectedUploadId = file.response.uuid" class="ui basic danger icon label" - :title="file.error"> - <i class="warning sign icon"></i> + :title="file.error" + @click.stop.prevent="selectedUploadId = file.response.uuid" + > + <i class="warning sign icon" /> </div> - <div v-else-if="file.active" class="ui active slow inline loader"></div> + <div + v-else-if="file.active" + class="ui active slow inline loader" + /> </div> <h4 class="ui header"> <template v-if="file.metadata.title"> @@ -77,20 +139,39 @@ <template v-if="file.response.uuid"> {{ file.size | humanSize }} <template v-if="file.response.duration"> - · <human-duration :duration="file.response.duration"></human-duration> + · <human-duration :duration="file.response.duration" /> </template> </template> <template v-else> - <translate key="1" v-if="file.active" translate-context="Channels/*/*">Uploading</translate> - <translate key="2" v-else-if="file.error" translate-context="Channels/*/*">Errored</translate> - <translate key="3" v-else translate-context="Channels/*/*">Pending</translate> + <translate + v-if="file.active" + key="1" + translate-context="Channels/*/*" + > + Uploading + </translate> + <translate + v-else-if="file.error" + key="2" + translate-context="Channels/*/*" + > + Errored + </translate> + <translate + v-else + key="3" + translate-context="Channels/*/*" + > + Pending + </translate> · {{ file.size | humanSize }} · {{ parseInt(file.progress) }}% </template> · <a @click.stop.prevent="remove(file)"> <translate translate-context="Content/Radio/Button.Label/Verb">Remove</translate> </a> - <template v-if="file.error"> · + <template v-if="file.error"> + · <a @click.stop.prevent="retry(file)"> <translate translate-context="*/*/*">Retry</translate> </a> @@ -100,20 +181,30 @@ </div> </div> <upload-metadata-form - :key="selectedUploadId" v-if="selectedUpload" + :key="selectedUploadId" :upload="selectedUpload" :values="uploadImportData[selectedUploadId]" - @values="setDynamic('uploadImportData', selectedUploadId, $event)"></upload-metadata-form> - <div class="ui message" v-if="step === 2"> + @values="setDynamic('uploadImportData', selectedUploadId, $event)" + /> + <div + v-if="step === 2" + class="ui message" + > <div class="content"> <p> - <i class="info icon"></i> - <translate translate-context="Content/Library/Paragraph" :translate-params="{extensions: $store.state.ui.supportedExtensions.join(', ')}">Supported extensions: %{ extensions }</translate> + <i class="info icon" /> + <translate + translate-context="Content/Library/Paragraph" + :translate-params="{extensions: $store.state.ui.supportedExtensions.join(', ')}" + > + Supported extensions: %{ extensions } + </translate> </p> </div> </div> <file-upload-widget + ref="upload" :class="['ui', 'icon', 'basic', 'button', 'channels', {hidden: step === 3}]" :post-action="uploadUrl" :multiple="true" @@ -121,21 +212,25 @@ :drop="true" :extensions="$store.state.ui.supportedExtensions" :value="files" - @input="updateFiles" name="audio_file" :thread="1" + @input="updateFiles" @input-file="inputFile" - ref="upload"> + > <div> - <i class="upload icon"></i> - <translate translate-context="Content/Channels/Paragraph">Drag and drop your files here or open the browser to upload your files</translate> + <i class="upload icon" /> + <translate translate-context="Content/Channels/Paragraph"> + Drag and drop your files here or open the browser to upload your files + </translate> </div> - <div class="ui very small divider"></div> + <div class="ui very small divider" /> <div> - <translate translate-context="*/*/*">Browse…</translate> + <translate translate-context="*/*/*"> + Browse… + </translate> </div> </file-upload-widget> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> </template> </template> </form> @@ -146,31 +241,31 @@ import $ from 'jquery' import LicenseSelect from '@/components/channels/LicenseSelect' import AlbumSelect from '@/components/channels/AlbumSelect' -import FileUploadWidget from "@/components/library/FileUploadWidget"; +import FileUploadWidget from '@/components/library/FileUploadWidget' import UploadMetadataForm from '@/components/channels/UploadMetadataForm' function setIfEmpty (obj, k, v) { - if (obj[k] != undefined) { + if (obj[k] !== undefined) { return } obj[k] = v } export default { - props: { - channel: {type: Object, default: null, required: false}, - }, components: { AlbumSelect, LicenseSelect, FileUploadWidget, - UploadMetadataForm, + UploadMetadataForm + }, + props: { + channel: { type: Object, default: null, required: false } }, data () { return { availableChannels: { results: [], - count: 0, + count: 0 }, audioMetadata: {}, uploadData: {}, @@ -180,29 +275,22 @@ export default { errors: [], removed: [], includeDraftUploads: null, - uploadUrl: this.$store.getters['instance/absoluteUrl']("/api/v1/uploads/"), + uploadUrl: this.$store.getters['instance/absoluteUrl']('/api/v1/uploads/'), quotaStatus: null, isLoadingStep1: true, step: 1, values: { channel: (this.channel || {}).uuid, license: null, - album: null, + album: null }, - selectedUploadId: null, + selectedUploadId: null } }, - async created () { - this.isLoadingStep1 = true - let p1 = this.fetchChannels() - await p1 - this.isLoadingStep1 = false - this.fetchQuota() - }, computed: { labels () { return { - editTitle: this.$pgettext('Content/*/Button.Label/Verb', 'Edit'), + editTitle: this.$pgettext('Content/*/Button.Label/Verb', 'Edit') } }, @@ -210,7 +298,7 @@ export default { return { channel: this.values.channel, import_status: 'draft', - import_metadata: {license: this.values.license, album: this.values.album || null} + import_metadata: { license: this.values.license, album: this.values.album || null } } }, remainingSpace () { @@ -220,18 +308,18 @@ export default { return Math.max(0, this.quotaStatus.remaining - (this.uploadedSize / (1000 * 1000))) }, selectedChannel () { - let self = this + const self = this return this.availableChannels.results.filter((c) => { return c.uuid === self.values.channel })[0] }, selectedUpload () { - let self = this + const self = this if (!this.selectedUploadId) { return null } - let selected = this.uploadedFiles.filter((f) => { - return f.response && f.response.uuid == self.selectedUploadId + const selected = this.uploadedFiles.filter((f) => { + return f.response && f.response.uuid === self.selectedUploadId })[0] return { ...selected.response, @@ -239,27 +327,24 @@ export default { } }, uploadedFilesById () { - let data = {} + const data = {} this.uploadedFiles.forEach((u) => { data[u.response.uuid] = u }) return data }, uploadedFiles () { - let self = this - self.uploadData - self.audioMetadata - let files = this.files.map((f) => { - let data = { + const self = this + const files = this.files.map((f) => { + const data = { ...f, _fileObj: f, metadata: {} } - let metadata = {} if (f.response && f.response.uuid) { - let uploadImportMetadata = self.uploadImportData[f.response.uuid] || self.uploadData[f.response.uuid].import_metadata + const uploadImportMetadata = self.uploadImportData[f.response.uuid] || self.uploadData[f.response.uuid].import_metadata data.metadata = { - ...uploadImportMetadata, + ...uploadImportMetadata } data.removed = self.removed.indexOf(f.response.uuid) >= 0 } @@ -308,7 +393,7 @@ export default { canSubmit: !this.activeFile && this.uploadedFiles.length > 0, speed, remaining, - quotaStatus: this.quotaStatus, + quotaStatus: this.quotaStatus } }, totalSize () { @@ -335,44 +420,92 @@ export default { })[0] } }, + watch: { + 'availableChannels.results' () { + this.setupChannelsDropdown() + }, + 'values.channel': { + async handler (v) { + this.files = [] + if (v) { + await this.fetchDraftUploads(v) + } + }, + immediate: true + }, + step: { + handler (value) { + this.$emit('step', value) + if (value === 2) { + this.selectedUploadId = null + } + }, + immediate: true + }, + async selectedUploadId (v, o) { + if (v) { + this.step = 3 + } else { + this.step = 2 + } + if (o) { + await this.patchUpload(o, { import_metadata: this.uploadImportData[o] }) + } + }, + summaryData: { + handler (v) { + this.$emit('status', v) + }, + immediate: true + + } + }, + async created () { + this.isLoadingStep1 = true + const p1 = this.fetchChannels() + await p1 + this.isLoadingStep1 = false + this.fetchQuota() + }, methods: { async fetchChannels () { - let response = await axios.get('channels/', {params: {scope: 'me'}}) + const response = await axios.get('channels/', { params: { scope: 'me' } }) this.availableChannels = response.data }, async patchUpload (id, data) { - let response = await axios.patch(`uploads/${id}/`, data) + const response = await axios.patch(`uploads/${id}/`, data) this.uploadData[id] = response.data this.uploadImportData[id] = response.data.import_metadata }, fetchQuota () { - let self = this + const self = this axios.get('users/me/').then((response) => { self.quotaStatus = response.data.quota_status }) }, publish () { - let self = this + const self = this self.isLoading = true self.errors = [] - let ids = this.uploadedFiles.map((f) => { + const ids = this.uploadedFiles.map((f) => { return f.response.uuid }) - let payload = { + const payload = { action: 'publish', - objects: ids, + objects: ids } return axios.post('uploads/action/', payload).then( response => { self.isLoading = false - self.$emit("published", { + self.$emit('published', { uploads: self.uploadedFiles.map((u) => { return { ...u.response, - import_status: 'pending', + import_status: 'pending' } }), - channel: self.selectedChannel}) + channel: self.selectedChannel + }) }, error => { self.errors = error.backendErrors @@ -380,32 +513,31 @@ export default { ) }, setupChannelsDropdown () { - let self = this + const self = this $(this.$el).find('#channel-dropdown').dropdown({ onChange (value, text, $choice) { self.values.channel = value }, values: this.availableChannels.results.map((c) => { - let d = { + const d = { name: c.artist.name, value: c.uuid, - selected: self.channel && self.channel.uuid === c.uuid, + selected: self.channel && self.channel.uuid === c.uuid } if (c.artist.cover && c.artist.cover.urls.medium_square_crop) { - let coverUrl = self.$store.getters['instance/absoluteUrl'](c.artist.cover.urls.medium_square_crop) + const coverUrl = self.$store.getters['instance/absoluteUrl'](c.artist.cover.urls.medium_square_crop) d.image = coverUrl if (c.artist.content_category === 'podcast') { d.imageClass = 'ui image' } else { - d.imageClass = "ui avatar image" + d.imageClass = 'ui avatar image' } } else { - d.icon = "user" + d.icon = 'user' if (c.artist.content_category === 'podcast') { - d.iconClass = "bordered icon" + d.iconClass = 'bordered icon' } else { - d.iconClass = "circular icon" - + d.iconClass = 'circular icon' } } return d @@ -413,23 +545,23 @@ export default { }) $(this.$el).find('#channel-dropdown').dropdown('hide') }, - inputFile(newFile, oldFile) { + inputFile (newFile, oldFile) { if (!newFile) { return } if (this.remainingSpace < newFile.size / (1000 * 1000)) { newFile.error = 'denied' } else { - this.$refs.upload.active = true; + this.$refs.upload.active = true } }, fetchAudioMetadata (uuid) { - let self = this + const self = this self.audioMetadata[uuid] = null axios.get(`uploads/${uuid}/audio-file-metadata/`).then((response) => { self.setDynamic('audioMetadata', uuid, response.data) - let uploadedFile = self.uploadedFilesById[uuid] - if (uploadedFile._fileObj && uploadedFile.response.import_metadata.title === uploadedFile._fileObj.name.replace(/\.[^/.]+$/, "") && response.data.title) { + const uploadedFile = self.uploadedFilesById[uuid] + if (uploadedFile._fileObj && uploadedFile.response.import_metadata.title === uploadedFile._fileObj.name.replace(/\.[^/.]+$/, '') && response.data.title) { // replace existing title deduced from file by the one in audio file metadat, if any self.uploadImportData[uuid].title = response.data.title } else { @@ -439,17 +571,17 @@ export default { setIfEmpty(self.uploadImportData[uuid], 'position', response.data.position) setIfEmpty(self.uploadImportData[uuid], 'tags', response.data.tags) setIfEmpty(self.uploadImportData[uuid], 'description', (response.data.description || {}).text) - self.patchUpload(uuid, {import_metadata: self.uploadImportData[uuid]}) + self.patchUpload(uuid, { import_metadata: self.uploadImportData[uuid] }) }) }, setDynamic (objName, key, data) { // cf https://vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats - let newData = {} + const newData = {} newData[key] = data this[objName] = Object.assign({}, this[objName], newData) }, updateFiles (value) { - let self = this + const self = this this.files = value this.files.forEach((f) => { if (f.response && f.response.uuid && self.audioMetadata[f.response.uuid] === undefined) { @@ -462,9 +594,9 @@ export default { }) }, async fetchDraftUploads (channel) { - let self = this + const self = this this.draftUploads = null - let response = await axios.get('uploads', {params: {import_status: 'draft', channel: channel}}) + const response = await axios.get('uploads', { params: { import_status: 'draft', channel: channel } }) this.draftUploads = response.data.results this.draftUploads.forEach((u) => { self.uploadImportData[u.uuid] = u.import_metadata @@ -479,49 +611,8 @@ export default { } }, retry (file) { - this.$refs.upload.update(file, {error: '', progress: '0.00'}) - this.$refs.upload.active = true; - - } - }, - watch: { - "availableChannels.results" () { - this.setupChannelsDropdown() - }, - "values.channel": { - async handler (v) { - this.files = [] - if (v) { - await this.fetchDraftUploads(v) - } - }, - immediate: true, - }, - step: { - handler (value) { - this.$emit('step', value) - if (value === 2) { - this.selectedUploadId = null - } - }, - immediate: true, - }, - async selectedUploadId (v, o) { - if (v) { - this.step = 3 - } else { - this.step = 2 - } - if (o) { - await this.patchUpload(o, {import_metadata: this.uploadImportData[o]}) - } - }, - summaryData: { - handler (v) { - this.$emit('status', v) - }, - immediate: true, - + this.$refs.upload.update(file, { error: '', progress: '0.00' }) + this.$refs.upload.active = true } } } diff --git a/front/src/components/channels/UploadMetadataForm.vue b/front/src/components/channels/UploadMetadataForm.vue index 8824e6e48a2cd6293200145a76cafaeafa99d16f..f7535883cebda7d9572c38864ecd3e187162f43d 100644 --- a/front/src/components/channels/UploadMetadataForm.vue +++ b/front/src/components/channels/UploadMetadataForm.vue @@ -4,59 +4,79 @@ <label for="upload-title"> <translate translate-context="*/*/*/Noun">Title</translate> </label> - <input type="text" v-model="newValues.title"> + <input + v-model="newValues.title" + type="text" + > </div> <attachment-input v-model="newValues.cover" :required="false" - @delete="newValues.cover = null"> - <translate translate-context="Content/Channel/*" slot="label">Track Picture</translate> + @delete="newValues.cover = null" + > + <translate + slot="label" + translate-context="Content/Channel/*" + > + Track Picture + </translate> </attachment-input> - <div class="ui small hidden divider"></div> + <div class="ui small hidden divider" /> <div class="ui two fields"> <div class="ui field"> <label for="upload-tags"> <translate translate-context="*/*/*/Noun">Tags</translate> </label> <tags-selector - v-model="newValues.tags" id="upload-tags" - :required="false"></tags-selector> + v-model="newValues.tags" + :required="false" + /> </div> <div class="ui field"> <label for="upload-position"> <translate translate-context="*/*/*/Short, Noun">Position</translate> </label> - <input type="number" min="1" step="1" v-model="newValues.position"> + <input + v-model="newValues.position" + type="number" + min="1" + step="1" + > </div> </div> <div class="ui field"> <label for="upload-description"> <translate translate-context="*/*/*">Description</translate> </label> - <content-form v-model="newValues.description" field-id="upload-description"></content-form> + <content-form + v-model="newValues.description" + field-id="upload-description" + /> </div> </div> </template> <script> -import axios from 'axios' import TagsSelector from '@/components/library/TagsSelector' import AttachmentInput from '@/components/common/AttachmentInput' export default { - props: ['upload', 'values'], components: { TagsSelector, AttachmentInput }, + props: { + upload: { type: Object, required: true }, + values: { type: Object, required: true } + }, data () { return { - newValues: {...this.values} || this.upload.import_metadata + newValues: { ...this.values } || this.upload.import_metadata } }, computed: { - isLoading () { + isLoading () { return !!this.metadata } }, @@ -66,7 +86,7 @@ export default { this.$emit('values', v) }, immediate: true - }, + } } } </script> diff --git a/front/src/components/channels/UploadModal.vue b/front/src/components/channels/UploadModal.vue index 977097ea6ac2e901ff3855470abd13b7369a138a..89acd42dd6f7513799055d95bf2a961b82bb4e6f 100644 --- a/front/src/components/channels/UploadModal.vue +++ b/front/src/components/channels/UploadModal.vue @@ -1,60 +1,142 @@ <template> - <modal class="small" @update:show="update" :show="$store.state.channels.showUploadModal"> + <modal + class="small" + :show="$store.state.channels.showUploadModal" + @update:show="update" + > <h4 class="header"> - <translate key="1" v-if="step === 1" translate-context="Popup/Channels/Title/Verb">Publish audio</translate> - <translate key="2" v-else-if="step === 2" translate-context="Popup/Channels/Title">Files to upload</translate> - <translate key="3" v-else-if="step === 3" translate-context="Popup/Channels/Title">Upload details</translate> - <translate key="4" v-else-if="step === 4" translate-context="Popup/Channels/Title">Processing uploads</translate> + <translate + v-if="step === 1" + key="1" + translate-context="Popup/Channels/Title/Verb" + > + Publish audio + </translate> + <translate + v-else-if="step === 2" + key="2" + translate-context="Popup/Channels/Title" + > + Files to upload + </translate> + <translate + v-else-if="step === 3" + key="3" + translate-context="Popup/Channels/Title" + > + Upload details + </translate> + <translate + v-else-if="step === 4" + key="4" + translate-context="Popup/Channels/Title" + > + Processing uploads + </translate> </h4> <div class="scrolling content"> <channel-upload-form ref="uploadForm" + :channel="$store.state.channels.uploadModalConfig.channel" @step="step = $event" @loading="isLoading = $event" @published="$store.commit('channels/publish', $event)" @status="statusData = $event" @submittable="submittable = $event" - :channel="$store.state.channels.uploadModalConfig.channel"></channel-upload-form> + /> </div> <div class="actions"> <div class="left floated text left align"> <template v-if="statusData && step >= 2"> {{ statusInfo.join(' · ') }} </template> - <div class="ui very small hidden divider"></div> + <div class="ui very small hidden divider" /> <template v-if="statusData && statusData.quotaStatus"> - <translate translate-context="Content/Library/Paragraph">Remaining storage space:</translate> + <translate translate-context="Content/Library/Paragraph"> + Remaining storage space: + </translate> {{ (statusData.quotaStatus.remaining * 1000 * 1000) - statusData.uploadedSize | humanSize }} </template> </div> - <div class="ui hidden clearing divider mobile-only"></div> - <button class="ui basic cancel button" v-if="step === 1"><translate translate-context="*/*/Button.Label/Verb">Cancel</translate></button> - <button class="ui basic button" v-else-if="step < 3" @click.stop.prevent="$refs.uploadForm.step -= 1"><translate translate-context="*/*/Button.Label/Verb">Previous step</translate></button> - <button class="ui basic button" v-else-if="step === 3" @click.stop.prevent="$refs.uploadForm.step -= 1"><translate translate-context="*/*/Button.Label/Verb">Update</translate></button> - <button v-if="step === 1" class="ui primary button" @click.stop.prevent="$refs.uploadForm.step += 1"> - <translate translate-context="*/*/Button.Label">Next step</translate> + <div class="ui hidden clearing divider mobile-only" /> + <button + v-if="step === 1" + class="ui basic cancel button" + > + <translate translate-context="*/*/Button.Label/Verb"> + Cancel + </translate> </button> - <div class="ui primary buttons" v-if="step === 2"> + <button + v-else-if="step < 3" + class="ui basic button" + @click.stop.prevent="$refs.uploadForm.step -= 1" + > + <translate translate-context="*/*/Button.Label/Verb"> + Previous step + </translate> + </button> + <button + v-else-if="step === 3" + class="ui basic button" + @click.stop.prevent="$refs.uploadForm.step -= 1" + > + <translate translate-context="*/*/Button.Label/Verb"> + Update + </translate> + </button> + <button + v-if="step === 1" + class="ui primary button" + @click.stop.prevent="$refs.uploadForm.step += 1" + > + <translate translate-context="*/*/Button.Label"> + Next step + </translate> + </button> + <div + v-if="step === 2" + class="ui primary buttons" + > <button :class="['ui', 'primary button', {loading: isLoading}]" type="submit" :disabled="!statusData || !statusData.canSubmit" - @click.prevent.stop="$refs.uploadForm.publish"> - <translate translate-context="*/Channels/Button.Label">Publish</translate> + @click.prevent.stop="$refs.uploadForm.publish" + > + <translate translate-context="*/Channels/Button.Label"> + Publish + </translate> </button> - <button class="ui floating dropdown icon button" ref="dropdown" v-dropdown :disabled="!statusData || !statusData.canSubmit"> - <i class="dropdown icon"></i> + <button + ref="dropdown" + v-dropdown + class="ui floating dropdown icon button" + :disabled="!statusData || !statusData.canSubmit" + > + <i class="dropdown icon" /> <div class="menu"> <div role="button" + class="basic item" @click="update(false)" - class="basic item"> - <translate translate-context="Content/*/Button.Label/Verb">Finish later</translate> + > + <translate translate-context="Content/*/Button.Label/Verb"> + Finish later + </translate> </div> </div> </button> </div> - <button class="ui basic cancel button" @click="update(false)" v-if="step === 4"><translate translate-context="*/*/Button.Label/Verb">Close</translate></button> + <button + v-if="step === 4" + class="ui basic cancel button" + @click="update(false)" + > + <translate translate-context="*/*/Button.Label/Verb"> + Close + </translate> + </button> </div> </modal> </template> @@ -62,7 +144,7 @@ <script> import Modal from '@/components/semantic/Modal' import ChannelUploadForm from '@/components/channels/UploadForm' -import {humanSize} from '@/filters' +import { humanSize } from '@/filters' export default { components: { @@ -74,14 +156,9 @@ export default { step: 1, isLoading: false, submittable: true, - statusData: null, + statusData: null } }, - methods: { - update (v) { - this.$store.commit('channels/showUploadModal', {show: v}) - }, - }, computed: { labels () { return {} @@ -90,14 +167,14 @@ export default { if (!this.statusData) { return [] } - let info = [] + const info = [] if (this.statusData.totalSize) { info.push(humanSize(this.statusData.totalSize)) } if (this.statusData.totalFiles) { - let msg = this.$npgettext('*/*/*', '%{ count } file', '%{ count } files', this.statusData.totalFiles) + const msg = this.$npgettext('*/*/*', '%{ count } file', '%{ count } files', this.statusData.totalFiles) info.push( - this.$gettextInterpolate(msg, {count: this.statusData.totalFiles}), + this.$gettextInterpolate(msg, { count: this.statusData.totalFiles }) ) } if (this.statusData.progress) { @@ -107,13 +184,17 @@ export default { info.push(`${humanSize(this.statusData.speed)}/s`) } return info - } }, watch: { '$store.state.route.path' () { - this.$store.commit('channels/showUploadModal', {show: false}) - }, + this.$store.commit('channels/showUploadModal', { show: false }) + } + }, + methods: { + update (v) { + this.$store.commit('channels/showUploadModal', { show: v }) + } } } </script> diff --git a/front/src/components/common/ActionFeedback.vue b/front/src/components/common/ActionFeedback.vue index 160db953d1683b309459172951a4440d4b211b41..e2a343f64df4d7b3d5f60e0e5549c1cc106a0178 100644 --- a/front/src/components/common/ActionFeedback.vue +++ b/front/src/components/common/ActionFeedback.vue @@ -1,32 +1,35 @@ <template> - <span class="feedback" v-if="isLoading || isDone"> - <span v-if="isLoading" :class="['ui', 'active', size, 'inline', 'loader']"></span> - <i v-if="isDone" :class="['success', size, 'check', 'icon']"></i> + <span + v-if="isLoading || isDone" + class="feedback" + > + <span + v-if="isLoading" + :class="['ui', 'active', size, 'inline', 'loader']" + /> + <i + v-if="isDone" + :class="['success', size, 'check', 'icon']" + /> </span> </template> <script> -import {hashCode, intToRGB} from '@/utils/color' export default { props: { - isLoading: {type: Boolean, required: true}, - size: {type: String, default: 'small'}, + isLoading: { type: Boolean, required: true }, + size: { type: String, default: 'small' } }, data () { return { timer: null, - isDone: false, - } - }, - destroyed () { - if (this.timer) { - clearTimeout(this.timer) + isDone: false } }, watch: { isLoading (v) { - let self = this + const self = this if (v && this.timer) { clearTimeout(this.timer) } @@ -36,10 +39,14 @@ export default { this.isDone = true this.timer = setTimeout(() => { self.isDone = false - }, (2000)); - + }, (2000)) } } + }, + destroyed () { + if (this.timer) { + clearTimeout(this.timer) + } } } </script> diff --git a/front/src/components/common/ActionTable.vue b/front/src/components/common/ActionTable.vue index 0fe89a2005233b98cedd510060813c9fcc817b5e..68a0c85941b2bb0747358229bf6ad55a2ea8f03d 100644 --- a/front/src/components/common/ActionTable.vue +++ b/front/src/components/common/ActionTable.vue @@ -4,111 +4,188 @@ <thead> <tr> <th colspan="1000"> - <div v-if="refreshable" class="right floated"> + <div + v-if="refreshable" + class="right floated" + > <span v-if="needsRefresh"> <translate translate-context="Content/*/Button.Help text.Paragraph">Content has been updated, click refresh to see up-to-date content</translate> </span> <button - @click="$emit('refresh')" class="ui basic icon button" :title="labels.refresh" - :aria-label="labels.refresh"> - <i class="refresh icon"></i> + :aria-label="labels.refresh" + @click="$emit('refresh')" + > + <i class="refresh icon" /> </button> </div> - <div class="ui small left floated form" v-if="actionUrl && actions.length > 0"> + <div + v-if="actionUrl && actions.length > 0" + class="ui small left floated form" + > <div class="ui inline fields"> <div class="field"> <label for="actions-select"><translate translate-context="Content/*/*/Noun">Actions</translate></label> - <select id="actions-select" class="ui dropdown" v-model="currentActionName"> - <option v-for="action in actions" :value="action.name"> + <select + id="actions-select" + v-model="currentActionName" + class="ui dropdown" + > + <option + v-for="(action, key) in actions" + :key="key" + :value="action.name" + > {{ action.label }} </option> </select> </div> <div class="field"> <dangerous-button - v-if="selectAll || currentAction.isDangerous" :class="['ui', {disabled: checked.length === 0}, {'loading': actionLoading}, 'button']" + v-if="selectAll || currentAction.isDangerous" + :class="['ui', {disabled: checked.length === 0}, {'loading': actionLoading}, 'button']" :confirm-color="currentAction.confirmColor || 'success'" - @confirm="launchAction" :aria-label="labels.performAction"> - <translate translate-context="Content/*/Button.Label/Short, Verb">Go</translate> + :aria-label="labels.performAction" + @confirm="launchAction" + > + <translate translate-context="Content/*/Button.Label/Short, Verb"> + Go + </translate> <p slot="modal-header"> - <translate translate-context="Modal/*/Title" + <translate key="1" + translate-context="Modal/*/Title" :translate-n="affectedObjectsCount" :translate-params="{count: affectedObjectsCount, action: currentActionName}" - translate-plural="Do you want to launch %{ action } on %{ count } elements?"> + translate-plural="Do you want to launch %{ action } on %{ count } elements?" + > Do you want to launch %{ action } on %{ count } element? </translate> </p> <p slot="modal-content"> - <template v-if="currentAction.confirmationMessage">{{ currentAction.confirmationMessage }}</template> - <translate v-else translate-context="Modal/*/Paragraph">This may affect a lot of elements or have irreversible consequences, please double check this is really what you want.</translate> + <template v-if="currentAction.confirmationMessage"> + {{ currentAction.confirmationMessage }} + </template> + <translate + v-else + translate-context="Modal/*/Paragraph" + > + This may affect a lot of elements or have irreversible consequences, please double check this is really what you want. + </translate> </p> - <div :aria-label="labels.performAction" slot="modal-confirm"><translate translate-context="Modal/*/Button.Label/Short, Verb">Launch</translate></div> + <div + slot="modal-confirm" + :aria-label="labels.performAction" + > + <translate translate-context="Modal/*/Button.Label/Short, Verb"> + Launch + </translate> + </div> </dangerous-button> <button v-else - @click="launchAction" :disabled="checked.length === 0" :aria-label="labels.performAction" - :class="['ui', {disabled: checked.length === 0}, {'loading': actionLoading}, 'button']"> - <translate translate-context="Content/*/Button.Label/Short, Verb">Go</translate></button> + :class="['ui', {disabled: checked.length === 0}, {'loading': actionLoading}, 'button']" + @click="launchAction" + > + <translate translate-context="Content/*/Button.Label/Short, Verb"> + Go + </translate> + </button> </div> <div class="count field"> - <translate translate-context="Content/*/Paragraph" - tag="span" + <translate v-if="selectAll" key="1" + translate-context="Content/*/Paragraph" + tag="span" :translate-n="objectsData.count" :translate-params="{count: objectsData.count, total: objectsData.count}" - translate-plural="All %{ count } elements selected"> + translate-plural="All %{ count } elements selected" + > All %{ count } element selected </translate> - <translate translate-context="Content/*/Paragraph" - tag="span" + <translate v-else key="2" + translate-context="Content/*/Paragraph" + tag="span" :translate-n="checked.length" :translate-params="{count: checked.length, total: objectsData.count}" - translate-plural="%{ count } on %{ total } selected"> + translate-plural="%{ count } on %{ total } selected" + > %{ count } on %{ total } selected </translate> <template v-if="currentAction.allowAll && checkable.length > 0 && checkable.length === checked.length"> - <a @click.prevent="selectAll = true" v-if="!selectAll" href=""> - <translate translate-context="Content/*/Link/Verb" + <a + v-if="!selectAll" + href="" + @click.prevent="selectAll = true" + > + <translate key="3" + translate-context="Content/*/Link/Verb" :translate-n="objectsData.count" :translate-params="{total: objectsData.count}" - translate-plural="Select all %{ total } elements"> + translate-plural="Select all %{ total } elements" + > Select one element </translate> </a> - <a @click.prevent="selectAll = false" v-else href=""> - <translate translate-context="Content/*/Link/Verb" key="4">Select only current page</translate> + <a + v-else + href="" + @click.prevent="selectAll = false" + > + <translate + key="4" + translate-context="Content/*/Link/Verb" + >Select only current page</translate> </a> </template> </div> </div> - <div v-if="actionErrors.length > 0" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Content/*/Error message/Header">Error while applying action</translate></h4> + <div + v-if="actionErrors.length > 0" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Content/*/Error message/Header"> + Error while applying action + </translate> + </h4> <ul class="list"> - <li v-for="error in actionErrors">{{ error }}</li> + <li + v-for="(error, key) in actionErrors" + :key="key" + > + {{ error }} + </li> </ul> </div> - <div v-if="actionResult" class="ui positive message"> + <div + v-if="actionResult" + class="ui positive message" + > <p> - <translate translate-context="Content/*/Paragraph" + <translate + translate-context="Content/*/Paragraph" :translate-n="actionResult.updated" :translate-params="{count: actionResult.updated, action: actionResult.action}" - translate-plural="Action %{ action } was launched successfully on %{ count } elements"> + translate-plural="Action %{ action } was launched successfully on %{ count } elements" + > Action %{ action } was launched successfully on %{ count } element </translate> </p> - <slot name="action-success-footer" :result="actionResult"> - </slot> + <slot + name="action-success-footer" + :result="actionResult" + /> </div> </div> </th> @@ -118,26 +195,37 @@ <div class="ui checkbox"> <input type="checkbox" - @change="toggleCheckAll" :aria-label="labels.selectAllItems" :disabled="checkable.length === 0" - :checked="checkable.length > 0 && checked.length === checkable.length"> + :checked="checkable.length > 0 && checked.length === checkable.length" + @change="toggleCheckAll" + > </div> </th> - <slot name="header-cells"></slot> + <slot name="header-cells" /> </tr> </thead> <tbody v-if="objectsData.count > 0"> - <tr v-for="(obj, index) in objects"> - <td v-if="actions.length > 0" class="collapsing"> + <tr + v-for="(obj, index) in objects" + :key="index" + > + <td + v-if="actions.length > 0" + class="collapsing" + > <input type="checkbox" :aria-label="labels.selectItem" :disabled="checkable.indexOf(getId(obj)) === -1" + :checked="checked.indexOf(getId(obj)) > -1" @click="toggleCheck($event, getId(obj), index)" - :checked="checked.indexOf(getId(obj)) > -1"> + > </td> - <slot name="row-cells" :obj="obj"></slot> + <slot + name="row-cells" + :obj="obj" + /> </tr> </tbody> </table> @@ -147,19 +235,19 @@ import axios from 'axios' export default { + 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 [] }}, + 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: {}, data () { - let d = { + const d = { checked: [], actionLoading: false, actionResult: null, @@ -173,6 +261,71 @@ export default { } return d }, + computed: { + currentAction () { + const self = this + return this.actions.filter((a) => { + return a.name === self.currentActionName + })[0] + }, + checkable () { + const self = this + if (!this.currentAction) { + return [] + } + let objs = this.objectsData.results + const filter = this.currentAction.filterCheckable + if (filter) { + objs = objs.filter((o) => { + return filter(o) + }) + } + return objs.map((o) => { return self.getId(o) }) + }, + objects () { + const self = this + return this.objectsData.results.map((o) => { + const custom = self.customObjects.filter((co) => { + return self.getId(co) === self.getId(o) + })[0] + if (custom) { + return custom + } + return o + }) + }, + labels () { + return { + refresh: this.$pgettext('Content/*/Button.Tooltip/Verb', 'Refresh table content'), + selectAllItems: this.$pgettext('Content/*/Select/Verb', 'Select all items'), + performAction: this.$pgettext('Content/*/Button.Label', 'Perform actions'), + selectItem: this.$pgettext('Content/*/Select/Verb', 'Select') + } + }, + affectedObjectsCount () { + if (this.selectAll) { + return this.objectsData.count + } + return this.checked.length + } + }, + watch: { + objectsData: { + handler () { + this.checked = [] + this.selectAll = false + }, + deep: true + }, + currentActionName () { + // we update checked status as some actions have specific filters + // on what is checkable or not + const self = this + this.checked = this.checked.filter(r => { + return self.checkable.indexOf(r) > -1 + }) + } + }, methods: { toggleCheckAll () { this.lastCheckedIndex = -1 @@ -184,7 +337,7 @@ export default { } }, toggleCheck (event, id, index) { - let self = this + const self = this let affectedIds = [id] let newValue = null if (this.checked.indexOf(id) > -1) { @@ -196,13 +349,13 @@ export default { } if (event.shiftKey && this.lastCheckedIndex > -1) { // we also add inbetween ids to the list of affected ids - let idxs = [index, this.lastCheckedIndex] + const idxs = [index, this.lastCheckedIndex] idxs.sort((a, b) => a - b) - let objs = this.objectsData.results.slice(idxs[0], idxs[1] + 1) + const 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 + const checked = self.checked.indexOf(i) > -1 if (newValue && !checked && self.checkable.indexOf(i) > -1) { return self.checked.push(i) } @@ -213,11 +366,11 @@ export default { this.lastCheckedIndex = index }, launchAction () { - let self = this + const self = this self.actionLoading = true self.result = null self.actionErrors = [] - let payload = { + const payload = { action: this.currentActionName, filters: this.filters } @@ -238,71 +391,6 @@ export default { getId (obj) { return obj[this.idField] } - }, - computed: { - currentAction () { - let self = this - return this.actions.filter((a) => { - return a.name === self.currentActionName - })[0] - }, - checkable () { - let self = this - if (!this.currentAction) { - return [] - } - let objs = this.objectsData.results - let filter = this.currentAction.filterCheckable - if (filter) { - objs = objs.filter((o) => { - return filter(o) - }) - } - return objs.map((o) => { return self.getId(o) }) - }, - objects () { - let self = this - return this.objectsData.results.map((o) => { - let custom = self.customObjects.filter((co) => { - return self.getId(co) === self.getId(o) - })[0] - if (custom) { - return custom - } - return o - }) - }, - labels () { - return { - refresh: this.$pgettext('Content/*/Button.Tooltip/Verb', 'Refresh table content'), - selectAllItems: this.$pgettext('Content/*/Select/Verb', 'Select all items'), - performAction: this.$pgettext('Content/*/Button.Label', 'Perform actions'), - selectItem: this.$pgettext('Content/*/Select/Verb', 'Select') - } - }, - affectedObjectsCount () { - if (this.selectAll) { - return this.objectsData.count - } - return this.checked.length - } - }, - watch: { - objectsData: { - handler () { - this.checked = [] - this.selectAll = false - }, - deep: true - }, - currentActionName () { - // we update checked status as some actions have specific filters - // on what is checkable or not - let self = this - this.checked = this.checked.filter(r => { - return self.checkable.indexOf(r) > -1 - }) - } } } </script> diff --git a/front/src/components/common/ActorAvatar.vue b/front/src/components/common/ActorAvatar.vue index 742271ab4baa65d712cb197ca4f9fd948e48275e..d4f2188cb51155d82a4016b3a019e82962c842f5 100644 --- a/front/src/components/common/ActorAvatar.vue +++ b/front/src/components/common/ActorAvatar.vue @@ -1,13 +1,22 @@ <template> - <img alt="" v-if="actor.icon && actor.icon.urls.original" :src="actor.icon.urls.medium_square_crop" class="ui avatar circular image" /> - <span v-else :style="defaultAvatarStyle" class="ui avatar circular label">{{ actor.preferred_username[0]}}</span> + <img + v-if="actor.icon && actor.icon.urls.original" + alt="" + :src="actor.icon.urls.medium_square_crop" + class="ui avatar circular image" + > + <span + v-else + :style="defaultAvatarStyle" + class="ui avatar circular label" + >{{ actor.preferred_username[0] }}</span> </template> <script> -import {hashCode, intToRGB} from '@/utils/color' +import { hashCode, intToRGB } from '@/utils/color' export default { - props: ['actor'], + props: { actor: { type: Object, required: true } }, computed: { actorColor () { return intToRGB(hashCode(this.actor.full_username)) diff --git a/front/src/components/common/ActorLink.vue b/front/src/components/common/ActorLink.vue index 81199e7efe2792bbf9afc1a789a9d18b7d1d3420..a30c717ca9f1e693c15c266d4866285feccce9e0 100644 --- a/front/src/components/common/ActorLink.vue +++ b/front/src/components/common/ActorLink.vue @@ -1,29 +1,33 @@ <template> - <router-link :to="url" :title="actor.full_username"> - <template v-if="avatar"><actor-avatar :actor="actor" /><span> </span></template><slot>{{ repr | truncate(truncateLength) }}</slot> + <router-link + :to="url" + :title="actor.full_username" + > + <template v-if="avatar"> + <actor-avatar :actor="actor" /><span> </span> + </template><slot>{{ repr | truncate(truncateLength) }}</slot> </router-link> </template> <script> -import {hashCode, intToRGB} from '@/utils/color' export default { props: { - actor: {type: Object}, - avatar: {type: Boolean, default: true}, - admin: {type: Boolean, default: false}, - displayName: {type: Boolean, default: false}, - truncateLength: {type: Number, default: 30}, + actor: { type: Object, required: true }, + avatar: { type: Boolean, default: true }, + admin: { type: Boolean, default: false }, + displayName: { type: Boolean, default: false }, + truncateLength: { type: Number, default: 30 } }, computed: { url () { if (this.admin) { - return {name: 'manage.moderation.accounts.detail', params: {id: this.actor.full_username}} + return { name: 'manage.moderation.accounts.detail', params: { id: this.actor.full_username } } } if (this.actor.is_local) { - return {name: 'profile.overview', params: {username: this.actor.preferred_username}} + return { name: 'profile.overview', params: { username: this.actor.preferred_username } } } else { - return {name: 'profile.full.overview', params: {username: this.actor.preferred_username, domain: this.actor.domain}} + return { name: 'profile.full.overview', params: { username: this.actor.preferred_username, domain: this.actor.domain } } } }, repr () { diff --git a/front/src/components/common/AjaxButton.vue b/front/src/components/common/AjaxButton.vue index 024c98515bf71bd5bc951284f28a8436d8ab5fc5..85172b13ae230f6f9f37fdbae6c78347128fae24 100644 --- a/front/src/components/common/AjaxButton.vue +++ b/front/src/components/common/AjaxButton.vue @@ -1,6 +1,9 @@ <template> - <button @click="ajaxCall" :class="['ui', {loading: isLoading}, 'button']"> - <slot></slot> + <button + :class="['ui', {loading: isLoading}, 'button']" + @click="ajaxCall" + > + <slot /> </button> </template> <script> @@ -8,17 +11,17 @@ import axios from 'axios' export default { props: { - url: {type: String, required: true}, - method: {type: String, required: true}, + url: { type: String, required: true }, + method: { type: String, required: true } }, data () { return { - isLoading: false, + isLoading: false } }, methods: { ajaxCall () { - var self = this + const self = this this.isLoading = true axios[this.method](this.url).then(response => { self.$emit('action-done', response.data) diff --git a/front/src/components/common/AttachmentInput.vue b/front/src/components/common/AttachmentInput.vue index cab78c85e8f546b0a68ba2d2c061d2c970ba3a3a..72bc5264f22d3cb821f5839923e09396ad68015c 100644 --- a/front/src/components/common/AttachmentInput.vue +++ b/front/src/components/common/AttachmentInput.vue @@ -1,36 +1,84 @@ <template> <div class="ui form"> - <div v-if="errors.length > 0" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Content/*/Error message.Title">Your attachment cannot be saved</translate></h4> + <div + v-if="errors.length > 0" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Content/*/Error message.Title"> + Your attachment cannot be saved + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> <div class="ui field"> <span id="avatarLabel"> - <slot name="label"></slot> + <slot name="label" /> </span> <div class="ui stackable grid row"> <div class="three wide column"> - <img alt="" :class="['ui', imageClass, 'image']" v-if="value && value === initialValue" :src="$store.getters['instance/absoluteUrl'](`api/v1/attachments/${value}/proxy?next=medium_square_crop`)" /> - <img alt="" :class="['ui', imageClass, 'image']" v-else-if="attachment" :src="$store.getters['instance/absoluteUrl'](`api/v1/attachments/${attachment.uuid}/proxy?next=medium_square_crop`)" /> - <div :class="['ui', imageClass, 'static', 'large placeholder image']" v-else></div> + <img + v-if="value && value === initialValue" + alt="" + :class="['ui', imageClass, 'image']" + :src="$store.getters['instance/absoluteUrl'](`api/v1/attachments/${value}/proxy?next=medium_square_crop`)" + > + <img + v-else-if="attachment" + alt="" + :class="['ui', imageClass, 'image']" + :src="$store.getters['instance/absoluteUrl'](`api/v1/attachments/${attachment.uuid}/proxy?next=medium_square_crop`)" + > + <div + v-else + :class="['ui', imageClass, 'static', 'large placeholder image']" + /> </div> <div class="eleven wide column"> <div class="file-input"> <label :for="attachmentId"> <translate translate-context="*/*/*">Upload New Picture…</translate> </label> - <input class="ui input" ref="attachment" type="file" :id="attachmentId" accept="image/x-png,image/jpeg" @change="submit" /> + <input + :id="attachmentId" + ref="attachment" + class="ui input" + type="file" + accept="image/x-png,image/jpeg" + @change="submit" + > </div> - <div class="ui very small hidden divider"></div> - <p><translate translate-context="Content/*/Paragraph">PNG or JPG. Dimensions should be between 1400x1400px and 3000x3000px. Maximum file size allowed is 5MB.</translate></p> - <button class="ui basic tiny button" v-if="value" @click.stop.prevent="remove(value)"> - <translate translate-context="Content/Radio/Button.Label/Verb">Remove</translate> + <div class="ui very small hidden divider" /> + <p> + <translate translate-context="Content/*/Paragraph"> + PNG or JPG. Dimensions should be between 1400x1400px and 3000x3000px. Maximum file size allowed is 5MB. + </translate> + </p> + <button + v-if="value" + class="ui basic tiny button" + @click.stop.prevent="remove(value)" + > + <translate translate-context="Content/Radio/Button.Label/Verb"> + Remove + </translate> </button> - <div v-if="isLoading" class="ui active inverted dimmer"> + <div + v-if="isLoading" + class="ui active inverted dimmer" + > <div class="ui indeterminate text loader"> - <translate translate-context="Content/*/*/Noun">Uploading file…</translate> + <translate translate-context="Content/*/*/Noun"> + Uploading file… + </translate> </div> </div> </div> @@ -43,8 +91,8 @@ import axios from 'axios' export default { props: { - value: {}, - imageClass: {default: '', required: false} + value: { type: String, required: true }, + imageClass: { type: String, default: '', required: false } }, data () { return { @@ -52,21 +100,29 @@ export default { isLoading: false, errors: [], initialValue: this.value, - attachmentId: Math.random().toString(36).substring(7), + attachmentId: Math.random().toString(36).substring(7) + } + }, + watch: { + value (v) { + if (this.attachment && v === this.initialValue) { + // we had a reset to initial value + this.remove(this.attachment.uuid) + } } }, methods: { - submit() { + submit () { this.isLoading = true this.errors = [] - let self = this + const self = this this.file = this.$refs.attachment.files[0] - let formData = new FormData() - formData.append("file", this.file) + const formData = new FormData() + formData.append('file', this.file) axios - .post(`attachments/`, formData, { + .post('attachments/', formData, { headers: { - "Content-Type": "multipart/form-data" + 'Content-Type': 'multipart/form-data' } }) .then( @@ -81,10 +137,10 @@ export default { } ) }, - remove(uuid) { + remove (uuid) { this.isLoading = true this.errors = [] - let self = this + const self = this axios.delete(`attachments/${uuid}/`) .then( response => { @@ -97,14 +153,6 @@ export default { self.errors = error.backendErrors } ) - }, - }, - watch: { - value (v) { - if (this.attachment && v === this.initialValue) { - // we had a reset to initial value - this.remove(this.attachment.uuid) - } } } } diff --git a/front/src/components/common/CollapseLink.vue b/front/src/components/common/CollapseLink.vue index 072a3282bad8bac66383f5d465ce4c8c659ca222..30d6dc326ec9e722c95859a0dd6eed02e53e2cee 100644 --- a/front/src/components/common/CollapseLink.vue +++ b/front/src/components/common/CollapseLink.vue @@ -1,15 +1,27 @@ <template> - <a role="button" class="collapse link" @click.prevent="$emit('input', !value)"> - <translate v-if="isCollapsed" key="1" translate-context="*/*/Button,Label">Expand</translate> - <translate v-else key="2" translate-context="*/*/Button,Label">Collapse</translate> - <i :class="[{down: !isCollapsed}, {right: isCollapsed}, 'angle', 'icon']"></i> + <a + role="button" + class="collapse link" + @click.prevent="$emit('input', !value)" + > + <translate + v-if="isCollapsed" + key="1" + translate-context="*/*/Button,Label" + >Expand</translate> + <translate + v-else + key="2" + translate-context="*/*/Button,Label" + >Collapse</translate> + <i :class="[{down: !isCollapsed}, {right: isCollapsed}, 'angle', 'icon']" /> </a> </template> <script> export default { props: { - value: {type: Boolean, required: true}, + value: { type: Boolean, required: true } }, computed: { isCollapsed () { diff --git a/front/src/components/common/ContentForm.vue b/front/src/components/common/ContentForm.vue index 4b1fd73c65fa3050ce303ff3154c1b92cea7b57e..29f333341d2102ef36f1cdc5dffc06c4847f07d7 100644 --- a/front/src/components/common/ContentForm.vue +++ b/front/src/components/common/ContentForm.vue @@ -2,48 +2,71 @@ <div class="content-form ui segments"> <div class="ui segment"> <div class="ui tiny secondary pointing menu"> - <button @click.prevent="isPreviewing = false" :class="[{active: !isPreviewing}, 'item']"> - <translate translate-context="*/Form/Menu.item">Write</translate> + <button + :class="[{active: !isPreviewing}, 'item']" + @click.prevent="isPreviewing = false" + > + <translate translate-context="*/Form/Menu.item"> + Write + </translate> </button> - <button @click.prevent="isPreviewing = true" :class="[{active: isPreviewing}, 'item']"> - <translate translate-context="*/Form/Menu.item">Preview</translate> + <button + :class="[{active: isPreviewing}, 'item']" + @click.prevent="isPreviewing = true" + > + <translate translate-context="*/Form/Menu.item"> + Preview + </translate> </button> </div> - <template v-if="isPreviewing" > - - <div class="ui placeholder" v-if="isLoadingPreview"> + <template v-if="isPreviewing"> + <div + v-if="isLoadingPreview" + class="ui placeholder" + > <div class="paragraph"> - <div class="line"></div> - <div class="line"></div> - <div class="line"></div> - <div class="line"></div> + <div class="line" /> + <div class="line" /> + <div class="line" /> + <div class="line" /> </div> </div> <p v-else-if="preview === null"> - <translate translate-context="*/Form/Paragraph">Nothing to preview.</translate> + <translate translate-context="*/Form/Paragraph"> + Nothing to preview. + </translate> </p> - <div v-html="preview" v-else></div> + <div + v-else + v-html="preview" + /> </template> <template v-else> <div class="ui transparent input"> <textarea + :id="fieldId" ref="textarea" + v-model="newValue" :name="fieldId" - :id="fieldId" :rows="rows" - v-model="newValue" :required="required" - :placeholder="placeholder || labels.placeholder"></textarea> + :placeholder="placeholder || labels.placeholder" + /> </div> - <div class="ui very small hidden divider"></div> + <div class="ui very small hidden divider" /> </template> </div> <div class="ui bottom attached segment"> - <span :class="['right', 'floated', {'ui danger text': remainingChars < 0}]" v-if="charLimit"> + <span + v-if="charLimit" + :class="['right', 'floated', {'ui danger text': remainingChars < 0}]" + > {{ remainingChars }} </span> <p> - <translate translate-context="*/Form/Paragraph">Markdown syntax is supported.</translate> + <translate translate-context="*/Form/Paragraph"> + Markdown syntax is supported. + </translate> </p> </div> </div> @@ -54,50 +77,31 @@ import axios from 'axios' export default { props: { - value: {type: String, default: ""}, - fieldId: {type: String, default: "change-content"}, - placeholder: {type: String, default: null}, - autofocus: {type: Boolean, default: false}, - charLimit: {type: Number, default: 5000, required: false}, - rows: {type: Number, default: 5, required: false}, - permissive: {type: Boolean, default: false}, - required: {type: Boolean, default: false}, + value: { type: String, default: '' }, + fieldId: { type: String, default: 'change-content' }, + placeholder: { type: String, default: null }, + autofocus: { type: Boolean, default: false }, + charLimit: { type: Number, default: 5000, required: false }, + rows: { type: Number, default: 5, required: false }, + permissive: { type: Boolean, default: false }, + required: { type: Boolean, default: false } }, data () { return { isPreviewing: false, preview: null, newValue: this.value, - isLoadingPreview: false, - } - }, - mounted () { - if (this.autofocus) { - this.$nextTick(() => { - this.$refs.textarea.focus() - }) - } - }, - methods: { - async loadPreview () { - this.isLoadingPreview = true - try { - let response = await axios.post('text-preview/', {text: this.newValue, permissive: this.permissive}) - this.preview = response.data.rendered - } catch { - - } - this.isLoadingPreview = false + isLoadingPreview: false } }, computed: { labels () { return { - placeholder: this.$pgettext("*/Form/Placeholder", "Write a few words here…") + placeholder: this.$pgettext('*/Form/Placeholder', 'Write a few words here…') } }, remainingChars () { - return this.charLimit - (this.value || "").length + return this.charLimit - (this.value || '').length } }, watch: { @@ -113,7 +117,7 @@ export default { await this.loadPreview() } }, - immediate: true, + immediate: true }, async isPreviewing (v) { if (v && !!this.value && this.preview === null && !this.isLoadingPreview) { @@ -125,6 +129,25 @@ export default { }) } } + }, + mounted () { + if (this.autofocus) { + this.$nextTick(() => { + this.$refs.textarea.focus() + }) + } + }, + methods: { + async loadPreview () { + this.isLoadingPreview = true + try { + const response = await axios.post('text-preview/', { text: this.newValue, permissive: this.permissive }) + this.preview = response.data.rendered + } catch { + + } + this.isLoadingPreview = false + } } } </script> diff --git a/front/src/components/common/CopyInput.vue b/front/src/components/common/CopyInput.vue index 0878e4edf8f3fc3c9a29669cd807048c35019a47..614b81382d5fc72cd8f607a869172838fd779290 100644 --- a/front/src/components/common/CopyInput.vue +++ b/front/src/components/common/CopyInput.vue @@ -1,21 +1,38 @@ <template> <div class="ui fluid action input component-copy-input"> - <p class="message" v-if="copied"> - <translate translate-context="Content/*/Paragraph">Text copied to clipboard!</translate> + <p + v-if="copied" + class="message" + > + <translate translate-context="Content/*/Paragraph"> + Text copied to clipboard! + </translate> </p> - <input :id="id" :name="id" ref="input" :value="value" type="text" readonly> - <button @click="copy" :class="['ui', buttonClasses, 'right', 'labeled', 'icon', 'button']"> - <i class="copy icon"></i> - <translate translate-context="*/*/Button.Label/Short, Verb">Copy</translate> + <input + :id="id" + ref="input" + :name="id" + :value="value" + type="text" + readonly + > + <button + :class="['ui', buttonClasses, 'right', 'labeled', 'icon', 'button']" + @click="copy" + > + <i class="copy icon" /> + <translate translate-context="*/*/Button.Label/Short, Verb"> + Copy + </translate> </button> </div> </template> <script> export default { props: { - value: {type: String}, - buttonClasses: {type: String, default: 'accent'}, - id: {type: String, default: 'copy-input'}, + value: { type: String, required: true }, + buttonClasses: { type: String, default: 'accent' }, + id: { type: String, default: 'copy-input' } }, data () { return { @@ -29,8 +46,8 @@ export default { clearTimeout(this.timeout) } this.$refs.input.select() - document.execCommand("Copy") - let self = this + document.execCommand('Copy') + const self = this self.copied = true this.timeout = setTimeout(() => { self.copied = false diff --git a/front/src/components/common/DangerousButton.vue b/front/src/components/common/DangerousButton.vue index 4ffe35c6fbbe26c824757805962359984c182f7d..25a25d2fc01b2c1114f20396a4744943c5061123 100644 --- a/front/src/components/common/DangerousButton.vue +++ b/front/src/components/common/DangerousButton.vue @@ -1,44 +1,59 @@ <template> - <button @click="showModal = true" :class="[{disabled: disabled}]" :disabled="disabled"> - <slot></slot> + <button + :class="[{disabled: disabled}]" + :disabled="disabled" + @click="showModal = true" + > + <slot /> - <modal class="small" :show.sync="showModal"> + <modal + class="small" + :show.sync="showModal" + > <h4 class="header"> <slot name="modal-header"> - <translate translate-context="Modal/*/Title">Do you want to confirm this action?</translate> + <translate translate-context="Modal/*/Title"> + Do you want to confirm this action? + </translate> </slot> </h4> <div class="scrolling content"> <div class="description"> - <slot name="modal-content"></slot> + <slot name="modal-content" /> </div> </div> <div class="actions"> <button class="ui basic cancel button"> - <translate translate-context="*/*/Button.Label/Verb">Cancel</translate> + <translate translate-context="*/*/Button.Label/Verb"> + Cancel + </translate> </button> - <button :class="['ui', 'confirm', confirmButtonColor, 'button']" @click="confirm"> + <button + :class="['ui', 'confirm', confirmButtonColor, 'button']" + @click="confirm" + > <slot name="modal-confirm"> - <translate translate-context="Modal/*/Button.Label/Short, Verb">Confirm</translate> + <translate translate-context="Modal/*/Button.Label/Short, Verb"> + Confirm + </translate> </slot> </button> </div> </modal> </button> - </template> <script> import Modal from '@/components/semantic/Modal' export default { - props: { - action: {type: Function, required: false}, - disabled: {type: Boolean, default: false}, - confirmColor: {type: String, default: "danger", required: false} - }, components: { Modal }, + props: { + action: { type: Function, required: false, default: () => {} }, + disabled: { type: Boolean, default: false }, + confirmColor: { type: String, default: 'danger', required: false } + }, data () { return { showModal: false diff --git a/front/src/components/common/Duration.vue b/front/src/components/common/Duration.vue index 4ee8ccfd168ead2571caaabc33094d3ce43b7d55..81a0b578f1b827e0a6f2c21d0115170ff3f7614f 100644 --- a/front/src/components/common/Duration.vue +++ b/front/src/components/common/Duration.vue @@ -1,18 +1,22 @@ <template> <span> - <translate translate-context="Content/*/Paragraph" + <translate v-if="durationData.hours > 0" - :translate-params="{minutes: durationData.minutes, hours: durationData.hours}">%{ hours } h %{ minutes } min</translate> - <translate translate-context="Content/*/Paragraph" + translate-context="Content/*/Paragraph" + :translate-params="{minutes: durationData.minutes, hours: durationData.hours}" + >%{ hours } h %{ minutes } min</translate> + <translate v-else - :translate-params="{minutes: durationData.minutes}">%{ minutes } min</translate> + translate-context="Content/*/Paragraph" + :translate-params="{minutes: durationData.minutes}" + >%{ minutes } min</translate> </span> </template> <script> -import {secondsToObject} from '@/filters' +import { secondsToObject } from '@/filters' export default { - props: ['seconds'], + props: { seconds: { type: Number, required: true } }, computed: { durationData () { return secondsToObject(this.seconds) diff --git a/front/src/components/common/EmptyState.vue b/front/src/components/common/EmptyState.vue index 661cc6ebfccb9e69cb7f12c10a61967991a5b20b..54e1b13e3765cdc99d66ae548d0dc4cf724d893b 100644 --- a/front/src/components/common/EmptyState.vue +++ b/front/src/components/common/EmptyState.vue @@ -3,8 +3,7 @@ <h4 class="ui header"> <div class="content"> <slot name="title"> - - <i class="search icon"></i> + <i class="search icon" /> <translate translate-context="Content/*/Paragraph"> No results were found. </translate> @@ -12,8 +11,12 @@ </div> </h4> <div class="inline center aligned text"> - <slot></slot> - <button v-if="refresh" class="ui button" @click="$emit('refresh')"> + <slot /> + <button + v-if="refresh" + class="ui button" + @click="$emit('refresh')" + > <translate translate-context="Content/*/Button.Label/Short, Verb"> Refresh </translate> @@ -24,7 +27,7 @@ <script> export default { props: { - refresh: {type: Boolean, default: false} + refresh: { type: Boolean, default: false } } } </script> diff --git a/front/src/components/common/ExpandableDiv.vue b/front/src/components/common/ExpandableDiv.vue index 2a95a11a27c98ad853148a82ecd9e6c260ae1c0f..54ddc7b36b6e0b01685b3d2f6d16724473b59cb9 100644 --- a/front/src/components/common/ExpandableDiv.vue +++ b/front/src/components/common/ExpandableDiv.vue @@ -3,10 +3,22 @@ <div :class="['expandable-content', {expandable: truncated.length < content.length}, {expanded: isExpanded}]"> <slot>{{ content }}</slot> </div> - <a v-if="truncated.length < content.length" role="button" @click.prevent="isExpanded = !isExpanded"> + <a + v-if="truncated.length < content.length" + role="button" + @click.prevent="isExpanded = !isExpanded" + > <br> - <translate v-if="isExpanded" key="1" translate-context="*/*/Button,Label">Show less</translate> - <translate v-else key="2" translate-context="*/*/Button,Label">Show more</translate> + <translate + v-if="isExpanded" + key="1" + translate-context="*/*/Button,Label" + >Show less</translate> + <translate + v-else + key="2" + translate-context="*/*/Button,Label" + >Show more</translate> </a> </div> </template> @@ -15,12 +27,12 @@ export default { props: { - content: {type: String, required: true}, - length: {type: Number, default: 150, required: false}, + content: { type: String, required: true }, + length: { type: Number, default: 150, required: false } }, data () { return { - isExpanded: false, + isExpanded: false } }, computed: { diff --git a/front/src/components/common/HumanDate.vue b/front/src/components/common/HumanDate.vue index fde04f141399428ff413154775ce51eada0d0623..5dc267adf3913dbecbf76160347721b9ece10730 100644 --- a/front/src/components/common/HumanDate.vue +++ b/front/src/components/common/HumanDate.vue @@ -1,15 +1,21 @@ <template> - <time :datetime="date" :title="date | moment"> - <i v-if="icon" class="outline clock icon"></i> + <time + :datetime="date" + :title="date | moment" + > + <i + v-if="icon" + class="outline clock icon" + /> {{ realDate | ago($store.state.ui.momentLocale) }} </time> </template> <script> -import {mapState} from 'vuex' +import { mapState } from 'vuex' export default { props: { - date: {required: true}, - icon: {type: Boolean, required: false, default: false}, + date: { type: String, required: true }, + icon: { type: Boolean, required: false, default: false } }, computed: { ...mapState({ diff --git a/front/src/components/common/HumanDuration.vue b/front/src/components/common/HumanDuration.vue index 07b4fdd03f3e5ae414256d96eed6e7d89f9b5227..84ed24c7999af660e0dce955e3b0b750541d8244 100644 --- a/front/src/components/common/HumanDuration.vue +++ b/front/src/components/common/HumanDuration.vue @@ -1,13 +1,12 @@ <template> <time :datetime="`${duration}s`"> - {{ duration | duration}} + {{ duration | duration }} </time> - </template> <script> export default { props: { - duration: {required: true}, - }, + duration: { type: Object, required: true } + } } </script> diff --git a/front/src/components/common/InlineSearchBar.vue b/front/src/components/common/InlineSearchBar.vue index 3a32b84c0fb736882895e4ddb45008edb1ccf584..590e989616f5319e25434253a43f9a05d84d0183 100644 --- a/front/src/components/common/InlineSearchBar.vue +++ b/front/src/components/common/InlineSearchBar.vue @@ -1,13 +1,34 @@ <template> - <form class="ui inline form" @submit.stop.prevent="$emit('search', value)"> + <form + class="ui inline form" + @submit.stop.prevent="$emit('search', value)" + > <div :class="['ui', 'action', {icon: isClearable}, 'input']"> - <label for="search-query" class="hidden"> + <label + for="search-query" + class="hidden" + > <translate translate-context="Content/Search/Input.Label/Noun">Search</translate> </label> - <input id="search-query" name="search-query" type="text" :placeholder="placeholder || labels.searchPlaceholder" :value="value" @input="$emit('input', $event.target.value)"> - <i v-if="isClearable" class="x link icon" :title="labels.clear" @click.stop.prevent="$emit('input', ''); $emit('search', value)"></i> - <button type="submit" class="ui icon basic button"> - <i class="search icon"></i> + <input + id="search-query" + name="search-query" + type="text" + :placeholder="placeholder || labels.searchPlaceholder" + :value="value" + @input="$emit('input', $event.target.value)" + > + <i + v-if="isClearable" + class="x link icon" + :title="labels.clear" + @click.stop.prevent="$emit('input', ''); $emit('search', value)" + /> + <button + type="submit" + class="ui icon basic button" + > + <i class="search icon" /> </button> </div> </form> @@ -15,14 +36,14 @@ <script> export default { props: { - value: {type: String, required: true}, - placeholder: {type: String, required: false}, + value: { type: String, required: true }, + placeholder: { type: String, required: false, default: '' } }, computed: { labels () { return { searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search…'), - clear: this.$pgettext("Content/Library/Button.Label", 'Clear'), + clear: this.$pgettext('Content/Library/Button.Label', 'Clear') } }, isClearable () { diff --git a/front/src/components/common/LoginModal.vue b/front/src/components/common/LoginModal.vue index 30cfb5b89337c79b345fbdc55213bb06c76e35b0..5f2d5b7c20497d47b5dc4b44967292d35e1e2214 100644 --- a/front/src/components/common/LoginModal.vue +++ b/front/src/components/common/LoginModal.vue @@ -1,64 +1,81 @@ <template> - <modal :show.sync="show"> - <h4 class="header">{{ labels.header }}</h4> - <div v-if="cover" class="image content"> - <div class="ui medium image"> - <img :src="cover.urls.medium_square_crop"> - </div> - <div class="description"> - <div class="ui header"> - {{ labels.description }} - </div> - <p> - {{ message }} - </p> - </div> + <modal :show.sync="show"> + <h4 class="header"> + {{ labels.header }} + </h4> + <div + v-if="cover" + class="image content" + > + <div class="ui medium image"> + <img :src="cover.urls.medium_square_crop"> </div> - <div v-else class="content"> - <div class="ui centered header"> + <div class="description"> + <div class="ui header"> {{ labels.description }} </div> - <p style="text-align: center;"> + <p> {{ message }} </p> </div> - <div class="actions"> - <router-link :to="{path: '/login', query: { next: nextRoute }}" class="ui labeled icon button"><i class="key icon"></i> - {{ labels.login }} - </router-link> - <router-link v-if="$store.state.instance.settings.users.registration_enabled.value" :to="{path: '/signup'}" class="ui labeled icon button"><i class="user icon"></i> - {{ labels.signup }} - </router-link> + </div> + <div + v-else + class="content" + > + <div class="ui centered header"> + {{ labels.description }} </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> <script> import Modal from '@/components/semantic/Modal' export default { - props: { - nextRoute: {type: String}, - message: {type: String}, - cover: {type: Object}, - }, components: { - Modal, + Modal }, - data() { + props: { + nextRoute: { type: String, required: true }, + message: { type: String, required: true }, + cover: { type: Object, required: true } + }, + data () { return { - show: false, + show: false } }, computed: { - labels() { + labels () { return { - header: this.$pgettext('Popup/Title/Noun', "Unauthenticated"), - login: this.$pgettext('*/*/Button.Label/Verb', "Log in"), - signup: this.$pgettext('*/*/Button.Label/Verb', "Sign up"), - description: this.$pgettext('Popup/*/Paragraph', "You don't have access!"), + header: this.$pgettext('Popup/Title/Noun', 'Unauthenticated'), + login: this.$pgettext('*/*/Button.Label/Verb', 'Log in'), + signup: this.$pgettext('*/*/Button.Label/Verb', 'Sign up'), + description: this.$pgettext('Popup/*/Paragraph', "You don't have access!") } - }, + } } } diff --git a/front/src/components/common/Message.vue b/front/src/components/common/Message.vue index 9bf713f9565b815856aa9bdabda11e875d5a6467..7c57e30d1196fffa6e28c12c7e4f18a6f1dc0d14 100644 --- a/front/src/components/common/Message.vue +++ b/front/src/components/common/Message.vue @@ -1,27 +1,27 @@ <template> - <div></div> + <div /> </template> <script> import $ from 'jquery' export default { - props: ['message'], + props: { message: { type: Object, required: true } }, mounted () { - let self = this - let params = { - context: "#app", + const self = this + const params = { + context: '#app', message: this.message.content, showProgress: 'top', - position: "bottom right", + position: 'bottom right', progressUp: true, onRemove () { - self.$store.commit("ui/removeMessage", self.message.key) + self.$store.commit('ui/removeMessage', self.message.key) }, - ...this.message, + ...this.message } - $("body").toast(params) + $('body').toast(params) - $(".ui.toast.visible").last().attr('role', 'alert') + $('.ui.toast.visible').last().attr('role', 'alert') } } </script> diff --git a/front/src/components/common/RenderedDescription.vue b/front/src/components/common/RenderedDescription.vue index b7db84d644c52d04f72dda7551f57e3c207d5e2b..674591264845a71aaf7938ddc072220165dcedb1 100644 --- a/front/src/components/common/RenderedDescription.vue +++ b/front/src/components/common/RenderedDescription.vue @@ -1,79 +1,113 @@ <template> <div> <template v-if="content && !isUpdating"> - <div v-html="html"></div> + <div v-html="html" /> <template v-if="isTruncated"> - <div class="ui small hidden divider"></div> - <a href="" @click.stop.prevent="showMore = true" v-if="showMore === false"> + <div class="ui small hidden divider" /> + <a + v-if="showMore === false" + href="" + @click.stop.prevent="showMore = true" + > <translate translate-context="*/*/Button,Label">Show more</translate> </a> - <a href="" @click.stop.prevent="showMore = false" v-else="showMore === true"> + <a + v-else + href="" + @click.stop.prevent="showMore = false" + > <translate translate-context="*/*/Button,Label">Show less</translate> </a> - </template> </template> <p v-else-if="!isUpdating"> - <translate translate-context="*/*/Placeholder">No description available</translate> + <translate translate-context="*/*/Placeholder"> + No description available + </translate> </p> <template v-if="!isUpdating && canUpdate && updateUrl"> - <div class="ui hidden divider"></div> - <span role="button" @click="isUpdating = true"> - <i class="pencil icon"></i> + <div class="ui hidden divider" /> + <span + role="button" + @click="isUpdating = true" + > + <i class="pencil icon" /> <translate translate-context="Content/*/Button.Label/Verb">Edit</translate> </span> </template> - <form v-if="isUpdating" class="ui form" @submit.prevent="submit()"> - <div v-if="errors.length > 0" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Content/Channels/Error message.Title">Error while updating description</translate></h4> + <form + v-if="isUpdating" + class="ui form" + @submit.prevent="submit()" + > + <div + v-if="errors.length > 0" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Content/Channels/Error message.Title"> + Error while updating description + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> - <content-form v-model="newText" :autofocus="true"></content-form> - <a @click.prevent="isUpdating = false" class="left floated"> + <content-form + v-model="newText" + :autofocus="true" + /> + <a + class="left floated" + @click.prevent="isUpdating = false" + > <translate translate-context="*/*/Button.Label/Verb">Cancel</translate> </a> - <button :class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']" type="submit" :disabled="isLoading"> - <translate translate-context="Content/Channels/Button.Label/Verb">Update description</translate> + <button + :class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']" + type="submit" + :disabled="isLoading" + > + <translate translate-context="Content/Channels/Button.Label/Verb"> + Update description + </translate> </button> - <div class="ui clearing hidden divider"></div> + <div class="ui clearing hidden divider" /> </form> </div> </template> <script> -import {secondsToObject} from '@/filters' import axios from 'axios' import clip from 'text-clipper' export default { props: { - content: {required: true}, - fieldName: {required: false, default: 'description'}, - updateUrl: {required: false, type: String}, - canUpdate: {required: false, default: true, type: Boolean}, - fetchHtml: {required: false, default: false, type: Boolean}, - permissive: {required: false, default: false, type: Boolean}, - truncateLength: {required: false, default: 500, type: Number}, + content: { type: String, required: true }, + fieldName: { type: String, required: false, default: 'description' }, + updateUrl: { required: false, type: String, default: '' }, + canUpdate: { required: false, default: true, type: Boolean }, + fetchHtml: { required: false, default: false, type: Boolean }, + permissive: { required: false, default: false, type: Boolean }, + truncateLength: { required: false, default: 500, type: Number } }, data () { return { isUpdating: false, showMore: false, - newText: (this.content || {text: ''}).text, - errors: null, + newText: (this.content || { text: '' }).text, isLoading: false, errors: [], preview: null } }, - async created () { - if (this.fetchHtml) { - await this.fetchPreview() - } - }, computed: { html () { if (this.fetchHtml) { @@ -91,21 +125,26 @@ export default { return this.truncateLength > 0 && this.truncatedHtml.length < this.content.html.length } }, + async created () { + if (this.fetchHtml) { + await this.fetchPreview() + } + }, methods: { async fetchPreview () { - let response = await axios.post('text-preview/', {text: this.content.text, permissive: this.permissive}) + const response = await axios.post('text-preview/', { text: this.content.text, permissive: this.permissive }) this.preview = response.data.rendered }, submit () { - let self = this + const self = this this.isLoading = true this.errors = [] - let payload = {} + const payload = {} payload[this.fieldName] = null if (this.newText) { payload[this.fieldName] = { - content_type: "text/markdown", - text: this.newText, + content_type: 'text/markdown', + text: this.newText } } axios.patch(this.updateUrl, payload).then((response) => { @@ -116,7 +155,7 @@ export default { self.errors = error.backendErrors self.isLoading = false }) - }, + } } } </script> diff --git a/front/src/components/common/Tooltip.vue b/front/src/components/common/Tooltip.vue index d9ba4c13cd9fbe4312c758cc6566d6734878bdd5..d59d5ef66756e0a75fd196a1be3e3590f8d0efde 100644 --- a/front/src/components/common/Tooltip.vue +++ b/front/src/components/common/Tooltip.vue @@ -1,12 +1,15 @@ <template> - <span class="tooltip" :data-tooltip="content"><i class="question circle icon"></i></span> + <span + class="tooltip" + :data-tooltip="content" + ><i class="question circle icon" /></span> </template> <script> export default { props: { - content: {type: String, required: true}, + content: { type: String, required: true } } } </script> diff --git a/front/src/components/common/UserLink.vue b/front/src/components/common/UserLink.vue index 6a372e20b3afcf2efc984f7047cb30a1937adda4..63831c56a525e06f454721b5af2db4b44bf7a05a 100644 --- a/front/src/components/common/UserLink.vue +++ b/front/src/components/common/UserLink.vue @@ -2,11 +2,16 @@ <span class="component-user-link"> <template v-if="avatar"> <img + v-if="user.avatar && user.avatar.urls.medium_square_crop" + v-lazy="$store.getters['instance/absoluteUrl'](user.avatar.urls.medium_square_crop)" class="ui tiny circular avatar" alt="" - v-if="user.avatar && user.avatar.urls.medium_square_crop" - v-lazy="$store.getters['instance/absoluteUrl'](user.avatar.urls.medium_square_crop)" /> - <span v-else :style="defaultAvatarStyle" class="ui circular label">{{ user.username[0]}}</span> + > + <span + v-else + :style="defaultAvatarStyle" + class="ui circular label" + >{{ user.username[0] }}</span> </template> @{{ user.username }} @@ -14,12 +19,12 @@ </template> <script> -import {hashCode, intToRGB} from '@/utils/color' +import { hashCode, intToRGB } from '@/utils/color' export default { props: { - user: {required: true}, - avatar: {type: Boolean, default: true} + user: { type: String, required: true }, + avatar: { type: Boolean, default: true } }, computed: { userColor () { diff --git a/front/src/components/common/UserModal.vue b/front/src/components/common/UserModal.vue index 928b12d9fb735d99df8aa850cc7d5c653dc5420b..f43e79be1542d25c93c96351d5a73a42202c54e8 100644 --- a/front/src/components/common/UserModal.vue +++ b/front/src/components/common/UserModal.vue @@ -101,12 +101,12 @@ </template> <div class="row"> <a - class="column" - href="https://funkwhale.audio/help" - target="_blank" - > - <i class="user-modal list-icon life ring outline icon" /> - <span class="user-modal list-item">{{ labels.help }}</span> + class="column" + href="https://funkwhale.audio/help" + target="_blank" + > + <i class="user-modal list-icon life ring outline icon" /> + <span class="user-modal list-item">{{ labels.help }}</span> </a> </div> <div class="row"> diff --git a/front/src/components/common/Username.vue b/front/src/components/common/Username.vue index 17fb34925c51249bb09f237c582ffbb4a9451c8d..7c25a124fd8d2665fcb4c617967f2cd1e9167a2e 100644 --- a/front/src/components/common/Username.vue +++ b/front/src/components/common/Username.vue @@ -3,6 +3,6 @@ </template> <script> export default { - props: ['username'] + props: { username: { type: String, required: true } } } </script> diff --git a/front/src/components/favorites/List.vue b/front/src/components/favorites/List.vue index 5618107bab3f3db6134e548647b45163823cb2f6..5b143c4f0b6aec9babf4684e3d3371ed4e50f20c 100644 --- a/front/src/components/favorites/List.vue +++ b/front/src/components/favorites/List.vue @@ -1,98 +1,158 @@ <template> - <main class="main pusher" v-title="labels.title"> + <main + v-title="labels.title" + class="main pusher" + > <section class="ui vertical center aligned stripe segment"> <div :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']"> <div class="ui text loader"> - <translate translate-context="Content/Favorites/Message">Loading your favorites…</translate> + <translate translate-context="Content/Favorites/Message"> + Loading your favorites… + </translate> </div> </div> - <h2 v-if="results" class="ui center aligned icon header"> - <i class="circular inverted heart pink icon"></i> + <h2 + v-if="results" + class="ui center aligned icon header" + > + <i class="circular inverted heart pink icon" /> <translate translate-plural="%{ count } favorites" :translate-n="$store.state.favorites.count" :translate-params="{count: results.count}" - translate-context="Content/Favorites/Title"> - %{ count } favorite + translate-context="Content/Favorites/Title" + > + %{ count } favorite </translate> </h2> - <radio-button v-if="hasFavorites" type="favorites"></radio-button> + <radio-button + v-if="hasFavorites" + type="favorites" + /> </section> - <section v-if="hasFavorites" class="ui vertical stripe segment"> + <section + v-if="hasFavorites" + class="ui vertical stripe segment" + > <div :class="['ui', {'loading': isLoading}, 'form']"> <div class="fields"> <div class="field"> <label for="favorites-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> - <select id="favorites-ordering" class="ui dropdown" v-model="ordering"> - <option v-for="option in orderingOptions" :value="option[0]" :key="option[0]"> + <select + id="favorites-ordering" + v-model="ordering" + class="ui dropdown" + > + <option + v-for="option in orderingOptions" + :key="option[0]" + :value="option[0]" + > {{ sharedLabels.filters[option[1]] }} </option> </select> </div> <div class="field"> <label for="favorites-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Order</translate></label> - <select id="favorites-ordering-direction" class="ui dropdown" v-model="orderingDirection"> - <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option> - <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option> + <select + id="favorites-ordering-direction" + v-model="orderingDirection" + class="ui dropdown" + > + <option value="+"> + <translate translate-context="Content/Search/Dropdown"> + Ascending + </translate> + </option> + <option value="-"> + <translate translate-context="Content/Search/Dropdown"> + Descending + </translate> + </option> </select> </div> <div class="field"> <label for="favorites-results"><translate translate-context="Content/Search/Dropdown.Label/Noun">Results per page</translate></label> - <select id="favorites-results" class="ui dropdown" v-model="paginateBy"> - <option :value="parseInt(12)">12</option> - <option :value="parseInt(25)">25</option> - <option :value="parseInt(50)">50</option> + <select + id="favorites-results" + v-model="paginateBy" + class="ui dropdown" + > + <option :value="parseInt(12)"> + 12 + </option> + <option :value="parseInt(25)"> + 25 + </option> + <option :value="parseInt(50)"> + 50 + </option> </select> </div> </div> </div> - <track-table :show-artist="true" :show-album="true" v-if="results" :tracks="results.results"></track-table> + <track-table + v-if="results" + :show-artist="true" + :show-album="true" + :tracks="results.results" + /> <div class="ui center aligned basic segment"> <pagination v-if="results && results.count > paginateBy" - @page-changed="selectPage" :current="page" :paginate-by="paginateBy" :total="results.count" - ></pagination> + @page-changed="selectPage" + /> </div> </section> - <div v-else class="ui placeholder segment"> + <div + v-else + class="ui placeholder segment" + > <div class="ui icon header"> - <i class="broken heart icon"></i> + <i class="broken heart icon" /> <translate translate-context="Content/Home/Placeholder" - >No tracks have been added to your favorites yet</translate> + > + No tracks have been added to your favorites yet + </translate> </div> - <router-link :to="'/library'" class="ui success labeled icon button"> - <i class="headphones icon"></i> - <translate translate-context="Content/*/Verb">Browse the library</translate> + <router-link + :to="'/library'" + class="ui success labeled icon button" + > + <i class="headphones icon" /> + <translate translate-context="Content/*/Verb"> + Browse the library + </translate> </router-link> </div> </main> </template> <script> -import axios from "axios" -import $ from "jquery" -import logger from "@/logging" -import RadioButton from "@/components/radios/Button" -import Pagination from "@/components/Pagination" -import OrderingMixin from "@/components/mixins/Ordering" -import PaginationMixin from "@/components/mixins/Pagination" -import TranslationsMixin from "@/components/mixins/Translations" -import {checkRedirectToLogin} from '@/utils' +import axios from 'axios' +import $ from 'jquery' +import logger from '@/logging' +import RadioButton from '@/components/radios/Button' +import Pagination from '@/components/Pagination' +import OrderingMixin from '@/components/mixins/Ordering' +import PaginationMixin from '@/components/mixins/Pagination' +import TranslationsMixin from '@/components/mixins/Translations' +import { checkRedirectToLogin } from '@/utils' import TrackTable from '@/components/audio/track/Table' -const FAVORITES_URL = "tracks/" +const FAVORITES_URL = 'tracks/' export default { - mixins: [OrderingMixin, PaginationMixin, TranslationsMixin], components: { RadioButton, Pagination, TrackTable }, - data() { + mixins: [OrderingMixin, PaginationMixin, TranslationsMixin], + data () { return { results: null, isLoading: false, @@ -100,33 +160,46 @@ export default { previousLink: null, page: parseInt(this.defaultPage), orderingOptions: [ - ["creation_date", "creation_date"], - ["title", "track_title"], - ["album__title", "album_title"], - ["artist__name", "artist_name"] + ['creation_date', 'creation_date'], + ['title', 'track_title'], + ['album__title', 'album_title'], + ['artist__name', 'artist_name'] ] } }, - created() { - checkRedirectToLogin(this.$store, this.$router) - this.fetchFavorites(FAVORITES_URL) - - }, - mounted() { - $(".ui.dropdown").dropdown() - }, computed: { - labels() { + labels () { return { title: this.$pgettext('Head/Favorites/Title', 'Your Favorites') } }, hasFavorites () { return this.$store.state.favorites.count > 0 + } + }, + watch: { + page: function () { + this.updateQueryString() }, + paginateBy: function () { + this.updateQueryString() + }, + orderingDirection: function () { + this.updateQueryString() + }, + ordering: function () { + this.updateQueryString() + } + }, + created () { + checkRedirectToLogin(this.$store, this.$router) + this.fetchFavorites(FAVORITES_URL) + }, + mounted () { + $('.ui.dropdown').dropdown() }, methods: { - updateQueryString: function() { + updateQueryString: function () { this.$router.replace({ query: { page: this.page, @@ -136,44 +209,30 @@ export default { }) this.fetchFavorites(FAVORITES_URL) }, - fetchFavorites(url) { - var self = this + fetchFavorites (url) { + const self = this this.isLoading = true - let params = { - favorites: "true", + const params = { + favorites: 'true', page: this.page, page_size: this.paginateBy, ordering: this.getOrderingAsString() } - logger.default.time("Loading user favorites") + logger.default.time('Loading user favorites') axios.get(url, { params: params }).then(response => { self.results = response.data self.nextLink = response.data.next self.previousLink = response.data.previous self.results.results.forEach(track => { - self.$store.commit("favorites/track", { id: track.id, value: true }) + self.$store.commit('favorites/track', { id: track.id, value: true }) }) - logger.default.timeEnd("Loading user favorites") + logger.default.timeEnd('Loading user favorites') self.isLoading = false }) }, - selectPage: function(page) { + selectPage: function (page) { this.page = page } - }, - watch: { - page: function() { - this.updateQueryString() - }, - paginateBy: function() { - this.updateQueryString() - }, - orderingDirection: function() { - this.updateQueryString() - }, - ordering: function() { - this.updateQueryString() - } } } </script> diff --git a/front/src/components/favorites/TrackFavoriteIcon.vue b/front/src/components/favorites/TrackFavoriteIcon.vue index de292ca877a8b2bbbccdac1b75be6ece40feb64e..80db865f54f6fe3f6b2160cd71eb564a6e11edc3 100644 --- a/front/src/components/favorites/TrackFavoriteIcon.vue +++ b/front/src/components/favorites/TrackFavoriteIcon.vue @@ -1,25 +1,40 @@ - <template> - <button @click.stop="$store.dispatch('favorites/toggle', track.id)" v-if="button" :class="['ui', 'pink', {'inverted': isFavorite}, {'favorited': isFavorite}, 'icon', 'labeled', 'button']"> - <i class="heart icon"></i> - <translate v-if="isFavorite" translate-context="Content/Track/Button.Message">In favorites</translate> - <translate v-else translate-context="Content/Track/*/Verb">Add to favorites</translate> +<template> + <button + v-if="button" + :class="['ui', 'pink', {'inverted': isFavorite}, {'favorited': isFavorite}, 'icon', 'labeled', 'button']" + @click.stop="$store.dispatch('favorites/toggle', track.id)" + > + <i class="heart icon" /> + <translate + v-if="isFavorite" + translate-context="Content/Track/Button.Message" + > + In favorites + </translate> + <translate + v-else + translate-context="Content/Track/*/Verb" + > + Add to favorites + </translate> </button> <button v-else - @click.stop="$store.dispatch('favorites/toggle', track.id)" :class="['ui', 'favorite-icon', {'pink': isFavorite}, {'favorited': isFavorite}, 'basic', 'circular', 'icon', {'really': !border}, 'button']" :aria-label="title" - :title="title"> - <i :class="['heart', {'pink': isFavorite}, 'basic', 'icon']"></i> + :title="title" + @click.stop="$store.dispatch('favorites/toggle', track.id)" + > + <i :class="['heart', {'pink': isFavorite}, 'basic', 'icon']" /> </button> </template> <script> export default { props: { - track: {type: Object}, - button: {type: Boolean, default: false}, - border: {type: Boolean, default: false}, + track: { type: Object, default: () => { return {} } }, + button: { type: Boolean, default: false }, + border: { type: Boolean, default: false } }, computed: { title () { diff --git a/front/src/components/federation/FetchButton.vue b/front/src/components/federation/FetchButton.vue index 65ff28a09c3a192f66e2948635eb26978d53b43a..699def21ba31da58b731943760d81bb2d41a818b 100644 --- a/front/src/components/federation/FetchButton.vue +++ b/front/src/components/federation/FetchButton.vue @@ -1,30 +1,73 @@ <template> - <div @click="createFetch" role="button"> + <div + role="button" + @click="createFetch" + > <div> - <slot></slot> + <slot /> </div> - <modal class="small" :show.sync="showModal"> + <modal + class="small" + :show.sync="showModal" + > <h3 class="header"> - <translate translate-context="Popup/*/Title">Refreshing object from remote server…</translate> + <translate translate-context="Popup/*/Title"> + Refreshing object from remote server… + </translate> </h3> <div class="scrolling content"> <template v-if="fetch && fetch.status != 'pending'"> - <div v-if="fetch.status === 'skipped'" class="ui message"> - <h4 class="header"><translate translate-context="Popup/*/Message.Title">Refresh was skipped</translate></h4> - <p><translate translate-context="Popup/*/Message.Content">The remote server answered, but returned data was unsupported by Funkwhale.</translate></p> + <div + v-if="fetch.status === 'skipped'" + class="ui message" + > + <h4 class="header"> + <translate translate-context="Popup/*/Message.Title"> + Refresh was skipped + </translate> + </h4> + <p> + <translate translate-context="Popup/*/Message.Content"> + The remote server answered, but returned data was unsupported by Funkwhale. + </translate> + </p> </div> - <div v-else-if="fetch.status === 'finished'" class="ui success message"> - <h4 class="header"><translate translate-context="Popup/*/Message.Title">Refresh successful</translate></h4> - <p><translate translate-context="Popup/*/Message.Content">Data was refreshed successfully from remote server.</translate></p> + <div + v-else-if="fetch.status === 'finished'" + class="ui success message" + > + <h4 class="header"> + <translate translate-context="Popup/*/Message.Title"> + Refresh successful + </translate> + </h4> + <p> + <translate translate-context="Popup/*/Message.Content"> + Data was refreshed successfully from remote server. + </translate> + </p> </div> - <div v-else-if="fetch.status === 'errored'" class="ui error message"> - <h4 class="header"><translate translate-context="Popup/*/Message.Title">Refresh error</translate></h4> - <p><translate translate-context="Popup/*/Message.Content">An error occurred while trying to refresh data:</translate></p> + <div + v-else-if="fetch.status === 'errored'" + class="ui error message" + > + <h4 class="header"> + <translate translate-context="Popup/*/Message.Title"> + Refresh error + </translate> + </h4> + <p> + <translate translate-context="Popup/*/Message.Content"> + An error occurred while trying to refresh data: + </translate> + </p> <table class="ui very basic collapsing celled table"> <tbody> <tr> <td> - <translate translate-context="Popup/Import/Table.Label/Noun">Error type</translate> + <translate translate-context="Popup/Import/Table.Label/Noun"> + Error type + </translate> </td> <td> {{ fetch.detail.error_code }} @@ -32,61 +75,136 @@ </tr> <tr> <td> - <translate translate-context="Popup/Import/Table.Label/Noun">Error detail</translate> + <translate translate-context="Popup/Import/Table.Label/Noun"> + Error detail + </translate> </td> <td> <translate v-if="fetch.detail.error_code === 'http' && fetch.detail.status_code" :translate-params="{status: fetch.detail.status_code}" - translate-context="*/*/Error">The remote server answered with HTTP %{ status }</translate> + translate-context="*/*/Error" + > + The remote server answered with HTTP %{ status } + </translate> <translate v-else-if="['http', 'request'].indexOf(fetch.detail.error_code) > -1" - translate-context="*/*/Error">An HTTP error occurred while contacting the remote server</translate> + translate-context="*/*/Error" + > + An HTTP error occurred while contacting the remote server + </translate> <translate v-else-if="fetch.detail.error_code === 'timeout'" - translate-context="*/*/Error">The remote server didn't respond quickly enough</translate> + translate-context="*/*/Error" + > + The remote server didn't respond quickly enough + </translate> <translate v-else-if="fetch.detail.error_code === 'connection'" - translate-context="*/*/Error">Impossible to connect to the remote server</translate> + translate-context="*/*/Error" + > + Impossible to connect to the remote server + </translate> <translate v-else-if="['invalid_json', 'invalid_jsonld', 'missing_jsonld_type'].indexOf(fetch.detail.error_code) > -1" - translate-context="*/*/Error">The remote server returned invalid JSON or JSON-LD data</translate> - <translate v-else-if="fetch.detail.error_code === 'validation'" translate-context="*/*/Error">Data returned by the remote server had invalid or missing attributes</translate> - <translate v-else-if="fetch.detail.error_code === 'unhandled'" translate-context="*/*/Error">Unknown error</translate> - <translate v-else translate-context="*/*/Error">Unknown error</translate> + translate-context="*/*/Error" + > + The remote server returned invalid JSON or JSON-LD data + </translate> + <translate + v-else-if="fetch.detail.error_code === 'validation'" + translate-context="*/*/Error" + > + Data returned by the remote server had invalid or missing attributes + </translate> + <translate + v-else-if="fetch.detail.error_code === 'unhandled'" + translate-context="*/*/Error" + > + Unknown error + </translate> + <translate + v-else + translate-context="*/*/Error" + > + Unknown error + </translate> </td> </tr> </tbody> </table> </div> </template> - <div v-else-if="isCreatingFetch" class="ui active inverted dimmer"> + <div + v-else-if="isCreatingFetch" + class="ui active inverted dimmer" + > <div class="ui text loader"> - <translate translate-context="Popup/*/Loading.Title">Requesting a fetch…</translate> + <translate translate-context="Popup/*/Loading.Title"> + Requesting a fetch… + </translate> </div> </div> - <div v-else-if="isWaitingFetch" class="ui active inverted dimmer"> + <div + v-else-if="isWaitingFetch" + class="ui active inverted dimmer" + > <div class="ui text loader"> - <translate translate-context="Popup/*/Loading.Title">Waiting for result…</translate> + <translate translate-context="Popup/*/Loading.Title"> + Waiting for result… + </translate> </div> </div> - <div v-if="errors.length > 0" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Content/*/Error message.Title">Error while saving settings</translate></h4> + <div + v-if="errors.length > 0" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Content/*/Error message.Title"> + Error while saving settings + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> - <div v-else-if="fetch && fetch.status === 'pending' && pollsCount >= maxPolls" role="alert" class="ui warning message"> - <h4 class="header"><translate translate-context="Popup/*/Message.Title">Refresh pending</translate></h4> - <p><translate translate-context="Popup/*/Message.Content">The refresh request hasn't been processed in time by our server. It will be processed later.</translate></p> + <div + v-else-if="fetch && fetch.status === 'pending' && pollsCount >= maxPolls" + role="alert" + class="ui warning message" + > + <h4 class="header"> + <translate translate-context="Popup/*/Message.Title"> + Refresh pending + </translate> + </h4> + <p> + <translate translate-context="Popup/*/Message.Content"> + The refresh request hasn't been processed in time by our server. It will be processed later. + </translate> + </p> </div> </div> <div class="actions"> <button class="ui basic cancel button"> - <translate translate-context="*/*/Button.Label/Verb">Close</translate> + <translate translate-context="*/*/Button.Label/Verb"> + Close + </translate> </button> - <button @click.prevent="showModal = false; $emit('refresh')" class="ui confirm success button" v-if="fetch && fetch.status === 'finished'"> - <translate translate-context="*/*/Button.Label/Verb">Close and reload page</translate> + <button + v-if="fetch && fetch.status === 'finished'" + class="ui confirm success button" + @click.prevent="showModal = false; $emit('refresh')" + > + <translate translate-context="*/*/Button.Label/Verb"> + Close and reload page + </translate> </button> </div> </modal> @@ -94,14 +212,14 @@ </template> <script> -import axios from "axios" +import axios from 'axios' import Modal from '@/components/semantic/Modal' export default { - props: ['url'], components: { Modal }, + props: { url: { type: String, required: true } }, data () { return { fetch: null, @@ -110,12 +228,12 @@ export default { showModal: false, isWaitingFetch: false, maxPolls: 15, - pollsCount: 0, + pollsCount: 0 } }, methods: { createFetch () { - let self = this + const self = this this.fetch = null this.pollsCount = 0 this.errors = [] @@ -134,8 +252,8 @@ export default { pollFetch () { this.isWaitingFetch = true this.pollsCount += 1 - let url = `federation/fetches/${this.fetch.id}/` - let self = this + const url = `federation/fetches/${this.fetch.id}/` + const self = this self.showModal = true axios.get(url).then((response) => { self.isCreatingFetch = false diff --git a/front/src/components/federation/LibraryWidget.vue b/front/src/components/federation/LibraryWidget.vue index 3ee0c93404f29cb6efdf261561f4a37fe31658f5..9ba011015da8f49346269031dcbc44a0c4cf3b02 100644 --- a/front/src/components/federation/LibraryWidget.vue +++ b/front/src/components/federation/LibraryWidget.vue @@ -1,27 +1,52 @@ <template> <div class="wrapper"> - <h3 v-if="!!this.$slots.title" class="ui header"> - <slot name="title"></slot> + <h3 + v-if="!!$slots.title" + class="ui header" + > + <slot name="title" /> </h3> - <p v-if="!isLoading && libraries.length > 0" class="ui subtitle"><slot name="subtitle"></slot></p> - <p v-if="!isLoading && libraries.length === 0" class="ui subtitle"><translate translate-context="Content/Federation/Paragraph">No matching library.</translate></p> - <div class="ui hidden divider"></div> + <p + v-if="!isLoading && libraries.length > 0" + class="ui subtitle" + > + <slot name="subtitle" /> + </p> + <p + v-if="!isLoading && libraries.length === 0" + class="ui subtitle" + > + <translate translate-context="Content/Federation/Paragraph"> + No matching library. + </translate> + </p> + <div class="ui hidden divider" /> <div class="ui cards"> - <div v-if="isLoading" class="ui inverted active dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui inverted active dimmer" + > + <div class="ui loader" /> </div> <library-card + v-for="library in libraries" + :key="library.uuid" :display-scan="false" :display-follow="$store.state.auth.authenticated && library.actor.full_username != $store.state.auth.fullUsername" :library="library" :display-copy-fid="true" - v-for="library in libraries" - :key="library.uuid"></library-card> + /> </div> <template v-if="nextPage"> - <div class="ui hidden divider"></div> - <button v-if="nextPage" @click="fetchData(nextPage)" :class="['ui', 'basic', 'button']"> - <translate translate-context="*/*/Button,Label">Show more</translate> + <div class="ui hidden divider" /> + <button + v-if="nextPage" + :class="['ui', 'basic', 'button']" + @click="fetchData(nextPage)" + > + <translate translate-context="*/*/Button,Label"> + Show more + </translate> </button> </template> </div> @@ -33,12 +58,12 @@ import axios from 'axios' import LibraryCard from '@/views/content/remote/Card' export default { - props: { - url: {type: String, required: true} - }, components: { LibraryCard }, + props: { + url: { type: String, required: true } + }, data () { return { libraries: [], @@ -49,17 +74,22 @@ export default { nextPage: null } }, + watch: { + offset () { + this.fetchData() + } + }, created () { this.fetchData(this.url) }, methods: { fetchData (url) { this.isLoading = true - let self = this - let params = _.clone({}) + const self = this + const params = _.clone({}) params.page_size = this.limit params.offset = this.offset - axios.get(url, {params: params}).then((response) => { + axios.get(url, { params: params }).then((response) => { self.previousPage = response.data.previous self.nextPage = response.data.next self.isLoading = false @@ -77,11 +107,6 @@ export default { this.offset = Math.max(this.offset - this.limit, 0) } } - }, - watch: { - offset () { - this.fetchData() - } } } </script> diff --git a/front/src/components/forms/PasswordInput.vue b/front/src/components/forms/PasswordInput.vue index 4b5dac7ea85dfe04d90ac5ee5da44cdfad8e95f5..5b37bf60f99510b788dc1203eea057bef1d9bad6 100644 --- a/front/src/components/forms/PasswordInput.vue +++ b/front/src/components/forms/PasswordInput.vue @@ -1,55 +1,60 @@ <template> <div class="ui fluid action input"> <input + :id="fieldId" required name="password" :type="passwordInputType" - @input="$emit('input', $event.target.value)" - :id="fieldId" :value="value" - /> + @input="$emit('input', $event.target.value)" + > <button - @click.prevent="showPassword = !showPassword" type="button" :title="labels.title" class="ui icon button" + @click.prevent="showPassword = !showPassword" > - <i class="eye icon"></i> + <i class="eye icon" /> </button> <button v-if="copyButton" - @click.prevent="copyPassword" type="button" class="ui icon button" :title="labels.copy" + @click.prevent="copyPassword" > - <i class="copy icon"></i> + <i class="copy icon" /> </button> </div> </template> <script> export default { - props: ["value", "defaultShow", "copyButton", "fieldId"], - data() { + props: { + value: { type: String, required: true }, + defaultShow: { type: Boolean, default: false }, + copyButton: { type: Boolean, default: false }, + fieldId: { type: Number, default: 0 } + }, + data () { return { - showPassword: this.defaultShow || false, - }; + showPassword: this.defaultShow || false + } }, computed: { labels () { return { title: this.$pgettext( - "Content/Settings/Button.Tooltip/Verb", - "Show/hide password" + 'Content/Settings/Button.Tooltip/Verb', + 'Show/hide password' ), - copy: this.$pgettext("*/*/Button.Label/Short, Verb", "Copy"), + copy: this.$pgettext('*/*/Button.Label/Short, Verb', 'Copy') } }, - passwordInputType() { + passwordInputType () { if (this.showPassword) { - return "text"; + return 'text' } - return "password"; + return 'password' } }, methods: { diff --git a/front/src/components/globals.js b/front/src/components/globals.js index e3eac9abb7418808b65a20074caf7613d4941949..34f802a158ec92a4f0dcb35e406d0ddb524bdaa9 100644 --- a/front/src/components/globals.js +++ b/front/src/components/globals.js @@ -1,23 +1,23 @@ import Vue from 'vue' -Vue.component('human-date', () => import(/* webpackChunkName: "common" */ "@/components/common/HumanDate")) -Vue.component('human-duration', () => import(/* webpackChunkName: "common" */ "@/components/common/HumanDuration")) -Vue.component('username', () => import(/* webpackChunkName: "common" */ "@/components/common/Username")) -Vue.component('user-link', () => import(/* webpackChunkName: "common" */ "@/components/common/UserLink")) -Vue.component('actor-link', () => import(/* webpackChunkName: "common" */ "@/components/common/ActorLink")) -Vue.component('actor-avatar', () => import(/* webpackChunkName: "common" */ "@/components/common/ActorAvatar")) -Vue.component('duration', () => import(/* webpackChunkName: "common" */ "@/components/common/Duration")) -Vue.component('dangerous-button', () => import(/* webpackChunkName: "common" */ "@/components/common/DangerousButton")) -Vue.component('message', () => import(/* webpackChunkName: "common" */ "@/components/common/Message")) -Vue.component('copy-input', () => import(/* webpackChunkName: "common" */ "@/components/common/CopyInput")) -Vue.component('ajax-button', () => import(/* webpackChunkName: "common" */ "@/components/common/AjaxButton")) -Vue.component('tooltip', () => import(/* webpackChunkName: "common" */ "@/components/common/Tooltip")) -Vue.component('empty-state', () => import(/* webpackChunkName: "common" */ "@/components/common/EmptyState")) -Vue.component('expandable-div', () => import(/* webpackChunkName: "common" */ "@/components/common/ExpandableDiv")) -Vue.component('collapse-link', () => import(/* webpackChunkName: "common" */ "@/components/common/CollapseLink")) -Vue.component('action-feedback', () => import(/* webpackChunkName: "common" */ "@/components/common/ActionFeedback")) -Vue.component('rendered-description', () => import(/* webpackChunkName: "common" */ "@/components/common/RenderedDescription")) -Vue.component('content-form', () => import(/* webpackChunkName: "common" */ "@/components/common/ContentForm")) -Vue.component('inline-search-bar', () => import(/* webpackChunkName: "common" */ "@/components/common/InlineSearchBar")) +Vue.component('HumanDate', () => import(/* webpackChunkName: "common" */ '@/components/common/HumanDate')) +Vue.component('HumanDuration', () => import(/* webpackChunkName: "common" */ '@/components/common/HumanDuration')) +Vue.component('Username', () => import(/* webpackChunkName: "common" */ '@/components/common/Username')) +Vue.component('UserLink', () => import(/* webpackChunkName: "common" */ '@/components/common/UserLink')) +Vue.component('ActorLink', () => import(/* webpackChunkName: "common" */ '@/components/common/ActorLink')) +Vue.component('ActorAvatar', () => import(/* webpackChunkName: "common" */ '@/components/common/ActorAvatar')) +Vue.component('Duration', () => import(/* webpackChunkName: "common" */ '@/components/common/Duration')) +Vue.component('DangerousButton', () => import(/* webpackChunkName: "common" */ '@/components/common/DangerousButton')) +Vue.component('Message', () => import(/* webpackChunkName: "common" */ '@/components/common/Message')) +Vue.component('CopyInput', () => import(/* webpackChunkName: "common" */ '@/components/common/CopyInput')) +Vue.component('AjaxButton', () => import(/* webpackChunkName: "common" */ '@/components/common/AjaxButton')) +Vue.component('Tooltip', () => import(/* webpackChunkName: "common" */ '@/components/common/Tooltip')) +Vue.component('EmptyState', () => import(/* webpackChunkName: "common" */ '@/components/common/EmptyState')) +Vue.component('ExpandableDiv', () => import(/* webpackChunkName: "common" */ '@/components/common/ExpandableDiv')) +Vue.component('CollapseLink', () => import(/* webpackChunkName: "common" */ '@/components/common/CollapseLink')) +Vue.component('ActionFeedback', () => import(/* webpackChunkName: "common" */ '@/components/common/ActionFeedback')) +Vue.component('RenderedDescription', () => import(/* webpackChunkName: "common" */ '@/components/common/RenderedDescription')) +Vue.component('ContentForm', () => import(/* webpackChunkName: "common" */ '@/components/common/ContentForm')) +Vue.component('InlineSearchBar', () => import(/* webpackChunkName: "common" */ '@/components/common/InlineSearchBar')) export default {} diff --git a/front/src/components/library/AlbumBase.vue b/front/src/components/library/AlbumBase.vue index 87e14deb3dfddfce0556f6156f25223c4c46f62c..c8bf353494bcc1cb2542124e35806c0aeef26b69 100644 --- a/front/src/components/library/AlbumBase.vue +++ b/front/src/components/library/AlbumBase.vue @@ -1,39 +1,90 @@ <template> <main> - <div v-if="isLoading" class="ui vertical segment" v-title="labels.title"> - <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> + <div + v-if="isLoading" + v-title="labels.title" + class="ui vertical segment" + > + <div :class="['ui', 'centered', 'active', 'inline', 'loader']" /> </div> <template v-if="object"> <section class="ui vertical stripe segment channel-serie"> <div class="ui stackable grid container"> <div class="ui seven wide column"> - <div v-if="isSerie" class="padded basic segment"> - <div class="ui two column grid" v-if="isSerie"> + <div + v-if="isSerie" + class="padded basic segment" + > + <div + v-if="isSerie" + class="ui two column grid" + > <div class="column"> <div class="large two-images"> - <img alt="" class="channel-image" v-if="object.cover && object.cover.urls.original" v-lazy="$store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)"> - <img alt="" class="channel-image" v-else src="../../assets/audio/default-cover.png"> - <img alt="" class="channel-image" v-if="object.cover && object.cover.urls.original" v-lazy="$store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)"> - <img alt="" class="channel-image" v-else src="../../assets/audio/default-cover.png"> + <img + v-if="object.cover && object.cover.urls.original" + v-lazy="$store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)" + alt="" + class="channel-image" + > + <img + v-else + alt="" + class="channel-image" + src="../../assets/audio/default-cover.png" + > + <img + v-if="object.cover && object.cover.urls.original" + v-lazy="$store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)" + alt="" + class="channel-image" + > + <img + v-else + alt="" + class="channel-image" + src="../../assets/audio/default-cover.png" + > </div> </div> <div class="ui column right aligned"> - <tags-list v-if="object.tags && object.tags.length > 0" :tags="object.tags"></tags-list> - <div class="ui small hidden divider"></div> - <human-duration v-if="totalDuration > 0" :duration="totalDuration"></human-duration> + <tags-list + v-if="object.tags && object.tags.length > 0" + :tags="object.tags" + /> + <div class="ui small hidden divider" /> + <human-duration + v-if="totalDuration > 0" + :duration="totalDuration" + /> <template v-if="totalTracks > 0"> - <div class="ui hidden very small divider"></div> - <translate key="1" v-if="isSerie" translate-context="Content/Channel/Paragraph" + <div class="ui hidden very small divider" /> + <translate + v-if="isSerie" + key="1" + translate-context="Content/Channel/Paragraph" translate-plural="%{ count } episodes" :translate-n="totalTracks" - :translate-params="{count: totalTracks}"> + :translate-params="{count: totalTracks}" + > %{ count } episode </translate> - <translate v-else translate-context="*/*/*" :translate-params="{count: totalTracks}" :translate-n="totalTracks" translate-plural="%{ count } tracks">%{ count } track</translate> + <translate + v-else + translate-context="*/*/*" + :translate-params="{count: totalTracks}" + :translate-n="totalTracks" + translate-plural="%{ count } tracks" + > + %{ count } track + </translate> </template> - <div class="ui small hidden divider"></div> - <play-button class="vibrant" :tracks="object.tracks"></play-button> - <div class="ui hidden horizontal divider"></div> + <div class="ui small hidden divider" /> + <play-button + class="vibrant" + :tracks="object.tracks" + /> + <div class="ui hidden horizontal divider" /> <album-dropdown :object="object" :public-libraries="publicLibraries" @@ -41,42 +92,86 @@ :is-album="isAlbum" :is-serie="isSerie" :is-channel="isChannel" - :artist="artist"></album-dropdown> + :artist="artist" + /> </div> </div> - <div class="ui small hidden divider"></div> + <div class="ui small hidden divider" /> <header> - <h2 class="ui header" :title="object.title"> + <h2 + class="ui header" + :title="object.title" + > {{ object.title }} </h2> - <artist-label :artist="artist"></artist-label> + <artist-label :artist="artist" /> </header> </div> - <div v-else class="ui center aligned text padded basic segment"> - <img alt="" class="channel-image" v-if="object.cover && object.cover.urls.original" v-lazy="$store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)"> - <img alt="" class="channel-image" v-else src="../../assets/audio/default-cover.png"> - <div class="ui hidden divider"></div> + <div + v-else + class="ui center aligned text padded basic segment" + > + <img + v-if="object.cover && object.cover.urls.original" + v-lazy="$store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)" + alt="" + class="channel-image" + > + <img + v-else + alt="" + class="channel-image" + src="../../assets/audio/default-cover.png" + > + <div class="ui hidden divider" /> <header> - <h2 class="ui header" :title="object.title"> + <h2 + class="ui header" + :title="object.title" + > {{ object.title }} </h2> - <artist-label class="rounded" :artist="artist"></artist-label> + <artist-label + class="rounded" + :artist="artist" + /> </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> <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-n="totalTracks" - :translate-params="{count: totalTracks}"> + :translate-params="{count: totalTracks}" + > %{ count } episode </translate> - <translate v-else translate-context="*/*/*" :translate-params="{count: totalTracks}" :translate-n="totalTracks" translate-plural="%{ count } tracks">%{ count } track</translate> · + <translate + v-else + translate-context="*/*/*" + :translate-params="{count: totalTracks}" + :translate-n="totalTracks" + translate-plural="%{ count } tracks" + > + %{ count } track + </translate> · </template> - <human-duration v-if="totalDuration > 0" :duration="totalDuration"></human-duration> - <div class="ui small hidden divider"></div> - <play-button class="vibrant" :album="object"></play-button> - <div class="ui horizontal hidden divider"></div> + <human-duration + v-if="totalDuration > 0" + :duration="totalDuration" + /> + <div class="ui small hidden divider" /> + <play-button + class="vibrant" + :album="object" + /> + <div class="ui horizontal hidden divider" /> <album-dropdown :object="object" :public-libraries="publicLibraries" @@ -85,40 +180,64 @@ :is-serie="isSerie" :is-channel="isChannel" :artist="artist" - ></album-dropdown> + /> <div v-if="(object.tags && object.tags.length > 0) || object.description || $store.state.auth.authenticated && object.is_local"> - <div class="ui small hidden divider"></div> - <div class="ui divider"></div> - <div class="ui small hidden divider"></div> - <template v-if="object.tags && object.tags.length > 0" > - <tags-list :tags="object.tags"></tags-list> - <div class="ui small hidden divider"></div> + <div class="ui small hidden divider" /> + <div class="ui divider" /> + <div class="ui small hidden divider" /> + <template v-if="object.tags && object.tags.length > 0"> + <tags-list :tags="object.tags" /> + <div class="ui small hidden divider" /> </template> <rendered-description v-if="object.description" :content="object.description" - :can-update="false"></rendered-description> - <router-link v-else-if="$store.state.auth.authenticated && object.is_local" :to="{name: 'library.albums.edit', params: {id: object.id }}"> - <i class="pencil icon"></i> - <translate translate-context="Content/*/Button.Label/Verb">Add a description…</translate> + :can-update="false" + /> + <router-link + v-else-if="$store.state.auth.authenticated && object.is_local" + :to="{name: 'library.albums.edit', params: {id: object.id }}" + > + <i class="pencil icon" /> + <translate translate-context="Content/*/Button.Label/Verb"> + Add a description… + </translate> </router-link> </div> </div> <template v-if="isSerie"> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <rendered-description v-if="object.description" :content="object.description" - :can-update="false"></rendered-description> - <router-link v-else-if="$store.state.auth.authenticated && object.is_local" :to="{name: 'library.albums.edit', params: {id: object.id }}"> - <i class="pencil icon"></i> - <translate translate-context="Content/*/Button.Label/Verb">Add a description…</translate> + :can-update="false" + /> + <router-link + v-else-if="$store.state.auth.authenticated && object.is_local" + :to="{name: 'library.albums.edit', params: {id: object.id }}" + > + <i class="pencil icon" /> + <translate translate-context="Content/*/Button.Label/Verb"> + Add a description… + </translate> </router-link> - </template> </div> <div class="nine wide column"> - <router-view v-if="object" :paginate-by="paginateBy" :page="page" :total-tracks="totalTracks" :is-serie="isSerie" :artist="artist" :discs="discs" @libraries-loaded="libraries = $event" :object="object" object-type="album" :key="$route.fullPath" @page-changed="page = $event"></router-view> + <router-view + v-if="object" + :key="$route.fullPath" + :paginate-by="paginateBy" + :page="page" + :total-tracks="totalTracks" + :is-serie="isSerie" + :artist="artist" + :discs="discs" + :object="object" + object-type="album" + @libraries-loaded="libraries = $event" + @page-changed="page = $event" + /> </div> </div> </section> @@ -127,17 +246,17 @@ </template> <script> -import axios from "axios" -import lodash from "@/lodash" -import PlayButton from "@/components/audio/PlayButton" -import TagsList from "@/components/tags/List" +import axios from 'axios' +import lodash from '@/lodash' +import PlayButton from '@/components/audio/PlayButton' +import TagsList from '@/components/tags/List' import ArtistLabel from '@/components/audio/ArtistLabel' import AlbumDropdown from './AlbumDropdown' -function groupByDisc(initial) { - function inner(acc, track) { - var dn = track.disc_number - initial - if (acc[dn] == undefined) { +function groupByDisc (initial) { + function inner (acc, track) { + const dn = track.disc_number - initial + if (acc[dn] === undefined) { acc.push([track]) } else { acc[dn].push(track) @@ -148,14 +267,14 @@ function groupByDisc(initial) { } export default { - props: ["id"], components: { PlayButton, TagsList, ArtistLabel, AlbumDropdown }, - data() { + props: { id: { type: Number, required: true } }, + data () { return { isLoading: true, object: null, @@ -163,39 +282,7 @@ export default { discs: [], libraries: [], page: 1, - paginateBy: 50, - } - }, - async created() { - await this.fetchData() - }, - methods: { - async fetchData() { - this.isLoading = true - let tracksResponse = axios.get(`tracks/`, {params: {ordering: 'disc_number,position', album: this.id, page_size: this.paginateBy, page:this.page, include_channels: 'true', playable: 'true'}}) - let albumResponse = await axios.get(`albums/${this.id}/`, {params: {refresh: 'true'}}) - let artistResponse = await axios.get(`artists/${albumResponse.data.artist.id}/`) - this.artist = artistResponse.data - if (this.artist.channel) { - this.artist.channel.artist = this.artist - } - tracksResponse = await tracksResponse - this.object = albumResponse.data - this.object.tracks = tracksResponse.data.results - this.discs = this.object.tracks.reduce(groupByDisc(this.object.tracks[0].disc_number), []) - this.isLoading = false - }, - remove () { - let self = this - self.isLoading = true - axios.delete(`albums/${this.object.id}`).then((response) => { - self.isLoading = false - self.$emit('deleted') - self.$router.push({name: 'library.artists.detail', params: {id: this.artist.id}}) - }, error => { - self.isLoading = false - self.errors = error.backendErrors - }) + paginateBy: 50 } }, computed: { @@ -212,7 +299,7 @@ export default { return this.object.artist.content_category === 'music' }, totalDuration () { - let durations = [0] + const durations = [0] this.object.tracks.forEach((t) => { if (t.uploads[0] && t.uploads[0].duration) { durations.push(t.uploads[0].duration) @@ -220,24 +307,56 @@ export default { }) return lodash.sum(durations) }, - labels() { + labels () { return { - title: this.$pgettext('*/*/*', 'Album'), + title: this.$pgettext('*/*/*', 'Album') } }, publicLibraries () { return this.libraries.filter(l => { return l.privacy_level === 'everyone' }) - }, + } }, watch: { - id() { + id () { this.fetchData() }, - page() { + page () { this.fetchData() } + }, + async created () { + await this.fetchData() + }, + methods: { + async fetchData () { + this.isLoading = true + let tracksResponse = axios.get('tracks/', { params: { ordering: 'disc_number,position', album: this.id, page_size: this.paginateBy, page: this.page, include_channels: 'true', playable: 'true' } }) + const albumResponse = await axios.get(`albums/${this.id}/`, { params: { refresh: 'true' } }) + const artistResponse = await axios.get(`artists/${albumResponse.data.artist.id}/`) + this.artist = artistResponse.data + if (this.artist.channel) { + this.artist.channel.artist = this.artist + } + tracksResponse = await tracksResponse + this.object = albumResponse.data + this.object.tracks = tracksResponse.data.results + this.discs = this.object.tracks.reduce(groupByDisc(this.object.tracks[0].disc_number), []) + this.isLoading = false + }, + remove () { + const self = this + self.isLoading = true + axios.delete(`albums/${this.object.id}`).then((response) => { + self.isLoading = false + self.$emit('deleted') + self.$router.push({ name: 'library.artists.detail', params: { id: this.artist.id } }) + }, error => { + self.isLoading = false + self.errors = error.backendErrors + }) + } } } </script> diff --git a/front/src/components/library/AlbumDetail.vue b/front/src/components/library/AlbumDetail.vue index a584a372a63bc9d0b6ccfc46be7798c8467c995a..635329c16670af57907496a2add48615170a4f85 100644 --- a/front/src/components/library/AlbumDetail.vue +++ b/front/src/components/library/AlbumDetail.vue @@ -1,21 +1,45 @@ <template> <div v-if="object"> <h2 class="ui header"> - <translate key="1" v-if="isSerie" translate-context="Content/Channels/*">Episodes</translate> - <translate key="2" v-else translate-context="*/*/*">Tracks</translate> + <translate + v-if="isSerie" + key="1" + translate-context="Content/Channels/*" + > + Episodes + </translate> + <translate + v-else + key="2" + translate-context="*/*/*" + > + Tracks + </translate> </h2> - <channel-entries v-if="artist.channel && isSerie" :is-podcast="isSerie" :limit="50" :filters="{channel: artist.channel.uuid, album: object.id, ordering: '-creation_date'}"> - </channel-entries> + <channel-entries + v-if="artist.channel && isSerie" + :is-podcast="isSerie" + :limit="50" + :filters="{channel: artist.channel.uuid, album: object.id, ordering: '-creation_date'}" + /> <template v-else-if="discs && discs.length > 1"> - <div v-for="tracks in discs" :key="tracks.disc_number"> - <div class="ui hidden divider"></div> - <play-button class="right floated mini inverted vibrant" :tracks="tracks"></play-button> + <div + v-for="tracks in discs" + :key="tracks.disc_number" + > + <div class="ui hidden divider" /> + <play-button + class="right floated mini inverted vibrant" + :tracks="tracks" + /> <translate tag="h3" :translate-params="{number: tracks[0].disc_number}" translate-context="Content/Album/" - >Volume %{ number }</translate> - <track-table + > + Volume %{ number } + </translate> + <track-table :is-album="true" :tracks="object.tracks" :show-position="true" @@ -26,12 +50,12 @@ :total="totalTracks" :paginate-by="paginateBy" :page="page" - @page-changed="updatePage"> - </track-table> + @page-changed="updatePage" + /> </div> </template> <template v-else> - <track-table + <track-table :is-album="true" :tracks="object.tracks" :show-position="true" @@ -42,15 +66,25 @@ :total="totalTracks" :paginate-by="paginateBy" :page="page" - @page-changed="updatePage"> - </track-table> + @page-changed="updatePage" + /> </template> <template v-if="!artist.channel && !isSerie"> <h2> - <translate translate-context="Content/*/Title/Noun">User libraries</translate> + <translate translate-context="Content/*/Title/Noun"> + User libraries + </translate> </h2> - <library-widget @loaded="$emit('libraries-loaded', $event)" :url="'albums/' + object.id + '/libraries/'"> - <translate slot="subtitle" translate-context="Content/Album/Paragraph">This album is present in the following libraries:</translate> + <library-widget + :url="'albums/' + object.id + '/libraries/'" + @loaded="$emit('libraries-loaded', $event)" + > + <translate + slot="subtitle" + translate-context="Content/Album/Paragraph" + > + This album is present in the following libraries: + </translate> </library-widget> </template> </div> @@ -58,30 +92,38 @@ <script> -import time from "@/utils/time" -import LibraryWidget from "@/components/federation/LibraryWidget" +import time from '@/utils/time' +import LibraryWidget from '@/components/federation/LibraryWidget' import ChannelEntries from '@/components/audio/ChannelEntries' import TrackTable from '@/components/audio/track/Table' -import PlayButton from "@/components/audio/PlayButton" +import PlayButton from '@/components/audio/PlayButton' export default { - props: ["object", "libraries", "discs", "isSerie", "artist", "page", "paginateBy", "totalTracks"], components: { LibraryWidget, TrackTable, ChannelEntries, PlayButton }, - data() { + props: { + object: { type: Object, required: true }, + discs: { type: Array, required: true }, + isSerie: { type: Boolean, required: true }, + artist: { type: Object, required: true }, + page: { type: Number, required: true }, + paginateBy: { type: Number, required: true }, + totalTracks: { type: Number, required: true } + }, + data () { return { time, - id: this.object.id, + id: this.object.id } }, methods: { - updatePage: function(page) { + updatePage: function (page) { this.$emit('page-changed', page) } - }, + } } </script> diff --git a/front/src/components/library/AlbumDropdown.vue b/front/src/components/library/AlbumDropdown.vue index 3cc42f6efb7b7538768022bf0a2d737469ae70f3..a7f12e155220cef900366dd1c792c542d05b2d1c 100644 --- a/front/src/components/library/AlbumDropdown.vue +++ b/front/src/components/library/AlbumDropdown.vue @@ -1,13 +1,19 @@ <template> <span> - <modal v-if="isEmbedable" :show.sync="showEmbedModal"> + <modal + v-if="isEmbedable" + :show.sync="showEmbedModal" + > <h4 class="header"> <translate translate-context="Popup/Album/Title/Verb">Embed this album on your website</translate> </h4> <div class="scrolling content"> <div class="description"> - <embed-wizard type="album" :id="object.id" /> + <embed-wizard + :id="object.id" + type="album" + /> </div> </div> @@ -17,46 +23,69 @@ </button> </div> </modal> - <button class="ui floating dropdown circular icon basic button" :title="labels.more" v-dropdown="{direction: 'downward'}"> - <i class="ellipsis vertical icon"></i> + <button + v-dropdown="{direction: 'downward'}" + class="ui floating dropdown circular icon basic button" + :title="labels.more" + > + <i class="ellipsis vertical icon" /> <div class="menu"> <a - :href="object.fid" v-if="domain != $store.getters['instance/domain']" + :href="object.fid" target="_blank" - class="basic item"> - <i class="external icon"></i> - <translate :translate-params="{domain: domain}" translate-context="Content/*/Button.Label/Verb">View on %{ domain }</translate> + class="basic item" + > + <i class="external icon" /> + <translate + :translate-params="{domain: domain}" + translate-context="Content/*/Button.Label/Verb" + >View on %{ domain }</translate> </a> <div - role="button" v-if="isEmbedable" + role="button" + class="basic item" @click="showEmbedModal = !showEmbedModal" - class="basic item"> - <i class="code icon"></i> + > + <i class="code icon" /> <translate translate-context="Content/*/Button.Label/Verb">Embed</translate> </div> - <a v-if="isAlbum && musicbrainzUrl" :href="musicbrainzUrl" target="_blank" rel="noreferrer noopener" class="basic item"> - <i class="external icon"></i> + <a + v-if="isAlbum && musicbrainzUrl" + :href="musicbrainzUrl" + target="_blank" + rel="noreferrer noopener" + class="basic item" + > + <i class="external icon" /> <translate translate-context="Content/*/*/Clickable, Verb">View on MusicBrainz</translate> </a> - <a v-if="!isChannel && isAlbum" :href="discogsUrl" target="_blank" rel="noreferrer noopener" class="basic item"> - <i class="external icon"></i> + <a + v-if="!isChannel && isAlbum" + :href="discogsUrl" + target="_blank" + rel="noreferrer noopener" + class="basic item" + > + <i class="external icon" /> <translate translate-context="Content/*/Button.Label/Verb">Search on Discogs</translate> - </a> + </a> <router-link v-if="object.is_local" :to="{name: 'library.albums.edit', params: {id: object.id }}" - class="basic item"> - <i class="edit icon"></i> + class="basic item" + > + <i class="edit icon" /> <translate translate-context="Content/*/Button.Label/Verb">Edit</translate> </router-link> <dangerous-button - :class="['ui', {loading: isLoading}, 'item']" v-if="artist && $store.state.auth.authenticated && artist.channel && artist.attributed_to.full_username === $store.state.auth.fullUsername" - @confirm="remove()"> - <i class="ui trash icon"></i> + :class="['ui', {loading: isLoading}, 'item']" + @confirm="remove()" + > + <i class="ui trash icon" /> <translate translate-context="*/*/*/Verb">Delete…</translate> <p slot="modal-header"><translate translate-context="Popup/Channel/Title">Delete this album?</translate></p> <div slot="modal-content"> @@ -64,26 +93,33 @@ </div> <p slot="modal-confirm"><translate translate-context="*/*/*/Verb">Delete</translate></p> </dangerous-button> - <div class="divider"></div> + <div class="divider" /> <div - role="button" - class="basic item" v-for="obj in getReportableObjs({album: object, channel: artist.channel})" :key="obj.target.type + obj.target.id" - @click.stop.prevent="$store.dispatch('moderation/report', obj.target)"> + role="button" + class="basic item" + @click.stop.prevent="$store.dispatch('moderation/report', obj.target)" + > <i class="share icon" /> {{ obj.label }} </div> - <div class="divider"></div> - <router-link class="basic item" v-if="$store.state.auth.availablePermissions['library']" :to="{name: 'manage.library.albums.detail', params: {id: object.id}}"> - <i class="wrench icon"></i> + <div class="divider" /> + <router-link + v-if="$store.state.auth.availablePermissions['library']" + class="basic item" + :to="{name: 'manage.library.albums.detail', params: {id: object.id}}" + > + <i class="wrench icon" /> <translate translate-context="Content/Moderation/Link">Open in moderation interface</translate> </router-link> <a v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser" class="basic item" :href="$store.getters['instance/absoluteUrl'](`/api/admin/music/album/${object.id}`)" - target="_blank" rel="noopener noreferrer"> - <i class="wrench icon"></i> + target="_blank" + rel="noopener noreferrer" + > + <i class="wrench icon" /> <translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate> </a> </div> @@ -91,30 +127,30 @@ </span> </template> <script> -import EmbedWizard from "@/components/audio/EmbedWizard" +import EmbedWizard from '@/components/audio/EmbedWizard' import Modal from '@/components/semantic/Modal' import ReportMixin from '@/components/mixins/Report' -import {getDomain} from '@/utils' +import { getDomain } from '@/utils' export default { + components: { + EmbedWizard, + Modal + }, mixins: [ReportMixin], props: { isLoading: Boolean, - artist: Object, - object: Object, - publicLibraries: Array, + artist: { type: Object, required: true }, + object: { type: Object, required: true }, + publicLibraries: { type: Array, required: true }, isAlbum: Boolean, isChannel: Boolean, - isSerie: Boolean, - }, - components: { - EmbedWizard, - Modal, + isSerie: Boolean }, data () { return { - showEmbedModal: false, + showEmbedModal: false } }, computed: { @@ -122,28 +158,30 @@ export default { if (this.object) { return getDomain(this.object.fid) } + return null }, - labels() { + labels () { return { - more: this.$pgettext('*/*/Button.Label/Noun', "More…"), + more: this.$pgettext('*/*/Button.Label/Noun', 'More…') } }, isEmbedable () { return (this.isChannel && this.artist.channel.actor) || this.publicLibraries.length > 0 }, - musicbrainzUrl() { + musicbrainzUrl () { if (this.object.mbid) { - return "https://musicbrainz.org/release/" + this.object.mbid + return 'https://musicbrainz.org/release/' + this.object.mbid } + return null }, - discogsUrl() { + discogsUrl () { return ( - "https://discogs.com/search/?type=release&title=" + - encodeURI(this.object.title) + "&artist=" + + 'https://discogs.com/search/?type=release&title=' + + encodeURI(this.object.title) + '&artist=' + encodeURI(this.object.artist.name) ) - }, + } } } </script> diff --git a/front/src/components/library/AlbumEdit.vue b/front/src/components/library/AlbumEdit.vue index b7c24737c1dd5e07b737dd1a6e088f1edc179920..d4d9559c384be96f72cc81e6bdbf6862841d277b 100644 --- a/front/src/components/library/AlbumEdit.vue +++ b/front/src/components/library/AlbumEdit.vue @@ -1,37 +1,56 @@ <template> - <section class="ui vertical stripe segment"> <div class="ui text container"> <h2> - <translate v-if="canEdit" key="1" translate-context="Content/*/Title">Edit this album</translate> - <translate v-else key="2" translate-context="Content/*/Title">Suggest an edit on this album</translate> + <translate + v-if="canEdit" + key="1" + translate-context="Content/*/Title" + > + Edit this album + </translate> + <translate + v-else + key="2" + translate-context="Content/*/Title" + > + Suggest an edit on this album + </translate> </h2> - <div class="ui message" v-if="!object.is_local"> - <translate translate-context="Content/*/Message">This object is managed by another server, you cannot edit it.</translate> + <div + v-if="!object.is_local" + class="ui message" + > + <translate translate-context="Content/*/Message"> + This object is managed by another server, you cannot edit it. + </translate> </div> <edit-form v-else :object-type="objectType" :object="object" - :can-edit="canEdit"></edit-form> + :can-edit="canEdit" + /> </div> </section> </template> <script> -import axios from "axios" - import EditForm from '@/components/library/EditForm' export default { - props: ["objectType", "object", "libraries"], - data() { - return { - id: this.object.id, - } - }, components: { EditForm }, + props: { + objectType: { type: String, required: true }, + object: { type: Object, required: true }, + libraries: { type: Array, required: true } + }, + data () { + return { + id: this.object.id + } + }, computed: { canEdit () { return true diff --git a/front/src/components/library/Albums.vue b/front/src/components/library/Albums.vue index 97589d80e3e64aa3ac2cb11bdee8d0534568cee1..14e499d9a4956f036a4fb33338a846e5d003f432 100644 --- a/front/src/components/library/Albums.vue +++ b/front/src/components/library/Albums.vue @@ -2,78 +2,131 @@ <main v-title="labels.title"> <section class="ui vertical stripe segment"> <h2 class="ui header"> - <translate translate-context="Content/Album/Title">Browsing albums</translate> + <translate translate-context="Content/Album/Title"> + Browsing albums + </translate> </h2> - <form :class="['ui', {'loading': isLoading}, 'form']" @submit.prevent="updatePage();updateQueryString();fetchData()"> + <form + :class="['ui', {'loading': isLoading}, 'form']" + @submit.prevent="updatePage();updateQueryString();fetchData()" + > <div class="fields"> <div class="field"> <label for="albums-search"> <translate translate-context="Content/Search/Input.Label/Noun">Search</translate> </label> <div class="ui action input"> - <input id="albums-search" type="text" name="search" v-model="query" :placeholder="labels.searchPlaceholder"/> - <button class="ui icon button" type="submit" :aria-label="$pgettext('Content/Search/Input.Label/Noun', 'Search')"> - <i class="search icon"></i> + <input + id="albums-search" + v-model="query" + type="text" + name="search" + :placeholder="labels.searchPlaceholder" + > + <button + class="ui icon button" + type="submit" + :aria-label="$pgettext('Content/Search/Input.Label/Noun', 'Search')" + > + <i class="search icon" /> </button> </div> </div> <div class="field"> <label for="tags-search"><translate translate-context="*/*/*/Noun">Tags</translate></label> - <tags-selector v-model="tags"></tags-selector> + <tags-selector v-model="tags" /> </div> <div class="field"> <label for="album-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> - <select id="album-ordering" class="ui dropdown" v-model="ordering"> - <option v-for="option in orderingOptions" :value="option[0]"> + <select + id="album-ordering" + v-model="ordering" + class="ui dropdown" + > + <option + v-for="(option, key) in orderingOptions" + :key="key" + :value="option[0]" + > {{ sharedLabels.filters[option[1]] }} </option> </select> </div> <div class="field"> <label for="album-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label> - <select id="album-ordering-direction" class="ui dropdown" v-model="orderingDirection"> - <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option> - <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option> + <select + id="album-ordering-direction" + v-model="orderingDirection" + class="ui dropdown" + > + <option value="+"> + <translate translate-context="Content/Search/Dropdown"> + Ascending + </translate> + </option> + <option value="-"> + <translate translate-context="Content/Search/Dropdown"> + Descending + </translate> + </option> </select> </div> <div class="field"> <label for="album-results"><translate translate-context="Content/Search/Dropdown.Label/Noun">Results per page</translate></label> - <select id="album-results" class="ui dropdown" v-model="paginateBy"> - <option :value="parseInt(12)">12</option> - <option :value="parseInt(25)">25</option> - <option :value="parseInt(50)">50</option> + <select + id="album-results" + v-model="paginateBy" + class="ui dropdown" + > + <option :value="parseInt(12)"> + 12 + </option> + <option :value="parseInt(25)"> + 25 + </option> + <option :value="parseInt(50)"> + 50 + </option> </select> </div> </div> </form> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <div v-if="result" transition-duration="0" item-selector=".column" percent-position="true" stagger="0" - class=""> + class="" + > <div v-if="result.results.length > 0" - class="ui app-cards cards"> + class="ui app-cards cards" + > <album-card v-for="album in result.results" :key="album.id" - :album="album"></album-card> + :album="album" + /> </div> - <div v-else class="ui placeholder segment sixteen wide column" style="text-align: center; display: flex; align-items: center"> + <div + v-else + class="ui placeholder segment sixteen wide column" + style="text-align: center; display: flex; align-items: center" + > <div class="ui icon header"> - <i class="compact disc icon"></i> + <i class="compact disc icon" /> <translate translate-context="Content/Albums/Placeholder"> No results matching your query </translate> </div> <router-link - v-if="$store.state.auth.authenticated" - :to="{name: 'content.index'}" - class="ui success button labeled icon"> - <i class="upload icon"></i> + v-if="$store.state.auth.authenticated" + :to="{name: 'content.index'}" + class="ui success button labeled icon" + > + <i class="upload icon" /> <translate translate-context="Content/*/Verb"> Add some music </translate> @@ -83,11 +136,11 @@ <div class="ui center aligned basic segment"> <pagination v-if="result && result.count > paginateBy" - @page-changed="selectPage" :current="page" :paginate-by="paginateBy" :total="result.count" - ></pagination> + @page-changed="selectPage" + /> </div> </section> </main> @@ -95,121 +148,120 @@ <script> import qs from 'qs' -import axios from "axios" -import _ from "@/lodash" -import $ from "jquery" +import axios from 'axios' +import $ from 'jquery' -import logger from "@/logging" +import logger from '@/logging' -import OrderingMixin from "@/components/mixins/Ordering" -import PaginationMixin from "@/components/mixins/Pagination" -import TranslationsMixin from "@/components/mixins/Translations" -import AlbumCard from "@/components/audio/album/Card" -import Pagination from "@/components/Pagination" +import OrderingMixin from '@/components/mixins/Ordering' +import PaginationMixin from '@/components/mixins/Pagination' +import TranslationsMixin from '@/components/mixins/Translations' +import AlbumCard from '@/components/audio/album/Card' +import Pagination from '@/components/Pagination' import TagsSelector from '@/components/library/TagsSelector' -const FETCH_URL = "albums/" +const FETCH_URL = 'albums/' export default { - mixins: [OrderingMixin, PaginationMixin, TranslationsMixin], - props: { - defaultQuery: { type: String, required: false, default: "" }, - defaultTags: { type: Array, required: false, default: () => { return [] } }, - scope: { type: String, required: false, default: "all" }, - }, components: { AlbumCard, Pagination, - TagsSelector, + TagsSelector }, - data() { + mixins: [OrderingMixin, PaginationMixin, TranslationsMixin], + props: { + defaultQuery: { type: String, required: false, default: '' }, + defaultTags: { type: Array, required: false, default: () => { return [] } }, + scope: { type: String, required: false, default: 'all' } + }, + data () { return { isLoading: true, result: null, page: parseInt(this.defaultPage), query: this.defaultQuery, tags: (this.defaultTags || []).filter((t) => { return t.length > 0 }), - orderingOptions: [["creation_date", "creation_date"], ["title", "album_title"],["release_date","release_date"]] + orderingOptions: [['creation_date', 'creation_date'], ['title', 'album_title'], ['release_date', 'release_date']] } }, - created() { - this.fetchData() - }, - mounted() { - $(".ui.dropdown").dropdown() - }, computed: { - labels() { - let searchPlaceholder = this.$pgettext('Content/Search/Input.Placeholder', "Enter album title…") - let title = this.$pgettext('*/*/*', "Albums") + labels () { + const searchPlaceholder = this.$pgettext('Content/Search/Input.Placeholder', 'Enter album title…') + const title = this.$pgettext('*/*/*', 'Albums') return { searchPlaceholder, title } } }, + watch: { + page () { + this.updateQueryString() + this.fetchData() + }, + '$store.state.moderation.lastUpdate': function () { + this.fetchData() + } + }, + created () { + this.fetchData() + }, + mounted () { + $('.ui.dropdown').dropdown() + }, methods: { - updateQueryString: function() { + updateQueryString: function () { history.pushState( {}, null, this.$route.path + '?' + new URLSearchParams( { - query: this.query, - page: this.page, - tag: this.tags, - paginateBy: this.paginateBy, - ordering: this.getOrderingAsString() - }).toString() + query: this.query, + page: this.page, + tag: this.tags, + paginateBy: this.paginateBy, + ordering: this.getOrderingAsString() + }).toString() ) }, - fetchData: function() { - var self = this + fetchData: function () { + const self = this this.isLoading = true - let url = FETCH_URL - let params = { + const url = FETCH_URL + const params = { scope: this.scope, page: this.page, page_size: this.paginateBy, q: this.query, ordering: this.getOrderingAsString(), - playable: "true", + playable: 'true', tag: this.tags, - include_channels: "true", - content_category: "music" + include_channels: 'true', + content_category: 'music' } - logger.default.debug("Fetching albums") + logger.default.debug('Fetching albums') axios.get( url, { params: params, - paramsSerializer: function(params) { + paramsSerializer: function (params) { return qs.stringify(params, { indices: false }) } } ).then(response => { self.result = response.data self.isLoading = false - }, error => { + }, () => { self.result = null self.isLoading = false }) }, - selectPage: function(page) { + selectPage: function (page) { this.page = page }, - updatePage() { + updatePage () { this.page = this.defaultPage } - }, - watch: { - page() { - this.updateQueryString() - this.fetchData() - }, - "$store.state.moderation.lastUpdate": function () { - this.fetchData() - } } } </script> diff --git a/front/src/components/library/ArtistBase.vue b/front/src/components/library/ArtistBase.vue index ebc7fe5c1d4a18abc3e0706f7e4feef2dd885011..cbc7d4518f142af12a5b358db7af0f17e6f9b11b 100644 --- a/front/src/components/library/ArtistBase.vue +++ b/front/src/components/library/ArtistBase.vue @@ -1,119 +1,195 @@ <template> <main v-title="labels.title"> - <div v-if="isLoading" class="ui vertical segment"> - <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> + <div + v-if="isLoading" + class="ui vertical segment" + > + <div :class="['ui', 'centered', 'active', 'inline', 'loader']" /> </div> <template v-if="object && !isLoading"> - <section :class="['ui', 'head', {'with-background': cover}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" :style="headerStyle" v-title="object.name"> + <section + v-title="object.name" + :class="['ui', 'head', {'with-background': cover}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" + :style="headerStyle" + > <div class="segment-content"> <h2 class="ui center aligned icon header"> - <i class="circular inverted users violet icon"></i> + <i class="circular inverted users violet icon" /> <div class="content"> {{ object.name }} - <div class="sub header" v-if="albums"> - <translate translate-context="Content/Artist/Paragraph" + <div + v-if="albums" + class="sub header" + > + <translate + translate-context="Content/Artist/Paragraph" tag="div" translate-plural="%{ count } tracks in %{ albumsCount } albums" :translate-n="totalTracks" - :translate-params="{count: totalTracks, albumsCount: totalAlbums}"> + :translate-params="{count: totalTracks, albumsCount: totalAlbums}" + > %{ count } track in %{ albumsCount } albums </translate> </div> </div> </h2> - <tags-list v-if="object.tags && object.tags.length > 0" :tags="object.tags"></tags-list> - <div class="ui hidden divider"></div> + <tags-list + v-if="object.tags && object.tags.length > 0" + :tags="object.tags" + /> + <div class="ui hidden divider" /> <div class="header-buttons"> <div class="ui buttons"> - <radio-button type="artist" :object-id="object.id"></radio-button> - + <radio-button + type="artist" + :object-id="object.id" + /> </div> <div class="ui buttons"> - <play-button :is-playable="isPlayable" class="vibrant" :artist="object"> - <translate translate-context="Content/Artist/Button.Label/Verb">Play all albums</translate> + <play-button + :is-playable="isPlayable" + class="vibrant" + :artist="object" + > + <translate translate-context="Content/Artist/Button.Label/Verb"> + Play all albums + </translate> </play-button> </div> - <modal :show.sync="showEmbedModal" v-if="publicLibraries.length > 0"> + <modal + v-if="publicLibraries.length > 0" + :show.sync="showEmbedModal" + > <h4 class="header"> - <translate translate-context="Popup/Artist/Title/Verb">Embed this artist work on your website</translate> + <translate translate-context="Popup/Artist/Title/Verb"> + Embed this artist work on your website + </translate> </h4> <div class="scrolling content"> <div class="description"> - <embed-wizard type="artist" :id="object.id" /> - + <embed-wizard + :id="object.id" + type="artist" + /> </div> </div> <div class="actions"> <button class="ui deny button"> - <translate translate-context="*/*/Button.Label/Verb">Cancel</translate> + <translate translate-context="*/*/Button.Label/Verb"> + Cancel + </translate> </button> </div> </modal> <div class="ui buttons"> - <button class="ui button" @click="$refs.dropdown.click()"> - <translate translate-context="*/*/Button.Label/Noun">More…</translate> + <button + class="ui button" + @click="$refs.dropdown.click()" + > + <translate translate-context="*/*/Button.Label/Noun"> + More… + </translate> </button> - <button class="ui floating dropdown icon button" ref="dropdown" v-dropdown> - <i class="dropdown icon"></i> + <button + ref="dropdown" + v-dropdown + class="ui floating dropdown icon button" + > + <i class="dropdown icon" /> <div class="menu"> <a - :href="object.fid" v-if="domain != $store.getters['instance/domain']" + :href="object.fid" target="_blank" - class="basic item"> - <i class="external icon"></i> - <translate :translate-params="{domain: domain}" translate-context="Content/*/Button.Label/Verb">View on %{ domain }</translate> + class="basic item" + > + <i class="external icon" /> + <translate + :translate-params="{domain: domain}" + translate-context="Content/*/Button.Label/Verb" + >View on %{ domain }</translate> </a> <button - role="button" v-if="publicLibraries.length > 0" + role="button" + class="basic item" @click.prevent="showEmbedModal = !showEmbedModal" - class="basic item"> - <i class="code icon"></i> - <translate translate-context="Content/*/Button.Label/Verb">Embed</translate> + > + <i class="code icon" /> + <translate translate-context="Content/*/Button.Label/Verb"> + Embed + </translate> </button> - <a :href="wikipediaUrl" target="_blank" rel="noreferrer noopener" class="basic item"> - <i class="wikipedia w icon"></i> + <a + :href="wikipediaUrl" + target="_blank" + rel="noreferrer noopener" + class="basic item" + > + <i class="wikipedia w icon" /> <translate translate-context="Content/*/Button.Label/Verb">Search on Wikipedia</translate> </a> - <a v-if="musicbrainzUrl" :href="musicbrainzUrl" target="_blank" rel="noreferrer noopener" class="basic item"> - <i class="external icon"></i> + <a + v-if="musicbrainzUrl" + :href="musicbrainzUrl" + target="_blank" + rel="noreferrer noopener" + class="basic item" + > + <i class="external icon" /> <translate translate-context="Content/*/*/Clickable, Verb">View on MusicBrainz</translate> </a> - <a :href="discogsUrl" target="_blank" rel="noreferrer noopener" class="basic item"> - <i class="external icon"></i> - <translate translate-context="Content/*/Button.Label/Verb">Search on Discogs</translate> - </a> + <a + :href="discogsUrl" + target="_blank" + rel="noreferrer noopener" + class="basic item" + > + <i class="external icon" /> + <translate translate-context="Content/*/Button.Label/Verb">Search on Discogs</translate> + </a> <router-link v-if="object.is_local" :to="{name: 'library.artists.edit', params: {id: object.id }}" - class="basic item"> - <i class="edit icon"></i> - <translate translate-context="Content/*/Button.Label/Verb">Edit</translate> + class="basic item" + > + <i class="edit icon" /> + <translate translate-context="Content/*/Button.Label/Verb"> + Edit + </translate> </router-link> - <div class="divider"></div> + <div class="divider" /> <div - role="button" - class="basic item" v-for="obj in getReportableObjs({artist: object})" :key="obj.target.type + obj.target.id" - @click.stop.prevent="$store.dispatch('moderation/report', obj.target)"> + role="button" + class="basic item" + @click.stop.prevent="$store.dispatch('moderation/report', obj.target)" + > <i class="share icon" /> {{ obj.label }} </div> - <div class="divider"></div> - <router-link class="basic item" v-if="$store.state.auth.availablePermissions['library']" :to="{name: 'manage.library.artists.detail', params: {id: object.id}}"> - <i class="wrench icon"></i> - <translate translate-context="Content/Moderation/Link">Open in moderation interface</translate> + <div class="divider" /> + <router-link + v-if="$store.state.auth.availablePermissions['library']" + class="basic item" + :to="{name: 'manage.library.artists.detail', params: {id: object.id}}" + > + <i class="wrench icon" /> + <translate translate-context="Content/Moderation/Link"> + Open in moderation interface + </translate> </router-link> <a v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser" class="basic item" :href="$store.getters['instance/absoluteUrl'](`/api/admin/music/artist/${object.id}`)" - target="_blank" rel="noopener noreferrer"> - <i class="wrench icon"></i> + target="_blank" + rel="noopener noreferrer" + > + <i class="wrench icon" /> <translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate> </a> </div> @@ -123,44 +199,43 @@ </div> </section> <router-view + :key="$route.fullPath" :tracks="tracks" :next-tracks-url="nextTracksUrl" :next-albums-url="nextAlbumsUrl" :albums="albums" :is-loading-albums="isLoadingAlbums" + :object="object" + object-type="artist" @libraries-loaded="libraries = $event" - :object="object" object-type="artist" - :key="$route.fullPath"></router-view> + /> </template> </main> </template> <script> -import axios from "axios" -import logger from "@/logging" -import backend from "@/audio/backend" -import PlayButton from "@/components/audio/PlayButton" -import EmbedWizard from "@/components/audio/EmbedWizard" +import axios from 'axios' +import logger from '@/logging' +import PlayButton from '@/components/audio/PlayButton' +import EmbedWizard from '@/components/audio/EmbedWizard' import Modal from '@/components/semantic/Modal' -import RadioButton from "@/components/radios/Button" -import TagsList from "@/components/tags/List" +import RadioButton from '@/components/radios/Button' +import TagsList from '@/components/tags/List' import ReportMixin from '@/components/mixins/Report' -import {getDomain} from '@/utils' - -const FETCH_URL = "albums/" +import { getDomain } from '@/utils' export default { - mixins: [ReportMixin], - props: ["id"], components: { PlayButton, EmbedWizard, Modal, RadioButton, - TagsList, + TagsList }, - data() { + mixins: [ReportMixin], + props: { id: { type: Number, required: true } }, + data () { return { isLoading: true, isLoadingAlbums: true, @@ -172,47 +247,7 @@ export default { nextAlbumsUrl: null, nextTracksUrl: null, totalAlbums: null, - totalTracks: null, - } - }, - async created() { - await this.fetchData() - }, - methods: { - async fetchData() { - var self = this - this.isLoading = true - logger.default.debug('Fetching artist "' + this.id + '"') - - let artistPromise = axios.get("artists/" + this.id + "/", {params: {refresh: 'true'}}).then(response => { - if (response.data.channel) { - self.$router.replace({name: 'channels.detail', params: {id: response.data.channel.uuid}}) - } else { - self.object = response.data - } - }) - await artistPromise - if (!self.object) { - return - } - let trackPromise = axios.get("tracks/", { params: { artist: this.id, hidden: '', ordering: "-creation_date" } }).then(response => { - self.tracks = response.data.results - self.nextTracksUrl = response.data.next - self.totalTracks = response.data.count - }) - let albumPromise = axios.get("albums/", { - params: { artist: self.id, ordering: "-release_date", hidden: '' } - }).then(response => { - self.nextAlbumsUrl = response.data.next - self.totalAlbums = response.data.count - let parsed = JSON.parse(JSON.stringify(response.data.results)) - self.albums = parsed - - }) - await trackPromise - await albumPromise - self.isLoadingAlbums = false - self.isLoading = false + totalTracks: null } }, computed: { @@ -220,37 +255,39 @@ export default { if (this.object) { return getDomain(this.object.fid) } + return null }, - isPlayable() { + isPlayable () { return ( this.object.albums.filter(a => { return a.is_playable }).length > 0 ) }, - labels() { + labels () { return { title: this.$pgettext('*/*/*', 'Album') } }, - wikipediaUrl() { + wikipediaUrl () { return ( - "https://en.wikipedia.org/w/index.php?search=" + + 'https://en.wikipedia.org/w/index.php?search=' + encodeURI(this.object.name) ) }, - musicbrainzUrl() { + musicbrainzUrl () { if (this.object.mbid) { - return "https://musicbrainz.org/artist/" + this.object.mbid + return 'https://musicbrainz.org/artist/' + this.object.mbid } + return null }, - discogsUrl() { + discogsUrl () { return ( - "https://discogs.com/search/?type=artist&title=" + - encodeURI(this.object.name) + 'https://discogs.com/search/?type=artist&title=' + + encodeURI(this.object.name) ) }, - cover() { + cover () { if (this.object.cover && this.object.cover.urls.original) { return this.object.cover } @@ -268,27 +305,65 @@ export default { return l.privacy_level === 'everyone' }) }, - headerStyle() { + headerStyle () { if (!this.cover || !this.cover.urls.original) { - return "" + return '' } return ( - "background-image: url(" + - this.$store.getters["instance/absoluteUrl"](this.cover.urls.original) + - ")" + 'background-image: url(' + + this.$store.getters['instance/absoluteUrl'](this.cover.urls.original) + + ')' ) }, contentFilter () { - let self = this return this.$store.getters['moderation/artistFilters']().filter((e) => { return e.target.id === this.object.id })[0] } }, watch: { - id() { + id () { this.fetchData() } + }, + async created () { + await this.fetchData() + }, + methods: { + async fetchData () { + const self = this + this.isLoading = true + logger.default.debug('Fetching artist "' + this.id + '"') + + const artistPromise = axios.get('artists/' + this.id + '/', { params: { refresh: 'true' } }).then(response => { + if (response.data.channel) { + self.$router.replace({ name: 'channels.detail', params: { id: response.data.channel.uuid } }) + } else { + self.object = response.data + } + }) + await artistPromise + if (!self.object) { + return + } + const trackPromise = axios.get('tracks/', { params: { artist: this.id, hidden: '', ordering: '-creation_date' } }).then(response => { + self.tracks = response.data.results + self.nextTracksUrl = response.data.next + self.totalTracks = response.data.count + }) + const albumPromise = axios.get('albums/', { + params: { artist: self.id, ordering: '-release_date', hidden: '' } + }).then(response => { + self.nextAlbumsUrl = response.data.next + self.totalAlbums = response.data.count + const parsed = JSON.parse(JSON.stringify(response.data.results)) + self.albums = parsed + }) + await trackPromise + await albumPromise + self.isLoadingAlbums = false + self.isLoading = false + } } } </script> diff --git a/front/src/components/library/ArtistDetail.vue b/front/src/components/library/ArtistDetail.vue index 3216842b97693a6c1d90b779a919d99092c0eba3..a1bab20f415dd1f8357a96d6cbf92b39ba3a32c8 100644 --- a/front/src/components/library/ArtistDetail.vue +++ b/front/src/components/library/ArtistDetail.vue @@ -1,69 +1,127 @@ <template> <div v-if="object"> - <div class="ui small text container" v-if="contentFilter"> - <div class="ui hidden divider"></div> + <div + v-if="contentFilter" + class="ui small text container" + > + <div class="ui hidden divider" /> <div class="ui message"> <p> - <translate translate-context="Content/Artist/Paragraph">You are currently hiding content related to this artist.</translate> + <translate translate-context="Content/Artist/Paragraph"> + You are currently hiding content related to this artist. + </translate> </p> - <router-link class="right floated" :to="{name: 'settings'}"> - <translate translate-context="Content/Moderation/Link">Review my filters</translate> + <router-link + class="right floated" + :to="{name: 'settings'}" + > + <translate translate-context="Content/Moderation/Link"> + Review my filters + </translate> </router-link> - <button @click="$store.dispatch('moderation/deleteContentFilter', contentFilter.uuid)" class="ui basic tiny button"> - <translate translate-context="Content/Moderation/Button.Label">Remove filter</translate> + <button + class="ui basic tiny button" + @click="$store.dispatch('moderation/deleteContentFilter', contentFilter.uuid)" + > + <translate translate-context="Content/Moderation/Button.Label"> + Remove filter + </translate> </button> </div> </div> - <section v-if="tracks.length > 0" class="ui vertical stripe segment"> - <track-table :is-artist="true" :show-position="false" :track-only="true" :tracks="tracks.slice(0,5)"> + <section + v-if="tracks.length > 0" + class="ui vertical stripe segment" + > + <track-table + :is-artist="true" + :show-position="false" + :track-only="true" + :tracks="tracks.slice(0,5)" + > <template slot="header"> <h2> - <translate translate-context="Content/Artist/Title">New tracks by this artist</translate> + <translate translate-context="Content/Artist/Title"> + New tracks by this artist + </translate> </h2> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> </template> </track-table> </section> - <section v-if="isLoadingAlbums" class="ui vertical stripe segment"> - <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> + <section + v-if="isLoadingAlbums" + class="ui vertical stripe segment" + > + <div :class="['ui', 'centered', 'active', 'inline', 'loader']" /> </section> - <section v-else-if="albums && albums.length > 0" class="ui vertical stripe segment"> + <section + v-else-if="albums && albums.length > 0" + class="ui vertical stripe segment" + > <h2> - <translate translate-context="Content/Artist/Title">Albums by this artist</translate> + <translate translate-context="Content/Artist/Title"> + Albums by this artist + </translate> </h2> <div class="ui cards app-cards"> - <album-card :album="album" :key="album.id" v-for="album in allAlbums"></album-card> + <album-card + v-for="album in allAlbums" + :key="album.id" + :album="album" + /> </div> - <div class="ui hidden divider"></div> - <button :class="['ui', {loading: isLoadingMoreAlbums}, 'button']" v-if="nextAlbumsUrl && loadMoreAlbumsUrl" @click="loadMoreAlbums(loadMoreAlbumsUrl)"> - <translate translate-context="Content/*/Button.Label">Load more…</translate> + <div class="ui hidden divider" /> + <button + v-if="nextAlbumsUrl && loadMoreAlbumsUrl" + :class="['ui', {loading: isLoadingMoreAlbums}, 'button']" + @click="loadMoreAlbums(loadMoreAlbumsUrl)" + > + <translate translate-context="Content/*/Button.Label"> + Load more… + </translate> </button> </section> <section class="ui vertical stripe segment"> <h2> - <translate translate-context="Content/*/Title/Noun">User libraries</translate> + <translate translate-context="Content/*/Title/Noun"> + User libraries + </translate> </h2> - <library-widget @loaded="$emit('libraries-loaded', $event)" :url="'artists/' + object.id + '/libraries/'"> - <translate translate-context="Content/Artist/Paragraph" slot="subtitle">This artist is present in the following libraries:</translate> + <library-widget + :url="'artists/' + object.id + '/libraries/'" + @loaded="$emit('libraries-loaded', $event)" + > + <translate + slot="subtitle" + translate-context="Content/Artist/Paragraph" + > + This artist is present in the following libraries: + </translate> </library-widget> </section> </div> </template> <script> -import _ from "@/lodash" -import axios from "axios" -import logger from "@/logging" -import AlbumCard from "@/components/audio/album/Card" -import TrackTable from "@/components/audio/track/Table" -import LibraryWidget from "@/components/federation/LibraryWidget" +import axios from 'axios' +import AlbumCard from '@/components/audio/album/Card' +import TrackTable from '@/components/audio/track/Table' +import LibraryWidget from '@/components/federation/LibraryWidget' export default { - props: ["object", "tracks", "albums", "isLoadingAlbums", "nextTracksUrl", "nextAlbumsUrl"], components: { AlbumCard, TrackTable, - LibraryWidget, + LibraryWidget + }, + props: { + object: { type: Object, required: true }, + tracks: { type: Array, required: true }, + albums: { type: Array, required: true }, + isLoadingAlbums: { type: Boolean, required: true }, + nextTracksUrl: { type: String, required: true }, + nextAlbumsUrl: { type: String, required: true } }, data () { return { @@ -74,26 +132,24 @@ export default { }, computed: { contentFilter () { - let self = this return this.$store.getters['moderation/artistFilters']().filter((e) => { return e.target.id === this.object.id })[0] }, - allAlbums () { + allAlbums () { return this.albums.concat(this.additionalAlbums) } }, methods: { loadMoreAlbums (url) { - let self = this + const self = this self.isLoadingMoreAlbums = true axios.get(url).then((response) => { self.additionalAlbums = self.additionalAlbums.concat(response.data.results) self.loadMoreAlbumsUrl = response.data.next self.isLoadingMoreAlbums = false - }, (error) => { + }, () => { self.isLoadingMoreAlbums = false - }) } } diff --git a/front/src/components/library/ArtistEdit.vue b/front/src/components/library/ArtistEdit.vue index 80a9ae0c3ce130720f62b389b6dac3a2c3c1baf9..976918ab30ca09ffbadac5387528c4dc0ec4fd6e 100644 --- a/front/src/components/library/ArtistEdit.vue +++ b/front/src/components/library/ArtistEdit.vue @@ -1,37 +1,56 @@ <template> - <section class="ui vertical stripe segment"> <div class="ui text container"> <h2> - <translate v-if="canEdit" key="1" translate-context="Content/*/Title">Edit this artist</translate> - <translate v-else key="2" translate-context="Content/*/Title">Suggest an edit on this artist</translate> + <translate + v-if="canEdit" + key="1" + translate-context="Content/*/Title" + > + Edit this artist + </translate> + <translate + v-else + key="2" + translate-context="Content/*/Title" + > + Suggest an edit on this artist + </translate> </h2> - <div class="ui message" v-if="!object.is_local"> - <translate translate-context="Content/*/Message">This object is managed by another server, you cannot edit it.</translate> + <div + v-if="!object.is_local" + class="ui message" + > + <translate translate-context="Content/*/Message"> + This object is managed by another server, you cannot edit it. + </translate> </div> <edit-form v-else :object-type="objectType" :object="object" - :can-edit="canEdit"></edit-form> + :can-edit="canEdit" + /> </div> </section> </template> <script> -import axios from "axios" - import EditForm from '@/components/library/EditForm' export default { - props: ["objectType", "object", "libraries"], - data() { - return { - id: this.object.id, - } - }, components: { EditForm }, + props: { + objectType: { type: String, required: true }, + object: { type: Object, required: true }, + libraries: { type: Array, required: true } + }, + data () { + return { + id: this.object.id + } + }, computed: { canEdit () { return true diff --git a/front/src/components/library/Artists.vue b/front/src/components/library/Artists.vue index 6bf7eaa60ef46aa3b17ebc9247364e718d1ef317..4ee16993b5c227b3261838cc8b6a1cdbaec7f8ef 100644 --- a/front/src/components/library/Artists.vue +++ b/front/src/components/library/Artists.vue @@ -2,67 +2,138 @@ <main v-title="labels.title"> <section class="ui vertical stripe segment"> <h2 class="ui header"> - <translate translate-context="Content/Artist/Title">Browsing artists</translate> + <translate translate-context="Content/Artist/Title"> + Browsing artists + </translate> </h2> - <form :class="['ui', {'loading': isLoading}, 'form']" @submit.prevent="updatePage();updateQueryString();fetchData()"> + <form + :class="['ui', {'loading': isLoading}, 'form']" + @submit.prevent="updatePage();updateQueryString();fetchData()" + > <div class="fields"> <div class="field"> <label for="artist-search"> <translate translate-context="Content/Search/Input.Label/Noun">Artist name</translate> </label> <div class="ui action input"> - <input id="artist-search" type="text" name="search" v-model="query" :placeholder="labels.searchPlaceholder"/> - <button class="ui icon button" type="submit" :aria-label="$pgettext('Content/Search/Input.Label/Noun', 'Search')"> - <i class="search icon"></i> + <input + id="artist-search" + v-model="query" + type="text" + name="search" + :placeholder="labels.searchPlaceholder" + > + <button + class="ui icon button" + type="submit" + :aria-label="$pgettext('Content/Search/Input.Label/Noun', 'Search')" + > + <i class="search icon" /> </button> </div> </div> <div class="field"> <label for="tags-search"><translate translate-context="*/*/*/Noun">Tags</translate></label> - <tags-selector v-model="tags"></tags-selector> + <tags-selector v-model="tags" /> </div> <div class="field"> <label for="artist-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> - <select id="artist-ordering" class="ui dropdown" v-model="ordering"> - <option v-for="option in orderingOptions" :value="option[0]"> + <select + id="artist-ordering" + v-model="ordering" + class="ui dropdown" + > + <option + v-for="(option, key) in orderingOptions" + :key="key" + :value="option[0]" + > {{ sharedLabels.filters[option[1]] }} </option> </select> </div> <div class="field"> <label for="artist-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label> - <select id="artist-ordering-direction" class="ui dropdown" v-model="orderingDirection"> - <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option> - <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option> + <select + id="artist-ordering-direction" + v-model="orderingDirection" + class="ui dropdown" + > + <option value="+"> + <translate translate-context="Content/Search/Dropdown"> + Ascending + </translate> + </option> + <option value="-"> + <translate translate-context="Content/Search/Dropdown"> + Descending + </translate> + </option> </select> </div> <div class="field"> <label for="artist-results"><translate translate-context="Content/Search/Dropdown.Label/Noun">Results per page</translate></label> - <select id="artist-results" class="ui dropdown" v-model="paginateBy"> - <option :value="parseInt(12)">12</option> - <option :value="parseInt(30)">30</option> - <option :value="parseInt(50)">50</option> + <select + id="artist-results" + v-model="paginateBy" + class="ui dropdown" + > + <option :value="parseInt(12)"> + 12 + </option> + <option :value="parseInt(30)"> + 30 + </option> + <option :value="parseInt(50)"> + 50 + </option> </select> </div> <div class="field"> <span id="excludeHeader">Exclude Compilation Artists</span> - <div id="excludeCompilation" class="ui toggle checkbox"> - <input id="exclude-compilation" v-model="excludeCompilation" true-value="true" false-value="null" type="checkbox"> - <label for="exclude-compilation" class="visually-hidden"><translate translate-context="Content/Search/Checkbox/Noun">Exclude Compilation Artists</translate></label> + <div + id="excludeCompilation" + class="ui toggle checkbox" + > + <input + id="exclude-compilation" + v-model="excludeCompilation" + true-value="true" + false-value="null" + type="checkbox" + > + <label + for="exclude-compilation" + class="visually-hidden" + ><translate translate-context="Content/Search/Checkbox/Noun">Exclude Compilation Artists</translate></label> </div> </div> </div> </form> - <div class="ui hidden divider"></div> - <div v-if="result && result.results.length > 0" class="ui five app-cards cards"> - <div v-if="isLoading" class="ui inverted active dimmer"> - <div class="ui loader"></div> + <div class="ui hidden divider" /> + <div + v-if="result && result.results.length > 0" + class="ui five app-cards cards" + > + <div + v-if="isLoading" + class="ui inverted active dimmer" + > + <div class="ui loader" /> </div> - <artist-card :artist="artist" v-for="artist in result.results" :key="artist.id"></artist-card> + <artist-card + v-for="artist in result.results" + :key="artist.id" + :artist="artist" + /> </div> - <div v-else-if="!isLoading" class="ui placeholder segment sixteen wide column" style="text-align: center; display: flex; align-items: center"> + <div + v-else-if="!isLoading" + class="ui placeholder segment sixteen wide column" + style="text-align: center; display: flex; align-items: center" + > <div class="ui icon header"> - <i class="compact disc icon"></i> + <i class="compact disc icon" /> <translate translate-context="Content/Artists/Placeholder"> No results matching your query </translate> @@ -70,21 +141,22 @@ <router-link v-if="$store.state.auth.authenticated" :to="{name: 'content.index'}" - class="ui success button labeled icon"> - <i class="upload icon"></i> + class="ui success button labeled icon" + > + <i class="upload icon" /> <translate translate-context="Content/*/Verb"> - Add some music + Add some music </translate> </router-link> </div> <div class="ui center aligned basic segment"> <pagination v-if="result && result.count > paginateBy" - @page-changed="selectPage" :current="page" :paginate-by="paginateBy" :total="result.count" - ></pagination> + @page-changed="selectPage" + /> </div> </section> </main> @@ -92,34 +164,33 @@ <script> import qs from 'qs' -import axios from "axios" -import _ from "@/lodash" -import $ from "jquery" +import axios from 'axios' +import $ from 'jquery' -import logger from "@/logging" +import logger from '@/logging' -import OrderingMixin from "@/components/mixins/Ordering" -import PaginationMixin from "@/components/mixins/Pagination" -import TranslationsMixin from "@/components/mixins/Translations" -import ArtistCard from "@/components/audio/artist/Card" -import Pagination from "@/components/Pagination" +import OrderingMixin from '@/components/mixins/Ordering' +import PaginationMixin from '@/components/mixins/Pagination' +import TranslationsMixin from '@/components/mixins/Translations' +import ArtistCard from '@/components/audio/artist/Card' +import Pagination from '@/components/Pagination' import TagsSelector from '@/components/library/TagsSelector' -const FETCH_URL = "artists/" +const FETCH_URL = 'artists/' export default { - mixins: [OrderingMixin, PaginationMixin, TranslationsMixin], - props: { - defaultQuery: { type: String, required: false, default: "" }, - defaultTags: { type: Array, required: false, default: () => { return [] } }, - scope: { type: String, required: false, default: "all" }, - }, components: { ArtistCard, Pagination, - TagsSelector, + TagsSelector + }, + 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() { + data () { return { isLoading: true, result: null, @@ -127,93 +198,93 @@ export default { page: parseInt(this.defaultPage), query: this.defaultQuery, tags: (this.defaultTags || []).filter((t) => { return t.length > 0 }), - orderingOptions: [["creation_date", "creation_date"], ["name", "name"]] + orderingOptions: [['creation_date', 'creation_date'], ['name', 'name']] } }, - created() { - this.fetchData() - }, - mounted() { - $(".ui.dropdown").dropdown() - }, computed: { - labels() { - let searchPlaceholder = this.$pgettext('Content/Search/Input.Placeholder', "Search…") - let title = this.$pgettext('*/*/*/Noun', "Artists") + labels () { + const searchPlaceholder = this.$pgettext('Content/Search/Input.Placeholder', 'Search…') + const title = this.$pgettext('*/*/*/Noun', 'Artists') return { searchPlaceholder, title } } }, + watch: { + page () { + this.updateQueryString() + this.fetchData() + }, + '$store.state.moderation.lastUpdate': function () { + this.fetchData() + }, + excludeCompilation () { + this.fetchData() + } + }, + created () { + this.fetchData() + }, + mounted () { + $('.ui.dropdown').dropdown() + }, methods: { - updateQueryString: function() { + updateQueryString: function () { history.pushState( {}, null, this.$route.path + '?' + new URLSearchParams( { - query: this.query, - page: this.page, - tag: this.tags, - paginateBy: this.paginateBy, - ordering: this.getOrderingAsString(), - content_category: 'music', - include_channels: true, - }).toString() + query: this.query, + page: this.page, + tag: this.tags, + paginateBy: this.paginateBy, + ordering: this.getOrderingAsString(), + content_category: 'music', + include_channels: true + }).toString() ) }, - fetchData: function() { - var self = this + fetchData: function () { + const self = this this.isLoading = true - let url = FETCH_URL - let params = { + const url = FETCH_URL + const params = { scope: this.scope, page: this.page, page_size: this.paginateBy, has_albums: this.excludeCompilation, q: this.query, ordering: this.getOrderingAsString(), - playable: "true", + playable: 'true', tag: this.tags, - include_channels: "true", - content_category: 'music', + include_channels: 'true', + content_category: 'music' } - logger.default.debug("Fetching artists") + logger.default.debug('Fetching artists') axios.get( url, { params: params, - paramsSerializer: function(params) { + paramsSerializer: function (params) { return qs.stringify(params, { indices: false }) } } ).then(response => { self.result = response.data self.isLoading = false - }, error => { + }, () => { self.result = null self.isLoading = false }) }, - selectPage: function(page) { + selectPage: function (page) { this.page = page }, - updatePage() { + updatePage () { this.page = this.defaultPage } - }, - watch: { - page() { - this.updateQueryString() - this.fetchData() - }, - "$store.state.moderation.lastUpdate": function () { - this.fetchData() - }, - excludeCompilation() { - this.fetchData() - } } } </script> diff --git a/front/src/components/library/EditCard.vue b/front/src/components/library/EditCard.vue index 39f3bbf99a8124f08fb91959c89ec9bb60cb7b73..a8fde61cf2a3d407e220ceda78154f1ea197b0a6 100644 --- a/front/src/components/library/EditCard.vue +++ b/front/src/components/library/EditCard.vue @@ -3,82 +3,147 @@ <div class="content"> <h4 class="header"> <router-link :to="detailUrl"> - <translate translate-context="Content/Library/Card/Short" :translate-params="{id: obj.uuid.substring(0, 8)}">Modification %{ id }</translate> + <translate + translate-context="Content/Library/Card/Short" + :translate-params="{id: obj.uuid.substring(0, 8)}" + > + Modification %{ id } + </translate> </router-link> </h4> <div class="meta"> <router-link v-if="obj.target && obj.target.type === 'track'" - :to="{name: 'library.tracks.detail', params: {id: obj.target.id }}"> - <i class="music icon"></i> - <translate translate-context="Content/Library/Card/Short" :translate-params="{id: obj.target.id, name: obj.target.repr}">Track #%{ id } - %{ name }</translate> + :to="{name: 'library.tracks.detail', params: {id: obj.target.id }}" + > + <i class="music icon" /> + <translate + translate-context="Content/Library/Card/Short" + :translate-params="{id: obj.target.id, name: obj.target.repr}" + > + Track #%{ id } - %{ name } + </translate> </router-link> <br> - <human-date :date="obj.creation_date" :icon="true"></human-date> + <human-date + :date="obj.creation_date" + :icon="true" + /> <span class="right floated"> <span v-if="obj.is_approved && obj.is_applied"> - <i class="success check icon"></i> + <i class="success check icon" /> <translate translate-context="Content/Library/Card/Short">Approved and applied</translate> </span> <span v-else-if="obj.is_approved"> - <i class="success check icon"></i> + <i class="success check icon" /> <translate translate-context="Content/*/*/Short">Approved</translate> </span> <span v-else-if="obj.is_approved === null"> - <i class="warning hourglass icon"></i> + <i class="warning hourglass icon" /> <translate translate-context="Content/Admin/*/Noun">Pending review</translate> </span> <span v-else-if="obj.is_approved === false"> - <i class="danger x icon"></i> + <i class="danger x icon" /> <translate translate-context="Content/Library/*/Short">Rejected</translate> </span> </span> </div> </div> - <div v-if="obj.summary" class="content"> + <div + v-if="obj.summary" + class="content" + > {{ obj.summary }} </div> <div class="content"> - <table v-if="obj.type === 'update'" class="ui celled very basic fixed stacking table"> + <table + v-if="obj.type === 'update'" + class="ui celled very basic fixed stacking table" + > <thead> <tr> - <th><translate translate-context="Content/Library/Card.Table.Header/Short">Field</translate></th> - <th><translate translate-context="Content/Library/Card.Table.Header/Short">Old value</translate></th> - <th><translate translate-context="Content/Library/Card.Table.Header/Short">New value</translate></th> + <th> + <translate translate-context="Content/Library/Card.Table.Header/Short"> + Field + </translate> + </th> + <th> + <translate translate-context="Content/Library/Card.Table.Header/Short"> + Old value + </translate> + </th> + <th> + <translate translate-context="Content/Library/Card.Table.Header/Short"> + New value + </translate> + </th> </tr> </thead> <tbody> - <tr v-for="field in updatedFields" :key="field.id"> + <tr + v-for="field in updatedFields" + :key="field.id" + > <td>{{ field.id }}</td> <td v-if="field.diff"> <template v-if="field.config.type === 'attachment' && field.oldRepr"> - <img class="ui image" alt="" :src="$store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.oldRepr}/proxy?next=medium_square_crop`)" /> + <img + class="ui image" + alt="" + :src="$store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.oldRepr}/proxy?next=medium_square_crop`)" + > </template> <template v-else> - <span v-if="!part.added" v-for="part in field.diff" :class="['diff', {removed: part.removed}]"> + <span + v-for="(part, key) in field.diff" + v-if="!part.added" + :key="key" + :class="['diff', {removed: part.removed}]" + > {{ part.value }} </span> </template> </td> <td v-else> - <translate translate-context="*/*/*">N/A</translate> + <translate translate-context="*/*/*"> + N/A + </translate> </td> - <td v-if="field.diff" :title="field.newRepr"> + <td + v-if="field.diff" + :title="field.newRepr" + > <template v-if="field.config.type === 'attachment' && field.newRepr"> - <img class="ui image" alt="" :src="$store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.newRepr}/proxy?next=medium_square_crop`)" /> + <img + class="ui image" + alt="" + :src="$store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.newRepr}/proxy?next=medium_square_crop`)" + > </template> <template v-else> - <span v-if="!part.removed" v-for="part in field.diff" :class="['diff', {added: part.added}]"> + <span + v-for="(part, key) in field.diff" + v-if="!part.removed" + :key="key" + :class="['diff', {added: part.added}]" + > {{ part.value }} </span> </template> </td> - <td v-else :title="field.newRepr"> + <td + v-else + :title="field.newRepr" + > <template v-if="field.config.type === 'attachment' && field.newRepr"> - <img class="ui image" alt="" :src="$store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.newRepr}/proxy?next=medium_square_crop`)" /> + <img + class="ui image" + alt="" + :src="$store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.newRepr}/proxy?next=medium_square_crop`)" + > </template> <template v-else> {{ field.newRepr }} @@ -88,32 +153,59 @@ </tbody> </table> </div> - <div v-if="obj.created_by" class="extra content"> + <div + v-if="obj.created_by" + class="extra content" + > <actor-link :actor="obj.created_by" /> </div> - <div v-if="canDelete || canApprove" class="ui bottom attached buttons"> + <div + v-if="canDelete || canApprove" + class="ui bottom attached buttons" + > <button v-if="canApprove && obj.is_approved !== true" + :class="['ui', {loading: isLoading}, 'success', 'basic', 'button']" @click="approve(true)" - :class="['ui', {loading: isLoading}, 'success', 'basic', 'button']"> - <translate translate-context="Content/*/Button.Label/Verb">Approve</translate> + > + <translate translate-context="Content/*/Button.Label/Verb"> + Approve + </translate> </button> <button v-if="canApprove && obj.is_approved === null" + :class="['ui', {loading: isLoading}, 'warning', 'basic', 'button']" @click="approve(false)" - :class="['ui', {loading: isLoading}, 'warning', 'basic', 'button']"> - <translate translate-context="Content/Library/Button.Label">Reject</translate> + > + <translate translate-context="Content/Library/Button.Label"> + Reject + </translate> </button> <dangerous-button v-if="canDelete" :class="['ui', {loading: isLoading}, 'basic danger button']" - :action="remove"> - <translate translate-context="*/*/*/Verb">Delete</translate> - <p slot="modal-header"><translate translate-context="Popup/Library/Title">Delete this suggestion?</translate></p> + :action="remove" + > + <translate translate-context="*/*/*/Verb"> + Delete + </translate> + <p slot="modal-header"> + <translate translate-context="Popup/Library/Title"> + Delete this suggestion? + </translate> + </p> <div slot="modal-content"> - <p><translate translate-context="Popup/Library/Paragraph">The suggestion will be completely removed, this action is irreversible.</translate></p> + <p> + <translate translate-context="Popup/Library/Paragraph"> + The suggestion will be completely removed, this action is irreversible. + </translate> + </p> </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> </div> </div> @@ -134,8 +226,8 @@ function castValue (value) { export default { props: { - obj: {required: true}, - currentState: {required: false} + obj: { type: Object, required: true }, + currentState: { type: Object, required: false, default: function () { return { } } } }, data () { return { @@ -161,7 +253,7 @@ export default { return '' } let namespace - let id = this.obj.target.id + const id = this.obj.target.id if (this.obj.target.type === 'track') { namespace = 'library.tracks.edit.detail' } @@ -171,22 +263,22 @@ export default { if (this.obj.target.type === 'artist') { namespace = 'library.artists.edit.detail' } - return this.$router.resolve({name: namespace, params: {id, editId: this.obj.uuid}}).href + return this.$router.resolve({ name: namespace, params: { id, editId: this.obj.uuid } }).href }, updatedFields () { if (!this.obj.target) { return [] } - let payload = this.obj.payload - let previousState = this.previousState - let fields = Object.keys(payload) - let self = this + const payload = this.obj.payload + const previousState = this.previousState + const fields = Object.keys(payload) + const self = this return fields.map((f) => { - let fieldConfig = edits.getFieldConfig(self.configs, this.obj.target.type, f) - let dummyRepr = (v) => { return v } - let getValueRepr = fieldConfig.getValueRepr || dummyRepr - let d = { + const fieldConfig = edits.getFieldConfig(self.configs, this.obj.target.type, f) + const dummyRepr = (v) => { return v } + const getValueRepr = fieldConfig.getValueRepr || dummyRepr + const d = { id: f, config: fieldConfig } @@ -206,12 +298,12 @@ export default { }, methods: { remove () { - let self = this + const self = this this.isLoading = true axios.delete(`mutations/${this.obj.uuid}/`).then((response) => { self.$emit('deleted') self.isLoading = false - }, error => { + }, () => { self.isLoading = false }) }, @@ -222,16 +314,16 @@ export default { } else { url = `mutations/${this.obj.uuid}/reject/` } - let self = this + const self = this this.isLoading = true axios.post(url).then((response) => { self.$emit('approved', approved) self.isLoading = false - self.$store.commit('ui/incrementNotifications', {count: -1, type: 'pendingReviewEdits'}) - }, error => { + self.$store.commit('ui/incrementNotifications', { count: -1, type: 'pendingReviewEdits' }) + }, () => { self.isLoading = false }) - }, + } } } </script> diff --git a/front/src/components/library/EditDetail.vue b/front/src/components/library/EditDetail.vue index 4a0c89434de8797d37022d0d9916df1fd719647a..dd617e21e00a7795d6d8c09700ce02337b94798b 100644 --- a/front/src/components/library/EditDetail.vue +++ b/front/src/components/library/EditDetail.vue @@ -1,46 +1,52 @@ <template> - <section :class="['ui', 'vertical', 'stripe', {loading: isLoading}, 'segment']"> <div class="ui text container"> - <edit-card v-if="obj" :obj="obj" :current-state="currentState" /> + <edit-card + v-if="obj" + :obj="obj" + :current-state="currentState" + /> </div> </section> </template> <script> -import axios from "axios" +import axios from 'axios' import edits from '@/edits' import EditCard from '@/components/library/EditCard' export default { - props: ["object", "objectType", "editId"], components: { EditCard }, + props: { + object: { type: Object, required: true }, + objectType: { type: String, required: true }, + editId: { type: Number, required: true } + }, data () { return { isLoading: true, - obj: null, + obj: null } }, - created () { - this.fetchData() - }, computed: { configs: edits.getConfigs, config: edits.getConfig, - currentState: edits.getCurrentState, currentState () { - let self = this - let s = {} + const self = this + const s = {} this.config.fields.forEach(f => { - s[f.id] = {value: f.getValue(self.object)} + s[f.id] = { value: f.getValue(self.object) } }) return s } }, + created () { + this.fetchData() + }, methods: { fetchData () { - var self = this + const self = this this.isLoading = true axios.get(`mutations/${this.editId}/`).then(response => { self.obj = response.data diff --git a/front/src/components/library/EditForm.vue b/front/src/components/library/EditForm.vue index c9b017411c8fe732ffdff550d3d066ad2db5fe50..60175df3d6f4508b87b2c3d4db3981032613882e 100644 --- a/front/src/components/library/EditForm.vue +++ b/front/src/components/library/EditForm.vue @@ -1,24 +1,41 @@ <template> <div v-if="submittedMutation"> <div class="ui positive message"> - <h4 class="header"><translate translate-context="Content/Library/Paragraph">Your edit was successfully submitted.</translate></h4> + <h4 class="header"> + <translate translate-context="Content/Library/Paragraph"> + Your edit was successfully submitted. + </translate> + </h4> </div> - <edit-card :obj="submittedMutation" :current-state="currentState" /> - <button class="ui button" @click.prevent="submittedMutation = null"> + <edit-card + :obj="submittedMutation" + :current-state="currentState" + /> + <button + class="ui button" + @click.prevent="submittedMutation = null" + > <translate translate-context="Content/Library/Button.Label"> Submit another edit </translate> </button> </div> <div v-else> - - <edit-list :filters="editListFilters" :url="mutationsUrl" :obj="object" :currentState="currentState"> + <edit-list + :filters="editListFilters" + :url="mutationsUrl" + :obj="object" + :current-state="currentState" + > <div slot="title"> <template v-if="showPendingReview"> <translate translate-context="Content/Library/Paragraph"> Recent edits awaiting review </translate> - <button class="ui tiny basic right floated button" @click.prevent="showPendingReview = false"> + <button + class="ui tiny basic right floated button" + @click.prevent="showPendingReview = false" + > <translate translate-context="Content/Library/Button.Label"> Show all edits </translate> @@ -28,7 +45,10 @@ <translate translate-context="Content/Library/Paragraph"> Recent edits </translate> - <button class="ui tiny basic right floated button" @click.prevent="showPendingReview = true"> + <button + class="ui tiny basic right floated button" + @click.prevent="showPendingReview = true" + > <translate translate-context="Content/Library/Button.Label"> Restrict to unreviewed edits </translate> @@ -41,91 +61,178 @@ </translate> </empty-state> </edit-list> - <form class="ui form" @submit.prevent="submit()"> - <div class="ui hidden divider"></div> - <div v-if="errors.length > 0" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Content/Library/Error message.Title">Error while submitting edit</translate></h4> + <form + class="ui form" + @submit.prevent="submit()" + > + <div class="ui hidden divider" /> + <div + v-if="errors.length > 0" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Content/Library/Error message.Title"> + Error while submitting edit + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> - <div v-if="!canEdit" class="ui message"> + <div + v-if="!canEdit" + class="ui message" + > <translate translate-context="Content/Library/Paragraph"> You don't have the permission to edit this object, but you can suggest changes. Once submitted, suggestions will be reviewed before approval. </translate> </div> - <div v-if="values" v-for="fieldConfig in config.fields" :key="fieldConfig.id" class="ui field"> + <div + v-for="fieldConfig in config.fields" + v-if="values" + :key="fieldConfig.id" + class="ui field" + > <template v-if="fieldConfig.type === 'text'"> <label :for="fieldConfig.id">{{ fieldConfig.label }}</label> - <input :type="fieldConfig.inputType || 'text'" v-model="values[fieldConfig.id]" :required="fieldConfig.required" :name="fieldConfig.id" :id="fieldConfig.id"> + <input + :id="fieldConfig.id" + v-model="values[fieldConfig.id]" + :type="fieldConfig.inputType || 'text'" + :required="fieldConfig.required" + :name="fieldConfig.id" + > </template> <template v-else-if="fieldConfig.type === 'license'"> <label :for="fieldConfig.id">{{ fieldConfig.label }}</label> <select + :id="fieldConfig.id" ref="license" v-model="values[fieldConfig.id]" :required="fieldConfig.required" - :id="fieldConfig.id" - class="ui fluid search dropdown"> - <option :value="null"><translate translate-context="*/*/*">N/A</translate></option> - <option v-for="license in licenses" :key="license.code" :value="license.code">{{ license.name}}</option> + class="ui fluid search dropdown" + > + <option :value="null"> + <translate translate-context="*/*/*"> + N/A + </translate> + </option> + <option + v-for="license in licenses" + :key="license.code" + :value="license.code" + > + {{ license.name }} + </option> </select> - <button class="ui tiny basic left floated button" form="noop" @click.prevent="values[fieldConfig.id] = null"> - <i class="x icon"></i> - <translate translate-context="Content/Library/Button.Label">Clear</translate> + <button + class="ui tiny basic left floated button" + form="noop" + @click.prevent="values[fieldConfig.id] = null" + > + <i class="x icon" /> + <translate translate-context="Content/Library/Button.Label"> + Clear + </translate> </button> - </template> <template v-else-if="fieldConfig.type === 'content'"> <label :for="fieldConfig.id">{{ fieldConfig.label }}</label> - <content-form v-model="values[fieldConfig.id].text" :field-id="fieldConfig.id" :rows="3"></content-form> + <content-form + v-model="values[fieldConfig.id].text" + :field-id="fieldConfig.id" + :rows="3" + /> </template> <template v-else-if="fieldConfig.type === 'attachment'"> <attachment-input + :id="fieldConfig.id" v-model="values[fieldConfig.id]" :initial-value="initialValues[fieldConfig.id]" :required="fieldConfig.required" :name="fieldConfig.id" - :id="fieldConfig.id" - @delete="values[fieldConfig.id] = initialValues[fieldConfig.id]"> + @delete="values[fieldConfig.id] = initialValues[fieldConfig.id]" + > <span slot="label">{{ fieldConfig.label }}</span> </attachment-input> - </template> <template v-else-if="fieldConfig.type === 'tags'"> <label :for="fieldConfig.id">{{ fieldConfig.label }}</label> <tags-selector + :id="fieldConfig.id" ref="tags" v-model="values[fieldConfig.id]" - :id="fieldConfig.id" - required="fieldConfig.required"></tags-selector> - <button class="ui tiny basic left floated button" form="noop" @click.prevent="values[fieldConfig.id] = []"> - <i class="x icon"></i> - <translate translate-context="Content/Library/Button.Label">Clear</translate> + required="fieldConfig.required" + /> + <button + class="ui tiny basic left floated button" + form="noop" + @click.prevent="values[fieldConfig.id] = []" + > + <i class="x icon" /> + <translate translate-context="Content/Library/Button.Label"> + Clear + </translate> </button> </template> <div v-if="!lodash.isEqual(values[fieldConfig.id], initialValues[fieldConfig.id])"> - <button class="ui tiny basic right floated reset button" form="noop" @click.prevent="values[fieldConfig.id] = lodash.clone(initialValues[fieldConfig.id])"> - <i class="undo icon"></i> - <translate translate-context="Content/Library/Button.Label">Reset to initial value</translate> + <button + class="ui tiny basic right floated reset button" + form="noop" + @click.prevent="values[fieldConfig.id] = lodash.clone(initialValues[fieldConfig.id])" + > + <i class="undo icon" /> + <translate translate-context="Content/Library/Button.Label"> + Reset to initial value + </translate> </button> </div> </div> <div class="field"> <label for="summary"><translate translate-context="*/*/*">Summary (optional)</translate></label> - <textarea name="change-summary" v-model="summary" id="change-summary" rows="3" :placeholder="labels.summaryPlaceholder"></textarea> + <textarea + id="change-summary" + v-model="summary" + name="change-summary" + rows="3" + :placeholder="labels.summaryPlaceholder" + /> </div> <router-link - class="ui left floated button" v-if="objectType === 'track'" + class="ui left floated button" :to="{name: 'library.tracks.detail', params: {id: object.id }}" > - <translate translate-context="*/*/Button.Label/Verb">Cancel</translate> + <translate translate-context="*/*/Button.Label/Verb"> + Cancel + </translate> </router-link> - <button :class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']" type="submit" :disabled="isLoading || !mutationPayload"> - <translate v-if="canEdit" key="1" translate-context="Content/Library/Button.Label/Verb">Submit and apply edit</translate> - <translate v-else key="2" translate-context="Content/Library/Button.Label/Verb">Submit suggestion</translate> + <button + :class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']" + type="submit" + :disabled="isLoading || !mutationPayload" + > + <translate + v-if="canEdit" + key="1" + translate-context="Content/Library/Button.Label/Verb" + > + Submit and apply edit + </translate> + <translate + v-else + key="2" + translate-context="Content/Library/Button.Label/Verb" + > + Submit suggestion + </translate> </button> </form> </div> @@ -134,24 +241,26 @@ <script> import $ from 'jquery' import _ from '@/lodash' -import axios from "axios" +import axios from 'axios' import AttachmentInput from '@/components/common/AttachmentInput' import EditList from '@/components/library/EditList' import EditCard from '@/components/library/EditCard' import TagsSelector from '@/components/library/TagsSelector' import edits from '@/edits' -import lodash from '@/lodash' - export default { - props: ["objectType", "object", "licenses"], components: { EditList, EditCard, TagsSelector, AttachmentInput }, - data() { + props: { + objectType: { type: String, required: true }, + object: { type: Object, required: true }, + licenses: { type: Array, required: true } + }, + data () { return { isLoading: false, errors: [], @@ -159,16 +268,9 @@ export default { initialValues: {}, summary: '', submittedMutation: null, - showPendingReview: true, - lodash, + showPendingReview: true } }, - created () { - this.setValues() - }, - mounted() { - $(".ui.dropdown").dropdown({fullTextSearch: true}) - }, computed: { configs: edits.getConfigs, config: edits.getConfig, @@ -176,7 +278,7 @@ export default { canEdit: edits.getCanEdit, labels () { return { - summaryPlaceholder: this.$pgettext('*/*/Placeholder', 'A short summary describing your changes.'), + summaryPlaceholder: this.$pgettext('*/*/Placeholder', 'A short summary describing your changes.') } }, mutationsUrl () { @@ -189,19 +291,20 @@ export default { if (this.objectType === 'artist') { return `artists/${this.object.id}/mutations/` } + return null }, mutationPayload () { - let self = this - let changedFields = this.config.fields.filter(f => { - return !lodash.isEqual(self.values[f.id], self.initialValues[f.id]) + const self = this + const changedFields = this.config.fields.filter(f => { + return !_.isEqual(self.values[f.id], self.initialValues[f.id]) }) if (changedFields.length === 0) { return null } - let payload = { + const payload = { type: 'update', payload: {}, - summary: this.summary, + summary: this.summary } changedFields.forEach((f) => { payload.payload[f.id] = self.values[f.id] @@ -210,26 +313,41 @@ export default { }, editListFilters () { if (this.showPendingReview) { - return {is_approved: 'null'} + return { is_approved: 'null' } } else { return {} } - }, + } + }, + watch: { + 'values.license' (newValue) { + if (newValue === null) { + $(this.$refs.license).dropdown('clear') + } else { + $(this.$refs.license).dropdown('set selected', newValue) + } + } + }, + created () { + this.setValues() + }, + mounted () { + $('.ui.dropdown').dropdown({ fullTextSearch: true }) }, methods: { setValues () { - let self = this + const self = this this.config.fields.forEach(f => { - self.$set(self.values, f.id, lodash.clone(f.getValue(self.object))) - self.$set(self.initialValues, f.id, lodash.clone(self.values[f.id])) + self.$set(self.values, f.id, _.clone(f.getValue(self.object))) + self.$set(self.initialValues, f.id, _.clone(self.values[f.id])) }) }, - submit() { - let self = this + submit () { + const self = this self.isLoading = true self.errors = [] - let payload = _.clone(this.mutationPayload || {}) + const payload = _.clone(this.mutationPayload || {}) if (this.canEdit) { payload.is_approved = true } @@ -244,15 +362,6 @@ export default { } ) } - }, - watch: { - 'values.license' (newValue) { - if (newValue === null) { - $(this.$refs.license).dropdown('clear') - } else { - $(this.$refs.license).dropdown('set selected', newValue) - } - } } } </script> diff --git a/front/src/components/library/EditList.vue b/front/src/components/library/EditList.vue index 2ff1fc72a0661e0e9a21c3e97e17fe73aabd1b39..e3efd0be31b494e12df8847bc2f09caffd7cb0eb 100644 --- a/front/src/components/library/EditList.vue +++ b/front/src/components/library/EditList.vue @@ -1,16 +1,43 @@ <template> <div class="wrapper"> <h3 class="ui header"> - <slot name="title"></slot> + <slot name="title" /> </h3> - <slot v-if="!isLoading && objects.length === 0" name="empty-state"></slot> - <button v-if="nextPage || previousPage" :disabled="!previousPage" @click="fetchData(previousPage)" :class="['ui', {disabled: !previousPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle left', 'icon']"></i></button> - <button v-if="nextPage || previousPage" :disabled="!nextPage" @click="fetchData(nextPage)" :class="['ui', {disabled: !nextPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle right', 'icon']"></i></button> - <div class="ui hidden divider"></div> - <div v-if="isLoading" class="ui inverted active dimmer"> - <div class="ui loader"></div> + <slot + v-if="!isLoading && objects.length === 0" + name="empty-state" + /> + <button + v-if="nextPage || previousPage" + :disabled="!previousPage" + :class="['ui', {disabled: !previousPage}, 'circular', 'icon', 'basic', 'button']" + @click="fetchData(previousPage)" + > + <i :class="['ui', 'angle left', 'icon']" /> + </button> + <button + v-if="nextPage || previousPage" + :disabled="!nextPage" + :class="['ui', {disabled: !nextPage}, 'circular', 'icon', 'basic', 'button']" + @click="fetchData(nextPage)" + > + <i :class="['ui', 'angle right', 'icon']" /> + </button> + <div class="ui hidden divider" /> + <div + v-if="isLoading" + class="ui inverted active dimmer" + > + <div class="ui loader" /> </div> - <edit-card @updated="fetchData(url)" @deleted="fetchData(url)" v-for="obj in objects" :key="obj.uuid" :obj="obj" :current-state="currentState" /> + <edit-card + v-for="obj in objects" + :key="obj.uuid" + :obj="obj" + :current-state="currentState" + @updated="fetchData(url)" + @deleted="fetchData(url)" + /> </div> </template> @@ -21,14 +48,14 @@ import axios from 'axios' import EditCard from '@/components/library/EditCard' export default { - props: { - url: {type: String, required: true}, - filters: {type: Object, required: false, default: () => {return {}}}, - currentState: {required: false}, - }, components: { EditCard }, + props: { + url: { type: String, required: true }, + filters: { type: Object, required: false, default: () => { return {} } }, + currentState: { type: Object, required: false, default: () => { return { } } } + }, data () { return { objects: [], @@ -39,6 +66,14 @@ export default { nextPage: null } }, + watch: { + filters: { + handler () { + this.fetchData(this.url) + }, + deep: true + } + }, created () { this.fetchData(this.url) }, @@ -48,10 +83,10 @@ export default { return } this.isLoading = true - let self = this - let params = _.clone(this.filters) + const self = this + const params = _.clone(this.filters) params.page_size = this.limit - axios.get(url, {params: params}).then((response) => { + axios.get(url, { params: params }).then((response) => { self.previousPage = response.data.previous self.nextPage = response.data.next self.isLoading = false @@ -60,14 +95,6 @@ export default { self.isLoading = false self.errors = error.backendErrors }) - }, - }, - watch: { - filters: { - handler () { - this.fetchData(this.url) - }, - deep: true } } } diff --git a/front/src/components/library/FileUpload.vue b/front/src/components/library/FileUpload.vue index bba0ad7c8158c4393bd8c7823ac9cbb674688db4..ca171c65d0160be305cfff0967287fca39fcf5c4 100644 --- a/front/src/components/library/FileUpload.vue +++ b/front/src/components/library/FileUpload.vue @@ -1,27 +1,53 @@ - <template> +<template> <div class="component-file-upload"> <div class="ui top attached tabular menu"> - <a href="" :class="['item', {active: currentTab === 'uploads'}]" @click.prevent="currentTab = 'uploads'"> + <a + href="" + :class="['item', {active: currentTab === 'uploads'}]" + @click.prevent="currentTab = 'uploads'" + > <translate translate-context="Content/Library/Tab.Title/Short">Uploading</translate> - <div v-if="files.length === 0" class="ui label"> + <div + v-if="files.length === 0" + class="ui label" + > 0 </div> - <div v-else-if="files.length > uploadedFilesCount + erroredFilesCount" class="ui warning label"> + <div + v-else-if="files.length > uploadedFilesCount + erroredFilesCount" + class="ui warning label" + > {{ uploadedFilesCount + erroredFilesCount }}/{{ files.length }} </div> - <div v-else :class="['ui', {'success': erroredFilesCount === 0}, {'danger': erroredFilesCount > 0}, 'label']"> + <div + v-else + :class="['ui', {'success': erroredFilesCount === 0}, {'danger': erroredFilesCount > 0}, 'label']" + > {{ uploadedFilesCount + erroredFilesCount }}/{{ files.length }} </div> </a> - <a href="" :class="['item', {active: currentTab === 'processing'}]" @click.prevent="currentTab = 'processing'"> + <a + href="" + :class="['item', {active: currentTab === 'processing'}]" + @click.prevent="currentTab = 'processing'" + > <translate translate-context="Content/Library/Tab.Title/Short">Processing</translate> - <div v-if="processableFiles === 0" class="ui label"> + <div + v-if="processableFiles === 0" + class="ui label" + > 0 </div> - <div v-else-if="processableFiles > processedFilesCount" class="ui warning label"> + <div + v-else-if="processableFiles > processedFilesCount" + class="ui warning label" + > {{ processedFilesCount }}/{{ processableFiles }} </div> - <div v-else :class="['ui', {'success': uploads.errored === 0}, {'danger': uploads.errored > 0}, 'label']"> + <div + v-else + :class="['ui', {'success': uploads.errored === 0}, {'danger': uploads.errored > 0}, 'label']" + > {{ processedFilesCount }}/{{ processableFiles }} </div> </a> @@ -30,177 +56,288 @@ <div :class="['ui', {loading: isLoadingQuota}, 'container']"> <div :class="['ui', {red: remainingSpace === 0}, {warning: remainingSpace > 0 && remainingSpace <= 50}, 'small', 'statistic']"> <div class="label"> - <translate translate-context="Content/Library/Paragraph">Remaining storage space</translate> + <translate translate-context="Content/Library/Paragraph"> + Remaining storage space + </translate> </div> <div class="value"> - {{ remainingSpace * 1000 * 1000 | humanSize}} + {{ remainingSpace * 1000 * 1000 | humanSize }} </div> </div> - <div class="ui divider"></div> - <h2 class="ui header"><translate translate-context="Content/Library/Title/Verb">Upload music from your local storage</translate></h2> + <div class="ui divider" /> + <h2 class="ui header"> + <translate translate-context="Content/Library/Title/Verb"> + Upload music from your local storage + </translate> + </h2> <div class="ui message"> - <p><translate translate-context="Content/Library/Paragraph">You are about to upload music to your library. Before proceeding, please ensure that:</translate></p> + <p> + <translate translate-context="Content/Library/Paragraph"> + You are about to upload music to your library. Before proceeding, please ensure that: + </translate> + </p> <ul> <li v-if="library.privacy_level != 'me'"> - <translate translate-context="Content/Library/List item">You are not uploading copyrighted content in a public library, otherwise you may be infringing the law</translate> + <translate translate-context="Content/Library/List item"> + You are not uploading copyrighted content in a public library, otherwise you may be infringing the law + </translate> </li> <li> - <translate translate-context="Content/Library/List item">The music files you are uploading are tagged properly.</translate> - <a href="http://picard.musicbrainz.org/" target='_blank'><translate translate-context="Content/Library/Link">We recommend using Picard for that purpose.</translate></a> + <translate translate-context="Content/Library/List item"> + The music files you are uploading are tagged properly. + </translate> + <a + href="http://picard.musicbrainz.org/" + target="_blank" + ><translate translate-context="Content/Library/Link">We recommend using Picard for that purpose.</translate></a> </li> <li> - <translate translate-context="Content/Library/List item">The music files you are uploading are in OGG, Flac, MP3 or AIFF format</translate> + <translate translate-context="Content/Library/List item"> + The music files you are uploading are in OGG, Flac, MP3 or AIFF format + </translate> </li> </ul> </div> <file-upload-widget + ref="upload" + v-model="files" :class="['ui', 'icon', 'basic', 'button']" :post-action="uploadUrl" :multiple="true" :data="uploadData" :drop="true" :extensions="supportedExtensions" - v-model="files" name="audio_file" :thread="1" @input-file="inputFile" - ref="upload"> - <i class="upload icon"></i> - <translate translate-context="Content/Library/Paragraph/Call to action">Click to select files to upload or drag and drop files or directories</translate> - <br /> - <br /> - <i><translate translate-context="Content/Library/Paragraph" :translate-params="{extensions: supportedExtensions.join(', ')}">Supported extensions: %{ extensions }</translate></i> + > + <i class="upload icon" /> + <translate translate-context="Content/Library/Paragraph/Call to action"> + Click to select files to upload or drag and drop files or directories + </translate> + <br> + <br> + <i><translate + translate-context="Content/Library/Paragraph" + :translate-params="{extensions: supportedExtensions.join(', ')}" + >Supported extensions: %{ extensions }</translate></i> </file-upload-widget> </div> - <div v-if="files.length > 0" class="table-wrapper"> - <div class="ui hidden divider"></div> + <div + v-if="files.length > 0" + class="table-wrapper" + > + <div class="ui hidden divider" /> <table class="ui unstackable table"> <thead> <tr> - <th class="ten wide"><translate translate-context="Content/Library/Table.Label">Filename</translate></th> - <th><translate translate-context="Content/*/*/Noun">Size</translate></th> - <th><translate translate-context="*/*/*">Status</translate></th> - <th><translate translate-context="*/*/*">Actions</translate></th> + <th class="ten wide"> + <translate translate-context="Content/Library/Table.Label"> + Filename + </translate> + </th> + <th> + <translate translate-context="Content/*/*/Noun"> + Size + </translate> + </th> + <th> + <translate translate-context="*/*/*"> + Status + </translate> + </th> + <th> + <translate translate-context="*/*/*"> + Actions + </translate> + </th> </tr> <tr v-if="retryableFiles.length > 1"> - <th class="ten wide"></th> - <th></th> - <th></th> + <th class="ten wide" /> + <th /> + <th /> <th> - <button class="ui right floated small basic button" @click.prevent="retry(retryableFiles)"> - <translate translate-context="Content/Library/Table">Retry failed uploads</translate> + <button + class="ui right floated small basic button" + @click.prevent="retry(retryableFiles)" + > + <translate translate-context="Content/Library/Table"> + Retry failed uploads + </translate> </button> </th> </tr> </thead> <tbody> - <tr v-for="(file, index) in sortedFiles" :key="file.id"> - <td :title="file.name">{{ file.name | truncate(60) }}</td> + <tr + v-for="file in sortedFiles" + :key="file.id" + > + <td :title="file.name"> + {{ file.name | truncate(60) }} + </td> <td>{{ file.size | humanSize }}</td> <td> - <span v-if="file.error" class="ui tooltip" :data-tooltip="labels.tooltips[file.error]"> + <span + v-if="file.error" + class="ui tooltip" + :data-tooltip="labels.tooltips[file.error]" + > <span class="ui danger icon label"> <i class="question circle outline icon" /> {{ file.error }} </span> </span> - <span v-else-if="file.success" class="ui success label"> - <translate translate-context="Content/Library/Table" key="1">Uploaded</translate> + <span + v-else-if="file.success" + class="ui success label" + > + <translate + key="1" + translate-context="Content/Library/Table" + >Uploaded</translate> </span> - <span v-else-if="file.active" class="ui warning label"> - <translate translate-context="Content/Library/Table" key="2">Uploading…</translate> + <span + v-else-if="file.active" + class="ui warning label" + > + <translate + key="2" + translate-context="Content/Library/Table" + >Uploading…</translate> ({{ parseInt(file.progress) }}%) </span> - <span v-else class="ui label"><translate translate-context="Content/Library/*/Short" key="3">Pending</translate></span> + <span + v-else + class="ui label" + ><translate + key="3" + translate-context="Content/Library/*/Short" + >Pending</translate></span> </td> <td> <template v-if="file.error"> <button + v-if="retryableFiles.indexOf(file) > -1" class="ui tiny basic icon right floated button" :title="labels.retry" @click.prevent="retry([file])" - v-if="retryableFiles.indexOf(file) > -1"> - <i class="redo icon"></i> + > + <i class="redo icon" /> </button> </template> <template v-else-if="!file.success"> - <button class="ui tiny basic danger icon right floated button" @click.prevent="$refs.upload.remove(file)"><i class="delete icon"></i></button> + <button + class="ui tiny basic danger icon right floated button" + @click.prevent="$refs.upload.remove(file)" + > + <i class="delete icon" /> + </button> </template> </td> </tr> </tbody> </table> </div> - <div class="ui divider"></div> - <h2 class="ui header"><translate translate-context="Content/Library/Title/Verb">Import music from your server</translate></h2> - <div v-if="fsErrors.length > 0" role="alert" class="ui negative message"> - <h3 class="header"><translate translate-context="Content/*/Error message.Title">Error while launching import</translate></h3> + <div class="ui divider" /> + <h2 class="ui header"> + <translate translate-context="Content/Library/Title/Verb"> + Import music from your server + </translate> + </h2> + <div + v-if="fsErrors.length > 0" + role="alert" + class="ui negative message" + > + <h3 class="header"> + <translate translate-context="Content/*/Error message.Title"> + Error while launching import + </translate> + </h3> <ul class="list"> - <li v-for="error in fsErrors">{{ error }}</li> + <li + v-for="(error, key) in fsErrors" + :key="key" + > + {{ error }} + </li> </ul> </div> <fs-browser v-model="fsPath" - @import="importFs" :loading="isLoadingFs" - :data="fsStatus"></fs-browser> + :data="fsStatus" + @import="importFs" + /> <template v-if="fsStatus && fsStatus.import"> - <h3 class="ui header"><translate translate-context="Content/Library/Title/Verb">Import status</translate></h3> + <h3 class="ui header"> + <translate translate-context="Content/Library/Title/Verb"> + Import status + </translate> + </h3> <p v-if="fsStatus.import.reference != importReference"> - <translate translate-context="Content/Library/Paragraph">Results of your previous import:</translate> + <translate translate-context="Content/Library/Paragraph"> + Results of your previous import: + </translate> </p> <p v-else> - <translate translate-context="Content/Library/Paragraph">Results of your import:</translate> + <translate translate-context="Content/Library/Paragraph"> + Results of your import: + </translate> </p> <button + v-if="fsStatus.import.status === 'started' || fsStatus.import.status === 'pending'" class="ui button" @click="cancelFsScan" - v-if="fsStatus.import.status === 'started' || fsStatus.import.status === 'pending'"> - <translate translate-context="*/*/Button.Label/Verb">Cancel</translate> + > + <translate translate-context="*/*/Button.Label/Verb"> + Cancel + </translate> </button> - <fs-logs :data="fsStatus.import"></fs-logs> + <fs-logs :data="fsStatus.import" /> </template> - - </div> <div :class="['ui', 'bottom', 'attached', 'segment', {hidden: currentTab != 'processing'}]"> <library-files-table :needs-refresh="needsRefresh" ordering-config-name="library.detail.upload" - @fetch-start="needsRefresh = false" :filters="{import_reference: importReference}" - :custom-objects="Object.values(uploads.objects)"></library-files-table> + :custom-objects="Object.values(uploads.objects)" + @fetch-start="needsRefresh = false" + /> </div> </div> </template> <script> -import _ from "@/lodash" -import $ from "jquery"; -import axios from "axios"; -import logger from "@/logging"; -import FileUploadWidget from "./FileUploadWidget"; -import FsBrowser from "./FsBrowser"; -import FsLogs from "./FsLogs"; -import LibraryFilesTable from "@/views/content/libraries/FilesTable"; -import moment from "moment"; +import _ from '@/lodash' +import axios from 'axios' +import FileUploadWidget from './FileUploadWidget' +import FsBrowser from './FsBrowser' +import FsLogs from './FsLogs' +import LibraryFilesTable from '@/views/content/libraries/FilesTable' +import moment from 'moment' export default { - props: ["library", "defaultImportReference"], components: { FileUploadWidget, LibraryFilesTable, FsBrowser, - FsLogs, + FsLogs + }, + props: { + library: { type: Object, required: true }, + defaultImportReference: { type: String, required: false, default: '' } }, - data() { - let importReference = this.defaultImportReference || moment().format(); - this.$router.replace({ query: { import: importReference } }); + data () { + const importReference = this.defaultImportReference || moment().format() + this.$router.replace({ query: { import: importReference } }) return { files: [], needsRefresh: false, - currentTab: "uploads", - uploadUrl: this.$store.getters['instance/absoluteUrl']("/api/v1/uploads/"), + currentTab: 'uploads', + uploadUrl: this.$store.getters['instance/absoluteUrl']('/api/v1/uploads/'), importReference, isLoadingQuota: false, quotaStatus: null, @@ -217,202 +354,80 @@ export default { isLoadingFs: false, fsInterval: null, fsErrors: [] - }; - }, - created() { - this.fetchStatus(); - if (this.$store.state.auth.availablePermissions['library']) { - this.fetchFs(true) - this.fsInterval = setInterval(() => { - this.fetchFs(false) - }, 5000); - } - this.fetchQuota(); - this.$store.commit("ui/addWebsocketEventHandler", { - eventName: "import.status_updated", - id: "fileUpload", - handler: this.handleImportEvent - }); - window.onbeforeunload = e => this.onBeforeUnload(e); - }, - destroyed() { - this.$store.commit("ui/removeWebsocketEventHandler", { - eventName: "import.status_updated", - id: "fileUpload" - }); - window.onbeforeunload = null; - if (this.fsInterval) { - clearInterval(this.fsInterval) - } - }, - methods: { - onBeforeUnload(e = {}) { - const returnValue = ('This page is asking you to confirm that you want to leave - data you have entered may not be saved.'); - if (!this.hasActiveUploads) return null; - Object.assign(e, { - returnValue, - }); - return returnValue; - }, - fetchQuota () { - let self = this - self.isLoadingQuota = true - axios.get('users/me/').then((response) => { - self.quotaStatus = response.data.quota_status - self.isLoadingQuota = false - }) - }, - fetchFs (updateLoading) { - let self = this - if (updateLoading) { - self.isLoadingFs = true - } - axios.get('libraries/fs-import', {params: {path: this.fsPath.join('/')}}).then((response) => { - self.fsStatus = response.data - if (updateLoading) { - self.isLoadingFs = false - } - }) - }, - importFs () { - let self = this - self.isLoadingFs = true - let payload = { - path: this.fsPath.join('/'), - library: this.library.uuid, - import_reference: this.importReference, - } - axios.post('libraries/fs-import', payload).then((response) => { - self.fsStatus = response.data - self.isLoadingFs = false - }, error => { - self.isLoadingFs = false - self.fsErrors = error.backendErrors - }) - }, - async cancelFsScan () { - await axios.delete('libraries/fs-import') - this.fetchFs() - }, - inputFile(newFile, oldFile) { - if (!newFile) { - return - } - if (this.remainingSpace < newFile.size / (1000 * 1000)) { - newFile.error = 'denied' - } else { - this.$refs.upload.active = true; - } - }, - fetchStatus() { - let self = this; - let statuses = ["pending", "errored", "skipped", "finished"]; - statuses.forEach(status => { - axios - .get("uploads/", { - params: { - import_reference: self.importReference, - import_status: status, - page_size: 1 - } - }) - .then(response => { - self.uploads[status] = response.data.count; - }); - }); - }, - handleImportEvent(event) { - let self = this; - if (event.upload.import_reference != self.importReference) { - return; - } - this.$nextTick(() => { - self.uploads[event.old_status] -= 1; - self.uploads[event.new_status] += 1; - self.uploads.objects[event.upload.uuid] = event.upload; - self.needsRefresh = true - }); - }, - retry (files) { - files.forEach((file) => { - this.$refs.upload.update(file, {error: '', progress: '0.00'}) - }) - this.$refs.upload.active = true; - } }, computed: { supportedExtensions () { return this.$store.state.ui.supportedExtensions }, - labels() { - let denied = this.$pgettext('Content/Library/Help text', - "Upload denied, ensure the file is not too big and that you have not reached your quota" - ); - let server = this.$pgettext('Content/Library/Help text', - "Cannot upload this file, ensure it is not too big" - ); - let network = this.$pgettext('Content/Library/Help text', - "A network error occurred while uploading this file" - ); - let timeout = this.$pgettext('Content/Library/Help text', "Upload timeout, please try again"); - let extension = this.$pgettext('Content/Library/Help text', - "Invalid file type, ensure you are uploading an audio file. Supported file extensions are %{ extensions }" - ); + labels () { + const denied = this.$pgettext('Content/Library/Help text', + 'Upload denied, ensure the file is not too big and that you have not reached your quota' + ) + const server = this.$pgettext('Content/Library/Help text', + 'Cannot upload this file, ensure it is not too big' + ) + const network = this.$pgettext('Content/Library/Help text', + 'A network error occurred while uploading this file' + ) + const timeout = this.$pgettext('Content/Library/Help text', 'Upload timeout, please try again') + const extension = this.$pgettext('Content/Library/Help text', + 'Invalid file type, ensure you are uploading an audio file. Supported file extensions are %{ extensions }' + ) return { tooltips: { denied, server, network, timeout, - retry: this.$pgettext('*/*/*/Verb', "Retry"), + retry: this.$pgettext('*/*/*/Verb', 'Retry'), extension: this.$gettextInterpolate(extension, { - extensions: this.supportedExtensions.join(", ") + extensions: this.supportedExtensions.join(', ') }) } - }; + } }, - uploadedFilesCount() { + uploadedFilesCount () { return this.files.filter(f => { - return f.success; - }).length; + return f.success + }).length }, - uploadingFilesCount() { + uploadingFilesCount () { return this.files.filter(f => { - return !f.success && !f.error; - }).length; + return !f.success && !f.error + }).length }, - erroredFilesCount() { + erroredFilesCount () { return this.files.filter(f => { - return f.error; - }).length; + return f.error + }).length }, retryableFiles () { return this.files.filter(f => { - return f.error; - }); + return f.error + }) }, - processableFiles() { + processableFiles () { return ( this.uploads.pending + this.uploads.skipped + this.uploads.errored + this.uploads.finished + this.uploadedFilesCount - ); + ) }, - processedFilesCount() { + processedFilesCount () { return ( this.uploads.skipped + this.uploads.errored + this.uploads.finished - ); + ) }, - uploadData: function() { + uploadData: function () { return { library: this.library.uuid, import_reference: this.importReference - }; + } }, - sortedFiles() { + sortedFiles () { // return errored files on top return _.sortBy(this.files.map(f => { @@ -447,12 +462,12 @@ export default { } }, watch: { - importReference: _.debounce(function() { - this.$router.replace({ query: { import: this.importReference } }); + importReference: _.debounce(function () { + this.$router.replace({ query: { import: this.importReference } }) }, 500), remainingSpace (newValue) { if (newValue <= 0) { - this.$refs.upload.active = false; + this.$refs.upload.active = false } }, 'uploads.finished' (v, o) { @@ -460,9 +475,130 @@ export default { this.$emit('uploads-finished', v - o) } }, - "fsPath" () { + 'fsPath' () { + this.fetchFs(true) + } + }, + created () { + this.fetchStatus() + if (this.$store.state.auth.availablePermissions.library) { this.fetchFs(true) + this.fsInterval = setInterval(() => { + this.fetchFs(false) + }, 5000) + } + this.fetchQuota() + this.$store.commit('ui/addWebsocketEventHandler', { + eventName: 'import.status_updated', + id: 'fileUpload', + handler: this.handleImportEvent + }) + window.onbeforeunload = e => this.onBeforeUnload(e) + }, + destroyed () { + this.$store.commit('ui/removeWebsocketEventHandler', { + eventName: 'import.status_updated', + id: 'fileUpload' + }) + window.onbeforeunload = null + if (this.fsInterval) { + clearInterval(this.fsInterval) + } + }, + methods: { + onBeforeUnload (e = {}) { + const returnValue = ('This page is asking you to confirm that you want to leave - data you have entered may not be saved.') + if (!this.hasActiveUploads) return null + Object.assign(e, { + returnValue + }) + return returnValue + }, + fetchQuota () { + const self = this + self.isLoadingQuota = true + axios.get('users/me/').then((response) => { + self.quotaStatus = response.data.quota_status + self.isLoadingQuota = false + }) + }, + fetchFs (updateLoading) { + const self = this + if (updateLoading) { + self.isLoadingFs = true + } + axios.get('libraries/fs-import', { params: { path: this.fsPath.join('/') } }).then((response) => { + self.fsStatus = response.data + if (updateLoading) { + self.isLoadingFs = false + } + }) + }, + importFs () { + const self = this + self.isLoadingFs = true + const payload = { + path: this.fsPath.join('/'), + library: this.library.uuid, + import_reference: this.importReference + } + axios.post('libraries/fs-import', payload).then((response) => { + self.fsStatus = response.data + self.isLoadingFs = false + }, error => { + self.isLoadingFs = false + self.fsErrors = error.backendErrors + }) + }, + async cancelFsScan () { + await axios.delete('libraries/fs-import') + this.fetchFs() + }, + inputFile (newFile, oldFile) { + if (!newFile) { + return + } + if (this.remainingSpace < newFile.size / (1000 * 1000)) { + newFile.error = 'denied' + } else { + this.$refs.upload.active = true + } + }, + fetchStatus () { + const self = this + const statuses = ['pending', 'errored', 'skipped', 'finished'] + statuses.forEach(status => { + axios + .get('uploads/', { + params: { + import_reference: self.importReference, + import_status: status, + page_size: 1 + } + }) + .then(response => { + self.uploads[status] = response.data.count + }) + }) + }, + handleImportEvent (event) { + const self = this + if (event.upload.import_reference !== self.importReference) { + return + } + this.$nextTick(() => { + self.uploads[event.old_status] -= 1 + self.uploads[event.new_status] += 1 + self.uploads.objects[event.upload.uuid] = event.upload + self.needsRefresh = true + }) + }, + retry (files) { + files.forEach((file) => { + this.$refs.upload.update(file, { error: '', progress: '0.00' }) + }) + this.$refs.upload.active = true } } -}; +} </script> diff --git a/front/src/components/library/FileUploadWidget.vue b/front/src/components/library/FileUploadWidget.vue index c91916c03795b774ef26233d41d982e178af9727..3f5077257cc0596b67443a643d1044e2b60d5b43 100644 --- a/front/src/components/library/FileUploadWidget.vue +++ b/front/src/components/library/FileUploadWidget.vue @@ -1,25 +1,25 @@ <script> import FileUpload from 'vue-upload-component' -import {setCsrf} from '@/utils' +import { setCsrf } from '@/utils' export default { extends: FileUpload, methods: { uploadHtml5 (file) { - let form = new window.FormData() - let filename = file.file.filename || file.name + const form = new window.FormData() + const filename = file.file.filename || file.name let value - let data = {...file.data} + const data = { ...file.data } if (data.import_metadata) { - data.import_metadata = {...(data.import_metadata || {})} + data.import_metadata = { ...(data.import_metadata || {}) } if (data.channel && !data.import_metadata.title) { - data.import_metadata.title = filename.replace(/\.[^/.]+$/, "") + data.import_metadata.title = filename.replace(/\.[^/.]+$/, '') } data.import_metadata = JSON.stringify(data.import_metadata) } - for (let key in data) { + for (const key in data) { value = data[key] - if (value && typeof value === 'object' && typeof value.toString !== 'function') { + if (value && typeof value === 'object' && typeof value.toString !== 'function') { if (value instanceof File) { form.append(key, value, value.name) } else { @@ -31,7 +31,7 @@ export default { } form.append('source', `upload://${filename}`) form.append(this.name, file.file, filename) - let xhr = new XMLHttpRequest() + const xhr = new XMLHttpRequest() xhr.open('POST', file.postAction) setCsrf(xhr) if (this.$store.state.auth.oauth.accessToken) { diff --git a/front/src/components/library/FsBrowser.vue b/front/src/components/library/FsBrowser.vue index 6140475b7b4c03cbb7d937455f54ff0c96735d10..fced6bef6dd8e7a3f8dffa2a371ece80755d9661 100644 --- a/front/src/components/library/FsBrowser.vue +++ b/front/src/components/library/FsBrowser.vue @@ -1,9 +1,18 @@ <template> <div :class="['ui', {loading}, 'segment']"> <div class="ui fluid action input"> - <input class="ui disabled" disabled :value="data.root + '/' + value.join('/')" /> - <button class="ui button" @click.prevent="$emit('import')"> - <translate translate-context="Content/Library/Button/Verb">Import</translate> + <input + class="ui disabled" + disabled + :value="data.root + '/' + value.join('/')" + > + <button + class="ui button" + @click.prevent="$emit('import')" + > + <translate translate-context="Content/Library/Button/Verb"> + Import + </translate> </button> </div> <div class="ui list component-fs-browser"> @@ -12,20 +21,27 @@ class="item" href="" @click.prevent="handleClick({name: '..', dir: true})" - > - <i class="folder icon"></i> + > + <i class="folder icon" /> <div class="content"> <div class="header">..</div> </div> </a> <a + v-for="e in data.content" + :key="e.name" class="item" href="" @click.prevent="handleClick(e)" - v-for="e in data.content" - :key="e.name"> - <i class="folder icon" v-if="e.dir"></i> - <i class="file icon" v-else></i> + > + <i + v-if="e.dir" + class="folder icon" + /> + <i + v-else + class="file icon" + /> <div class="content"> <div class="header">{{ e.name }}</div> </div> @@ -35,14 +51,18 @@ </template> <script> export default { - props: ["data", "loading", "value"], + props: { + data: { type: Object, required: true }, + loading: { type: Boolean, required: true }, + value: { type: String, required: true } + }, methods: { handleClick (element) { if (!element.dir) { return } - if (element.name === "..") { - let newValue = [...this.value] + if (element.name === '..') { + const newValue = [...this.value] newValue.pop() this.$emit('input', newValue) } else { @@ -51,4 +71,4 @@ export default { } } } -</script> \ No newline at end of file +</script> diff --git a/front/src/components/library/FsLogs.vue b/front/src/components/library/FsLogs.vue index 8c26ca49e86dc52f25896eb555081ea0aa4a627a..5180767087a379fac2045d427006e141bb1d9f3d 100644 --- a/front/src/components/library/FsLogs.vue +++ b/front/src/components/library/FsLogs.vue @@ -1,17 +1,27 @@ <template> <div class="ui segment component-fs-logs"> - <div class="ui active dimmer" v-if="data.status === 'pending'"> + <div + v-if="data.status === 'pending'" + class="ui active dimmer" + > <div class="ui text loader"> - <translate translate-context="Content/Library/Paragraph">Import hasn't started yet</translate> + <translate translate-context="Content/Library/Paragraph"> + Import hasn't started yet + </translate> </div> </div> - <template v-else v-for="(row, idx) in data.logs"> - <p :key="idx">{{ row }}</p> + <template + v-for="(row, idx) in data.logs" + v-else + > + <p :key="idx"> + {{ row }} + </p> </template> </div> </template> <script> export default { - props: ["data"], + props: { data: { type: Object, required: true } } } -</script> \ No newline at end of file +</script> diff --git a/front/src/components/library/Home.vue b/front/src/components/library/Home.vue index e591d6ad4e481f43ac8fd4cdc4b83124d1770325..e70eaebb8236f870930238e4469db0bfc9b1be09 100644 --- a/front/src/components/library/Home.vue +++ b/front/src/components/library/Home.vue @@ -1,97 +1,125 @@ <template> - <main v-title="labels.title" :key="$router.currentRoute.name"> + <main + :key="$router.currentRoute.name" + v-title="labels.title" + > <section class="ui vertical stripe segment"> <div class="ui stackable three column grid"> <div class="column"> - <track-widget :url="'history/listenings/'" :filters="{scope: scope, ordering: '-creation_date'}"> - <template slot="title"><translate translate-context="Content/Home/Title">Recently listened</translate></template> + <track-widget + :url="'history/listenings/'" + :filters="{scope: scope, ordering: '-creation_date'}" + > + <template slot="title"> + <translate translate-context="Content/Home/Title"> + Recently listened + </translate> + </template> </track-widget> </div> <div class="column"> - <track-widget :url="'favorites/tracks/'" :filters="{scope: scope, ordering: '-creation_date'}"> - <template slot="title"><translate translate-context="Content/Home/Title">Recently favorited</translate></template> + <track-widget + :url="'favorites/tracks/'" + :filters="{scope: scope, ordering: '-creation_date'}" + > + <template slot="title"> + <translate translate-context="Content/Home/Title"> + Recently favorited + </translate> + </template> </track-widget> </div> <div class="column"> - <playlist-widget :url="'playlists/'" :filters="{scope: scope, playable: true, ordering: '-modification_date'}"> - <template slot="title"><translate translate-context="*/*/*">Playlists</translate></template> + <playlist-widget + :url="'playlists/'" + :filters="{scope: scope, playable: true, ordering: '-modification_date'}" + > + <template slot="title"> + <translate translate-context="*/*/*"> + Playlists + </translate> + </template> </playlist-widget> </div> </div> - <div class="ui section hidden divider"></div> + <div class="ui section hidden divider" /> <div class="ui stackable one column grid"> <div class="column"> <album-widget :filters="{scope: scope, playable: true, ordering: '-creation_date'}"> - <template slot="title"><translate translate-context="Content/Home/Title">Recently added</translate></template> + <template slot="title"> + <translate translate-context="Content/Home/Title"> + Recently added + </translate> + </template> </album-widget> </div> </div> <template v-if="scope === 'all'"> - <h3 class="ui header" > - <translate translate-context="*/*/*">New channels</translate> + <h3 class="ui header"> + <translate translate-context="*/*/*"> + New channels + </translate> </h3> - <channels-widget :show-modification-date="true" :limit="12" :filters="{ordering: '-creation_date', external: 'false'}"></channels-widget> + <channels-widget + :show-modification-date="true" + :limit="12" + :filters="{ordering: '-creation_date', external: 'false'}" + /> </template> - - </section> </main> </template> <script> -import axios from "axios" -import Search from "@/components/audio/Search" -import logger from "@/logging" -import ChannelsWidget from "@/components/audio/ChannelsWidget" -import ArtistCard from "@/components/audio/artist/Card" -import TrackWidget from "@/components/audio/track/Widget" -import AlbumWidget from "@/components/audio/album/Widget" -import PlaylistWidget from "@/components/playlists/Widget" +import axios from 'axios' +import logger from '@/logging' +import ChannelsWidget from '@/components/audio/ChannelsWidget' +import TrackWidget from '@/components/audio/track/Widget' +import AlbumWidget from '@/components/audio/album/Widget' +import PlaylistWidget from '@/components/playlists/Widget' -const ARTISTS_URL = "artists/" +const ARTISTS_URL = 'artists/' export default { - name: "library", - props: { - scope: {default: 'all'} - }, + name: 'Library', components: { - Search, - ArtistCard, TrackWidget, AlbumWidget, PlaylistWidget, - ChannelsWidget, + ChannelsWidget + }, + props: { + scope: { type: String, default: 'all' } }, - data() { + data () { return { artists: [], - isLoadingArtists: false, + isLoadingArtists: false } }, - created() { - this.fetchArtists() - }, computed: { - labels() { + labels () { return { - title: this.$pgettext('Head/Home/Title', "Library") + title: this.$pgettext('Head/Home/Title', 'Library') } } }, + created () { + this.fetchArtists() + }, methods: { - fetchArtists() { - var self = this + fetchArtists () { + const self = this this.isLoadingArtists = true - let params = { - ordering: "-creation_date", + const params = { + ordering: '-creation_date', playable: true } - let url = ARTISTS_URL - logger.default.time("Loading latest artists") + const url = ARTISTS_URL + logger.default.time('Loading latest artists') axios.get(url, { params: params }).then(response => { self.artists = response.data.results - logger.default.timeEnd("Loading latest artists") + logger.default.timeEnd('Loading latest artists') self.isLoadingArtists = false }) } diff --git a/front/src/components/library/ImportStatusModal.vue b/front/src/components/library/ImportStatusModal.vue index d228ef6c0c3b5cf8173599728e252bbead2adb61..b20531f211fad0dfa1a74ba8746afaed8b18e04e 100644 --- a/front/src/components/library/ImportStatusModal.vue +++ b/front/src/components/library/ImportStatusModal.vue @@ -1,29 +1,56 @@ <template> - <modal :show.sync="showModal"> <h4 class="header"> - <translate translate-context="Popup/Import/Title">Import detail</translate> + <translate translate-context="Popup/Import/Title"> + Import detail + </translate> </h4> - <div class="content" v-if="upload"> + <div + v-if="upload" + class="content" + > <div class="description"> - <div class="ui message" v-if="upload.import_status === 'pending'"> - <translate translate-context="Popup/Import/Message">Upload is still pending and will soon be processed by the server.</translate> + <div + v-if="upload.import_status === 'pending'" + class="ui message" + > + <translate translate-context="Popup/Import/Message"> + Upload is still pending and will soon be processed by the server. + </translate> </div> - <div class="ui success message" v-if="upload.import_status === 'finished'"> - <translate translate-context="Popup/Import/Message">Upload was successfully processed by the server.</translate> + <div + v-if="upload.import_status === 'finished'" + class="ui success message" + > + <translate translate-context="Popup/Import/Message"> + Upload was successfully processed by the server. + </translate> </div> - <div role="alert" class="ui warning message" v-if="upload.import_status === 'skipped'"> - <translate translate-context="Popup/Import/Message">Upload was skipped because a similar one is already available in one of your libraries.</translate> + <div + v-if="upload.import_status === 'skipped'" + role="alert" + class="ui warning message" + > + <translate translate-context="Popup/Import/Message"> + Upload was skipped because a similar one is already available in one of your libraries. + </translate> </div> - <div class="ui error message" v-if="upload.import_status === 'errored'"> - <translate translate-context="Popup/Import/Message">An error occurred during upload processing. You will find more information below.</translate> + <div + v-if="upload.import_status === 'errored'" + class="ui error message" + > + <translate translate-context="Popup/Import/Message"> + An error occurred during upload processing. You will find more information below. + </translate> </div> <template v-if="upload.import_status === 'errored'"> <table class="ui very basic collapsing celled table"> <tbody> <tr> <td> - <translate translate-context="Popup/Import/Table.Label/Noun">Error type</translate> + <translate translate-context="Popup/Import/Table.Label/Noun"> + Error type + </translate> </td> <td> {{ getErrorData(upload).label }} @@ -31,30 +58,43 @@ </tr> <tr> <td> - <translate translate-context="Popup/Import/Table.Label/Noun">Error detail</translate> + <translate translate-context="Popup/Import/Table.Label/Noun"> + Error detail + </translate> </td> <td> {{ getErrorData(upload).detail }} <ul v-if="getErrorData(upload).errorRows.length > 0"> - <li v-for="row in getErrorData(upload).errorRows"> - {{ row.key}}: {{ row.value}} + <li + v-for="row in getErrorData(upload).errorRows" + :key="row.key" + > + {{ row.key }}: {{ row.value }} </li> </ul> </td> </tr> <tr> <td> - <translate translate-context="Footer/*/Link">Getting help</translate> + <translate translate-context="Footer/*/Link"> + Getting help + </translate> </td> <td> <ul> <li> - <a :href="getErrorData(upload).documentationUrl" target="_blank"> + <a + :href="getErrorData(upload).documentationUrl" + target="_blank" + > <translate translate-context="Popup/Import/Table.Label/Value">Read our documentation for this error</translate> </a> </li> <li> - <a :href="getErrorData(upload).supportUrl" target="_blank"> + <a + :href="getErrorData(upload).supportUrl" + target="_blank" + > <translate translate-context="Popup/Import/Table.Label/Value">Open a support thread (include the debug information below in your message)</translate> </a> </li> @@ -63,11 +103,17 @@ </tr> <tr> <td> - <translate translate-context="Popup/Import/Table.Label/Noun">Debug information</translate> + <translate translate-context="Popup/Import/Table.Label/Noun"> + Debug information + </translate> </td> <td> <div class="ui form"> - <textarea class="ui textarea" rows="10" :value="getErrorData(upload).debugInfo"></textarea> + <textarea + class="ui textarea" + rows="10" + :value="getErrorData(upload).debugInfo" + /> </div> </td> </tr> @@ -78,7 +124,9 @@ </div> <div class="actions"> <button class="ui deny button"> - <translate translate-context="*/*/Button.Label/Verb">Close</translate> + <translate translate-context="*/*/Button.Label/Verb"> + Close + </translate> </button> </div> </modal> @@ -86,11 +134,11 @@ <script> import Modal from '@/components/semantic/Modal' -function getErrors(payload) { - let errors = [] - for (var k in payload) { - if (payload.hasOwnProperty(k)) { - let value = payload[k] +function getErrors (payload) { + const errors = [] + for (const k in payload) { + if (Object.prototype.hasOwnProperty.call(payload, k)) { + const value = payload[k] if (Array.isArray(value)) { errors.push({ key: k, @@ -113,19 +161,30 @@ function getErrors(payload) { } export default { - props: ['upload', "show"], components: { Modal }, + props: { + upload: { type: Object, required: true }, + show: { type: Boolean } + }, data () { return { showModal: this.show } }, + watch: { + showModal (v) { + this.$emit('update:show', v) + }, + show (v) { + this.showModal = v + } + }, methods: { getErrorData (upload) { - let payload = upload.import_details || {} - let d = { + const payload = upload.import_details || {} + const d = { supportUrl: 'https://forum.funkwhale.audio/t/support', errorRows: [] } @@ -138,27 +197,19 @@ export default { if (d.errorCode === 'invalid_metadata') { d.label = this.$pgettext('Popup/Import/Error.Label', 'Invalid metadata') d.detail = this.$pgettext('Popup/Import/Error.Label', 'The metadata included in the file is invalid or some mandatory fields are missing.') - let detail = payload.detail || {} + const detail = payload.detail || {} d.errorRows = getErrors(detail) } else { d.label = this.$pgettext('*/*/Error', 'Unknown error') d.detail = this.$pgettext('Popup/Import/Error.Label', 'An unknown error occurred') } - let debugInfo = { + const debugInfo = { source: upload.source, - ...payload, + ...payload } d.debugInfo = JSON.stringify(debugInfo, null, 4) return d } - }, - watch: { - showModal (v) { - this.$emit('update:show', v) - }, - show (v) { - this.showModal = v - } } } </script> diff --git a/front/src/components/library/Library.vue b/front/src/components/library/Library.vue index 73d31071539c2a1d404fd9250864ec3cd264a05f..784cc5aba466ec64eb7541c10040ee1a5581ea53 100644 --- a/front/src/components/library/Library.vue +++ b/front/src/components/library/Library.vue @@ -1,21 +1,21 @@ <template> <div class="main pusher page-library"> - <router-view :key="$router.currentRoute.fullPath"></router-view> + <router-view :key="$router.currentRoute.fullPath" /> </div> </template> <script> export default { computed: { - showImports() { + showImports () { return ( - this.$store.state.auth.availablePermissions["upload"] || - this.$store.state.auth.availablePermissions["library"] + this.$store.state.auth.availablePermissions.upload || + this.$store.state.auth.availablePermissions.library ) }, - labels() { + labels () { return { - secondaryMenu: this.$pgettext('Menu/*/Hidden text', "Secondary menu") + secondaryMenu: this.$pgettext('Menu/*/Hidden text', 'Secondary menu') } } } diff --git a/front/src/components/library/Podcasts.vue b/front/src/components/library/Podcasts.vue index 75607ab08bfae2c5b37fbee702918056df26c858..b003731be4a28a0a62939406cf9032536e812f31 100644 --- a/front/src/components/library/Podcasts.vue +++ b/front/src/components/library/Podcasts.vue @@ -2,60 +2,119 @@ <main v-title="labels.title"> <section class="ui vertical stripe segment"> <h2 class="ui header"> - <translate translate-context="Content/Podcasts/Title">Browsing podcasts</translate> + <translate translate-context="Content/Podcasts/Title"> + Browsing podcasts + </translate> </h2> - <form :class="['ui', {'loading': isLoading}, 'form']" @submit.prevent="updatePage();updateQueryString();fetchData()"> + <form + :class="['ui', {'loading': isLoading}, 'form']" + @submit.prevent="updatePage();updateQueryString();fetchData()" + > <div class="fields"> <div class="field"> <label for="artist-search"> <translate translate-context="Content/Search/Input.Label/Noun">Podcast title</translate> </label> <div class="ui action input"> - <input id="artist-search" type="text" name="search" v-model="query" :placeholder="labels.searchPlaceholder"/> - <button class="ui icon button" type="submit" :aria-label="$pgettext('Content/Search/Input.Label/Noun', 'Search')"> - <i class="search icon"></i> + <input + id="artist-search" + v-model="query" + type="text" + name="search" + :placeholder="labels.searchPlaceholder" + > + <button + class="ui icon button" + type="submit" + :aria-label="$pgettext('Content/Search/Input.Label/Noun', 'Search')" + > + <i class="search icon" /> </button> </div> </div> <div class="field"> <label for="tags-search"><translate translate-context="*/*/*/Noun">Tags</translate></label> - <tags-selector v-model="tags"></tags-selector> + <tags-selector v-model="tags" /> </div> <div class="field"> <label for="artist-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> - <select id="artist-ordering" class="ui dropdown" v-model="ordering"> - <option v-for="option in orderingOptions" :value="option[0]"> + <select + id="artist-ordering" + v-model="ordering" + class="ui dropdown" + > + <option + v-for="(option, key) in orderingOptions" + :key="key" + :value="option[0]" + > {{ sharedLabels.filters[option[1]] }} </option> </select> </div> <div class="field"> <label for="artist-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label> - <select id="artist-ordering-direction" class="ui dropdown" v-model="orderingDirection"> - <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option> - <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option> + <select + id="artist-ordering-direction" + v-model="orderingDirection" + class="ui dropdown" + > + <option value="+"> + <translate translate-context="Content/Search/Dropdown"> + Ascending + </translate> + </option> + <option value="-"> + <translate translate-context="Content/Search/Dropdown"> + Descending + </translate> + </option> </select> </div> <div class="field"> <label for="artist-results"><translate translate-context="Content/Search/Dropdown.Label/Noun">Results per page</translate></label> - <select id="artist-results" class="ui dropdown" v-model="paginateBy"> - <option :value="parseInt(12)">12</option> - <option :value="parseInt(30)">30</option> - <option :value="parseInt(50)">50</option> + <select + id="artist-results" + v-model="paginateBy" + class="ui dropdown" + > + <option :value="parseInt(12)"> + 12 + </option> + <option :value="parseInt(30)"> + 30 + </option> + <option :value="parseInt(50)"> + 50 + </option> </select> </div> </div> </form> - <div class="ui hidden divider"></div> - <div v-if="result && result.results.length > 0" class="ui five app-cards cards"> - <div v-if="isLoading" class="ui inverted active dimmer"> - <div class="ui loader"></div> + <div class="ui hidden divider" /> + <div + v-if="result && result.results.length > 0" + class="ui five app-cards cards" + > + <div + v-if="isLoading" + class="ui inverted active dimmer" + > + <div class="ui loader" /> </div> - <artist-card :artist="artist" v-for="artist in result.results" :key="artist.id"></artist-card> + <artist-card + v-for="artist in result.results" + :key="artist.id" + :artist="artist" + /> </div> - <div v-else-if="!isLoading" class="ui placeholder segment sixteen wide column" style="text-align: center; display: flex; align-items: center"> + <div + v-else-if="!isLoading" + class="ui placeholder segment sixteen wide column" + style="text-align: center; display: flex; align-items: center" + > <div class="ui icon header"> - <i class="podcast icon"></i> + <i class="podcast icon" /> <translate translate-context="Content/Artists/Placeholder"> No results matching your query </translate> @@ -63,184 +122,204 @@ <router-link v-if="$store.state.auth.authenticated" :to="{name: 'content.index'}" - class="ui success button labeled icon"> - <i class="upload icon"></i> + class="ui success button labeled icon" + > + <i class="upload icon" /> <translate translate-context="Content/*/Verb"> Create a Channel </translate> </router-link> - <h1 v-if ="$store.state.auth.authenticated" class="ui with-actions header"> - <div class="actions"> - <a @click.stop.prevent="showSubscribeModal = true"> - <i class="plus icon"></i> - <translate translate-context="Content/Profile/Button">Subscribe to feed</translate> - </a> - </div> - </h1> + <h1 + v-if="$store.state.auth.authenticated" + class="ui with-actions header" + > + <div class="actions"> + <a @click.stop.prevent="showSubscribeModal = true"> + <i class="plus icon" /> + <translate translate-context="Content/Profile/Button">Subscribe to feed</translate> + </a> + </div> + </h1> </div> <div class="ui center aligned basic segment"> <pagination v-if="result && result.count > paginateBy" - @page-changed="selectPage" :current="page" :paginate-by="paginateBy" :total="result.count" - ></pagination> + @page-changed="selectPage" + /> </div> </section> - <modal class="tiny" :show.sync="showSubscribeModal" :fullscreen="false"> - <h2 class="header"> - <translate translate-context="*/*/*/Noun">Subscription</translate> - </h2> - <div class="scrolling content" ref="modalContent"> - <remote-search-form - type="both" - :show-submit="false" - :standalone="false" - @subscribed="showSubscribeModal = false; fetchData()" - :redirect="true"></remote-search-form> - </div> - <div class="actions"> - <button class="ui basic deny button"> - <translate translate-context="*/*/Button.Label/Verb">Cancel</translate> - </button> - <button form="remote-search" type="submit" class="ui primary button"> - <i class="bookmark icon"></i> - <translate translate-context="*/*/*/Verb">Subscribe</translate> - </button> - </div> - </modal> - + <modal + class="tiny" + :show.sync="showSubscribeModal" + :fullscreen="false" + > + <h2 class="header"> + <translate translate-context="*/*/*/Noun"> + Subscription + </translate> + </h2> + <div + ref="modalContent" + class="scrolling content" + > + <remote-search-form + type="both" + :show-submit="false" + :standalone="false" + :redirect="true" + @subscribed="showSubscribeModal = false; fetchData()" + /> + </div> + <div class="actions"> + <button class="ui basic deny button"> + <translate translate-context="*/*/Button.Label/Verb"> + Cancel + </translate> + </button> + <button + form="remote-search" + type="submit" + class="ui primary button" + > + <i class="bookmark icon" /> + <translate translate-context="*/*/*/Verb"> + Subscribe + </translate> + </button> + </div> + </modal> </main> </template> <script> import qs from 'qs' -import axios from "axios" -import _ from "@/lodash" -import $ from "jquery" +import axios from 'axios' +import $ from 'jquery' -import logger from "@/logging" +import logger from '@/logging' -import OrderingMixin from "@/components/mixins/Ordering" -import PaginationMixin from "@/components/mixins/Pagination" -import TranslationsMixin from "@/components/mixins/Translations" -import ArtistCard from "@/components/audio/artist/Card" -import Pagination from "@/components/Pagination" +import OrderingMixin from '@/components/mixins/Ordering' +import PaginationMixin from '@/components/mixins/Pagination' +import TranslationsMixin from '@/components/mixins/Translations' +import ArtistCard from '@/components/audio/artist/Card' +import Pagination from '@/components/Pagination' import TagsSelector from '@/components/library/TagsSelector' import Modal from '@/components/semantic/Modal' -import RemoteSearchForm from "@/components/RemoteSearchForm" +import RemoteSearchForm from '@/components/RemoteSearchForm' -const FETCH_URL = "artists/" +const FETCH_URL = 'artists/' export default { - mixins: [OrderingMixin, PaginationMixin, TranslationsMixin], - props: { - defaultQuery: { type: String, required: false, default: "" }, - defaultTags: { type: Array, required: false, default: () => { return [] } }, - scope: { type: String, required: false, default: "all" }, - }, components: { ArtistCard, Pagination, TagsSelector, RemoteSearchForm, - Modal, + Modal + }, + 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() { + data () { return { isLoading: true, result: null, page: parseInt(this.defaultPage), query: this.defaultQuery, tags: (this.defaultTags || []).filter((t) => { return t.length > 0 }), - orderingOptions: [["creation_date", "creation_date"], ["name", "name"]], - showSubscribeModal: false, + orderingOptions: [['creation_date', 'creation_date'], ['name', 'name']], + showSubscribeModal: false } }, - created() { - this.fetchData() - }, - mounted() { - $(".ui.dropdown").dropdown() - }, computed: { - labels() { - let searchPlaceholder = this.$pgettext('Content/Search/Input.Placeholder', "Search…") - let title = this.$pgettext('*/*/*/Noun', "Podcasts") + labels () { + const searchPlaceholder = this.$pgettext('Content/Search/Input.Placeholder', 'Search…') + const title = this.$pgettext('*/*/*/Noun', 'Podcasts') return { searchPlaceholder, title } } }, + watch: { + page () { + this.updateQueryString() + this.fetchData() + }, + '$store.state.moderation.lastUpdate': function () { + this.fetchData() + }, + excludeCompilation () { + this.fetchData() + } + }, + created () { + this.fetchData() + }, + mounted () { + $('.ui.dropdown').dropdown() + }, methods: { - updateQueryString: function() { + updateQueryString: function () { history.pushState( {}, null, this.$route.path + '?' + new URLSearchParams( { - query: this.query, - page: this.page, - tag: this.tags, - paginateBy: this.paginateBy, - ordering: this.getOrderingAsString(), - include_channels: true, - content_category: 'podcast', - }).toString() + query: this.query, + page: this.page, + tag: this.tags, + paginateBy: this.paginateBy, + ordering: this.getOrderingAsString(), + include_channels: true, + content_category: 'podcast' + }).toString() ) }, - fetchData: function() { - var self = this + fetchData: function () { + const self = this this.isLoading = true - let url = FETCH_URL - let params = { + const url = FETCH_URL + const params = { scope: this.scope, page: this.page, page_size: this.paginateBy, has_albums: this.excludeCompilation, q: this.query, ordering: this.getOrderingAsString(), - playable: "true", + playable: 'true', tag: this.tags, - include_channels: "true", - content_category: 'podcast', + include_channels: 'true', + content_category: 'podcast' } - logger.default.debug("Fetching artists") + logger.default.debug('Fetching artists') axios.get( url, { params: params, - paramsSerializer: function(params) { + paramsSerializer: function (params) { return qs.stringify(params, { indices: false }) } } ).then(response => { self.result = response.data self.isLoading = false - }, error => { + }, () => { self.result = null self.isLoading = false }) }, - selectPage: function(page) { + selectPage: function (page) { this.page = page }, - updatePage() { + updatePage () { this.page = this.defaultPage - }, - }, - watch: { - page() { - this.updateQueryString() - this.fetchData() - }, - "$store.state.moderation.lastUpdate": function () { - this.fetchData() - }, - excludeCompilation() { - this.fetchData() } } } diff --git a/front/src/components/library/Radios.vue b/front/src/components/library/Radios.vue index 8f2b551ae2b6059c74d79561dae57d102fd792d3..feb946b9900ea66482cd6fcde39b664563860641 100644 --- a/front/src/components/library/Radios.vue +++ b/front/src/components/library/Radios.vue @@ -2,83 +2,148 @@ <main v-title="labels.title"> <section class="ui vertical stripe segment"> <h2 class="ui header"> - <translate translate-context="Content/Radio/Title">Browsing radios</translate> + <translate translate-context="Content/Radio/Title"> + Browsing radios + </translate> </h2> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <div class="ui row"> <h3 class="ui header"> - <translate translate-context="Content/Radio/Title">Instance radios</translate> + <translate translate-context="Content/Radio/Title"> + Instance radios + </translate> </h3> <div class="ui cards"> - <radio-card v-if="isAuthenticated" :type="'actor-content'" :object-id="$store.state.auth.fullUsername"></radio-card> - <radio-card v-if="isAuthenticated && hasFavorites" :type="'favorites'"></radio-card> - <radio-card :type="'random'"></radio-card> - <radio-card :type="'recently-added'"></radio-card> - <radio-card v-if="$store.state.auth.authenticated" :type="'less-listened'"></radio-card> + <radio-card + v-if="isAuthenticated" + :type="'actor-content'" + :object-id="$store.state.auth.fullUsername" + /> + <radio-card + v-if="isAuthenticated && hasFavorites" + :type="'favorites'" + /> + <radio-card :type="'random'" /> + <radio-card :type="'recently-added'" /> + <radio-card + v-if="$store.state.auth.authenticated" + :type="'less-listened'" + /> </div> </div> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <h3 class="ui header"> - <translate translate-context="Content/Radio/Title">User radios</translate> + <translate translate-context="Content/Radio/Title"> + User radios + </translate> </h3> - <router-link class="ui success button" to="/library/radios/build" exact> - <translate translate-context="Content/Radio/Button.Label/Verb">Create your own radio</translate> + <router-link + class="ui success button" + to="/library/radios/build" + exact + > + <translate translate-context="Content/Radio/Button.Label/Verb"> + Create your own radio + </translate> </router-link> - <div class="ui hidden divider"></div> - <form :class="['ui', {'loading': isLoading}, 'form']" @submit.prevent="updateQueryString();fetchData()"> + <div class="ui hidden divider" /> + <form + :class="['ui', {'loading': isLoading}, 'form']" + @submit.prevent="updateQueryString();fetchData()" + > <div class="fields"> <div class="field"> <label for="radios-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label> <div class="ui action input"> - <input id ="radios-search" type="text" name="search" v-model="query" :placeholder="labels.searchPlaceholder"/> - <button class="ui icon button" type="submit" :aria-label="$pgettext('Content/Search/Input.Label/Noun', 'Search')"> - <i class="search icon"></i> + <input + id="radios-search" + v-model="query" + type="text" + name="search" + :placeholder="labels.searchPlaceholder" + > + <button + class="ui icon button" + type="submit" + :aria-label="$pgettext('Content/Search/Input.Label/Noun', 'Search')" + > + <i class="search icon" /> </button> </div> </div> <div class="field"> <label for="radios-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> - <select id="radios-ordering" class="ui dropdown" v-model="ordering"> - <option v-for="option in orderingOptions" :value="option[0]"> + <select + id="radios-ordering" + v-model="ordering" + class="ui dropdown" + > + <option + v-for="(option, key) in orderingOptions" + :key="key" + :value="option[0]" + > {{ sharedLabels.filters[option[1]] }} </option> </select> </div> <div class="field"> <label for="radios-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Order</translate></label> - <select id="radios-ordering-direction" class="ui dropdown" v-model="orderingDirection"> + <select + id="radios-ordering-direction" + v-model="orderingDirection" + class="ui dropdown" + > <option value="+"> - <translate translate-context="Content/Search/Dropdown">Ascending</translate> + <translate translate-context="Content/Search/Dropdown"> + Ascending + </translate> </option> <option value="-"> - <translate translate-context="Content/Search/Dropdown">Descending</translate> + <translate translate-context="Content/Search/Dropdown"> + Descending + </translate> </option> </select> </div> <div class="field"> <label for="radios-results"><translate translate-context="Content/Search/Dropdown.Label/Noun">Results per page</translate></label> - <select id="radios-results" class="ui dropdown" v-model="paginateBy"> - <option :value="parseInt(12)">12</option> - <option :value="parseInt(25)">25</option> - <option :value="parseInt(50)">50</option> + <select + id="radios-results" + v-model="paginateBy" + class="ui dropdown" + > + <option :value="parseInt(12)"> + 12 + </option> + <option :value="parseInt(25)"> + 25 + </option> + <option :value="parseInt(50)"> + 50 + </option> </select> </div> </div> </form> - <div class="ui hidden divider"></div> - <div v-if="result && !result.results.length > 0" class="ui placeholder segment"> + <div class="ui hidden divider" /> + <div + v-if="result && !result.results.length > 0" + class="ui placeholder segment" + > <div class="ui icon header"> - <i class="feed icon"></i> + <i class="feed icon" /> <translate translate-context="Content/Radios/Placeholder"> No results matching your query </translate> </div> <router-link - v-if="$store.state.auth.authenticated" - :to="{name: 'library.radios.build'}" - class="ui success button labeled icon"> - <i class="rss icon"></i> + v-if="$store.state.auth.authenticated" + :to="{name: 'library.radios.build'}" + class="ui success button labeled icon" + > + <i class="rss icon" /> <translate translate-context="Content/*/Verb"> Create a radio </translate> @@ -86,70 +151,65 @@ </div> <div v-if="result && result.results.length > 0" - class="ui cards"> + class="ui cards" + > <radio-card - type="custom" v-for="radio in result.results" :key="radio.id" - :custom-radio="radio"></radio-card> + type="custom" + :custom-radio="radio" + /> </div> <div class="ui center aligned basic segment"> <pagination v-if="result && result.count > paginateBy" - @page-changed="selectPage" :current="page" :paginate-by="paginateBy" :total="result.count" - ></pagination> + @page-changed="selectPage" + /> </div> </section> </main> </template> <script> -import axios from "axios" -import _ from "@/lodash" -import $ from "jquery" +import axios from 'axios' +import $ from 'jquery' -import logger from "@/logging" +import logger from '@/logging' -import OrderingMixin from "@/components/mixins/Ordering" -import PaginationMixin from "@/components/mixins/Pagination" -import TranslationsMixin from "@/components/mixins/Translations" -import RadioCard from "@/components/radios/Card" -import Pagination from "@/components/Pagination" +import OrderingMixin from '@/components/mixins/Ordering' +import PaginationMixin from '@/components/mixins/Pagination' +import TranslationsMixin from '@/components/mixins/Translations' +import RadioCard from '@/components/radios/Card' +import Pagination from '@/components/Pagination' -const FETCH_URL = "radios/radios/" +const FETCH_URL = 'radios/radios/' export default { - mixins: [OrderingMixin, PaginationMixin, TranslationsMixin], - props: { - defaultQuery: { type: String, required: false, default: "" }, - scope: { type: String, required: false, default: "all" }, - }, components: { RadioCard, Pagination }, - data() { + mixins: [OrderingMixin, PaginationMixin, TranslationsMixin], + props: { + defaultQuery: { type: String, required: false, default: '' }, + scope: { type: String, required: false, default: 'all' } + }, + data () { return { isLoading: true, result: null, page: parseInt(this.defaultPage), query: this.defaultQuery, - orderingOptions: [["creation_date", "creation_date"], ["name", "name"]] + orderingOptions: [['creation_date', 'creation_date'], ['name', 'name']] } }, - created() { - this.fetchData() - }, - mounted() { - $(".ui.dropdown").dropdown() - }, computed: { - labels() { - let searchPlaceholder = this.$pgettext('Content/Search/Input.Placeholder', "Enter a radio name…") - let title = this.$pgettext('*/*/*', "Radios") + labels () { + const searchPlaceholder = this.$pgettext('Content/Search/Input.Placeholder', 'Enter a radio name…') + const title = this.$pgettext('*/*/*', 'Radios') return { searchPlaceholder, title @@ -160,48 +220,54 @@ export default { }, hasFavorites () { return this.$store.state.favorites.count > 0 - }, + } + }, + watch: { + page () { + this.updateQueryString() + this.fetchData() + } + }, + created () { + this.fetchData() + }, + mounted () { + $('.ui.dropdown').dropdown() }, methods: { - updateQueryString: function() { + updateQueryString: function () { history.pushState( {}, null, this.$route.path + '?' + new URLSearchParams( { - query: this.query, - page: this.page, - paginateBy: this.paginateBy, - ordering: this.getOrderingAsString() - }).toString() + query: this.query, + page: this.page, + paginateBy: this.paginateBy, + ordering: this.getOrderingAsString() + }).toString() ) }, - fetchData: function() { - var self = this + fetchData: function () { + const self = this this.isLoading = true - let url = FETCH_URL - let params = { + const url = FETCH_URL + const params = { scope: this.scope, page: this.page, page_size: this.paginateBy, name__icontains: this.query, - ordering: this.getOrderingAsString(), + ordering: this.getOrderingAsString() } - logger.default.debug("Fetching radios") + logger.default.debug('Fetching radios') axios.get(url, { params: params }).then(response => { self.result = response.data self.isLoading = false }) }, - selectPage: function(page) { + selectPage: function (page) { this.page = page } - }, - watch: { - page() { - this.updateQueryString() - this.fetchData() - }, } } </script> diff --git a/front/src/components/library/TagDetail.vue b/front/src/components/library/TagDetail.vue index 9706153a4d65fed9518fa2face50f56199c2c4ad..2ede588e3d66b44f6eda5bd698c4b304253c8917 100644 --- a/front/src/components/library/TagDetail.vue +++ b/front/src/components/library/TagDetail.vue @@ -6,70 +6,109 @@ {{ labels.title }} </span> </h2> - <radio-button type="tag" :object-id="id"></radio-button> - <router-link class="ui right floated button" v-if="$store.state.auth.availablePermissions['library']" :to="{name: 'manage.library.tags.detail', params: {id: id}}"> - <i class="wrench icon"></i> - <translate translate-context="Content/Moderation/Link">Open in moderation interface</translate> + <radio-button + type="tag" + :object-id="id" + /> + <router-link + v-if="$store.state.auth.availablePermissions['library']" + class="ui right floated button" + :to="{name: 'manage.library.tags.detail', params: {id: id}}" + > + <i class="wrench icon" /> + <translate translate-context="Content/Moderation/Link"> + Open in moderation interface + </translate> </router-link> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <div class="ui row"> - <artist-widget :key="id" :controls="false" :filters="{playable: true, ordering: '-creation_date', tag: id, include_channels: 'false'}"> + <artist-widget + :key="id" + :controls="false" + :filters="{playable: true, ordering: '-creation_date', tag: id, include_channels: 'false'}" + > <template slot="title"> <router-link :to="{name: 'library.artists.browse', query: {tag: id}}"> - <translate translate-context="*/*/*/Noun">Artists</translate> + <translate translate-context="*/*/*/Noun"> + Artists + </translate> </router-link> </template> </artist-widget> - <div class="ui hidden divider"></div> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> + <div class="ui hidden divider" /> <h3 class="ui header"> - <translate translate-context="*/*/*">Channels</translate> + <translate translate-context="*/*/*"> + Channels + </translate> </h3> - <channels-widget :key="id" :show-modification-date="true" :limit="12" :filters="{tag: id, ordering: '-creation_date'}"></channels-widget> - <div class="ui hidden divider"></div> - <div class="ui hidden divider"></div> - <album-widget :key="id" :show-count="true" :controls="false" :filters="{playable: true, ordering: '-creation_date', tag: id}"> + <channels-widget + :key="id" + :show-modification-date="true" + :limit="12" + :filters="{tag: id, ordering: '-creation_date'}" + /> + <div class="ui hidden divider" /> + <div class="ui hidden divider" /> + <album-widget + :key="id" + :show-count="true" + :controls="false" + :filters="{playable: true, ordering: '-creation_date', tag: id}" + > <template slot="title"> <router-link :to="{name: 'library.albums.browse', query: {tag: id}}"> - <translate translate-context="*/*/*">Albums</translate> + <translate translate-context="*/*/*"> + Albums + </translate> </router-link> </template> </album-widget> - <div class="ui hidden divider"></div> - <div class="ui hidden divider"></div> - <track-widget :key="id" :show-count="true" :limit="12" item-classes="track-item inline" :url="'/tracks/'" :is-activity="false" :filters="{playable: true, ordering: '-creation_date', tag: id}"> + <div class="ui hidden divider" /> + <div class="ui hidden divider" /> + <track-widget + :key="id" + :show-count="true" + :limit="12" + item-classes="track-item inline" + :url="'/tracks/'" + :is-activity="false" + :filters="{playable: true, ordering: '-creation_date', tag: id}" + > <template slot="title"> - <translate translate-context="*/*/*">Tracks</translate> + <translate translate-context="*/*/*"> + Tracks + </translate> </template> </track-widget> - <div class="ui clearing hidden divider"></div> + <div class="ui clearing hidden divider" /> </div> </section> </main> </template> <script> -import ChannelsWidget from "@/components/audio/ChannelsWidget" -import TrackWidget from "@/components/audio/track/Widget" -import AlbumWidget from "@/components/audio/album/Widget" -import ArtistWidget from "@/components/audio/artist/Widget" -import RadioButton from "@/components/radios/Button" +import ChannelsWidget from '@/components/audio/ChannelsWidget' +import TrackWidget from '@/components/audio/track/Widget' +import AlbumWidget from '@/components/audio/album/Widget' +import ArtistWidget from '@/components/audio/artist/Widget' +import RadioButton from '@/components/radios/Button' export default { - props: { - id: { type: String, required: true } - }, components: { ArtistWidget, AlbumWidget, TrackWidget, RadioButton, - ChannelsWidget, + ChannelsWidget + }, + props: { + id: { type: String, required: true } }, computed: { - labels() { - let title = `#${this.id}` + labels () { + const title = `#${this.id}` return { title } @@ -79,7 +118,7 @@ export default { }, hasFavorites () { return this.$store.state.favorites.count > 0 - }, - }, + } + } } </script> diff --git a/front/src/components/library/TagsSelector.vue b/front/src/components/library/TagsSelector.vue index c19a5ece48521faa6d18749b3c9b1bee95a17f7c..c968bb72c91472231c0e7fcd2ffa7caef20fade4 100644 --- a/front/src/components/library/TagsSelector.vue +++ b/front/src/components/library/TagsSelector.vue @@ -1,10 +1,19 @@ <template> - <div ref="dropdown" class="ui multiple search selection dropdown"> + <div + ref="dropdown" + class="ui multiple search selection dropdown" + > <input type="hidden"> - <i class="dropdown icon"></i> - <input id="tags-search" type="text" class="search"> + <i class="dropdown icon" /> + <input + id="tags-search" + type="text" + class="search" + > <div class="default text"> - <translate translate-context="*/Dropdown/Placeholder/Verb">Search…</translate> + <translate translate-context="*/Dropdown/Placeholder/Verb"> + Search… + </translate> </div> </div> </template> @@ -13,54 +22,62 @@ import $ from 'jquery' import lodash from '@/lodash' export default { - props: ['value'], + props: { value: { type: String, required: true } }, + watch: { + value: { + handler (v) { + const current = $(this.$refs.dropdown).dropdown('get value').split(',').sort() + if (!lodash.isEqual([...v].sort(), current)) { + $(this.$refs.dropdown).dropdown('set exactly', v) + } + }, + deep: true + } + }, mounted () { this.$nextTick(() => { this.initDropdown() - }) }, methods: { initDropdown () { - let self = this - let handleUpdate = () => { - let value = $(self.$refs.dropdown).dropdown('get value').split(',') + const self = this + const handleUpdate = () => { + const value = $(self.$refs.dropdown).dropdown('get value').split(',') self.$emit('input', value) return value } - let settings = { - keys : { - delimiter : 32, + const settings = { + keys: { + delimiter: 32 }, forceSelection: false, saveRemoteData: false, filterRemoteData: true, - preserveHTML : false, + preserveHTML: false, apiSettings: { url: this.$store.getters['instance/absoluteUrl']('/api/v1/tags/?name__startswith={query}&ordering=length&page_size=5'), beforeXHR: function (xhrObject) { - if (self.$store.state.auth.oauth.accessToken) { xhrObject.setRequestHeader('Authorization', self.$store.getters['auth/header']) } return xhrObject }, - onResponse(response) { - let currentSearch = $(self.$refs.dropdown).dropdown('get query') + onResponse (response) { + const currentSearch = $(self.$refs.dropdown).dropdown('get query') response = { results: [], - ...response, + ...response } if (currentSearch) { - let existingTag = response.results.find((result) => result.name === currentSearch) + const existingTag = response.results.find((result) => result.name === currentSearch) if (existingTag) { if (response.results.indexOf(existingTag) !== 0) { response.results = [existingTag, ...response.results] response.results.splice(response.results.indexOf(existingTag) + 1, 1) } - } - else { - response.results = [{name: currentSearch}, ...response.results] + } else { + response.results = [{ name: currentSearch }, ...response.results] } } return response @@ -75,22 +92,11 @@ export default { onAdd: handleUpdate, onRemove: handleUpdate, onLabelRemove: handleUpdate, - onChange: handleUpdate, + onChange: handleUpdate } $(this.$refs.dropdown).dropdown(settings) $(this.$refs.dropdown).dropdown('set exactly', this.value) } - }, - watch: { - value: { - handler (v) { - let current = $(this.$refs.dropdown).dropdown('get value').split(',').sort() - if (!lodash.isEqual([...v].sort(), current)) { - $(this.$refs.dropdown).dropdown('set exactly', v) - } - }, - deep: true - } } } </script> diff --git a/front/src/components/library/TrackBase.vue b/front/src/components/library/TrackBase.vue index cd8447c082d9d9f6c2af6b6c05a96d1a89991d7e..5107cc58a7a0c468ec0a6e7cdcda7a6e9c62712b 100644 --- a/front/src/components/library/TrackBase.vue +++ b/front/src/components/library/TrackBase.vue @@ -1,112 +1,203 @@ <template> <main> - <div v-if="isLoading" class="ui vertical segment" v-title="labels.title"> - <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> + <div + v-if="isLoading" + v-title="labels.title" + class="ui vertical segment" + > + <div :class="['ui', 'centered', 'active', 'inline', 'loader']" /> </div> <template v-if="track"> <section - :class="['ui', 'head', 'vertical', 'center', 'aligned', 'stripe', 'segment']" v-title="track.title" + :class="['ui', 'head', 'vertical', 'center', 'aligned', 'stripe', 'segment']" > <div class="ui basic padded segment"> <div class="ui stackable grid row container"> <div class="eight wide left aligned column"> <h1 class="ui header"> {{ track.title }} - <div class="sub header" v-html="subtitle"></div> + <div + class="sub header" + v-html="subtitle" + /> </h1> </div> <div class="eight wide right aligned column button-group"> - <play-button class="vibrant" :track="track"> - <translate translate-context="*/Queue/Button.Label/Short, Verb">Play</translate> + <play-button + class="vibrant" + :track="track" + > + <translate translate-context="*/Queue/Button.Label/Short, Verb"> + Play + </translate> </play-button> - <track-favorite-icon v-if="$store.state.auth.authenticated" :border="true" :track="track"></track-favorite-icon> - <track-playlist-icon class="circular" v-if="$store.state.auth.authenticated" :border="true" :track="track"></track-playlist-icon> - <a role="button" :aria-label="labels.download" v-if="upload" :href="downloadUrl" target="_blank" class="ui basic circular icon button" :title="labels.download"> - <i class="download icon"></i> + <track-favorite-icon + v-if="$store.state.auth.authenticated" + :border="true" + :track="track" + /> + <track-playlist-icon + v-if="$store.state.auth.authenticated" + class="circular" + :border="true" + :track="track" + /> + <a + v-if="upload" + role="button" + :aria-label="labels.download" + :href="downloadUrl" + target="_blank" + class="ui basic circular icon button" + :title="labels.download" + > + <i class="download icon" /> </a> - <modal v-if="isEmbedable" :show.sync="showEmbedModal"> + <modal + v-if="isEmbedable" + :show.sync="showEmbedModal" + > <h4 class="header"> - <translate translate-context="Popup/Track/Title">Embed this track on your website</translate> + <translate translate-context="Popup/Track/Title"> + Embed this track on your website + </translate> </h4> <div class="scrolling content"> <div class="description"> - <embed-wizard type="track" :id="track.id" /> + <embed-wizard + :id="track.id" + type="track" + /> </div> </div> <div class="actions"> <button class="ui basic deny button"> - <translate translate-context="*/*/Button.Label/Verb">Cancel</translate> + <translate translate-context="*/*/Button.Label/Verb"> + Cancel + </translate> </button> </div> </modal> - <button class="ui floating dropdown circular icon basic button" :title="labels.more" v-dropdown="{direction: 'downward'}"> - <i class="ellipsis vertical icon"></i> - <div class="menu" style="right: 0; left: auto"> + <button + v-dropdown="{direction: 'downward'}" + class="ui floating dropdown circular icon basic button" + :title="labels.more" + > + <i class="ellipsis vertical icon" /> + <div + class="menu" + style="right: 0; left: auto" + > <a - :href="track.fid" v-if="domain != $store.getters['instance/domain']" + :href="track.fid" target="_blank" - class="basic item"> - <i class="external icon"></i> - <translate :translate-params="{domain: domain}" translate-context="Content/*/Button.Label/Verb">View on %{ domain }</translate> + class="basic item" + > + <i class="external icon" /> + <translate + :translate-params="{domain: domain}" + translate-context="Content/*/Button.Label/Verb" + >View on %{ domain }</translate> </a> <div - role="button" v-if="isEmbedable" + role="button" + class="basic item" @click="showEmbedModal = !showEmbedModal" - class="basic item"> - <i class="code icon"></i> - <translate translate-context="Content/*/Button.Label/Verb">Embed</translate> + > + <i class="code icon" /> + <translate translate-context="Content/*/Button.Label/Verb"> + Embed + </translate> </div> - <a :href="wikipediaUrl" target="_blank" rel="noreferrer noopener" class="basic item"> - <i class="wikipedia w icon"></i> + <a + :href="wikipediaUrl" + target="_blank" + rel="noreferrer noopener" + class="basic item" + > + <i class="wikipedia w icon" /> <translate translate-context="Content/*/Button.Label/Verb">Search on Wikipedia</translate> </a> - <a v-if="discogsUrl" :href="discogsUrl" target="_blank" rel="noreferrer noopener" class="basic item"> - <i class="external icon"></i> + <a + v-if="discogsUrl" + :href="discogsUrl" + target="_blank" + rel="noreferrer noopener" + class="basic item" + > + <i class="external icon" /> <translate translate-context="Content/*/Button.Label/Verb">Search on Discogs</translate> </a> <router-link v-if="track.is_local" :to="{name: 'library.tracks.edit', params: {id: track.id }}" - class="basic item"> - <i class="edit icon"></i> - <translate translate-context="Content/*/Button.Label/Verb">Edit</translate> + class="basic item" + > + <i class="edit icon" /> + <translate translate-context="Content/*/Button.Label/Verb"> + Edit + </translate> </router-link> <dangerous-button - :class="['ui', {loading: isLoading}, 'item']" v-if="artist && $store.state.auth.authenticated && artist.channel && artist.attributed_to.full_username === $store.state.auth.fullUsername" - @confirm="remove()"> - <i class="ui trash icon"></i> - <translate translate-context="*/*/*/Verb">Delete…</translate> - <p slot="modal-header"><translate translate-context="Popup/Channel/Title">Delete this track?</translate></p> + :class="['ui', {loading: isLoading}, 'item']" + @confirm="remove()" + > + <i class="ui trash icon" /> + <translate translate-context="*/*/*/Verb"> + Delete… + </translate> + <p slot="modal-header"> + <translate translate-context="Popup/Channel/Title"> + Delete this track? + </translate> + </p> <div slot="modal-content"> - <p><translate translate-context="Content/Moderation/Paragraph">The track will be deleted, as well as any related files and data. This action is irreversible.</translate></p> + <p> + <translate translate-context="Content/Moderation/Paragraph"> + The track will be deleted, as well as any related files and data. This action is irreversible. + </translate> + </p> </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> - <div class="divider"></div> + <div class="divider" /> <div - role="button" - class="basic item" v-for="obj in getReportableObjs({track})" :key="obj.target.type + obj.target.id" - @click.stop.prevent="$store.dispatch('moderation/report', obj.target)"> + role="button" + class="basic item" + @click.stop.prevent="$store.dispatch('moderation/report', obj.target)" + > <i class="share icon" /> {{ obj.label }} </div> - <div class="divider"></div> - <router-link class="basic item" v-if="$store.state.auth.availablePermissions['library']" :to="{name: 'manage.library.tracks.detail', params: {id: track.id}}"> - <i class="wrench icon"></i> - <translate translate-context="Content/Moderation/Link">Open in moderation interface</translate> + <div class="divider" /> + <router-link + v-if="$store.state.auth.availablePermissions['library']" + class="basic item" + :to="{name: 'manage.library.tracks.detail', params: {id: track.id}}" + > + <i class="wrench icon" /> + <translate translate-context="Content/Moderation/Link"> + Open in moderation interface + </translate> </router-link> <a v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser" class="basic item" :href="$store.getters['instance/absoluteUrl'](`/api/admin/music/track/${track.id}`)" - target="_blank" rel="noopener noreferrer"> - <i class="wrench icon"></i> + target="_blank" + rel="noopener noreferrer" + > + <i class="wrench icon" /> <translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate> </a> </div> @@ -115,50 +206,54 @@ </div> </div> </section> - <router-view v-if="track" @libraries-loaded="libraries = $event" :track="track" :object="track" object-type="track" :key="$route.fullPath"></router-view> + <router-view + v-if="track" + :key="$route.fullPath" + :track="track" + :object="track" + object-type="track" + @libraries-loaded="libraries = $event" + /> </template> </main> </template> <script> -import time from "@/utils/time" -import axios from "axios" -import url from "@/utils/url" -import {getDomain} from '@/utils' -import logger from "@/logging" -import PlayButton from "@/components/audio/PlayButton" -import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon" -import TrackPlaylistIcon from "@/components/playlists/TrackPlaylistIcon" +import time from '@/utils/time' +import axios from 'axios' +import url from '@/utils/url' +import { getDomain } from '@/utils' +import logger from '@/logging' +import PlayButton from '@/components/audio/PlayButton' +import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon' +import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon' import Modal from '@/components/semantic/Modal' -import EmbedWizard from "@/components/audio/EmbedWizard" +import EmbedWizard from '@/components/audio/EmbedWizard' import ReportMixin from '@/components/mixins/Report' -import {momentFormat} from '@/filters' - -const FETCH_URL = "tracks/" +import { momentFormat } from '@/filters' +const FETCH_URL = 'tracks/' - -function escapeHtml(unsafe) { +function escapeHtml (unsafe) { return unsafe - .replace(/&/g, "&") - .replace(/</g, "<") - .replace(/>/g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') } - export default { - props: ["id"], - mixins: [ReportMixin], components: { PlayButton, TrackPlaylistIcon, TrackFavoriteIcon, Modal, - EmbedWizard, + EmbedWizard }, - data() { + mixins: [ReportMixin], + props: { id: { type: Number, required: true } }, + data () { return { time, isLoading: true, @@ -168,41 +263,12 @@ export default { libraries: [] } }, - created() { - this.fetchData() - }, - methods: { - fetchData() { - var self = this - this.isLoading = true - let url = FETCH_URL + this.id + "/" - logger.default.debug('Fetching track "' + this.id + '"') - axios.get(url, {params: {refresh: 'true'}}).then(response => { - self.track = response.data - axios.get(`artists/${response.data.artist.id}/`).then(response => { - self.artist = response.data - }) - self.isLoading = false - }) - }, - remove () { - let self = this - self.isLoading = true - axios.delete(`tracks/${this.track.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: { domain () { if (this.track) { return getDomain(this.track.fid) } + return null }, publicLibraries () { return this.libraries.filter(l => { @@ -210,49 +276,50 @@ export default { }) }, isEmbedable () { - let self = this - return self.artist && self.artist.channel && self.artist.channel.actor || this.publicLibraries.length > 0 - }, - upload() { + const self = this + return (self.artist && self.artist.channel && self.artist.channel.actor) || this.publicLibraries.length > 0 + }, + upload () { if (this.track.uploads) { return this.track.uploads[0] } + return null }, - labels() { + labels () { return { - title: this.$pgettext('*/*/*/Noun', "Track"), - download: this.$pgettext('Content/Track/Link/Verb', "Download"), - more: this.$pgettext('*/*/Button.Label/Noun', "More…"), + title: this.$pgettext('*/*/*/Noun', 'Track'), + download: this.$pgettext('Content/Track/Link/Verb', 'Download'), + more: this.$pgettext('*/*/Button.Label/Noun', 'More…') } }, - wikipediaUrl() { + wikipediaUrl () { return ( - "https://en.wikipedia.org/w/index.php?search=" + - encodeURI(this.track.title + " " + this.track.artist.name) + 'https://en.wikipedia.org/w/index.php?search=' + + encodeURI(this.track.title + ' ' + this.track.artist.name) ) }, - discogsUrl() { + discogsUrl () { if (this.track.album) { return ( - "https://discogs.com/search/?type=release&title=" + - encodeURI(this.track.album.title) + "&artist=" + - encodeURI(this.track.artist.name) + "&track=" + + 'https://discogs.com/search/?type=release&title=' + + encodeURI(this.track.album.title) + '&artist=' + + encodeURI(this.track.artist.name) + '&track=' + encodeURI(this.track.title) ) - } + return null }, - downloadUrl() { - let u = this.$store.getters["instance/absoluteUrl"]( + downloadUrl () { + let u = this.$store.getters['instance/absoluteUrl']( this.upload.listen_url ) if (this.$store.state.auth.authenticated) { - let param = "jwt" + let param = 'jwt' let value = this.$store.state.auth.token if (this.$store.state.auth.scopedTokens && this.$store.state.auth.scopedTokens.listen) { // used scoped tokens instead of JWT to reduce the attack surface if the token // is leaked - param = "token" + param = 'token' value = this.$store.state.auth.scopedTokens.listen } u = url.updateQueryString( @@ -264,7 +331,7 @@ export default { return u }, attributedToUrl () { - let route = this.$router.resolve({ + const route = this.$router.resolve({ name: 'profile.full.overview', params: { username: this.track.attributed_to.preferred_username, @@ -274,21 +341,21 @@ export default { return route.href }, albumUrl () { - let route = this.$router.resolve({name: 'library.albums.detail', params: {id: this.track.album.id }}) + const route = this.$router.resolve({ name: 'library.albums.detail', params: { id: this.track.album.id } }) return route.href }, artistUrl () { - let route = this.$router.resolve({name: 'library.artists.detail', params: {id: this.track.artist.id }}) + const route = this.$router.resolve({ name: 'library.artists.detail', params: { id: this.track.artist.id } }) return route.href }, - headerStyle() { + headerStyle () { if (!this.cover || !this.cover.urls.original) { - return "" + return '' } return ( - "background-image: url(" + - this.$store.getters["instance/absoluteUrl"](this.cover.urls.original) + - ")" + 'background-image: url(' + + this.$store.getters['instance/absoluteUrl'](this.cover.urls.original) + + ')' ) }, subtitle () { @@ -299,21 +366,51 @@ export default { uploaderUrl: this.attributedToUrl, uploader: escapeHtml(`@${this.track.attributed_to.full_username}`), date: escapeHtml(this.track.creation_date), - prettyDate: escapeHtml(momentFormat(this.track.creation_date, 'LL')), + prettyDate: escapeHtml(momentFormat(this.track.creation_date, 'LL')) }) } else { msg = this.$pgettext('Content/Track/Paragraph', 'Uploaded on <time title="%{ date }" datetime="%{ date }">%{ prettyDate }</time>') return this.$gettextInterpolate(msg, { date: escapeHtml(this.track.creation_date), - prettyDate: escapeHtml(momentFormat(this.track.creation_date, 'LL')), + prettyDate: escapeHtml(momentFormat(this.track.creation_date, 'LL')) }) } } }, watch: { - id() { + id () { this.fetchData() + } + }, + created () { + this.fetchData() + }, + methods: { + fetchData () { + const self = this + this.isLoading = true + const url = FETCH_URL + this.id + '/' + logger.default.debug('Fetching track "' + this.id + '"') + axios.get(url, { params: { refresh: 'true' } }).then(response => { + self.track = response.data + axios.get(`artists/${response.data.artist.id}/`).then(response => { + self.artist = response.data + }) + self.isLoading = false + }) }, + remove () { + const self = this + self.isLoading = true + axios.delete(`tracks/${this.track.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> diff --git a/front/src/components/library/TrackDetail.vue b/front/src/components/library/TrackDetail.vue index 3d54a53a7a7fea4cb43e67f89b361200c5a5bcd5..182eba921d163ce4c7ea1a40c38f1bd25d0c2698 100644 --- a/front/src/components/library/TrackDetail.vue +++ b/front/src/components/library/TrackDetail.vue @@ -1,57 +1,116 @@ <template> - <div v-if="track"> <section class="ui vertical stripe segment"> <div class="ui stackable grid row container"> <div class="six wide column"> <template v-if="upload"> - <img alt="Cover Image" class="ui fluid image track-cover-image" v-if="track.cover && track.cover.urls.large_square_crop" v-lazy="$store.getters['instance/absoluteUrl'](track.cover.urls.large_square_crop)"> - <img alt="Cover Image" class="ui fluid image track-cover-image" v-else-if="track.album.cover && track.album.cover.urls.large_square_crop" v-lazy="$store.getters['instance/absoluteUrl'](track.album.cover.urls.large_square_crop)"> + <img + v-if="track.cover && track.cover.urls.large_square_crop" + v-lazy="$store.getters['instance/absoluteUrl'](track.cover.urls.large_square_crop)" + alt="Cover Image" + class="ui fluid image track-cover-image" + > + <img + v-else-if="track.album.cover && track.album.cover.urls.large_square_crop" + v-lazy="$store.getters['instance/absoluteUrl'](track.album.cover.urls.large_square_crop)" + alt="Cover Image" + class="ui fluid image track-cover-image" + > <h3 class="ui header"> - <translate key="1" v-if="track.artist.content_category === 'music'" translate-context="Content/*/*">Track Details</translate> - <translate key="2" v-else translate-context="Content/*/*">Episode Details</translate> + <translate + v-if="track.artist.content_category === 'music'" + key="1" + translate-context="Content/*/*" + > + Track Details + </translate> + <translate + v-else + key="2" + translate-context="Content/*/*" + > + Episode Details + </translate> </h3> <table class="ui basic table"> <tbody> <tr> <td> - <translate translate-context="Content/*/*">Duration</translate> + <translate translate-context="Content/*/*"> + Duration + </translate> </td> <td class="right aligned"> - <template v-if="upload.duration">{{ upload.duration | duration }}</template> - <translate v-else translate-context="*/*/*">N/A</translate> + <template v-if="upload.duration"> + {{ upload.duration | duration }} + </template> + <translate + v-else + translate-context="*/*/*" + > + N/A + </translate> </td> </tr> <tr> <td> - <translate translate-context="Content/*/*/Noun">Size</translate> + <translate translate-context="Content/*/*/Noun"> + Size + </translate> </td> <td class="right aligned"> - <template v-if="upload.size">{{ upload.size | humanSize }}</template> - <translate v-else translate-context="*/*/*">N/A</translate> + <template v-if="upload.size"> + {{ upload.size | humanSize }} + </template> + <translate + v-else + translate-context="*/*/*" + > + N/A + </translate> </td> </tr> <tr> <td> - <translate translate-context="Content/*/*/Noun">Codec</translate> + <translate translate-context="Content/*/*/Noun"> + Codec + </translate> </td> <td class="right aligned"> - <template v-if="upload.extension">{{ upload.extension }}</template> - <translate v-else translate-context="*/*/*">N/A</translate> + <template v-if="upload.extension"> + {{ upload.extension }} + </template> + <translate + v-else + translate-context="*/*/*" + > + N/A + </translate> </td> </tr> <tr> <td> - <translate translate-context="Content/Track/*/Noun">Bitrate</translate> + <translate translate-context="Content/Track/*/Noun"> + Bitrate + </translate> </td> <td class="right aligned"> - <template v-if="upload.bitrate">{{ upload.bitrate | humanSize }}/s</template> - <translate v-else translate-context="*/*/*">N/A</translate> + <template v-if="upload.bitrate"> + {{ upload.bitrate | humanSize }}/s + </template> + <translate + v-else + translate-context="*/*/*" + > + N/A + </translate> </td> </tr> <tr> <td> - <translate translate-context="Content/*/*">Downloads</translate> + <translate translate-context="Content/*/*"> + Downloads + </translate> </td> <td class="right aligned"> {{ track.downloads_count }} @@ -59,26 +118,30 @@ </tr> </tbody> </table> - </template> </div> <div class="ten wide column"> <template v-if="track.tags && track.tags.length > 0"> - <tags-list :tags="track.tags"></tags-list> - <div class="ui hidden divider"></div> + <tags-list :tags="track.tags" /> + <div class="ui hidden divider" /> </template> <rendered-description :content="track.description" - :can-update="false"></rendered-description> + :can-update="false" + /> <h2 class="ui header"> - <translate translate-context="Content/*/*">Release Details</translate> + <translate translate-context="Content/*/*"> + Release Details + </translate> </h2> <table class="ui basic table ellipsis-rows"> <tbody> <tr> <td> - <translate translate-context="*/*/*/Noun">Artist</translate> + <translate translate-context="*/*/*/Noun"> + Artist + </translate> </td> <td class="right aligned"> <router-link :to="{name: 'library.artists.detail', params: {id: track.artist.id}}"> @@ -88,8 +151,20 @@ </tr> <tr v-if="track.album"> <td> - <translate key="1" v-if="track.album.artist.content_category === 'music'" translate-context="*/*/*/Noun">Album</translate> - <translate key="2" v-else translate-context="*/*/*">Serie</translate> + <translate + v-if="track.album.artist.content_category === 'music'" + key="1" + translate-context="*/*/*/Noun" + > + Album + </translate> + <translate + v-else + key="2" + translate-context="*/*/*" + > + Serie + </translate> </td> <td class="right aligned"> <router-link :to="{name: 'library.albums.detail', params: {id: track.album.id}}"> @@ -99,64 +174,112 @@ </tr> <tr> <td> - <translate translate-context="*/*/*">Year</translate> + <translate translate-context="*/*/*"> + Year + </translate> </td> <td class="right aligned"> <template v-if="track.album && track.album.release_date"> {{ track.album.release_date | moment('Y') }} </template> <template v-else> - <translate translate-context="*/*/*">N/A</translate> + <translate translate-context="*/*/*"> + N/A + </translate> </template> </td> </tr> <tr> <td> - <translate translate-context="Content/Track/*/Noun">Copyright</translate> + <translate translate-context="Content/Track/*/Noun"> + Copyright + </translate> </td> <td class="right aligned"> - <span v-if="track.copyright" :title="track.copyright">{{ track.copyright|truncate(50) }}</span> + <span + v-if="track.copyright" + :title="track.copyright" + >{{ track.copyright|truncate(50) }}</span> <template v-else> - <translate translate-context="*/*/*">N/A</translate> + <translate translate-context="*/*/*"> + N/A + </translate> </template> </td> </tr> <tr> <td> - <translate translate-context="Content/*/*/Noun">License</translate> + <translate translate-context="Content/*/*/Noun"> + License + </translate> </td> <td class="right aligned"> - <a v-if="license" :href="license.url" target="_blank" rel="noopener noreferrer">{{ license.name }}</a> - <translate v-else translate-context="*/*/*">N/A</translate> + <a + v-if="license" + :href="license.url" + target="_blank" + rel="noopener noreferrer" + >{{ license.name }}</a> + <translate + v-else + translate-context="*/*/*" + > + N/A + </translate> </td> </tr> <tr v-if="!track.is_local"> <td> - <translate translate-context="Content/*/*/Noun">URL</translate> + <translate translate-context="Content/*/*/Noun"> + URL + </translate> </td> <td :title="track.fid"> - <a :href="track.fid" target="_blank" rel="noopener noreferrer"> - {{ track.fid|truncate(65)}} + <a + :href="track.fid" + target="_blank" + rel="noopener noreferrer" + > + {{ track.fid|truncate(65) }} </a> </td> </tr> </tbody> </table> - <a v-if="musicbrainzUrl" :href="musicbrainzUrl" target="_blank" rel="noreferrer noopener"> - <i class="external icon"></i> + <a + v-if="musicbrainzUrl" + :href="musicbrainzUrl" + target="_blank" + rel="noreferrer noopener" + > + <i class="external icon" /> <translate translate-context="Content/*/*/Clickable, Verb">View on MusicBrainz</translate> </a> <h2 class="ui header"> - <translate translate-context="Content/*/Title/Noun">Related Playlists</translate> + <translate translate-context="Content/*/Title/Noun"> + Related Playlists + </translate> </h2> - <playlist-widget :url="'playlists/'" :filters="{track: track.id, playable: true, ordering: '-modification_date'}"> - </playlist-widget> + <playlist-widget + :url="'playlists/'" + :filters="{track: track.id, playable: true, ordering: '-modification_date'}" + /> <h2 class="ui header"> - <translate translate-context="Content/*/Title/Noun">Related Libraries</translate> + <translate translate-context="Content/*/Title/Noun"> + Related Libraries + </translate> </h2> - <library-widget @loaded="$emit('libraries-loaded', $event)" :url="'tracks/' + id + '/libraries/'"> - <translate translate-context="Content/Track/Paragraph" slot="subtitle">This track is present in the following libraries:</translate> + <library-widget + :url="'tracks/' + id + '/libraries/'" + @loaded="$emit('libraries-loaded', $event)" + > + <translate + slot="subtitle" + translate-context="Content/Track/Paragraph" + > + This track is present in the following libraries: + </translate> </library-widget> </div> </div> @@ -165,59 +288,46 @@ </template> <script> -import axios from "axios" -import url from "@/utils/url" -import logger from "@/logging" -import LibraryWidget from "@/components/federation/LibraryWidget" -import TagsList from "@/components/tags/List" -import PlaylistWidget from "@/components/playlists/Widget" - -const FETCH_URL = "tracks/" +import axios from 'axios' +import LibraryWidget from '@/components/federation/LibraryWidget' +import TagsList from '@/components/tags/List' +import PlaylistWidget from '@/components/playlists/Widget' export default { - props: ["track", "libraries"], components: { LibraryWidget, TagsList, - PlaylistWidget, + PlaylistWidget + }, + props: { + track: { type: Object, required: true }, + libraries: { type: Array, required: true } }, - data() { + data () { return { id: this.track.id, licenseData: null } }, - created() { - if (this.track && this.track.license) { - this.fetchLicenseData(this.track.license) - } - }, - methods: { - fetchLicenseData(licenseId) { - var self = this - let url = `licenses/${licenseId}` - axios.get(url).then(response => { - self.licenseData = response.data - }) - }, - }, computed: { - labels() { + labels () { return { - title: this.$pgettext('*/*/*/Noun', "Track") + title: this.$pgettext('*/*/*/Noun', 'Track') } }, - musicbrainzUrl() { + musicbrainzUrl () { if (this.track.mbid) { - return "https://musicbrainz.org/recording/" + this.track.mbid + return 'https://musicbrainz.org/recording/' + this.track.mbid } + return null }, - upload() { + upload () { if (this.track.uploads) { return this.track.uploads[0] } + return null }, - license() { + license () { if (!this.track || !this.track.license) { return null } @@ -230,7 +340,8 @@ export default { if (this.track.album && this.track.album.cover) { return this.track.album.cover } - }, + return null + } }, watch: { track (v) { @@ -238,6 +349,20 @@ export default { this.fetchLicenseData(v.license) } } + }, + created () { + if (this.track && this.track.license) { + this.fetchLicenseData(this.track.license) + } + }, + methods: { + fetchLicenseData (licenseId) { + const self = this + const url = `licenses/${licenseId}` + axios.get(url).then(response => { + self.licenseData = response.data + }) + } } } </script> diff --git a/front/src/components/library/TrackEdit.vue b/front/src/components/library/TrackEdit.vue index 18e71e8fa62ea25884ff72819699c2dafbba591d..add5c4485214259e2e5512d47ed5c7026e4e32f3 100644 --- a/front/src/components/library/TrackEdit.vue +++ b/front/src/components/library/TrackEdit.vue @@ -1,60 +1,84 @@ <template> - <section class="ui vertical stripe segment"> <div class="ui text container"> <h2> - <translate v-if="canEdit" key="1" translate-context="Content/*/Title">Edit this track</translate> - <translate v-else key="2" translate-context="Content/*/Title">Suggest an edit on this track</translate> + <translate + v-if="canEdit" + key="1" + translate-context="Content/*/Title" + > + Edit this track + </translate> + <translate + v-else + key="2" + translate-context="Content/*/Title" + > + Suggest an edit on this track + </translate> </h2> - <div class="ui message" v-if="!object.is_local"> - <translate translate-context="Content/*/Message">This object is managed by another server, you cannot edit it.</translate> + <div + v-if="!object.is_local" + class="ui message" + > + <translate translate-context="Content/*/Message"> + This object is managed by another server, you cannot edit it. + </translate> </div> <edit-form v-else-if="!isLoadingLicenses" :object-type="objectType" :object="object" :can-edit="canEdit" - :licenses="licenses"></edit-form> - <div v-else class="ui inverted active dimmer"> - <div class="ui loader"></div> + :licenses="licenses" + /> + <div + v-else + class="ui inverted active dimmer" + > + <div class="ui loader" /> </div> </div> </section> </template> <script> -import axios from "axios" +import axios from 'axios' import EditForm from '@/components/library/EditForm' export default { - props: ["objectType", "object", "libraries"], - data() { + components: { + EditForm + }, + props: { + objectType: { type: String, required: true }, + object: { type: Object, required: true }, + libraries: { type: Array, required: true } + }, + data () { return { id: this.object.id, isLoadingLicenses: false, licenses: [] } }, - components: { - EditForm + computed: { + canEdit () { + return true + } }, created () { this.fetchLicenses() }, methods: { fetchLicenses () { - let self = this + const self = this self.isLoadingLicenses = true axios.get('licenses/').then((response) => { self.isLoadingLicenses = false self.licenses = response.data.results }) } - }, - computed: { - canEdit () { - return true - } } } </script> diff --git a/front/src/components/library/UploadDetail.vue b/front/src/components/library/UploadDetail.vue index ce3a1e3a82b828dbe2c1c7e7734abaea17e9cb10..89d4a4cce0be674a5a5e0b09f6a8b69b61cafc2e 100644 --- a/front/src/components/library/UploadDetail.vue +++ b/front/src/components/library/UploadDetail.vue @@ -1,28 +1,30 @@ <template> <main> - <div v-if="isLoading" class="ui vertical segment"> - <div class="ui centered active inline loader"></div> + <div + v-if="isLoading" + class="ui vertical segment" + > + <div class="ui centered active inline loader" /> </div> </main> </template> <script> -import axios from "axios" - +import axios from 'axios' export default { - props: ["id"], - async created() { - let upload = await this.fetchData() - this.$router.replace({name: "library.tracks.detail", params: {id: upload.track.id}}) + props: { id: { type: Number, required: true } }, + async created () { + const upload = await this.fetchData() + this.$router.replace({ name: 'library.tracks.detail', params: { id: upload.track.id } }) }, methods: { - async fetchData() { + async fetchData () { this.isLoading = true - let response = await axios.get(`uploads/${this.id}/`, {params: {refresh: 'true', include_channels: 'true'}}) + const response = await axios.get(`uploads/${this.id}/`, { params: { refresh: 'true', include_channels: 'true' } }) this.isLoading = false return response.data - }, + } } } </script> diff --git a/front/src/components/library/radios/Builder.vue b/front/src/components/library/radios/Builder.vue index 5a29708b41c0cf26c736ba8a2f54fe034246abb4..a2853c4f250b8dff45a2672622d49a8ef4ab1e10 100644 --- a/front/src/components/library/radios/Builder.vue +++ b/front/src/components/library/radios/Builder.vue @@ -1,53 +1,117 @@ <template> - <div class="ui vertical stripe segment" v-title="labels.title"> + <div + v-title="labels.title" + class="ui vertical stripe segment" + > <div> <section> <h2 class="ui header"> - <translate translate-context="Content/Radio/Title">Builder</translate> + <translate translate-context="Content/Radio/Title"> + Builder + </translate> </h2> - <p><translate translate-context="Content/Radio/Paragraph">You can use this interface to build your own custom radio, which will play tracks according to your criteria.</translate></p> + <p> + <translate translate-context="Content/Radio/Paragraph"> + You can use this interface to build your own custom radio, which will play tracks according to your criteria. + </translate> + </p> <div class="ui form"> - <div v-if="success" class="ui positive message"> + <div + v-if="success" + class="ui positive message" + > <h4 class="header"> <template v-if="radioName"> - <translate translate-context="Content/Radio/Message">Radio updated</translate> + <translate translate-context="Content/Radio/Message"> + Radio updated + </translate> </template> <template v-else> - <translate translate-context="Content/Radio/Message">Radio created</translate> + <translate translate-context="Content/Radio/Message"> + Radio created + </translate> </template> </h4> </div> <div class=""> <div class="field"> <label for="name"><translate translate-context="Content/Radio/Input.Label/Noun">Radio name</translate></label> - <input id="name" name="name" type="text" v-model="radioName" :placeholder="labels.placeholder.name" /> + <input + id="name" + v-model="radioName" + name="name" + type="text" + :placeholder="labels.placeholder.name" + > </div> <div class="field"> <label for="description"><translate translate-context="*/*/*/Noun">Description</translate></label> - <textarea rows="2" id="description" type="text" v-model="radioDesc" :placeholder="labels.placeholder.description" /> + <textarea + id="description" + v-model="radioDesc" + rows="2" + type="text" + :placeholder="labels.placeholder.description" + /> </div> <div class="ui toggle checkbox"> - <input id="public" type="checkbox" v-model="isPublic" /> + <input + id="public" + v-model="isPublic" + type="checkbox" + > <label for="public"><translate translate-context="Content/Radio/Checkbox.Label/Verb">Display publicly</translate></label> </div> - <div class="ui hidden divider"></div> - <button :disabled="!canSave" @click="save" :class="['ui', 'success', {loading: isLoading}, 'button']"> - <translate translate-context="Content/*/Button.Label/Verb">Save</translate> + <div class="ui hidden divider" /> + <button + :disabled="!canSave" + :class="['ui', 'success', {loading: isLoading}, 'button']" + @click="save" + > + <translate translate-context="Content/*/Button.Label/Verb"> + Save + </translate> </button> - <radio-button v-if="id" type="custom" :custom-radio-id="id"></radio-button> + <radio-button + v-if="id" + type="custom" + :custom-radio-id="id" + /> </div> </div> <div class="ui form"> <div class="inline field"> - <label id="radioFilterLabel" for="radio-filters"><translate translate-context="Content/Radio/Paragraph">Add filters to customize your radio</translate></label> - <select id="radio-filters" class="ui dropdown" v-model="currentFilterType"> + <label + id="radioFilterLabel" + for="radio-filters" + ><translate translate-context="Content/Radio/Paragraph">Add filters to customize your radio</translate></label> + <select + id="radio-filters" + v-model="currentFilterType" + class="ui dropdown" + > <option value=""> - <translate translate-context="Content/Radio/Dropdown.Placeholder/Verb">Select a filter</translate> + <translate translate-context="Content/Radio/Dropdown.Placeholder/Verb"> + Select a filter + </translate> + </option> + <option + v-for="(f, key) in availableFilters" + :key="key" + :value="f.type" + > + {{ f.label }} </option> - <option v-for="f in availableFilters" :value="f.type">{{ f.label }}</option> </select> - <button id="addFilter" :disabled="!currentFilterType" @click="add" class="ui button"> - <translate translate-context="Content/Radio/Button.Label/Verb">Add filter</translate> + <button + id="addFilter" + :disabled="!currentFilterType" + class="ui button" + @click="add" + > + <translate translate-context="Content/Radio/Button.Label/Verb"> + Add filter + </translate> </button> </div> <p v-if="currentFilter"> @@ -57,11 +121,31 @@ <table class="ui table"> <thead> <tr> - <th class="two wide"><translate translate-context="Content/Radio/Table.Label/Noun">Filter name</translate></th> - <th class="one wide"><translate translate-context="Content/Radio/Table.Label/Verb">Exclude</translate></th> - <th class="six wide"><translate translate-context="Content/Radio/Table.Label/Verb (Value is a List of Parameters)">Config</translate></th> - <th class="five wide"><translate translate-context="Content/Radio/Table.Label/Noun (Value is a number of Tracks)">Candidates</translate></th> - <th class="two wide"><translate translate-context="Content/*/*/Noun">Actions</translate></th> + <th class="two wide"> + <translate translate-context="Content/Radio/Table.Label/Noun"> + Filter name + </translate> + </th> + <th class="one wide"> + <translate translate-context="Content/Radio/Table.Label/Verb"> + Exclude + </translate> + </th> + <th class="six wide"> + <translate translate-context="Content/Radio/Table.Label/Verb (Value is a List of Parameters)"> + Config + </translate> + </th> + <th class="five wide"> + <translate translate-context="Content/Radio/Table.Label/Noun (Value is a number of Tracks)"> + Candidates + </translate> + </th> + <th class="two wide"> + <translate translate-context="Content/*/*/Noun"> + Actions + </translate> + </th> </tr> </thead> <tbody> @@ -69,52 +153,54 @@ v-for="(f, index) in filters" :key="(f, index, f.hash)" :index="index" + :config="f.config" + :filter="f.filter" @update-config="updateConfig" @delete="deleteFilter" - :config="f.config" - :filter="f.filter"> - </builder-filter> + /> </tbody> </table> <template v-if="checkResult && checkResult.candidates && checkResult.candidates.count"> <h3 - class="ui header" v-translate="{count: checkResult.candidates.count}" + class="ui header" :translate-n="checkResult.candidates.count" translate-plural="%{ count } tracks matching combined filters" - translate-context="Content/Radio/Table.Paragraph/Short"> + translate-context="Content/Radio/Table.Paragraph/Short" + > %{ count } track matching combined filters </h3> - <track-table - v-if="checkResult.candidates.sample" - :tracks="checkResult.candidates.sample" + <track-table + v-if="checkResult.candidates.sample" + :tracks="checkResult.candidates.sample" :playable="true" :show-position="false" :show-duration="false" - :display-actions="false"></track-table> + :display-actions="false" + /> </template> </section> </div> </div> </template> <script> -import axios from "axios" -import $ from "jquery" -import _ from "@/lodash" -import BuilderFilter from "./Filter" -import TrackTable from "@/components/audio/track/Table" -import RadioButton from "@/components/radios/Button" +import axios from 'axios' +import $ from 'jquery' +import _ from '@/lodash' +import BuilderFilter from './Filter' +import TrackTable from '@/components/audio/track/Table' +import RadioButton from '@/components/radios/Button' export default { - props: { - id: { required: false } - }, components: { BuilderFilter, TrackTable, RadioButton }, - data: function() { + props: { + id: { type: Number, required: false, default: 0 } + }, + data: function () { return { isLoading: false, success: false, @@ -122,31 +208,68 @@ export default { currentFilterType: null, filters: [], checkResult: null, - radioName: "", - radioDesc: "", + radioName: '', + radioDesc: '', isPublic: true } }, - created: function() { - let self = this + computed: { + labels () { + const title = this.$pgettext('Head/Radio/Title', 'Radio Builder') + const placeholder = { + name: this.$pgettext('Content/Radio/Input.Placeholder', 'My awesome radio'), + description: this.$pgettext('Content/Radio/Input.Placeholder', 'My awesome description') + } + return { + title, + placeholder + } + }, + canSave: function () { + return this.radioName.length > 0 && this.checkErrors.length === 0 + }, + checkErrors: function () { + if (!this.checkResult) { + return [] + } + const errors = this.checkResult.errors + return errors + }, + currentFilter: function () { + const self = this + return this.availableFilters.filter(e => { + return e.type === self.currentFilterType + })[0] + } + }, + watch: { + filters: { + handler: function () { + this.fetchCandidates() + }, + deep: true + } + }, + created: function () { + const self = this this.fetchFilters().then(() => { if (self.id) { self.fetch() } }) }, - mounted() { - $(".ui.dropdown").dropdown() + mounted () { + $('.ui.dropdown').dropdown() }, methods: { - fetchFilters: function() { - let self = this - let url = "radios/radios/filters/" + fetchFilters: function () { + const self = this + const url = 'radios/radios/filters/' return axios.get(url).then(response => { self.availableFilters = response.data }) }, - add() { + add () { this.filters.push({ config: {}, filter: this.currentFilter, @@ -154,18 +277,18 @@ export default { }) this.fetchCandidates() }, - updateConfig(index, field, value) { + updateConfig (index, field, value) { this.filters[index].config[field] = value this.fetchCandidates() }, - deleteFilter(index) { + deleteFilter (index) { this.filters.splice(index, 1) this.fetchCandidates() }, - fetch: function() { - let self = this + fetch: function () { + const self = this self.isLoading = true - let url = "radios/radios/" + this.id + "/" + const url = 'radios/radios/' + this.id + '/' axios.get(url).then(response => { self.filters = response.data.config.map(f => { return { @@ -182,28 +305,28 @@ export default { self.isLoading = false }) }, - fetchCandidates: function() { - let self = this - let url = "radios/radios/validate/" + fetchCandidates: function () { + const self = this + const url = 'radios/radios/validate/' let final = this.filters.map(f => { - let c = _.clone(f.config) + const c = _.clone(f.config) c.type = f.filter.type return c }) final = { - filters: [{ type: "group", filters: final }] + filters: [{ type: 'group', filters: final }] } axios.post(url, final).then(response => { self.checkResult = response.data.filters[0] }) }, - save: function() { - let self = this + save: function () { + const self = this self.success = false self.isLoading = true let final = this.filters.map(f => { - let c = _.clone(f.config) + const c = _.clone(f.config) c.type = f.filter.type return c }) @@ -214,18 +337,18 @@ export default { config: final } if (this.id) { - let url = "radios/radios/" + this.id + "/" + const url = 'radios/radios/' + this.id + '/' axios.put(url, final).then(response => { self.isLoading = false self.success = true }) } else { - let url = "radios/radios/" + const url = 'radios/radios/' axios.post(url, final).then(response => { self.success = true self.isLoading = false self.$router.push({ - name: "library.radios.detail", + name: 'library.radios.detail', params: { id: response.data.id } @@ -233,43 +356,6 @@ export default { }) } } - }, - computed: { - labels() { - let title = this.$pgettext('Head/Radio/Title', "Radio Builder") - let placeholder = { - name: this.$pgettext('Content/Radio/Input.Placeholder', "My awesome radio"), - description: this.$pgettext('Content/Radio/Input.Placeholder', "My awesome description") - } - return { - title, - placeholder - } - }, - canSave: function() { - return this.radioName.length > 0 && this.checkErrors.length === 0 - }, - checkErrors: function() { - if (!this.checkResult) { - return [] - } - let errors = this.checkResult.errors - return errors - }, - currentFilter: function() { - let self = this - return this.availableFilters.filter(e => { - return e.type === self.currentFilterType - })[0] - } - }, - watch: { - filters: { - handler: function() { - this.fetchCandidates() - }, - deep: true - } } } </script> diff --git a/front/src/components/library/radios/Filter.vue b/front/src/components/library/radios/Filter.vue index 1c7fe286b4501030c86c13afc43f1f63f127c2e1..04b3bfc5afa0f7d90f698a58a5e9dc17049e560f 100644 --- a/front/src/components/library/radios/Filter.vue +++ b/front/src/components/library/radios/Filter.vue @@ -3,32 +3,55 @@ <td>{{ filter.label }}</td> <td> <div class="ui toggle checkbox"> - <input id="exclude-filter" name="public" type="checkbox" v-model="exclude" @change="$emit('update-config', index, 'not', exclude)"> - <label for="exclude-filter" class="visually-hidden"> + <input + id="exclude-filter" + v-model="exclude" + name="public" + type="checkbox" + @change="$emit('update-config', index, 'not', exclude)" + > + <label + for="exclude-filter" + class="visually-hidden" + > <translate translate-context="Popup/Radio/Title/Noun">Exclude</translate> </label> </div> </td> <td> <div - v-for="(f, index) in filter.fields" + v-for="f in filter.fields" + :key="f.name" + :ref="f.name" class="ui field" - :key="(f.name, index)" - :ref="f.name"> + > <div :class="['ui', 'search', 'selection', 'dropdown', {'autocomplete': f.autocomplete}, {'multiple': f.type === 'list'}]"> - <i class="dropdown icon"></i> - <div class="default text">{{ f.placeholder }}</div> - <input :id="f.name" v-if="f.type === 'list' && config[f.name]" :value="config[f.name].join(',')" type="hidden"> - <div v-if="config[f.name]" class="ui menu"> + <i class="dropdown icon" /> + <div class="default text"> + {{ f.placeholder }} + </div> + <input + v-if="f.type === 'list' && config[f.name]" + :id="f.name" + :value="config[f.name].join(',')" + type="hidden" + > + <div + v-if="config[f.name]" + class="ui menu" + > <div - v-for="(v, index) in config[f.name]" + v-for="v in config[f.name]" + :key="v" class="ui item" :data-value="v" - :key="v"> - <template v-if="config.names"> - {{ config.names[index] }} - </template> - <template v-else>{{ v }}</template> + > + <template v-if="config.names"> + {{ config.names[index] }} + </template> + <template v-else> + {{ v }} + </template> </div> </div> </div> @@ -36,30 +59,48 @@ </td> <td> <a + v-if="checkResult" href="" + :class="['ui', {'success': checkResult.candidates.count > 10}, 'label']" @click.prevent="showCandidadesModal = !showCandidadesModal" - v-if="checkResult" - :class="['ui', {'success': checkResult.candidates.count > 10}, 'label']"> + > {{ checkResult.candidates.count }} tracks matching filter </a> - <modal v-if="checkResult" :show.sync="showCandidadesModal"> + <modal + v-if="checkResult" + :show.sync="showCandidadesModal" + > <h4 class="header"> - <translate translate-context="Popup/Radio/Title/Noun">Tracks matching filter</translate> + <translate translate-context="Popup/Radio/Title/Noun"> + Tracks matching filter + </translate> </h4> <div class="content"> <div class="description"> - <track-table v-if="checkResult.candidates.count > 0" :tracks="checkResult.candidates.sample"></track-table> + <track-table + v-if="checkResult.candidates.count > 0" + :tracks="checkResult.candidates.sample" + /> </div> </div> <div class="actions"> <button class="ui deny button"> - <translate translate-context="*/*/Button.Label/Verb">Cancel</translate> + <translate translate-context="*/*/Button.Label/Verb"> + Cancel + </translate> </button> </div> </modal> </td> <td> - <button @click="$emit('delete', index)" class="ui danger button"><translate translate-context="Content/Radio/Button.Label/Verb">Remove</translate></button> + <button + class="ui danger button" + @click="$emit('delete', index)" + > + <translate translate-context="Content/Radio/Button.Label/Verb"> + Remove + </translate> + </button> </td> </tr> </template> @@ -70,18 +111,16 @@ import _ from '@/lodash' import Modal from '@/components/semantic/Modal' import TrackTable from '@/components/audio/track/Table' -import BuilderFilter from './Filter' export default { components: { - BuilderFilter, TrackTable, Modal }, props: { - filter: {type: Object}, - config: {type: Object}, - index: {type: Number} + filter: { type: Object, required: true }, + config: { type: Object, required: true }, + index: { type: Number, required: true } }, data: function () { return { @@ -90,11 +129,16 @@ export default { exclude: this.config.not } }, + watch: { + exclude: function () { + this.fetchCandidates() + } + }, mounted: function () { - let self = this + const self = this this.filter.fields.forEach(f => { - let selector = ['.dropdown'] - let settings = { + const selector = ['.dropdown'] + const settings = { onChange: function (value, text, $choice) { value = $(this).dropdown('get value').split(',') if (f.type === 'list' && f.subtype === 'number') { @@ -126,7 +170,7 @@ export default { if (settings.fields.remoteValues) { return initialResponse } - return {results: initialResponse.results} + return { results: initialResponse.results } } } } @@ -135,20 +179,15 @@ export default { }, methods: { fetchCandidates: function () { - let self = this - let url = 'radios/radios/validate/' + const self = this + const url = 'radios/radios/validate/' let final = _.clone(this.config) final.type = this.filter.type - final = {'filters': [final]} + final = { filters: [final] } axios.post(url, final).then((response) => { self.checkResult = response.data.filters[0] }) } - }, - watch: { - exclude: function () { - this.fetchCandidates() - } } } </script> diff --git a/front/src/components/manage/ChannelsTable.vue b/front/src/components/manage/ChannelsTable.vue index 337ecfbfefddd0853b77bdbce1b98b86b45b700d..129860c1ed505f0b0a6921f0f0c303db8e659ed3 100644 --- a/front/src/components/manage/ChannelsTable.vue +++ b/front/src/components/manage/ChannelsTable.vue @@ -5,75 +5,163 @@ <div class="ui six wide field"> <label for="channel-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label> <form @submit.prevent="search.query = $refs.search.value"> - <input id="channel-search" name="search" ref="search" type="text" :value="search.query" :placeholder="labels.searchPlaceholder" /> + <input + id="channel-search" + ref="search" + name="search" + type="text" + :value="search.query" + :placeholder="labels.searchPlaceholder" + > </form> </div> <div class="field"> <label for="channel-category"><translate translate-context="*/*/*">Category</translate></label> - <select id="channel-category" class="ui dropdown" @change="addSearchToken('category', $event.target.value)" :value="getTokenValue('category', '')"> - <option value=""><translate translate-context="Content/*/Dropdown">All</translate></option> - <option value="podcast">{{ sharedLabels.fields.content_category.choices.podcast }}</option> - <option value="music">{{ sharedLabels.fields.content_category.choices.music }}</option> - <option value="other">{{ sharedLabels.fields.content_category.choices.other }}</option> + <select + id="channel-category" + class="ui dropdown" + :value="getTokenValue('category', '')" + @change="addSearchToken('category', $event.target.value)" + > + <option value=""> + <translate translate-context="Content/*/Dropdown"> + All + </translate> + </option> + <option value="podcast"> + {{ sharedLabels.fields.content_category.choices.podcast }} + </option> + <option value="music"> + {{ sharedLabels.fields.content_category.choices.music }} + </option> + <option value="other"> + {{ sharedLabels.fields.content_category.choices.other }} + </option> </select> </div> <div class="field"> <label for="channel-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> - <select id="channel-ordering" class="ui dropdown" v-model="ordering"> - <option v-for="option in orderingOptions" :value="option[0]"> + <select + id="channel-ordering" + v-model="ordering" + class="ui dropdown" + > + <option + v-for="(option, key) in orderingOptions" + :key="key" + :value="option[0]" + > {{ sharedLabels.filters[option[1]] }} </option> </select> </div> <div class="field"> <label for="channel-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label> - <select id="channel-ordering-direction" class="ui dropdown" v-model="orderingDirection"> - <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option> - <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option> + <select + id="channel-ordering-direction" + v-model="orderingDirection" + class="ui dropdown" + > + <option value="+"> + <translate translate-context="Content/Search/Dropdown"> + Ascending + </translate> + </option> + <option value="-"> + <translate translate-context="Content/Search/Dropdown"> + Descending + </translate> + </option> </select> </div> </div> - </div> + </div> <div class="dimmable"> - <div v-if="isLoading" class="ui active inverted dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui active inverted dimmer" + > + <div class="ui loader" /> </div> <action-table v-if="result" - @action-launched="fetchData" :objects-data="result" :actions="actions" action-url="manage/library/artists/action/" - :filters="actionFilters"> + :filters="actionFilters" + @action-launched="fetchData" + > <template slot="header-cells"> - <th><translate translate-context="*/*/*/Noun">Name</translate></th> - <th><translate translate-context="*/*/*/Noun">Account</translate></th> - <th><translate translate-context="Content/Moderation/*/Noun">Domain</translate></th> - <th><translate translate-context="*/*/*">Albums</translate></th> - <th><translate translate-context="*/*/*">Tracks</translate></th> - <th><translate translate-context="Content/*/*/Noun">Creation date</translate></th> + <th> + <translate translate-context="*/*/*/Noun"> + Name + </translate> + </th> + <th> + <translate translate-context="*/*/*/Noun"> + Account + </translate> + </th> + <th> + <translate translate-context="Content/Moderation/*/Noun"> + Domain + </translate> + </th> + <th> + <translate translate-context="*/*/*"> + Albums + </translate> + </th> + <th> + <translate translate-context="*/*/*"> + Tracks + </translate> + </th> + <th> + <translate translate-context="Content/*/*/Noun"> + Creation date + </translate> + </th> </template> - <template slot="row-cells" slot-scope="scope"> + <template + slot="row-cells" + slot-scope="scope" + > <td> - <router-link :to="{name: 'manage.channels.detail', params: {id: scope.obj.actor.full_username }}">{{ scope.obj.artist.name }}</router-link> + <router-link :to="{name: 'manage.channels.detail', params: {id: scope.obj.actor.full_username }}"> + {{ scope.obj.artist.name }} + </router-link> </td> <td> <router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: scope.obj.attributed_to.full_username }}"> - <i class="wrench icon"></i> + <i class="wrench icon" /> <span class="visually-hidden">{{ labels.openModeration }}</span> </router-link> - <a href="" class="discrete link" @click.prevent="addSearchToken('account', scope.obj.attributed_to.full_username)">{{ scope.obj.attributed_to.preferred_username }}</a> + <a + href="" + class="discrete link" + @click.prevent="addSearchToken('account', scope.obj.attributed_to.full_username)" + >{{ scope.obj.attributed_to.preferred_username }}</a> </td> <td> <template v-if="!scope.obj.is_local"> <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: scope.obj.attributed_to.domain }}"> - <i class="wrench icon"></i> + <i class="wrench icon" /> <span class="visually-hidden">{{ labels.openModeration }}</span> </router-link> - <a href="" class="discrete link" @click.prevent="addSearchToken('domain', scope.obj.attributed_to.domain)">{{ scope.obj.attributed_to.domain }}</a> + <a + href="" + class="discrete link" + @click.prevent="addSearchToken('domain', scope.obj.attributed_to.domain)" + >{{ scope.obj.attributed_to.domain }}</a> </template> - <a href="" v-else class="ui tiny accent icon link label" @click.prevent="addSearchToken('domain', scope.obj.attributed_to.domain)"> - <i class="home icon"></i> + <a + v-else + href="" + class="ui tiny accent icon link label" + @click.prevent="addSearchToken('domain', scope.obj.attributed_to.domain)" + > + <i class="home icon" /> <translate translate-context="Content/Moderation/*/Short, Noun">Local</translate> </a> </td> @@ -84,7 +172,7 @@ {{ scope.obj.artist.tracks_count }} </td> <td> - <human-date :date="scope.obj.creation_date"></human-date> + <human-date :date="scope.obj.creation_date" /> </td> </template> </action-table> @@ -92,16 +180,18 @@ <div> <pagination v-if="result && result.count > paginateBy" - @page-changed="selectPage" :compact="true" :current="page" :paginate-by="paginateBy" :total="result.count" - ></pagination> + @page-changed="selectPage" + /> <span v-if="result && result.results.length > 0"> - <translate translate-context="Content/*/Paragraph" - :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}"> + <translate + translate-context="Content/*/Paragraph" + :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}" + > Showing results %{ start }-%{ end } on %{ total } </translate> </span> @@ -113,25 +203,24 @@ import axios from 'axios' import _ from '@/lodash' import time from '@/utils/time' -import {normalizeQuery, parseTokens} from '@/search' +import { normalizeQuery, parseTokens } from '@/search' import Pagination from '@/components/Pagination' import ActionTable from '@/components/common/ActionTable' import OrderingMixin from '@/components/mixins/Ordering' import TranslationsMixin from '@/components/mixins/Translations' import SmartSearchMixin from '@/components/mixins/SmartSearch' - export default { - mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], - props: { - filters: {type: Object, required: false}, - }, components: { Pagination, ActionTable }, + mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], + props: { + filters: { type: Object, required: false, default: () => { return {} } } + }, data () { - let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') + const defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') return { time, isLoading: false, @@ -146,36 +235,10 @@ export default { ordering: defaultOrdering.field, orderingOptions: [ ['creation_date', 'creation_date'], - ["name", "name"], + ['name', 'name'] ] } }, - created () { - this.fetchData() - }, - methods: { - fetchData () { - let params = _.merge({ - 'page': this.page, - 'page_size': this.paginateBy, - 'q': this.search.query, - 'ordering': this.getOrderingAsString() - }, this.filters) - let self = this - self.isLoading = true - self.checked = [] - axios.get('/manage/channels/', {params: params}).then((response) => { - self.result = response.data - self.isLoading = false - }, error => { - self.isLoading = false - self.errors = error.backendErrors - }) - }, - selectPage: function (page) { - this.page = page - }, - }, computed: { labels () { return { @@ -184,7 +247,7 @@ export default { } }, actionFilters () { - var currentFilters = { + const currentFilters = { q: this.search.query } if (this.filters) { @@ -222,6 +285,32 @@ export default { orderingDirection () { this.fetchData() } + }, + created () { + this.fetchData() + }, + methods: { + fetchData () { + const params = _.merge({ + page: this.page, + page_size: this.paginateBy, + q: this.search.query, + ordering: this.getOrderingAsString() + }, this.filters) + const self = this + self.isLoading = true + self.checked = [] + axios.get('/manage/channels/', { params: params }).then((response) => { + self.result = response.data + self.isLoading = false + }, error => { + self.isLoading = false + self.errors = error.backendErrors + }) + }, + selectPage: function (page) { + this.page = page + } } } </script> diff --git a/front/src/components/manage/library/AlbumsTable.vue b/front/src/components/manage/library/AlbumsTable.vue index 3bff5035296438cf5133123c7b49b3eb69eb50cb..bdaaf9c6854245f72ae0a43f95ae1a40ef5ced6c 100644 --- a/front/src/components/manage/library/AlbumsTable.vue +++ b/front/src/components/manage/library/AlbumsTable.vue @@ -5,66 +5,139 @@ <div class="ui six wide field"> <label for="albums-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label> <form @submit.prevent="search.query = $refs.search.value"> - <input id="albums-search" name="search" ref="search" type="text" :value="search.query" :placeholder="labels.searchPlaceholder" /> + <input + id="albums-search" + ref="search" + name="search" + type="text" + :value="search.query" + :placeholder="labels.searchPlaceholder" + > </form> </div> <div class="field"> <label for="albums-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> - <select id="albums-ordering" class="ui dropdown" v-model="ordering"> - <option v-for="option in orderingOptions" :value="option[0]"> + <select + id="albums-ordering" + v-model="ordering" + class="ui dropdown" + > + <option + v-for="(option, key) in orderingOptions" + :key="key" + :value="option[0]" + > {{ sharedLabels.filters[option[1]] }} </option> </select> </div> <div class="field"> <label for="albums-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label> - <select class="ui dropdown" v-model="orderingDirection"> - <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option> - <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option> + <select + v-model="orderingDirection" + class="ui dropdown" + > + <option value="+"> + <translate translate-context="Content/Search/Dropdown"> + Ascending + </translate> + </option> + <option value="-"> + <translate translate-context="Content/Search/Dropdown"> + Descending + </translate> + </option> </select> </div> </div> - </div> + </div> <div class="dimmable"> - <div v-if="isLoading" class="ui active inverted dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui active inverted dimmer" + > + <div class="ui loader" /> </div> <action-table v-if="result" - @action-launched="fetchData" :objects-data="result" :actions="actions" action-url="manage/library/albums/action/" - :filters="actionFilters"> + :filters="actionFilters" + @action-launched="fetchData" + > <template slot="header-cells"> - <th><translate translate-context="*/*/*/Noun">Title</translate></th> - <th><translate translate-context="*/*/*/Noun">Artist</translate></th> - <th><translate translate-context="Content/Moderation/*/Noun">Domain</translate></th> - <th><translate translate-context="*/*/*">Tracks</translate></th> - <th><translate translate-context="Content/*/*/Noun">Release date</translate></th> - <th><translate translate-context="Content/*/*/Noun">Creation date</translate></th> + <th> + <translate translate-context="*/*/*/Noun"> + Title + </translate> + </th> + <th> + <translate translate-context="*/*/*/Noun"> + Artist + </translate> + </th> + <th> + <translate translate-context="Content/Moderation/*/Noun"> + Domain + </translate> + </th> + <th> + <translate translate-context="*/*/*"> + Tracks + </translate> + </th> + <th> + <translate translate-context="Content/*/*/Noun"> + Release date + </translate> + </th> + <th> + <translate translate-context="Content/*/*/Noun"> + Creation date + </translate> + </th> </template> - <template slot="row-cells" slot-scope="scope"> + <template + slot="row-cells" + slot-scope="scope" + > <td> - <router-link :to="{name: 'manage.library.albums.detail', params: {id: scope.obj.id }}">{{ scope.obj.title }}</router-link> + <router-link :to="{name: 'manage.library.albums.detail', params: {id: scope.obj.id }}"> + {{ scope.obj.title }} + </router-link> </td> <td> <router-link :to="{name: 'manage.library.artists.detail', params: {id: scope.obj.artist.id }}"> - <i class="wrench icon"></i> + <i class="wrench icon" /> <span class="visually-hidden">{{ labels.openModeration }}</span> </router-link> - <a href="" class="discrete link" @click.prevent="addSearchToken('artist', scope.obj.artist.name)" :title="scope.obj.artist.name">{{ scope.obj.artist.name }}</a> + <a + href="" + class="discrete link" + :title="scope.obj.artist.name" + @click.prevent="addSearchToken('artist', scope.obj.artist.name)" + >{{ scope.obj.artist.name }}</a> </td> <td> <template v-if="!scope.obj.is_local"> <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: scope.obj.domain }}"> - <i class="wrench icon"></i> + <i class="wrench icon" /> <span class="visually-hidden">{{ labels.openModeration }}</span> </router-link> - <a href="" class="discrete link" @click.prevent="addSearchToken('domain', scope.obj.domain)">{{ scope.obj.domain }}</a> + <a + href="" + class="discrete link" + @click.prevent="addSearchToken('domain', scope.obj.domain)" + >{{ scope.obj.domain }}</a> </template> - <a href="" v-else class="ui tiny accent icon link label" @click.prevent="addSearchToken('domain', scope.obj.domain)"> - <i class="home icon"></i> + <a + v-else + href="" + class="ui tiny accent icon link label" + @click.prevent="addSearchToken('domain', scope.obj.domain)" + > + <i class="home icon" /> <translate translate-context="Content/Moderation/*/Short, Noun">Local</translate> </a> </td> @@ -72,12 +145,19 @@ {{ scope.obj.tracks_count }} </td> <td> - <human-date v-if="scope.obj.release_date" :date="scope.obj.release_date"></human-date> - <translate v-else translate-context="*/*/*">N/A</translate> - + <human-date + v-if="scope.obj.release_date" + :date="scope.obj.release_date" + /> + <translate + v-else + translate-context="*/*/*" + > + N/A + </translate> </td> <td> - <human-date :date="scope.obj.creation_date"></human-date> + <human-date :date="scope.obj.creation_date" /> </td> </template> </action-table> @@ -85,16 +165,18 @@ <div> <pagination v-if="result && result.count > paginateBy" - @page-changed="selectPage" :compact="true" :current="page" :paginate-by="paginateBy" :total="result.count" - ></pagination> + @page-changed="selectPage" + /> <span v-if="result && result.results.length > 0"> - <translate translate-context="Content/*/Paragraph" - :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}"> + <translate + translate-context="Content/*/Paragraph" + :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}" + > Showing results %{ start }-%{ end } on %{ total } </translate> </span> @@ -106,25 +188,24 @@ import axios from 'axios' import _ from '@/lodash' import time from '@/utils/time' -import {normalizeQuery, parseTokens} from '@/search' +import { normalizeQuery, parseTokens } from '@/search' import Pagination from '@/components/Pagination' import ActionTable from '@/components/common/ActionTable' import OrderingMixin from '@/components/mixins/Ordering' import TranslationsMixin from '@/components/mixins/Translations' import SmartSearchMixin from '@/components/mixins/SmartSearch' - export default { - mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], - props: { - filters: {type: Object, required: false}, - }, components: { Pagination, ActionTable }, + mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], + props: { + filters: { type: Object, required: false, default: () => { return {} } } + }, data () { - let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') + const defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') return { time, isLoading: false, @@ -140,36 +221,10 @@ export default { orderingOptions: [ ['creation_date', 'creation_date'], ['release_date', 'release_date'], - ["name", "name"], + ['name', 'name'] ] } }, - created () { - this.fetchData() - }, - methods: { - fetchData () { - let params = _.merge({ - 'page': this.page, - 'page_size': this.paginateBy, - 'q': this.search.query, - 'ordering': this.getOrderingAsString() - }, this.filters) - let self = this - self.isLoading = true - self.checked = [] - axios.get('/manage/library/albums/', {params: params}).then((response) => { - self.result = response.data - self.isLoading = false - }, error => { - self.isLoading = false - self.errors = error.backendErrors - }) - }, - selectPage: function (page) { - this.page = page - }, - }, computed: { labels () { return { @@ -178,7 +233,7 @@ export default { } }, actionFilters () { - var currentFilters = { + const currentFilters = { q: this.search.query } if (this.filters) { @@ -188,8 +243,8 @@ export default { } }, actions () { - let deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete') - let confirmationMessage = this.$pgettext('Popup/*/Paragraph', 'The selected albums will be removed, as well as associated tracks, uploads, favorites and listening history. This action is irreversible.') + const deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete') + const confirmationMessage = this.$pgettext('Popup/*/Paragraph', 'The selected albums will be removed, as well as associated tracks, uploads, favorites and listening history. This action is irreversible.') return [ { name: 'delete', @@ -197,8 +252,8 @@ export default { confirmationMessage: confirmationMessage, isDangerous: true, allowAll: false, - confirmColor: 'danger', - }, + confirmColor: 'danger' + } ] } }, @@ -216,6 +271,32 @@ export default { orderingDirection () { this.fetchData() } + }, + created () { + this.fetchData() + }, + methods: { + fetchData () { + const params = _.merge({ + page: this.page, + page_size: this.paginateBy, + q: this.search.query, + ordering: this.getOrderingAsString() + }, this.filters) + const self = this + self.isLoading = true + self.checked = [] + axios.get('/manage/library/albums/', { params: params }).then((response) => { + self.result = response.data + self.isLoading = false + }, error => { + self.isLoading = false + self.errors = error.backendErrors + }) + }, + selectPage: function (page) { + this.page = page + } } } </script> diff --git a/front/src/components/manage/library/ArtistsTable.vue b/front/src/components/manage/library/ArtistsTable.vue index 454496d35303c3a41db4fa16f96c0c046658e606..fa332278d5a24780a707bb713a78bfa86897d985 100644 --- a/front/src/components/manage/library/ArtistsTable.vue +++ b/front/src/components/manage/library/ArtistsTable.vue @@ -5,54 +5,123 @@ <div class="ui six wide field"> <label for="artists-serarch"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label> <form @submit.prevent="search.query = $refs.search.value"> - <input id="artists-search" name="search" ref="search" type="text" :value="search.query" :placeholder="labels.searchPlaceholder" /> + <input + id="artists-search" + ref="search" + name="search" + type="text" + :value="search.query" + :placeholder="labels.searchPlaceholder" + > </form> </div> <div class="field"> <label for="artists-category"><translate translate-context="*/*/*">Category</translate></label> - <select id="artists-category" class="ui dropdown" @change="addSearchToken('category', $event.target.value)" :value="getTokenValue('category', '')"> - <option value=""><translate translate-context="Content/*/Dropdown">All</translate></option> - <option value="podcast">{{ sharedLabels.fields.content_category.choices.podcast }}</option> - <option value="music">{{ sharedLabels.fields.content_category.choices.music }}</option> - <option value="other">{{ sharedLabels.fields.content_category.choices.other }}</option> + <select + id="artists-category" + class="ui dropdown" + :value="getTokenValue('category', '')" + @change="addSearchToken('category', $event.target.value)" + > + <option value=""> + <translate translate-context="Content/*/Dropdown"> + All + </translate> + </option> + <option value="podcast"> + {{ sharedLabels.fields.content_category.choices.podcast }} + </option> + <option value="music"> + {{ sharedLabels.fields.content_category.choices.music }} + </option> + <option value="other"> + {{ sharedLabels.fields.content_category.choices.other }} + </option> </select> </div> <div class="field"> <label for="artists-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> - <select id="artists-ordering" class="ui dropdown" v-model="ordering"> - <option v-for="option in orderingOptions" :value="option[0]"> + <select + id="artists-ordering" + v-model="ordering" + class="ui dropdown" + > + <option + v-for="(option, key) in orderingOptions" + :key="key" + :value="option[0]" + > {{ sharedLabels.filters[option[1]] }} </option> </select> </div> <div class="field"> <label for="artists-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label> - <select id="artists-ordering-direction" class="ui dropdown" v-model="orderingDirection"> - <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option> - <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option> + <select + id="artists-ordering-direction" + v-model="orderingDirection" + class="ui dropdown" + > + <option value="+"> + <translate translate-context="Content/Search/Dropdown"> + Ascending + </translate> + </option> + <option value="-"> + <translate translate-context="Content/Search/Dropdown"> + Descending + </translate> + </option> </select> </div> </div> - </div> + </div> <div class="dimmable"> - <div v-if="isLoading" class="ui active inverted dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui active inverted dimmer" + > + <div class="ui loader" /> </div> <action-table v-if="result" - @action-launched="fetchData" :objects-data="result" :actions="actions" action-url="manage/library/artists/action/" - :filters="actionFilters"> + :filters="actionFilters" + @action-launched="fetchData" + > <template slot="header-cells"> - <th><translate translate-context="*/*/*/Noun">Name</translate></th> - <th><translate translate-context="Content/Moderation/*/Noun">Domain</translate></th> - <th><translate translate-context="*/*/*">Albums</translate></th> - <th><translate translate-context="*/*/*">Tracks</translate></th> - <th><translate translate-context="Content/*/*/Noun">Creation date</translate></th> + <th> + <translate translate-context="*/*/*/Noun"> + Name + </translate> + </th> + <th> + <translate translate-context="Content/Moderation/*/Noun"> + Domain + </translate> + </th> + <th> + <translate translate-context="*/*/*"> + Albums + </translate> + </th> + <th> + <translate translate-context="*/*/*"> + Tracks + </translate> + </th> + <th> + <translate translate-context="Content/*/*/Noun"> + Creation date + </translate> + </th> </template> - <template slot="row-cells" slot-scope="scope"> + <template + slot="row-cells" + slot-scope="scope" + > <td> <router-link :to="getUrl(scope.obj)"> {{ scope.obj.name }} @@ -61,12 +130,22 @@ <td> <template v-if="!scope.obj.is_local"> <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: scope.obj.domain }}"> - <i class="wrench icon"></i> + <i class="wrench icon" /> </router-link> - <a href="" class="discrete link" @click.prevent="addSearchToken('domain', scope.obj.domain)" :title="scope.obj.domain">{{ scope.obj.domain }}</a> + <a + href="" + class="discrete link" + :title="scope.obj.domain" + @click.prevent="addSearchToken('domain', scope.obj.domain)" + >{{ scope.obj.domain }}</a> </template> - <a href="" v-else class="ui tiny accent icon link label" @click.prevent="addSearchToken('domain', scope.obj.domain)"> - <i class="home icon"></i> + <a + v-else + href="" + class="ui tiny accent icon link label" + @click.prevent="addSearchToken('domain', scope.obj.domain)" + > + <i class="home icon" /> <translate translate-context="Content/Moderation/*/Short, Noun">Local</translate> </a> </td> @@ -77,7 +156,7 @@ {{ scope.obj.tracks_count }} </td> <td> - <human-date :date="scope.obj.creation_date"></human-date> + <human-date :date="scope.obj.creation_date" /> </td> </template> </action-table> @@ -85,16 +164,18 @@ <div> <pagination v-if="result && result.count > paginateBy" - @page-changed="selectPage" :compact="true" :current="page" :paginate-by="paginateBy" :total="result.count" - ></pagination> + @page-changed="selectPage" + /> <span v-if="result && result.results.length > 0"> - <translate translate-context="Content/*/Paragraph" - :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}"> + <translate + translate-context="Content/*/Paragraph" + :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}" + > Showing results %{ start }-%{ end } on %{ total } </translate> </span> @@ -106,25 +187,24 @@ import axios from 'axios' import _ from '@/lodash' import time from '@/utils/time' -import {normalizeQuery, parseTokens} from '@/search' +import { normalizeQuery, parseTokens } from '@/search' import Pagination from '@/components/Pagination' import ActionTable from '@/components/common/ActionTable' import OrderingMixin from '@/components/mixins/Ordering' import TranslationsMixin from '@/components/mixins/Translations' import SmartSearchMixin from '@/components/mixins/SmartSearch' - export default { - mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], - props: { - filters: {type: Object, required: false}, - }, components: { Pagination, ActionTable }, + mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], + props: { + filters: { type: Object, required: false, default: () => { return {} } } + }, data () { - let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') + const defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') return { time, isLoading: false, @@ -139,42 +219,10 @@ export default { ordering: defaultOrdering.field, orderingOptions: [ ['creation_date', 'creation_date'], - ["name", "name"], + ['name', 'name'] ] } }, - created () { - this.fetchData() - }, - methods: { - getUrl (artist) { - if (artist.channel) { - return {name: 'manage.channels.detail', params: {id: artist.channel }} - } - return {name: 'manage.library.artists.detail', params: {id: artist.id }} - }, - fetchData () { - let params = _.merge({ - 'page': this.page, - 'page_size': this.paginateBy, - 'q': this.search.query, - 'ordering': this.getOrderingAsString() - }, this.filters) - let self = this - self.isLoading = true - self.checked = [] - axios.get('/manage/library/artists/', {params: params}).then((response) => { - self.result = response.data - self.isLoading = false - }, error => { - self.isLoading = false - self.errors = error.backendErrors - }) - }, - selectPage: function (page) { - this.page = page - }, - }, computed: { labels () { return { @@ -182,7 +230,7 @@ export default { } }, actionFilters () { - var currentFilters = { + const currentFilters = { q: this.search.query } if (this.filters) { @@ -192,8 +240,8 @@ export default { } }, actions () { - let deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete') - let confirmationMessage = this.$pgettext('Popup/*/Paragraph', 'The selected artist will be removed, as well as associated uploads, tracks, albums, favorites and listening history. This action is irreversible.') + const deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete') + const confirmationMessage = this.$pgettext('Popup/*/Paragraph', 'The selected artist will be removed, as well as associated uploads, tracks, albums, favorites and listening history. This action is irreversible.') return [ { name: 'delete', @@ -201,8 +249,8 @@ export default { confirmationMessage: confirmationMessage, isDangerous: true, allowAll: false, - confirmColor: 'danger', - }, + confirmColor: 'danger' + } ] } }, @@ -220,6 +268,38 @@ export default { orderingDirection () { this.fetchData() } + }, + created () { + this.fetchData() + }, + methods: { + getUrl (artist) { + if (artist.channel) { + return { name: 'manage.channels.detail', params: { id: artist.channel } } + } + return { name: 'manage.library.artists.detail', params: { id: artist.id } } + }, + fetchData () { + const params = _.merge({ + page: this.page, + page_size: this.paginateBy, + q: this.search.query, + ordering: this.getOrderingAsString() + }, this.filters) + const self = this + self.isLoading = true + self.checked = [] + axios.get('/manage/library/artists/', { params: params }).then((response) => { + self.result = response.data + self.isLoading = false + }, error => { + self.isLoading = false + self.errors = error.backendErrors + }) + }, + selectPage: function (page) { + this.page = page + } } } </script> diff --git a/front/src/components/manage/library/EditsCardList.vue b/front/src/components/manage/library/EditsCardList.vue index 01196238e775ab3cdaa7d6babf99fac519734cf9..d253aaf6950c6ac43e0c96445fd5d0810c760b96 100644 --- a/front/src/components/manage/library/EditsCardList.vue +++ b/front/src/components/manage/library/EditsCardList.vue @@ -1,77 +1,127 @@ <template> <div class="ui text container"> - <slot></slot> + <slot /> <div class="ui inline form"> <div class="fields"> <div class="ui field"> <label for="search-edits"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label> <form @submit.prevent="search.query = $refs.search.value"> - <input id="search-edits" name="search" ref="search" type="text" :value="search.query" :placeholder="labels.searchPlaceholder" /> + <input + id="search-edits" + ref="search" + name="search" + type="text" + :value="search.query" + :placeholder="labels.searchPlaceholder" + > </form> </div> <div class="field"> <label for="edit-status"><translate translate-context="*/*/*">Status</translate></label> - <select id="edit-status" class="ui dropdown" @change="addSearchToken('is_approved', $event.target.value)" :value="getTokenValue('is_approved', '')"> + <select + id="edit-status" + class="ui dropdown" + :value="getTokenValue('is_approved', '')" + @change="addSearchToken('is_approved', $event.target.value)" + > <option value=""> - <translate translate-context="Content/*/Dropdown">All</translate> + <translate translate-context="Content/*/Dropdown"> + All + </translate> </option> <option value="null"> - <translate translate-context="Content/Admin/*/Noun">Pending review</translate> + <translate translate-context="Content/Admin/*/Noun"> + Pending review + </translate> </option> <option value="yes"> - <translate translate-context="Content/*/*/Short">Approved</translate> + <translate translate-context="Content/*/*/Short"> + Approved + </translate> </option> <option value="no"> - <translate translate-context="Content/Library/*/Short">Rejected</translate> + <translate translate-context="Content/Library/*/Short"> + Rejected + </translate> </option> </select> </div> <div class="field"> <label for="edit-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> - <select id="edit-ordering" class="ui dropdown" v-model="ordering"> - <option v-for="option in orderingOptions" :value="option[0]"> + <select + id="edit-ordering" + v-model="ordering" + class="ui dropdown" + > + <option + v-for="(option, key) in orderingOptions" + :key="key" + :value="option[0]" + > {{ sharedLabels.filters[option[1]] }} </option> </select> </div> <div class="field"> <label for="edit-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Order</translate></label> - <select id="edit-ordering-direction" class="ui dropdown" v-model="orderingDirection"> - <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option> - <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option> + <select + id="edit-ordering-direction" + v-model="orderingDirection" + class="ui dropdown" + > + <option value="+"> + <translate translate-context="Content/Search/Dropdown"> + Ascending + </translate> + </option> + <option value="-"> + <translate translate-context="Content/Search/Dropdown"> + Descending + </translate> + </option> </select> </div> </div> </div> <div class="dimmable"> - <div v-if="isLoading" class="ui active inverted dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui active inverted dimmer" + > + <div class="ui loader" /> </div> <div v-else-if="result && result.count > 0"> <edit-card + v-for="obj in result.results" + :key="obj.uuid" :obj="obj" :current-state="getCurrentState(obj.target)" - v-for="obj in result.results" @deleted="handle('delete', obj.uuid, null)" @approved="handle('approved', obj.uuid, $event)" - :key="obj.uuid" /> + /> </div> - <empty-state v-else :refresh="true" @refresh="fetchData()"></empty-state> + <empty-state + v-else + :refresh="true" + @refresh="fetchData()" + /> </div> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <div> <pagination v-if="result && result.count > paginateBy" - @page-changed="selectPage" :compact="true" :current="page" :paginate-by="paginateBy" :total="result.count" - ></pagination> + @page-changed="selectPage" + /> <span v-if="result && result.results.length > 0"> - <translate translate-context="Content/*/Paragraph" - :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}"> + <translate + translate-context="Content/*/Paragraph" + :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}" + > Showing results %{ start }-%{ end } on %{ total } </translate> </span> @@ -87,23 +137,22 @@ import Pagination from '@/components/Pagination' import OrderingMixin from '@/components/mixins/Ordering' import TranslationsMixin from '@/components/mixins/Translations' import EditCard from '@/components/library/EditCard' -import {normalizeQuery, parseTokens} from '@/search' +import { normalizeQuery, parseTokens } from '@/search' import SmartSearchMixin from '@/components/mixins/SmartSearch' import edits from '@/edits' - export default { - mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], - props: { - filters: {type: Object, required: false} - }, components: { Pagination, EditCard }, + mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], + props: { + filters: { type: Object, required: false, default: () => { return {} } } + }, data () { - let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') + const defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') return { time, isLoading: false, @@ -118,28 +167,50 @@ export default { ordering: defaultOrdering.field, orderingOptions: [ ['creation_date', 'creation_date'], - ['applied_date', 'applied_date'], + ['applied_date', 'applied_date'] ], targets: { track: {} } } }, + computed: { + labels () { + return { + searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by account, summary, domain…') + } + } + }, + watch: { + search (newValue) { + this.page = 1 + this.fetchData() + }, + page () { + this.fetchData() + }, + ordering () { + this.fetchData() + }, + orderingDirection () { + this.fetchData() + } + }, created () { this.fetchData() }, methods: { fetchData () { - let params = _.merge({ - 'page': this.page, - 'page_size': this.paginateBy, - 'q': this.search.query, - 'ordering': this.getOrderingAsString() + const params = _.merge({ + page: this.page, + page_size: this.paginateBy, + q: this.search.query, + ordering: this.getOrderingAsString() }, this.filters) - let self = this + const self = this self.isLoading = true this.result = null - axios.get('mutations/', {params: params}).then((response) => { + axios.get('mutations/', { params: params }).then((response) => { self.result = response.data self.isLoading = false self.fetchTargets() @@ -151,25 +222,25 @@ export default { fetchTargets () { // we request target data via the API so we can display previous state // additionnal data next to the edit card - let self = this - let typesAndIds = { + const self = this + const typesAndIds = { track: { url: 'tracks/', - ids: [], + ids: [] } } this.result.results.forEach((m) => { if (!m.target || !typesAndIds[m.target.type]) { return } - typesAndIds[m.target.type]['ids'].push(m.target.id) + typesAndIds[m.target.type].ids.push(m.target.id) }) Object.keys(typesAndIds).forEach((k) => { - let config = typesAndIds[k] + const config = typesAndIds[k] if (config.ids.length === 0) { return } - axios.get(config.url, {params: {id: _.uniq(config.ids), hidden: 'null'}}).then((response) => { + axios.get(config.url, { params: { id: _.uniq(config.ids), hidden: 'null' } }).then((response) => { response.data.results.forEach((e) => { self.$set(self.targets[k], e.id, { payload: e, @@ -204,28 +275,6 @@ export default { } return {} } - }, - computed: { - labels () { - return { - searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by account, summary, domain…') - } - }, - }, - watch: { - search (newValue) { - this.page = 1 - this.fetchData() - }, - page () { - this.fetchData() - }, - ordering () { - this.fetchData() - }, - orderingDirection () { - this.fetchData() - } } } </script> diff --git a/front/src/components/manage/library/LibrariesTable.vue b/front/src/components/manage/library/LibrariesTable.vue index b5bb8b9b8051c91676d753d0d6e2579f6d6b9c03..3c3cc77c4a5e6987a80b2d5c9d38797ace5e8f0f 100644 --- a/front/src/components/manage/library/LibrariesTable.vue +++ b/front/src/components/manage/library/LibrariesTable.vue @@ -5,74 +5,168 @@ <div class="ui six wide field"> <label for="libraries-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label> <form @submit.prevent="search.query = $refs.search.value"> - <input id="libraries-search" name="search" ref="search" type="text" :value="search.query" :placeholder="labels.searchPlaceholder" /> + <input + id="libraries-search" + ref="search" + name="search" + type="text" + :value="search.query" + :placeholder="labels.searchPlaceholder" + > </form> </div> <div class="field"> <label for="libraries-visibility"><translate translate-context="*/*/*">Visibility</translate></label> - <select id="libraries-visibility" class="ui dropdown" @change="addSearchToken('privacy_level', $event.target.value)" :value="getTokenValue('privacy_level', '')"> - <option value=""><translate translate-context="Content/*/Dropdown">All</translate></option> - <option value="me">{{ sharedLabels.fields.privacy_level.shortChoices.me }}</option> - <option value="instance">{{ sharedLabels.fields.privacy_level.shortChoices.instance }}</option> - <option value="everyone">{{ sharedLabels.fields.privacy_level.shortChoices.everyone }}</option> + <select + id="libraries-visibility" + class="ui dropdown" + :value="getTokenValue('privacy_level', '')" + @change="addSearchToken('privacy_level', $event.target.value)" + > + <option value=""> + <translate translate-context="Content/*/Dropdown"> + All + </translate> + </option> + <option value="me"> + {{ sharedLabels.fields.privacy_level.shortChoices.me }} + </option> + <option value="instance"> + {{ sharedLabels.fields.privacy_level.shortChoices.instance }} + </option> + <option value="everyone"> + {{ sharedLabels.fields.privacy_level.shortChoices.everyone }} + </option> </select> </div> <div class="field"> <label for="libraries-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> - <select id="libraries-ordering" class="ui dropdown" v-model="ordering"> - <option v-for="option in orderingOptions" :value="option[0]"> + <select + id="libraries-ordering" + v-model="ordering" + class="ui dropdown" + > + <option + v-for="(option, key) in orderingOptions" + :key="key" + :value="option[0]" + > {{ sharedLabels.filters[option[1]] }} </option> </select> </div> <div class="field"> <label for="libraries-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label> - <select id="libraries-ordering-direction" class="ui dropdown" v-model="orderingDirection"> - <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option> - <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option> + <select + id="libraries-ordering-direction" + v-model="orderingDirection" + class="ui dropdown" + > + <option value="+"> + <translate translate-context="Content/Search/Dropdown"> + Ascending + </translate> + </option> + <option value="-"> + <translate translate-context="Content/Search/Dropdown"> + Descending + </translate> + </option> </select> </div> </div> - </div> + </div> <div class="dimmable"> - <div v-if="isLoading" class="ui active inverted dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui active inverted dimmer" + > + <div class="ui loader" /> </div> <action-table v-if="result" - @action-launched="fetchData" :objects-data="result" :actions="actions" action-url="manage/library/libraries/action/" - :filters="actionFilters"> + :filters="actionFilters" + @action-launched="fetchData" + > <template slot="header-cells"> - <th><translate translate-context="*/*/*/Noun">Name</translate></th> - <th><translate translate-context="*/*/*/Noun">Account</translate></th> - <th><translate translate-context="Content/Moderation/*/Noun">Domain</translate></th> - <th><translate translate-context="*/*/*">Visibility</translate></th> - <th><translate translate-context="*/*/*">Uploads</translate></th> - <th><translate translate-context="Content/Federation/*/Noun">Followers</translate></th> - <th><translate translate-context="Content/*/*/Noun">Creation date</translate></th> + <th> + <translate translate-context="*/*/*/Noun"> + Name + </translate> + </th> + <th> + <translate translate-context="*/*/*/Noun"> + Account + </translate> + </th> + <th> + <translate translate-context="Content/Moderation/*/Noun"> + Domain + </translate> + </th> + <th> + <translate translate-context="*/*/*"> + Visibility + </translate> + </th> + <th> + <translate translate-context="*/*/*"> + Uploads + </translate> + </th> + <th> + <translate translate-context="Content/Federation/*/Noun"> + Followers + </translate> + </th> + <th> + <translate translate-context="Content/*/*/Noun"> + Creation date + </translate> + </th> </template> - <template slot="row-cells" slot-scope="scope"> + <template + slot="row-cells" + slot-scope="scope" + > <td> - <router-link :to="{name: 'manage.library.libraries.detail', params: {id: scope.obj.uuid }}">{{ scope.obj.name }}</router-link> + <router-link :to="{name: 'manage.library.libraries.detail', params: {id: scope.obj.uuid }}"> + {{ scope.obj.name }} + </router-link> </td> <td> <router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: scope.obj.actor.full_username }}"> - <i class="wrench icon"></i> + <i class="wrench icon" /> </router-link> - <a href="" class="discrete link" @click.prevent="addSearchToken('account', scope.obj.actor.full_username)" :title="scope.obj.actor.full_username">{{ scope.obj.actor.preferred_username }}</a> + <a + href="" + class="discrete link" + :title="scope.obj.actor.full_username" + @click.prevent="addSearchToken('account', scope.obj.actor.full_username)" + >{{ scope.obj.actor.preferred_username }}</a> </td> <td> <template v-if="!scope.obj.is_local"> <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: scope.obj.domain }}"> - <i class="wrench icon"></i> + <i class="wrench icon" /> </router-link> - <a href="" class="discrete link" @click.prevent="addSearchToken('domain', scope.obj.domain)" :title="scope.obj.domain">{{ scope.obj.domain }}</a> + <a + href="" + class="discrete link" + :title="scope.obj.domain" + @click.prevent="addSearchToken('domain', scope.obj.domain)" + >{{ scope.obj.domain }}</a> </template> - <a href="" v-else class="ui tiny accent icon link label" @click.prevent="addSearchToken('domain', scope.obj.domain)"> - <i class="home icon"></i> + <a + v-else + href="" + class="ui tiny accent icon link label" + @click.prevent="addSearchToken('domain', scope.obj.domain)" + > + <i class="home icon" /> <translate translate-context="Content/Moderation/*/Short, Noun">Local</translate> </a> </td> @@ -80,8 +174,9 @@ <a href="" class="discrete link" + :title="sharedLabels.fields.privacy_level.shortChoices[scope.obj.privacy_level]" @click.prevent="addSearchToken('privacy_level', scope.obj.privacy_level)" - :title="sharedLabels.fields.privacy_level.shortChoices[scope.obj.privacy_level]"> + > {{ sharedLabels.fields.privacy_level.shortChoices[scope.obj.privacy_level] }} </a> </td> @@ -92,7 +187,7 @@ {{ scope.obj.followers_count }} </td> <td> - <human-date :date="scope.obj.creation_date"></human-date> + <human-date :date="scope.obj.creation_date" /> </td> </template> </action-table> @@ -100,16 +195,18 @@ <div> <pagination v-if="result && result.count > paginateBy" - @page-changed="selectPage" :compact="true" :current="page" :paginate-by="paginateBy" :total="result.count" - ></pagination> + @page-changed="selectPage" + /> <span v-if="result && result.results.length > 0"> - <translate translate-context="Content/*/Paragraph" - :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}"> + <translate + translate-context="Content/*/Paragraph" + :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}" + > Showing results %{ start }-%{ end } on %{ total } </translate> </span> @@ -121,25 +218,24 @@ import axios from 'axios' import _ from '@/lodash' import time from '@/utils/time' -import {normalizeQuery, parseTokens} from '@/search' +import { normalizeQuery, parseTokens } from '@/search' import Pagination from '@/components/Pagination' import ActionTable from '@/components/common/ActionTable' import OrderingMixin from '@/components/mixins/Ordering' import TranslationsMixin from '@/components/mixins/Translations' import SmartSearchMixin from '@/components/mixins/SmartSearch' - export default { - mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], - props: { - filters: {type: Object, required: false}, - }, components: { Pagination, ActionTable }, + mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], + props: { + filters: { type: Object, required: false, default: () => { return {} } } + }, data () { - let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') + const defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') return { time, isLoading: false, @@ -155,36 +251,10 @@ export default { orderingOptions: [ ['creation_date', 'creation_date'], ['followers_count', 'followers'], - ['uploads_count', 'uploads'], + ['uploads_count', 'uploads'] ] } }, - created () { - this.fetchData() - }, - methods: { - fetchData () { - let params = _.merge({ - 'page': this.page, - 'page_size': this.paginateBy, - 'q': this.search.query, - 'ordering': this.getOrderingAsString() - }, this.filters) - let self = this - self.isLoading = true - self.checked = [] - axios.get('/manage/library/libraries/', {params: params}).then((response) => { - self.result = response.data - self.isLoading = false - }, error => { - self.isLoading = false - self.errors = error.backendErrors - }) - }, - selectPage: function (page) { - this.page = page - }, - }, computed: { labels () { return { @@ -192,7 +262,7 @@ export default { } }, actionFilters () { - var currentFilters = { + const currentFilters = { q: this.search.query } if (this.filters) { @@ -202,8 +272,8 @@ export default { } }, actions () { - let deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete') - let confirmationMessage = this.$pgettext('Popup/*/Paragraph', 'The selected library will be removed, as well as associated uploads and follows. This action is irreversible.') + const deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete') + const confirmationMessage = this.$pgettext('Popup/*/Paragraph', 'The selected library will be removed, as well as associated uploads and follows. This action is irreversible.') return [ { name: 'delete', @@ -211,8 +281,8 @@ export default { confirmationMessage: confirmationMessage, isDangerous: true, allowAll: false, - confirmColor: 'danger', - }, + confirmColor: 'danger' + } ] } }, @@ -230,6 +300,32 @@ export default { orderingDirection () { this.fetchData() } + }, + created () { + this.fetchData() + }, + methods: { + fetchData () { + const params = _.merge({ + page: this.page, + page_size: this.paginateBy, + q: this.search.query, + ordering: this.getOrderingAsString() + }, this.filters) + const self = this + self.isLoading = true + self.checked = [] + axios.get('/manage/library/libraries/', { params: params }).then((response) => { + self.result = response.data + self.isLoading = false + }, error => { + self.isLoading = false + self.errors = error.backendErrors + }) + }, + selectPage: function (page) { + this.page = page + } } } </script> diff --git a/front/src/components/manage/library/TagsTable.vue b/front/src/components/manage/library/TagsTable.vue index 60fa74eacc847f420f85fb862da883d735c9aa78..a7f1f5759373c8ccd6b7d7c917a484efb88ebb24 100644 --- a/front/src/components/manage/library/TagsTable.vue +++ b/front/src/components/manage/library/TagsTable.vue @@ -5,47 +5,104 @@ <div class="ui six wide field"> <label for="tags-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label> <form @submit.prevent="search.query = $refs.search.value"> - <input id="tags-search" name="search" ref="search" type="text" :value="search.query" :placeholder="labels.searchPlaceholder" /> + <input + id="tags-search" + ref="search" + name="search" + type="text" + :value="search.query" + :placeholder="labels.searchPlaceholder" + > </form> </div> <div class="field"> <label for="tags-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> - <select id="tags-ordering" class="ui dropdown" v-model="ordering"> - <option v-for="option in orderingOptions" :value="option[0]"> + <select + id="tags-ordering" + v-model="ordering" + class="ui dropdown" + > + <option + v-for="(option, key) in orderingOptions" + :key="key" + :value="option[0]" + > {{ sharedLabels.filters[option[1]] }} </option> </select> </div> <div class="field"> <label for="tags-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label> - <select id="tags-ordering-direction" class="ui dropdown" v-model="orderingDirection"> - <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option> - <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option> + <select + id="tags-ordering-direction" + v-model="orderingDirection" + class="ui dropdown" + > + <option value="+"> + <translate translate-context="Content/Search/Dropdown"> + Ascending + </translate> + </option> + <option value="-"> + <translate translate-context="Content/Search/Dropdown"> + Descending + </translate> + </option> </select> </div> </div> </div> - <import-status-modal :upload="detailedUpload" :show.sync="showUploadDetailModal" /> + <import-status-modal + :upload="detailedUpload" + :show.sync="showUploadDetailModal" + /> <div class="dimmable"> - <div v-if="isLoading" class="ui active inverted dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui active inverted dimmer" + > + <div class="ui loader" /> </div> <action-table v-if="result" - @action-launched="fetchData" :objects-data="result" :actions="actions" action-url="manage/tags/action/" - idField="name" - :filters="actionFilters"> + id-field="name" + :filters="actionFilters" + @action-launched="fetchData" + > <template slot="header-cells"> - <th><translate translate-context="*/*/*/Noun">Name</translate></th> - <th><translate translate-context="*/*/*/Noun">Artists</translate></th> - <th><translate translate-context="*/*/*">Albums</translate></th> - <th><translate translate-context="*/*/*">Tracks</translate></th> - <th><translate translate-context="Content/*/*/Noun">Creation date</translate></th> + <th> + <translate translate-context="*/*/*/Noun"> + Name + </translate> + </th> + <th> + <translate translate-context="*/*/*/Noun"> + Artists + </translate> + </th> + <th> + <translate translate-context="*/*/*"> + Albums + </translate> + </th> + <th> + <translate translate-context="*/*/*"> + Tracks + </translate> + </th> + <th> + <translate translate-context="Content/*/*/Noun"> + Creation date + </translate> + </th> </template> - <template slot="row-cells" slot-scope="scope"> + <template + slot="row-cells" + slot-scope="scope" + > <td> <router-link :to="{name: 'manage.library.tags.detail', params: {id: scope.obj.name }}"> {{ scope.obj.name|truncate(30, "…", true) }} @@ -61,7 +118,7 @@ {{ scope.obj.tracks_count }} </td> <td> - <human-date :date="scope.obj.creation_date"></human-date> + <human-date :date="scope.obj.creation_date" /> </td> </template> </action-table> @@ -69,16 +126,18 @@ <div> <pagination v-if="result && result.count > paginateBy" - @page-changed="selectPage" :compact="true" :current="page" :paginate-by="paginateBy" :total="result.count" - ></pagination> + @page-changed="selectPage" + /> <span v-if="result && result.results.length > 0"> - <translate translate-context="Content/*/Paragraph" - :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}"> + <translate + translate-context="Content/*/Paragraph" + :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}" + > Showing results %{ start }-%{ end } on %{ total } </translate> </span> @@ -90,7 +149,7 @@ import axios from 'axios' import _ from '@/lodash' import time from '@/utils/time' -import {normalizeQuery, parseTokens} from '@/search' +import { normalizeQuery, parseTokens } from '@/search' import Pagination from '@/components/Pagination' import ActionTable from '@/components/common/ActionTable' import OrderingMixin from '@/components/mixins/Ordering' @@ -98,19 +157,18 @@ import TranslationsMixin from '@/components/mixins/Translations' import SmartSearchMixin from '@/components/mixins/SmartSearch' import ImportStatusModal from '@/components/library/ImportStatusModal' - export default { - mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], - props: { - filters: {type: Object, required: false}, - }, components: { Pagination, ActionTable, ImportStatusModal }, + mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], + props: { + filters: { type: Object, required: false, default: () => { return {} } } + }, data () { - let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') + const defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') return { detailedUpload: null, showUploadDetailModal: false, @@ -129,36 +187,10 @@ export default { ['creation_date', 'creation_date'], ['name', 'name'], ['length', 'length'], - ['items_count', 'items_count'], + ['items_count', 'items_count'] ] } }, - created () { - this.fetchData() - }, - methods: { - fetchData () { - let params = _.merge({ - 'page': this.page, - 'page_size': this.paginateBy, - 'q': this.search.query, - 'ordering': this.getOrderingAsString() - }, this.filters) - let self = this - self.isLoading = true - self.checked = [] - axios.get('/manage/tags/', {params: params}).then((response) => { - self.isLoading = false - self.result = response.data - }, error => { - self.isLoading = false - self.errors = error.backendErrors - }) - }, - selectPage: function (page) { - this.page = page - }, - }, computed: { labels () { return { @@ -166,7 +198,7 @@ export default { } }, actionFilters () { - var currentFilters = { + const currentFilters = { q: this.search.query } if (this.filters) { @@ -176,8 +208,8 @@ export default { } }, actions () { - let deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete') - let confirmationMessage = this.$pgettext('Popup/*/Paragraph', 'The selected tag will be removed and unlinked with existing content, if any. This action is irreversible.') + const deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete') + const confirmationMessage = this.$pgettext('Popup/*/Paragraph', 'The selected tag will be removed and unlinked with existing content, if any. This action is irreversible.') return [ { name: 'delete', @@ -185,8 +217,8 @@ export default { confirmationMessage: confirmationMessage, isDangerous: true, allowAll: false, - confirmColor: 'danger', - }, + confirmColor: 'danger' + } ] } }, @@ -204,6 +236,32 @@ export default { orderingDirection () { this.fetchData() } + }, + created () { + this.fetchData() + }, + methods: { + fetchData () { + const params = _.merge({ + page: this.page, + page_size: this.paginateBy, + q: this.search.query, + ordering: this.getOrderingAsString() + }, this.filters) + const self = this + self.isLoading = true + self.checked = [] + axios.get('/manage/tags/', { params: params }).then((response) => { + self.isLoading = false + self.result = response.data + }, error => { + self.isLoading = false + self.errors = error.backendErrors + }) + }, + selectPage: function (page) { + this.page = page + } } } </script> diff --git a/front/src/components/manage/library/TracksTable.vue b/front/src/components/manage/library/TracksTable.vue index 9c12ea11d92861f27b3a8ccd3aeeeab83780a10b..1d565ca19247af013f5aa0214e2dad67e2638479 100644 --- a/front/src/components/manage/library/TracksTable.vue +++ b/front/src/components/manage/library/TracksTable.vue @@ -5,81 +5,172 @@ <div class="ui six wide field"> <label for="tracks-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label> <form @submit.prevent="search.query = $refs.search.value"> - <input id="tracks-search" name="search" ref="search" type="text" :value="search.query" :placeholder="labels.searchPlaceholder" /> + <input + id="tracks-search" + ref="search" + name="search" + type="text" + :value="search.query" + :placeholder="labels.searchPlaceholder" + > </form> </div> <div class="field"> <label for="tracks-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> - <select id="tracks-ordering" class="ui dropdown" v-model="ordering"> - <option v-for="option in orderingOptions" :value="option[0]"> + <select + id="tracks-ordering" + v-model="ordering" + class="ui dropdown" + > + <option + v-for="(option, key) in orderingOptions" + :key="key" + :value="option[0]" + > {{ sharedLabels.filters[option[1]] }} </option> </select> </div> <div class="field"> <label for="tracks-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label> - <select id="tracks-ordering-direction" class="ui dropdown" v-model="orderingDirection"> - <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option> - <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option> + <select + id="tracks-ordering-direction" + v-model="orderingDirection" + class="ui dropdown" + > + <option value="+"> + <translate translate-context="Content/Search/Dropdown"> + Ascending + </translate> + </option> + <option value="-"> + <translate translate-context="Content/Search/Dropdown"> + Descending + </translate> + </option> </select> </div> </div> - </div> + </div> <div class="dimmable"> - <div v-if="isLoading" class="ui active inverted dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui active inverted dimmer" + > + <div class="ui loader" /> </div> <action-table v-if="result" - @action-launched="fetchData" :objects-data="result" :actions="actions" action-url="manage/library/tracks/action/" - :filters="actionFilters"> + :filters="actionFilters" + @action-launched="fetchData" + > <template slot="header-cells"> - <th><translate translate-context="*/*/*/Noun">Title</translate></th> - <th><translate translate-context="*/*/*">Album</translate></th> - <th><translate translate-context="*/*/*/Noun">Artist</translate></th> - <th><translate translate-context="Content/Moderation/*/Noun">Domain</translate></th> - <th><translate translate-context="Content/*/*/Noun">License</translate></th> - <th><translate translate-context="Content/*/*/Noun">Creation date</translate></th> + <th> + <translate translate-context="*/*/*/Noun"> + Title + </translate> + </th> + <th> + <translate translate-context="*/*/*"> + Album + </translate> + </th> + <th> + <translate translate-context="*/*/*/Noun"> + Artist + </translate> + </th> + <th> + <translate translate-context="Content/Moderation/*/Noun"> + Domain + </translate> + </th> + <th> + <translate translate-context="Content/*/*/Noun"> + License + </translate> + </th> + <th> + <translate translate-context="Content/*/*/Noun"> + Creation date + </translate> + </th> </template> - <template slot="row-cells" slot-scope="scope"> + <template + slot="row-cells" + slot-scope="scope" + > <td> - <router-link :to="{name: 'manage.library.tracks.detail', params: {id: scope.obj.id }}">{{ scope.obj.title }}</router-link> + <router-link :to="{name: 'manage.library.tracks.detail', params: {id: scope.obj.id }}"> + {{ scope.obj.title }} + </router-link> </td> <td> <template v-if="scope.obj.album"> <router-link :to="{name: 'manage.library.albums.detail', params: {id: scope.obj.album.id }}"> - <i class="wrench icon"></i> + <i class="wrench icon" /> </router-link> - <a href="" class="discrete link" @click.prevent="addSearchToken('album_id', scope.obj.album.id)" :title="scope.obj.album.title">{{ scope.obj.album.title }}</a> + <a + href="" + class="discrete link" + :title="scope.obj.album.title" + @click.prevent="addSearchToken('album_id', scope.obj.album.id)" + >{{ scope.obj.album.title }}</a> </template> </td> <td> <router-link :to="{name: 'manage.library.artists.detail', params: {id: scope.obj.artist.id }}"> - <i class="wrench icon"></i> + <i class="wrench icon" /> </router-link> - <a href="" class="discrete link" @click.prevent="addSearchToken('artist_id', scope.obj.artist.id)" :title="scope.obj.artist.name">{{ scope.obj.artist.name }}</a> + <a + href="" + class="discrete link" + :title="scope.obj.artist.name" + @click.prevent="addSearchToken('artist_id', scope.obj.artist.id)" + >{{ scope.obj.artist.name }}</a> </td> <td> <template v-if="!scope.obj.is_local"> <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: scope.obj.domain }}"> - <i class="wrench icon"></i> + <i class="wrench icon" /> </router-link> - <a href="" class="discrete link" @click.prevent="addSearchToken('domain', scope.obj.domain)" :title="scope.obj.domain">{{ scope.obj.domain }}</a> + <a + href="" + class="discrete link" + :title="scope.obj.domain" + @click.prevent="addSearchToken('domain', scope.obj.domain)" + >{{ scope.obj.domain }}</a> </template> - <a href="" v-else class="ui tiny accent icon link label" @click.prevent="addSearchToken('domain', scope.obj.domain)"> - <i class="home icon"></i> + <a + v-else + href="" + class="ui tiny accent icon link label" + @click.prevent="addSearchToken('domain', scope.obj.domain)" + > + <i class="home icon" /> <translate translate-context="Content/Moderation/*/Short, Noun">Local</translate> </a> </td> <td> - <a href="" v-if="scope.obj.license" class="discrete link" @click.prevent="addSearchToken('license', scope.obj.license)" :title="scope.obj.license">{{ scope.obj.license }}</a> - <translate v-else translate-context="*/*/*">N/A</translate> + <a + v-if="scope.obj.license" + href="" + class="discrete link" + :title="scope.obj.license" + @click.prevent="addSearchToken('license', scope.obj.license)" + >{{ scope.obj.license }}</a> + <translate + v-else + translate-context="*/*/*" + > + N/A + </translate> </td> <td> - <human-date :date="scope.obj.creation_date"></human-date> + <human-date :date="scope.obj.creation_date" /> </td> </template> </action-table> @@ -87,16 +178,18 @@ <div> <pagination v-if="result && result.count > paginateBy" - @page-changed="selectPage" :compact="true" :current="page" :paginate-by="paginateBy" :total="result.count" - ></pagination> + @page-changed="selectPage" + /> <span v-if="result && result.results.length > 0"> - <translate translate-context="Content/*/Paragraph" - :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}"> + <translate + translate-context="Content/*/Paragraph" + :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}" + > Showing results %{ start }-%{ end } on %{ total } </translate> </span> @@ -108,25 +201,24 @@ import axios from 'axios' import _ from '@/lodash' import time from '@/utils/time' -import {normalizeQuery, parseTokens} from '@/search' +import { normalizeQuery, parseTokens } from '@/search' import Pagination from '@/components/Pagination' import ActionTable from '@/components/common/ActionTable' import OrderingMixin from '@/components/mixins/Ordering' import TranslationsMixin from '@/components/mixins/Translations' import SmartSearchMixin from '@/components/mixins/SmartSearch' - export default { - mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], - props: { - filters: {type: Object, required: false}, - }, components: { Pagination, ActionTable }, + mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], + props: { + filters: { type: Object, required: false, default: () => { return {} } } + }, data () { - let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') + const defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') return { time, isLoading: false, @@ -140,36 +232,10 @@ export default { orderingDirection: defaultOrdering.direction || '+', ordering: defaultOrdering.field, orderingOptions: [ - ['creation_date', 'creation_date'], + ['creation_date', 'creation_date'] ] } }, - created () { - this.fetchData() - }, - methods: { - fetchData () { - let params = _.merge({ - 'page': this.page, - 'page_size': this.paginateBy, - 'q': this.search.query, - 'ordering': this.getOrderingAsString() - }, this.filters) - let self = this - self.isLoading = true - self.checked = [] - axios.get('/manage/library/tracks/', {params: params}).then((response) => { - self.result = response.data - self.isLoading = false - }, error => { - self.isLoading = false - self.errors = error.backendErrors - }) - }, - selectPage: function (page) { - this.page = page - }, - }, computed: { labels () { return { @@ -177,7 +243,7 @@ export default { } }, actionFilters () { - var currentFilters = { + const currentFilters = { q: this.search.query } if (this.filters) { @@ -187,8 +253,8 @@ export default { } }, actions () { - let deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete') - let confirmationMessage = this.$pgettext('Popup/*/Paragraph', 'The selected tracks will be removed, as well as associated uploads, favorites and listening history. This action is irreversible.') + const deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete') + const confirmationMessage = this.$pgettext('Popup/*/Paragraph', 'The selected tracks will be removed, as well as associated uploads, favorites and listening history. This action is irreversible.') return [ { name: 'delete', @@ -196,8 +262,8 @@ export default { confirmationMessage: confirmationMessage, isDangerous: true, allowAll: false, - confirmColor: 'danger', - }, + confirmColor: 'danger' + } ] } }, @@ -215,6 +281,32 @@ export default { orderingDirection () { this.fetchData() } + }, + created () { + this.fetchData() + }, + methods: { + fetchData () { + const params = _.merge({ + page: this.page, + page_size: this.paginateBy, + q: this.search.query, + ordering: this.getOrderingAsString() + }, this.filters) + const self = this + self.isLoading = true + self.checked = [] + axios.get('/manage/library/tracks/', { params: params }).then((response) => { + self.result = response.data + self.isLoading = false + }, error => { + self.isLoading = false + self.errors = error.backendErrors + }) + }, + selectPage: function (page) { + this.page = page + } } } </script> diff --git a/front/src/components/manage/library/UploadsTable.vue b/front/src/components/manage/library/UploadsTable.vue index f3f50584ab3b73778858204edb6045b95a96d6e3..31f964833c0eabadb94e1a3e62d68b8ea3dd7c4c 100644 --- a/front/src/components/manage/library/UploadsTable.vue +++ b/front/src/components/manage/library/UploadsTable.vue @@ -5,69 +5,182 @@ <div class="ui six wide field"> <label for="uploads-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label> <form @submit.prevent="search.query = $refs.search.value"> - <input id="uploads-search" name="search" ref="search" type="text" :value="search.query" :placeholder="labels.searchPlaceholder" /> + <input + id="uploads-search" + ref="search" + name="search" + type="text" + :value="search.query" + :placeholder="labels.searchPlaceholder" + > </form> </div> <div class="field"> <label for="uploads-visibility"><translate translate-context="*/*/*">Visibility</translate></label> - <select id="uploads-visibility" class="ui dropdown" @change="addSearchToken('privacy_level', $event.target.value)" :value="getTokenValue('privacy_level', '')"> - <option value=""><translate translate-context="Content/*/Dropdown">All</translate></option> - <option value="me">{{ sharedLabels.fields.privacy_level.shortChoices.me }}</option> - <option value="instance">{{ sharedLabels.fields.privacy_level.shortChoices.instance }}</option> - <option value="everyone">{{ sharedLabels.fields.privacy_level.shortChoices.everyone }}</option> + <select + id="uploads-visibility" + class="ui dropdown" + :value="getTokenValue('privacy_level', '')" + @change="addSearchToken('privacy_level', $event.target.value)" + > + <option value=""> + <translate translate-context="Content/*/Dropdown"> + All + </translate> + </option> + <option value="me"> + {{ sharedLabels.fields.privacy_level.shortChoices.me }} + </option> + <option value="instance"> + {{ sharedLabels.fields.privacy_level.shortChoices.instance }} + </option> + <option value="everyone"> + {{ sharedLabels.fields.privacy_level.shortChoices.everyone }} + </option> </select> </div> <div class="field"> <label for="uploads-status"><translate translate-context="Content/*/*/Noun">Import status</translate></label> - <select id="uploads-status" class="ui dropdown" @change="addSearchToken('status', $event.target.value)" :value="getTokenValue('status', '')"> - <option value=""><translate translate-context="Content/*/Dropdown">All</translate></option> - <option value="pending"><translate translate-context="Content/Library/*/Short">Pending</translate></option> - <option value="skipped"><translate translate-context="Content/Library/*">Skipped</translate></option> - <option value="errored"><translate translate-context="Content/Library/Dropdown">Failed</translate></option> - <option value="finished"><translate translate-context="Content/Library/*">Finished</translate></option> + <select + id="uploads-status" + class="ui dropdown" + :value="getTokenValue('status', '')" + @change="addSearchToken('status', $event.target.value)" + > + <option value=""> + <translate translate-context="Content/*/Dropdown"> + All + </translate> + </option> + <option value="pending"> + <translate translate-context="Content/Library/*/Short"> + Pending + </translate> + </option> + <option value="skipped"> + <translate translate-context="Content/Library/*"> + Skipped + </translate> + </option> + <option value="errored"> + <translate translate-context="Content/Library/Dropdown"> + Failed + </translate> + </option> + <option value="finished"> + <translate translate-context="Content/Library/*"> + Finished + </translate> + </option> </select> </div> <div class="field"> <label for="uploads-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> - <select id="uploads-ordering" class="ui dropdown" v-model="ordering"> - <option v-for="option in orderingOptions" :value="option[0]"> + <select + id="uploads-ordering" + v-model="ordering" + class="ui dropdown" + > + <option + v-for="(option, key) in orderingOptions" + :key="key" + :value="option[0]" + > {{ sharedLabels.filters[option[1]] }} </option> </select> </div> <div class="field"> <label for="uploads-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label> - <select id="uploads-ordering-direction" class="ui dropdown" v-model="orderingDirection"> - <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option> - <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option> + <select + id="uploads-ordering-direction" + v-model="orderingDirection" + class="ui dropdown" + > + <option value="+"> + <translate translate-context="Content/Search/Dropdown"> + Ascending + </translate> + </option> + <option value="-"> + <translate translate-context="Content/Search/Dropdown"> + Descending + </translate> + </option> </select> </div> </div> </div> - <import-status-modal :upload="detailedUpload" :show.sync="showUploadDetailModal" /> + <import-status-modal + :upload="detailedUpload" + :show.sync="showUploadDetailModal" + /> <div class="dimmable"> - <div v-if="isLoading" class="ui active inverted dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui active inverted dimmer" + > + <div class="ui loader" /> </div> <action-table v-if="result" - @action-launched="fetchData" :objects-data="result" :actions="actions" action-url="manage/library/uploads/action/" - :filters="actionFilters"> + :filters="actionFilters" + @action-launched="fetchData" + > <template slot="header-cells"> - <th><translate translate-context="*/*/*/Noun">Name</translate></th> - <th><translate translate-context="*/*/*/Noun">Library</translate></th> - <th><translate translate-context="*/*/*/Noun">Account</translate></th> - <th><translate translate-context="Content/Moderation/*/Noun">Domain</translate></th> - <th><translate translate-context="*/*/*">Visibility</translate></th> - <th><translate translate-context="Content/*/*/Noun">Import status</translate></th> - <th><translate translate-context="Content/*/*/Noun">Size</translate></th> - <th><translate translate-context="Content/*/*/Noun">Creation date</translate></th> - <th><translate translate-context="Content/*/*/Noun">Accessed date</translate></th> + <th> + <translate translate-context="*/*/*/Noun"> + Name + </translate> + </th> + <th> + <translate translate-context="*/*/*/Noun"> + Library + </translate> + </th> + <th> + <translate translate-context="*/*/*/Noun"> + Account + </translate> + </th> + <th> + <translate translate-context="Content/Moderation/*/Noun"> + Domain + </translate> + </th> + <th> + <translate translate-context="*/*/*"> + Visibility + </translate> + </th> + <th> + <translate translate-context="Content/*/*/Noun"> + Import status + </translate> + </th> + <th> + <translate translate-context="Content/*/*/Noun"> + Size + </translate> + </th> + <th> + <translate translate-context="Content/*/*/Noun"> + Creation date + </translate> + </th> + <th> + <translate translate-context="Content/*/*/Noun"> + Accessed date + </translate> + </th> </template> - <template slot="row-cells" slot-scope="scope"> + <template + slot="row-cells" + slot-scope="scope" + > <td> <router-link :to="{name: 'manage.library.uploads.detail', params: {id: scope.obj.uuid }}"> {{ displayName(scope.obj)|truncate(30, "…", true) }} @@ -75,28 +188,45 @@ </td> <td> <router-link :to="{name: 'manage.library.libraries.detail', params: {id: scope.obj.library.uuid }}"> - <i class="wrench icon"></i> + <i class="wrench icon" /> </router-link> - <a href="" class="discrete link" + <a + href="" + class="discrete link" + :title="scope.obj.library.name" @click.prevent="addSearchToken('library_id', scope.obj.library.id)" - :title="scope.obj.library.name"> + > {{ scope.obj.library.name | truncate(20) }} </a> </td> <td> - <router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: scope.obj.library.actor.full_username }}"> - </router-link> - <a href="" class="discrete link" @click.prevent="addSearchToken('account', scope.obj.library.actor.full_username)" :title="scope.obj.library.actor.full_username">{{ scope.obj.library.actor.preferred_username }}</a> + <router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: scope.obj.library.actor.full_username }}" /> + <a + href="" + class="discrete link" + :title="scope.obj.library.actor.full_username" + @click.prevent="addSearchToken('account', scope.obj.library.actor.full_username)" + >{{ scope.obj.library.actor.preferred_username }}</a> </td> <td> <template v-if="!scope.obj.is_local"> <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: scope.obj.domain }}"> - <i class="wrench icon"></i> + <i class="wrench icon" /> </router-link> - <a href="" class="discrete link" @click.prevent="addSearchToken('domain', scope.obj.domain)" :title="scope.obj.domain">{{ scope.obj.domain }}</a> + <a + href="" + class="discrete link" + :title="scope.obj.domain" + @click.prevent="addSearchToken('domain', scope.obj.domain)" + >{{ scope.obj.domain }}</a> </template> - <a href="" v-else class="ui tiny accent icon link label" @click.prevent="addSearchToken('domain', scope.obj.domain)"> - <i class="home icon"></i> + <a + v-else + href="" + class="ui tiny accent icon link label" + @click.prevent="addSearchToken('domain', scope.obj.domain)" + > + <i class="home icon" /> <translate translate-context="Content/Moderation/*/Short, Noun">Local</translate> </a> </td> @@ -104,29 +234,52 @@ <a href="" class="discrete link" + :title="sharedLabels.fields.privacy_level.shortChoices[scope.obj.library.privacy_level]" @click.prevent="addSearchToken('privacy_level', scope.obj.library.privacy_level)" - :title="sharedLabels.fields.privacy_level.shortChoices[scope.obj.library.privacy_level]"> + > {{ sharedLabels.fields.privacy_level.shortChoices[scope.obj.library.privacy_level] }} </a> </td> <td> - <a href="" class="discrete link" @click.prevent="addSearchToken('status', scope.obj.import_status)" :title="sharedLabels.fields.import_status.choices[scope.obj.import_status].help"> + <a + href="" + class="discrete link" + :title="sharedLabels.fields.import_status.choices[scope.obj.import_status].help" + @click.prevent="addSearchToken('status', scope.obj.import_status)" + > {{ sharedLabels.fields.import_status.choices[scope.obj.import_status].label }} </a> - <button class="ui tiny basic icon button" :title="sharedLabels.fields.import_status.detailTitle" @click="detailedUpload = scope.obj; showUploadDetailModal = true"> - <i class="question circle outline icon"></i> + <button + class="ui tiny basic icon button" + :title="sharedLabels.fields.import_status.detailTitle" + @click="detailedUpload = scope.obj; showUploadDetailModal = true" + > + <i class="question circle outline icon" /> </button> </td> <td> <span v-if="scope.obj.size">{{ scope.obj.size | humanSize }}</span> - <translate v-else translate-context="*/*/*">N/A</translate> + <translate + v-else + translate-context="*/*/*" + > + N/A + </translate> </td> <td> - <human-date :date="scope.obj.creation_date"></human-date> + <human-date :date="scope.obj.creation_date" /> </td> <td> - <human-date v-if="scope.obj.accessed_date" :date="scope.obj.accessed_date"></human-date> - <translate v-else translate-context="*/*/*">N/A</translate> + <human-date + v-if="scope.obj.accessed_date" + :date="scope.obj.accessed_date" + /> + <translate + v-else + translate-context="*/*/*" + > + N/A + </translate> </td> </template> </action-table> @@ -134,16 +287,18 @@ <div> <pagination v-if="result && result.count > paginateBy" - @page-changed="selectPage" :compact="true" :current="page" :paginate-by="paginateBy" :total="result.count" - ></pagination> + @page-changed="selectPage" + /> <span v-if="result && result.results.length > 0"> - <translate translate-context="Content/*/Paragraph" - :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}"> + <translate + translate-context="Content/*/Paragraph" + :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}" + > Showing results %{ start }-%{ end } on %{ total } </translate> </span> @@ -155,7 +310,7 @@ import axios from 'axios' import _ from '@/lodash' import time from '@/utils/time' -import {normalizeQuery, parseTokens} from '@/search' +import { normalizeQuery, parseTokens } from '@/search' import Pagination from '@/components/Pagination' import ActionTable from '@/components/common/ActionTable' import OrderingMixin from '@/components/mixins/Ordering' @@ -163,19 +318,18 @@ import TranslationsMixin from '@/components/mixins/Translations' import SmartSearchMixin from '@/components/mixins/SmartSearch' import ImportStatusModal from '@/components/library/ImportStatusModal' - export default { - mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], - props: { - filters: {type: Object, required: false}, - }, components: { Pagination, ActionTable, ImportStatusModal }, + mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], + props: { + filters: { type: Object, required: false, default: function () { return {} } } + }, data () { - let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') + const defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') return { detailedUpload: null, showUploadDetailModal: false, @@ -196,45 +350,10 @@ export default { ['accessed_date', 'accessed_date'], ['size', 'size'], ['bitrate', 'bitrate'], - ['duration', 'duration'], + ['duration', 'duration'] ] } }, - created () { - this.fetchData() - }, - methods: { - fetchData () { - let params = _.merge({ - 'page': this.page, - 'page_size': this.paginateBy, - 'q': this.search.query, - 'ordering': this.getOrderingAsString() - }, this.filters) - let self = this - self.isLoading = true - self.checked = [] - axios.get('/manage/library/uploads/', {params: params}).then((response) => { - self.result = response.data - self.isLoading = false - }, error => { - self.isLoading = false - self.errors = error.backendErrors - }) - }, - selectPage: function (page) { - this.page = page - }, - displayName (upload) { - if (upload.filename) { - return upload.filename - } - if (upload.source) { - return upload.source - } - return upload.uuid - } - }, computed: { labels () { return { @@ -242,7 +361,7 @@ export default { } }, actionFilters () { - var currentFilters = { + const currentFilters = { q: this.search.query } if (this.filters) { @@ -252,8 +371,8 @@ export default { } }, actions () { - let deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete') - let confirmationMessage = this.$pgettext('Popup/*/Paragraph', 'The selected upload will be removed. This action is irreversible.') + const deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete') + const confirmationMessage = this.$pgettext('Popup/*/Paragraph', 'The selected upload will be removed. This action is irreversible.') return [ { name: 'delete', @@ -261,8 +380,8 @@ export default { confirmationMessage: confirmationMessage, isDangerous: true, allowAll: false, - confirmColor: 'danger', - }, + confirmColor: 'danger' + } ] } }, @@ -280,6 +399,41 @@ export default { orderingDirection () { this.fetchData() } + }, + created () { + this.fetchData() + }, + methods: { + fetchData () { + const params = _.merge({ + page: this.page, + page_size: this.paginateBy, + q: this.search.query, + ordering: this.getOrderingAsString() + }, this.filters) + const self = this + self.isLoading = true + self.checked = [] + axios.get('/manage/library/uploads/', { params: params }).then((response) => { + self.result = response.data + self.isLoading = false + }, error => { + self.isLoading = false + self.errors = error.backendErrors + }) + }, + selectPage: function (page) { + this.page = page + }, + displayName (upload) { + if (upload.filename) { + return upload.filename + } + if (upload.source) { + return upload.source + } + return upload.uuid + } } } </script> diff --git a/front/src/components/manage/moderation/AccountsTable.vue b/front/src/components/manage/moderation/AccountsTable.vue index 124efcbca07a519473612f7182abd6614457cebc..2170f635cccad00544b0d4bff03585dadf5634ab 100644 --- a/front/src/components/manage/moderation/AccountsTable.vue +++ b/front/src/components/manage/moderation/AccountsTable.vue @@ -5,58 +5,128 @@ <div class="ui six wide field"> <label for="accounts-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label> <form @submit.prevent="search.query = $refs.search.value"> - <input id="accounts-search" name="search" ref="search" type="text" :value="search.query" :placeholder="labels.searchPlaceholder" /> + <input + id="accounts-search" + ref="search" + name="search" + type="text" + :value="search.query" + :placeholder="labels.searchPlaceholder" + > </form> </div> <div class="field"> <label for="accounts-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> - <select id="accounts-ordering" class="ui dropdown" v-model="ordering"> - <option v-for="option in orderingOptions" :value="option[0]"> + <select + id="accounts-ordering" + v-model="ordering" + class="ui dropdown" + > + <option + v-for="(option, key) in orderingOptions" + :key="key" + :value="option[0]" + > {{ sharedLabels.filters[option[1]] }} </option> </select> </div> <div class="field"> <label for="accounts-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label> - <select id="accounts-ordering-direction" class="ui dropdown" v-model="orderingDirection"> - <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option> - <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option> + <select + id="accounts-ordering-direction" + v-model="orderingDirection" + class="ui dropdown" + > + <option value="+"> + <translate translate-context="Content/Search/Dropdown"> + Ascending + </translate> + </option> + <option value="-"> + <translate translate-context="Content/Search/Dropdown"> + Descending + </translate> + </option> </select> </div> </div> - </div> + </div> <div class="dimmable"> - <div v-if="isLoading" class="ui active inverted dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui active inverted dimmer" + > + <div class="ui loader" /> </div> <action-table v-if="result" - @action-launched="fetchData" :objects-data="result" :actions="actions" action-url="manage/accounts/action/" - :filters="actionFilters"> + :filters="actionFilters" + @action-launched="fetchData" + > <template slot="header-cells"> - <th><translate translate-context="*/*/*/Noun">Name</translate></th> - <th><translate translate-context="Content/Moderation/*/Noun">Domain</translate></th> - <th><translate translate-context="*/*/*">Uploads</translate></th> - <th><translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate></th> - <th><translate translate-context="Content/Moderation/Table.Label/Noun">Last seen</translate></th> - <th><translate translate-context="Content/Moderation/Table.Label/Short">Under moderation rule</translate></th> + <th> + <translate translate-context="*/*/*/Noun"> + Name + </translate> + </th> + <th> + <translate translate-context="Content/Moderation/*/Noun"> + Domain + </translate> + </th> + <th> + <translate translate-context="*/*/*"> + Uploads + </translate> + </th> + <th> + <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)"> + First seen + </translate> + </th> + <th> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Last seen + </translate> + </th> + <th> + <translate translate-context="Content/Moderation/Table.Label/Short"> + Under moderation rule + </translate> + </th> </template> - <template slot="row-cells" slot-scope="scope"> + <template + slot="row-cells" + slot-scope="scope" + > <td> - <router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: scope.obj.full_username }}">{{ scope.obj.preferred_username }}</router-link> + <router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: scope.obj.full_username }}"> + {{ scope.obj.preferred_username }} + </router-link> </td> <td> <template v-if="!scope.obj.user"> <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: scope.obj.domain }}"> - <i class="wrench icon"></i> + <i class="wrench icon" /> </router-link> - <a href="" class="discrete link" @click.prevent="addSearchToken('domain', scope.obj.domain)" :title="scope.obj.domain">{{ scope.obj.domain }}</a> + <a + href="" + class="discrete link" + :title="scope.obj.domain" + @click.prevent="addSearchToken('domain', scope.obj.domain)" + >{{ scope.obj.domain }}</a> </template> - <a href="" v-else class="ui tiny accent icon link label" @click.prevent="addSearchToken('domain', scope.obj.domain)"> - <i class="home icon"></i> + <a + v-else + href="" + class="ui tiny accent icon link label" + @click.prevent="addSearchToken('domain', scope.obj.domain)" + > + <i class="home icon" /> <translate translate-context="Content/Moderation/*/Short, Noun">Local account</translate> </a> </td> @@ -64,13 +134,16 @@ {{ scope.obj.uploads_count }} </td> <td> - <human-date :date="scope.obj.creation_date"></human-date> + <human-date :date="scope.obj.creation_date" /> </td> <td> - <human-date v-if="scope.obj.last_fetch_date" :date="scope.obj.last_fetch_date"></human-date> + <human-date + v-if="scope.obj.last_fetch_date" + :date="scope.obj.last_fetch_date" + /> </td> <td> - <span v-if="scope.obj.instance_policy"><i class="shield icon"></i> <translate translate-context="*/*/*">Yes</translate></span> + <span v-if="scope.obj.instance_policy"><i class="shield icon" /> <translate translate-context="*/*/*">Yes</translate></span> </td> </template> </action-table> @@ -78,16 +151,18 @@ <div> <pagination v-if="result && result.count > paginateBy" - @page-changed="selectPage" :compact="true" :current="page" :paginate-by="paginateBy" :total="result.count" - ></pagination> + @page-changed="selectPage" + /> <span v-if="result && result.results.length > 0"> - <translate translate-context="Content/*/Paragraph" - :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}"> + <translate + translate-context="Content/*/Paragraph" + :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}" + > Showing results %{ start }-%{ end } on %{ total } </translate> </span> @@ -99,25 +174,24 @@ import axios from 'axios' import _ from '@/lodash' import time from '@/utils/time' -import {normalizeQuery, parseTokens} from '@/search' +import { normalizeQuery, parseTokens } from '@/search' import Pagination from '@/components/Pagination' import ActionTable from '@/components/common/ActionTable' import OrderingMixin from '@/components/mixins/Ordering' import TranslationsMixin from '@/components/mixins/Translations' import SmartSearchMixin from '@/components/mixins/SmartSearch' - export default { - mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], - props: { - filters: {type: Object, required: false}, - }, components: { Pagination, ActionTable }, + mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], + props: { + filters: { type: Object, required: false, default: function () { return {} } } + }, data () { - let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') + const defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') return { time, isLoading: false, @@ -132,39 +206,13 @@ export default { ordering: defaultOrdering.field, orderingOptions: [ ['creation_date', 'first_seen'], - ["last_fetch_date", "last_seen"], - ["preferred_username", "username"], - ["domain", "domain"], - ["uploads_count", "uploads"], + ['last_fetch_date', 'last_seen'], + ['preferred_username', 'username'], + ['domain', 'domain'], + ['uploads_count', 'uploads'] ] } }, - created () { - this.fetchData() - }, - methods: { - fetchData () { - let params = _.merge({ - 'page': this.page, - 'page_size': this.paginateBy, - 'q': this.search.query, - 'ordering': this.getOrderingAsString() - }, this.filters) - let self = this - self.isLoading = true - self.checked = [] - axios.get('/manage/accounts/', {params: params}).then((response) => { - self.result = response.data - self.isLoading = false - }, error => { - self.isLoading = false - self.errors = error.backendErrors - }) - }, - selectPage: function (page) { - this.page = page - } - }, computed: { labels () { return { @@ -172,7 +220,7 @@ export default { } }, actionFilters () { - var currentFilters = { + const currentFilters = { q: this.search.query } if (this.filters) { @@ -205,6 +253,32 @@ export default { orderingDirection () { this.fetchData() } + }, + created () { + this.fetchData() + }, + methods: { + fetchData () { + const params = _.merge({ + page: this.page, + page_size: this.paginateBy, + q: this.search.query, + ordering: this.getOrderingAsString() + }, this.filters) + const self = this + self.isLoading = true + self.checked = [] + axios.get('/manage/accounts/', { params: params }).then((response) => { + self.result = response.data + self.isLoading = false + }, error => { + self.isLoading = false + self.errors = error.backendErrors + }) + }, + selectPage: function (page) { + this.page = page + } } } </script> diff --git a/front/src/components/manage/moderation/DomainsTable.vue b/front/src/components/manage/moderation/DomainsTable.vue index 56dd457c082f40c721bd8622330dbf20f8c7b619..be3953a6d211bb5f1d9c07015ef5859a64dc48e4 100644 --- a/front/src/components/manage/moderation/DomainsTable.vue +++ b/front/src/components/manage/moderation/DomainsTable.vue @@ -4,57 +4,133 @@ <div class="fields"> <div class="ui field"> <label for="domains-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label> - <input id="domains-search" name="search" type="text" v-model="search" :placeholder="labels.searchPlaceholder" /> + <input + id="domains-search" + v-model="search" + name="search" + type="text" + :placeholder="labels.searchPlaceholder" + > </div> - <div class="field" v-if="allowListEnabled"> + <div + v-if="allowListEnabled" + class="field" + > <label for="domains-allow-list"><translate translate-context="Content/Moderation/*/Adjective">Is present on allow-list</translate></label> - <select id="domains-allow-list" class="ui dropdown" v-model="allowed"> - <option :value="null"><translate translate-context="Content/*/Dropdown">All</translate></option> - <option :value="true"><translate translate-context="*/*/*">Yes</translate></option> - <option :value="false"><translate translate-context="*/*/*">No</translate></option> + <select + id="domains-allow-list" + v-model="allowed" + class="ui dropdown" + > + <option :value="null"> + <translate translate-context="Content/*/Dropdown"> + All + </translate> + </option> + <option :value="true"> + <translate translate-context="*/*/*"> + Yes + </translate> + </option> + <option :value="false"> + <translate translate-context="*/*/*"> + No + </translate> + </option> </select> </div> <div class="field"> <label for="domains-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> - <select id="domains-ordering" class="ui dropdown" v-model="ordering"> - <option v-for="option in orderingOptions" :value="option[0]"> + <select + id="domains-ordering" + v-model="ordering" + class="ui dropdown" + > + <option + v-for="(option, key) in orderingOptions" + :key="key" + :value="option[0]" + > {{ sharedLabels.filters[option[1]] }} </option> </select> </div> <div class="field"> <label for="domains-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label> - <select id="domains-ordering-direction" class="ui dropdown" v-model="orderingDirection"> - <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option> - <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option> + <select + id="domains-ordering-direction" + v-model="orderingDirection" + class="ui dropdown" + > + <option value="+"> + <translate translate-context="Content/Search/Dropdown"> + Ascending + </translate> + </option> + <option value="-"> + <translate translate-context="Content/Search/Dropdown"> + Descending + </translate> + </option> </select> </div> </div> - </div> + </div> <div class="dimmable"> - <div v-if="isLoading" class="ui active inverted dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui active inverted dimmer" + > + <div class="ui loader" /> </div> <action-table v-if="result && result.results.length > 0" - @action-launched="fetchData" :objects-data="result" :actions="actions" action-url="manage/federation/domains/action/" - idField="name" - :filters="actionFilters"> + id-field="name" + :filters="actionFilters" + @action-launched="fetchData" + > <template slot="header-cells"> - <th><translate translate-context="*/*/*/Noun">Name</translate></th> - <th><translate translate-context="*/*/*/Noun">Users</translate></th> - <th><translate translate-context="Content/Moderation/*/Noun">Received messages</translate></th> - <th><translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate></th> - <th><translate translate-context="Content/Moderation/Table.Label/Short">Under moderation rule</translate></th> + <th> + <translate translate-context="*/*/*/Noun"> + Name + </translate> + </th> + <th> + <translate translate-context="*/*/*/Noun"> + Users + </translate> + </th> + <th> + <translate translate-context="Content/Moderation/*/Noun"> + Received messages + </translate> + </th> + <th> + <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)"> + First seen + </translate> + </th> + <th> + <translate translate-context="Content/Moderation/Table.Label/Short"> + Under moderation rule + </translate> + </th> </template> - <template slot="row-cells" slot-scope="scope"> + <template + slot="row-cells" + slot-scope="scope" + > <td> <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: scope.obj.name }}"> {{ scope.obj.name }} - <i v-if="allowListEnabled && scope.obj.allowed" class="success check icon" :title="labels.allowListTitle"></i> + <i + v-if="allowListEnabled && scope.obj.allowed" + class="success check icon" + :title="labels.allowListTitle" + /> </router-link> </td> <td> @@ -64,33 +140,40 @@ {{ scope.obj.outbox_activities_count }} </td> <td> - <human-date :date="scope.obj.creation_date"></human-date> + <human-date :date="scope.obj.creation_date" /> </td> <td> - <span v-if="scope.obj.instance_policy"><i class="shield icon"></i> <translate translate-context="*/*/*">Yes</translate></span> + <span v-if="scope.obj.instance_policy"><i class="shield icon" /> <translate translate-context="*/*/*">Yes</translate></span> </td> </template> </action-table> - <div v-else class="ui placeholder segment"> + <div + v-else + class="ui placeholder segment" + > <div class="ui icon header"> - <i class="server icon"></i> - <translate translate-context="Content/Home/Placeholder">No other pods found</translate> + <i class="server icon" /> + <translate translate-context="Content/Home/Placeholder"> + No other pods found + </translate> </div> </div> </div> <div> <pagination v-if="result && result.count > paginateBy" - @page-changed="selectPage" :compact="true" :current="page" :paginate-by="paginateBy" :total="result.count" - ></pagination> + @page-changed="selectPage" + /> <span v-if="result && result.results.length > 0"> - <translate translate-context="Content/*/Paragraph" - :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}"> + <translate + translate-context="Content/*/Paragraph" + :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}" + > Showing results %{ start }-%{ end } on %{ total } </translate> </span> @@ -108,17 +191,17 @@ import OrderingMixin from '@/components/mixins/Ordering' import TranslationsMixin from '@/components/mixins/Translations' export default { - mixins: [OrderingMixin, TranslationsMixin], - props: { - filters: {type: Object, required: false}, - allowListEnabled: {type: Boolean, default: false}, - }, components: { Pagination, ActionTable }, + mixins: [OrderingMixin, TranslationsMixin], + props: { + filters: { type: Object, required: false, default: function () { return {} } }, + allowListEnabled: { type: Boolean, default: false } + }, data () { - let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') + const defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') return { time, isLoading: false, @@ -138,45 +221,15 @@ export default { } }, - created () { - this.fetchData() - }, - methods: { - fetchData () { - let baseFilters = { - 'page': this.page, - 'page_size': this.paginateBy, - 'q': this.search, - 'ordering': this.getOrderingAsString(), - } - if (this.allowed !== null) { - baseFilters.allowed = this.allowed - } - let params = _.merge(baseFilters, this.filters) - let self = this - self.isLoading = true - self.checked = [] - axios.get('/manage/federation/domains/', {params: params}).then((response) => { - self.result = response.data - self.isLoading = false - }, error => { - self.isLoading = false - self.errors = error.backendErrors - }) - }, - selectPage: function (page) { - this.page = page - } - }, computed: { labels () { return { searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by name…'), - allowListTitle: this.$pgettext('Content/Moderation/Popup', 'This domain is present in your allow-list'), + allowListTitle: this.$pgettext('Content/Moderation/Popup', 'This domain is present in your allow-list') } }, actionFilters () { - var currentFilters = { + const currentFilters = { q: this.search } if (this.filters) { @@ -205,7 +258,7 @@ export default { filterCheckable: (obj) => { return obj.allowed } - }, + } ] } }, @@ -226,6 +279,36 @@ export default { orderingDirection () { this.fetchData() } + }, + created () { + this.fetchData() + }, + methods: { + fetchData () { + const baseFilters = { + page: this.page, + page_size: this.paginateBy, + q: this.search, + ordering: this.getOrderingAsString() + } + if (this.allowed !== null) { + baseFilters.allowed = this.allowed + } + const params = _.merge(baseFilters, this.filters) + const self = this + self.isLoading = true + self.checked = [] + axios.get('/manage/federation/domains/', { params: params }).then((response) => { + self.result = response.data + self.isLoading = false + }, error => { + self.isLoading = false + self.errors = error.backendErrors + }) + }, + selectPage: function (page) { + this.page = page + } } } </script> diff --git a/front/src/components/manage/moderation/InstancePolicyCard.vue b/front/src/components/manage/moderation/InstancePolicyCard.vue index ec5bf1c58f4bb4b282cc009e771d2a93e2368b65..ec68a3223becff10bb12d3669b5b9c34ccba49a8 100644 --- a/front/src/components/manage/moderation/InstancePolicyCard.vue +++ b/front/src/components/manage/moderation/InstancePolicyCard.vue @@ -1,49 +1,83 @@ <template> <div> - <slot></slot> + <slot /> <p> - <i class="clock outline icon"></i><human-date :date="object.creation_date" /> - <i class="user icon"></i>{{ object.actor }} + <i class="clock outline icon" /><human-date :date="object.creation_date" /> + <i class="user icon" />{{ object.actor }} <template v-if="object.is_active"> - <i class="play icon"></i> - <translate translate-context="*/*/*/State of feature">Enabled</translate> + <i class="play icon" /> + <translate translate-context="*/*/*/State of feature"> + Enabled + </translate> </template> <template v-if="!object.is_active"> - <i class="pause icon"></i> - <translate translate-context="Content/Moderation/Card.List item">Paused</translate> + <i class="pause icon" /> + <translate translate-context="Content/Moderation/Card.List item"> + Paused + </translate> </template> </p> <div> <p><strong><translate translate-context="Content/Moderation/Card.Title/Noun">Rule</translate></strong></p> <p v-if="object.block_all"> - <i class="ban icon"></i> - <translate translate-context="Content/Moderation/*/Verb">Block everything</translate> + <i class="ban icon" /> + <translate translate-context="Content/Moderation/*/Verb"> + Block everything + </translate> </p> - <div v-else class="ui list"> - <div class="ui item" v-if="object.silence_activity"> - <i class="feed icon"></i> - <div class="content"><translate translate-context="Content/Moderation/*/Verb">Mute activity</translate></div> + <div + v-else + class="ui list" + > + <div + v-if="object.silence_activity" + class="ui item" + > + <i class="feed icon" /> + <div class="content"> + <translate translate-context="Content/Moderation/*/Verb"> + Mute activity + </translate> + </div> </div> - <div class="ui item" v-if="object.silence_notifications"> - <i class="bell icon"></i> - <div class="content"><translate translate-context="Content/Moderation/*/Verb">Mute notifications</translate></div> + <div + v-if="object.silence_notifications" + class="ui item" + > + <i class="bell icon" /> + <div class="content"> + <translate translate-context="Content/Moderation/*/Verb"> + Mute notifications + </translate> + </div> </div> - <div class="ui item" v-if="object.reject_media"> - <i class="file icon"></i> - <div class="content"><translate translate-context="Content/Moderation/*/Verb">Reject media</translate></div> + <div + v-if="object.reject_media" + class="ui item" + > + <i class="file icon" /> + <div class="content"> + <translate translate-context="Content/Moderation/*/Verb"> + Reject media + </translate> + </div> </div> - </div> </div> <div v-if="markdown && object.summary"> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <p><strong><translate translate-context="Content/Moderation/*/Noun">Reason</translate></strong></p> - <div v-html="markdown.makeHtml(object.summary)"></div> + <div v-html="markdown.makeHtml(object.summary)" /> </div> - <div class="ui hidden divider"></div> - <button @click="$emit('update')" class="ui right floated labeled icon button"> - <i class="edit icon"></i> - <translate translate-context="Content/*/Button.Label/Verb">Edit</translate> + <div class="ui hidden divider" /> + <button + class="ui right floated labeled icon button" + @click="$emit('update')" + > + <i class="edit icon" /> + <translate translate-context="Content/*/Button.Label/Verb"> + Edit + </translate> </button> </div> </template> @@ -52,7 +86,7 @@ export default { props: { - object: {type: Object, default: null}, + object: { type: Object, default: null } }, data () { return { @@ -60,9 +94,9 @@ export default { } }, created () { - let self = this + const self = this import(/* webpackChunkName: "showdown" */ 'showdown').then(module => { - self.markdown = new module.default.Converter({simplifiedAutoLink: true, openLinksInNewWindow: true}) + self.markdown = new module.default.Converter({ simplifiedAutoLink: true, openLinksInNewWindow: true }) }) } } diff --git a/front/src/components/manage/moderation/InstancePolicyForm.vue b/front/src/components/manage/moderation/InstancePolicyForm.vue index 5fc072165cd6c9352cf0af7755c4593d34a83d52..abb3df423528b98c4ebf3df635a930ce0273d723 100644 --- a/front/src/components/manage/moderation/InstancePolicyForm.vue +++ b/front/src/components/manage/moderation/InstancePolicyForm.vue @@ -1,22 +1,65 @@ <template> - <form class="ui form" @submit.prevent="createOrUpdate"> + <form + class="ui form" + @submit.prevent="createOrUpdate" + > <h3 class="ui header"> - <translate translate-context="Content/Moderation/Card.Title/Verb" v-if="object" key="1">Edit moderation rule</translate> - <translate translate-context="Content/Moderation/Card.Button.Label/Verb" v-else key="2">Add a new moderation rule</translate> + <translate + v-if="object" + key="1" + translate-context="Content/Moderation/Card.Title/Verb" + > + Edit moderation rule + </translate> + <translate + v-else + key="2" + translate-context="Content/Moderation/Card.Button.Label/Verb" + > + Add a new moderation rule + </translate> </h3> - <div v-if="errors && errors.length > 0" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Content/Moderation/Error message.Title">Error while creating rule</translate></h4> + <div + v-if="errors && errors.length > 0" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Content/Moderation/Error message.Title"> + Error while creating rule + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> - <div class="field" v-if="object"> + <div + v-if="object" + class="field" + > <div class="ui toggle checkbox"> - <input id="policy-is-active" v-model="current.isActive" type="checkbox"> + <input + id="policy-is-active" + v-model="current.isActive" + type="checkbox" + > <label for="policy-is-active"> - <translate translate-context="*/*/*/State of feature" v-if="current.isActive" key="1">Enabled</translate> - <translate translate-context="*/*/*/State of feature" v-else key="2">Disabled</translate> + <translate + v-if="current.isActive" + key="1" + translate-context="*/*/*/State of feature" + >Enabled</translate> + <translate + v-else + key="2" + translate-context="*/*/*/State of feature" + >Disabled</translate> <tooltip :content="labels.isActiveHelp" /> </label> </div> @@ -26,11 +69,20 @@ <translate translate-context="Content/Moderation/*/Noun">Reason</translate> <tooltip :content="labels.summaryHelp" /> </label> - <textarea name="policy-summary" id="policy-summary" rows="5" v-model="current.summary"></textarea> + <textarea + id="policy-summary" + v-model="current.summary" + name="policy-summary" + rows="5" + /> </div> <div class="field"> <div class="ui toggle checkbox"> - <input id="policy-is-active" v-model="current.blockAll" type="checkbox"> + <input + id="policy-is-active" + v-model="current.blockAll" + type="checkbox" + > <label for="policy-is-active"> <translate translate-context="Content/Moderation/*/Verb">Block everything</translate> <tooltip :content="labels.blockAllHelp" /> @@ -38,36 +90,78 @@ </div> </div> <div class="ui horizontal divider"> - <translate translate-context="Content/Moderation/Card.Title">Or customize your rule</translate> + <translate translate-context="Content/Moderation/Card.Title"> + Or customize your rule + </translate> </div> - <div v-for="config in fieldConfig" :class="['field']"> + <div + v-for="(config, key) in fieldConfig" + :key="key" + :class="['field']" + > <div class="ui toggle checkbox"> - <input :id="'policy-' + config.id" v-model="current[config.id]" type="checkbox"> + <input + :id="'policy-' + config.id" + v-model="current[config.id]" + type="checkbox" + > <label :for="'policy-' + config.id"> - <i :class="[config.icon, 'icon']"></i> + <i :class="[config.icon, 'icon']" /> {{ labels[config.id].label }} <tooltip :content="labels[config.id].help" /> </label> </div> </div> - <div class="ui hidden divider"></div> - <button @click.prevent="$emit('cancel')" class="ui basic left floated button"> - <translate translate-context="*/*/Button.Label/Verb">Cancel</translate> + <div class="ui hidden divider" /> + <button + class="ui basic left floated button" + @click.prevent="$emit('cancel')" + > + <translate translate-context="*/*/Button.Label/Verb"> + Cancel + </translate> </button> - <button :class="['ui', 'right', 'floated', 'success', {'disabled loading': isLoading}, 'button']" :disabled="isLoading"> - <translate translate-context="Content/Moderation/Card.Button.Label/Verb" v-if="object" key="1">Update</translate> - <translate translate-context="Content/Moderation/Card.Button.Label/Verb" v-else key="2">Create</translate> + <button + :class="['ui', 'right', 'floated', 'success', {'disabled loading': isLoading}, 'button']" + :disabled="isLoading" + > + <translate + v-if="object" + key="1" + translate-context="Content/Moderation/Card.Button.Label/Verb" + > + Update + </translate> + <translate + v-else + key="2" + translate-context="Content/Moderation/Card.Button.Label/Verb" + > + Create + </translate> </button> - <dangerous-button v-if="object" class="ui right floated basic danger button" @confirm="remove"> - <translate translate-context="*/*/*/Verb">Delete</translate> + <dangerous-button + v-if="object" + class="ui right floated basic danger button" + @confirm="remove" + > + <translate translate-context="*/*/*/Verb"> + Delete + </translate> <p slot="modal-header"> - <translate translate-context="Popup/Moderation/Title">Delete this moderation rule?</translate> + <translate translate-context="Popup/Moderation/Title"> + Delete this moderation rule? + </translate> </p> <p slot="modal-content"> - <translate translate-context="Popup/Moderation/Paragraph">This action is irreversible.</translate> + <translate translate-context="Popup/Moderation/Paragraph"> + This action is irreversible. + </translate> </p> <div slot="modal-confirm"> - <translate translate-context="Popup/Moderation/Button.Label/Verb">Delete moderation rule</translate> + <translate translate-context="Popup/Moderation/Button.Label/Verb"> + Delete moderation rule + </translate> </div> </dangerous-button> </form> @@ -79,12 +173,12 @@ import _ from '@/lodash' export default { props: { - type: {type: String, required: true}, - object: {type: Object, default: null}, - target: {type: String, required: true}, + type: { type: String, required: true }, + object: { type: Object, default: null }, + target: { type: String, required: true } }, data () { - let current = this.object || {} + const current = this.object || {} return { isLoading: false, errors: [], @@ -94,13 +188,13 @@ export default { blockAll: _.get(current, 'block_all', true), silenceActivity: _.get(current, 'silence_activity', false), silenceNotifications: _.get(current, 'silence_notifications', false), - rejectMedia: _.get(current, 'reject_media', false), + rejectMedia: _.get(current, 'reject_media', false) }, fieldConfig: [ // we hide those until we actually have the related features implemented :) // {id: "silenceActivity", icon: "feed"}, // {id: "silenceNotifications", icon: "bell"}, - {id: "rejectMedia", icon: "file"}, + { id: 'rejectMedia', icon: 'file' } ] } }, @@ -108,30 +202,55 @@ export default { labels () { return { summaryHelp: this.$pgettext('Content/Moderation/Help text', "Explain why you're applying this policy: this will help you remember why you added this rule. Depending on your pod configuration, this may be displayed publicly to help users understand the moderation rules in place."), - isActiveHelp: this.$pgettext('Content/Moderation/Help text', "Use this setting to temporarily enable/disable the policy without completely removing it."), - blockAllHelp: this.$pgettext('Content/Moderation/Help text', "Block everything from this account or domain. This will prevent any interaction with the entity, and purge related content (uploads, libraries, follows, etc.)"), + isActiveHelp: this.$pgettext('Content/Moderation/Help text', 'Use this setting to temporarily enable/disable the policy without completely removing it.'), + blockAllHelp: this.$pgettext('Content/Moderation/Help text', 'Block everything from this account or domain. This will prevent any interaction with the entity, and purge related content (uploads, libraries, follows, etc.)'), silenceActivity: { - help: this.$pgettext('Content/Moderation/Help text', "Hide account or domain content, except from followers."), - label: this.$pgettext('Content/Moderation/*/Verb', "Mute activity"), + help: this.$pgettext('Content/Moderation/Help text', 'Hide account or domain content, except from followers.'), + label: this.$pgettext('Content/Moderation/*/Verb', 'Mute activity') }, silenceNotifications: { - help: this.$pgettext('Content/Moderation/Help text', "Prevent account or domain from triggering notifications, except from followers."), - label: this.$pgettext('Content/Moderation/*/Verb', "Mute notifications"), + help: this.$pgettext('Content/Moderation/Help text', 'Prevent account or domain from triggering notifications, except from followers.'), + label: this.$pgettext('Content/Moderation/*/Verb', 'Mute notifications') }, rejectMedia: { - help: this.$pgettext('Content/Moderation/Help text', "Do not download any media file (audio, album cover, account avatar…) from this account or domain. This will purge existing content as well."), - label: this.$pgettext('Content/Moderation/*/Verb', "Reject media"), + help: this.$pgettext('Content/Moderation/Help text', 'Do not download any media file (audio, album cover, account avatar…) from this account or domain. This will purge existing content as well.'), + label: this.$pgettext('Content/Moderation/*/Verb', 'Reject media') } } } }, + watch: { + 'current.silenceActivity': function (v) { + if (v) { + this.current.blockAll = false + } + }, + 'current.silenceNotifications': function (v) { + if (v) { + this.current.blockAll = false + } + }, + 'current.rejectMedia': function (v) { + if (v) { + this.current.blockAll = false + } + }, + 'current.blockAll': function (v) { + if (v) { + const self = this + this.fieldConfig.forEach((f) => { + self.current[f.id] = false + }) + } + } + }, methods: { createOrUpdate () { - let self = this + const self = this this.isLoading = true this.errors = [] let url, method - let data = { + const data = { summary: this.current.summary, is_active: this.current.isActive, block_all: this.current.blockAll, @@ -140,14 +259,14 @@ export default { reject_media: this.current.rejectMedia, target: { type: this.type, - id: this.target, + id: this.target } } if (this.object) { url = `manage/moderation/instance-policies/${this.object.id}/` method = 'patch' } else { - url = `manage/moderation/instance-policies/` + url = 'manage/moderation/instance-policies/' method = 'post' } axios[method](url, data).then((response) => { @@ -159,11 +278,11 @@ export default { }) }, remove () { - let self = this + const self = this this.isLoading = true this.errors = [] - let url = `manage/moderation/instance-policies/${this.object.id}/` + const url = `manage/moderation/instance-policies/${this.object.id}/` axios.delete(url).then((response) => { this.isLoading = false self.$emit('delete') @@ -172,31 +291,6 @@ export default { self.errors = error.backendErrors }) } - }, - watch: { - 'current.silenceActivity': function (v) { - if (v) { - this.current.blockAll = false - } - }, - 'current.silenceNotifications': function (v) { - if (v) { - this.current.blockAll = false - } - }, - 'current.rejectMedia': function (v) { - if (v) { - this.current.blockAll = false - } - }, - 'current.blockAll': function (v) { - if (v) { - let self = this - this.fieldConfig.forEach((f) => { - self.current[f.id] = false - }) - } - } } } </script> diff --git a/front/src/components/manage/moderation/InstancePolicyModal.vue b/front/src/components/manage/moderation/InstancePolicyModal.vue index b09d0d32218e6fd310ea7c1ceb3b6d19723dac34..555c0df645c65c42947c5ed656f406b8de49abd2 100644 --- a/front/src/components/manage/moderation/InstancePolicyModal.vue +++ b/front/src/components/manage/moderation/InstancePolicyModal.vue @@ -1,60 +1,84 @@ <template> - <button class="ui button" @click.prevent="show = !show"> - <i class="shield icon"></i> + <button + class="ui button" + @click.prevent="show = !show" + > + <i class="shield icon" /> <slot> - <translate translate-context="Content/Moderation/Button.Label">Moderation rules…</translate> + <translate translate-context="Content/Moderation/Button.Label"> + Moderation rules… + </translate> </slot> - <modal :show.sync="show" @show="fetchData"> + <modal + :show.sync="show" + @show="fetchData" + > <h4 class="header"> - <translate :translate-params="{obj: target}" translate-context="Popup/Moderation/Title/Verb">Manage moderation rules for %{ obj }</translate> + <translate + :translate-params="{obj: target}" + translate-context="Popup/Moderation/Title/Verb" + > + Manage moderation rules for %{ obj } + </translate> </h4> <div class="content"> <div class="description"> - <div v-if="isLoading" class="ui active loader"></div> - <instance-policy-card v-else-if="obj && !showForm" :object="obj" @update="showForm = true"> + <div + v-if="isLoading" + class="ui active loader" + /> + <instance-policy-card + v-else-if="obj && !showForm" + :object="obj" + @update="showForm = true" + > <header class="ui header"> <h3> - <translate translate-context="Content/Moderation/Card.Title">This entity is subject to specific moderation rules</translate> + <translate translate-context="Content/Moderation/Card.Title"> + This entity is subject to specific moderation rules + </translate> </h3> </header> - </instance-policy-card> - <instance-policy-form - v-else - @cancel="showForm = false" - @save="showForm = false; result = {count: 1, results: [$event]}" - @delete="result = {count: 0, results: []}; showForm = false" - :object="obj" - :type="type" - :target="target" /> + </instance-policy-card> + <instance-policy-form + v-else + :object="obj" + :type="type" + :target="target" + @cancel="showForm = false" + @save="showForm = false; result = {count: 1, results: [$event]}" + @delete="result = {count: 0, results: []}; showForm = false" + /> </div> - <div class="ui hidden divider"></div> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> + <div class="ui hidden divider" /> </div> <div class="actions"> <button class="ui deny button"> - <translate translate-context="*/*/Button.Label/Verb">Close</translate> + <translate translate-context="*/*/Button.Label/Verb"> + Close + </translate> </button> </div> </modal> - </button> </template> <script> import axios from 'axios' -import InstancePolicyForm from "@/components/manage/moderation/InstancePolicyForm" -import InstancePolicyCard from "@/components/manage/moderation/InstancePolicyCard" +import InstancePolicyForm from '@/components/manage/moderation/InstancePolicyForm' +import InstancePolicyCard from '@/components/manage/moderation/InstancePolicyCard' import Modal from '@/components/semantic/Modal' export default { - props: { - target: {required: true}, - type: {required: true}, - }, components: { InstancePolicyForm, InstancePolicyCard, - Modal, + Modal + }, + props: { + target: { type: String, required: true }, + type: { type: String, required: true } }, data () { return { @@ -62,7 +86,7 @@ export default { isLoading: false, errors: [], showForm: false, - result: null, + result: null } }, computed: { @@ -75,18 +99,18 @@ export default { }, methods: { fetchData () { - let params = {} + const params = {} if (this.type === 'domain') { params.target_domain = this.target } if (this.type === 'actor') { - let parts = this.target.split('@') + const parts = this.target.split('@') params.target_account_username = parts[0] params.target_account_domain = parts[1] } - let self = this + const self = this self.isLoading = true - axios.get('/manage/moderation/instance-policies/', {params: params}).then((response) => { + axios.get('/manage/moderation/instance-policies/', { params: params }).then((response) => { self.result = response.data self.isLoading = false }, error => { diff --git a/front/src/components/manage/moderation/NoteForm.vue b/front/src/components/manage/moderation/NoteForm.vue index d22bfd91651d488055d652121a96576596b1acbf..20632c8ddb5b8491062dd9d4a7ef1bfcdeddd465 100644 --- a/front/src/components/manage/moderation/NoteForm.vue +++ b/front/src/components/manage/moderation/NoteForm.vue @@ -1,16 +1,44 @@ <template> - <form 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/Moderation/Error message.Title">Error while submitting note</translate></h4> + <form + 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/Moderation/Error message.Title"> + Error while submitting note + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> <div class="field"> - <content-form field-id="change-summary" :required="true" v-model="summary" :rows="3" :placeholder="labels.summaryPlaceholder"></content-form> + <content-form + v-model="summary" + field-id="change-summary" + :required="true" + :rows="3" + :placeholder="labels.summaryPlaceholder" + /> </div> - <button :class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']" type="submit" :disabled="isLoading"> - <translate translate-context="Content/Moderation/Button.Label/Verb">Add note</translate> + <button + :class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']" + type="submit" + :disabled="isLoading" + > + <translate translate-context="Content/Moderation/Button.Label/Verb"> + Add note + </translate> </button> </form> </template> @@ -21,33 +49,33 @@ import showdown from 'showdown' export default { props: { - target: {required: true}, + target: { type: String, required: true } }, data () { - return { + return { markdown: new showdown.Converter(), isLoading: false, summary: '', - errors: [], + errors: [] } }, computed: { labels () { return { - summaryPlaceholder: this.$pgettext('Content/Moderation/Placeholder', 'Describe what actions have been taken, or any other related updates…'), + summaryPlaceholder: this.$pgettext('Content/Moderation/Placeholder', 'Describe what actions have been taken, or any other related updates…') } - }, + } }, methods: { submit () { - let self = this + const self = this this.isLoading = true - let payload = { + const payload = { target: this.target, summary: this.summary } this.errors = [] - axios.post(`manage/moderation/notes/`, payload).then((response) => { + axios.post('manage/moderation/notes/', payload).then((response) => { self.$emit('created', response.data) self.summary = '' self.isLoading = false @@ -55,7 +83,7 @@ export default { self.errors = error.backendErrors self.isLoading = false }) - }, + } } } </script> diff --git a/front/src/components/manage/moderation/NotesThread.vue b/front/src/components/manage/moderation/NotesThread.vue index 9c2d7570095d21a49214663d3bb3301af54fa81a..69fcb5f6f0a8697ba464688a256ae633bd47626e 100644 --- a/front/src/components/manage/moderation/NotesThread.vue +++ b/front/src/components/manage/moderation/NotesThread.vue @@ -1,32 +1,54 @@ <template> <div class="ui feed"> - <div class="event" v-for="note in notes" :key="note.uuid"> + <div + v-for="note in notes" + :key="note.uuid" + class="event" + > <div class="label"> - <i class="comment outline icon"></i> + <i class="comment outline icon" /> </div> <div class="content"> <div class="summary"> - <actor-link :admin="true" :actor="note.author"></actor-link> + <actor-link + :admin="true" + :actor="note.author" + /> <div class="date"> - <human-date :date="note.creation_date"></human-date> + <human-date :date="note.creation_date" /> </div> </div> <div class="extra text"> <expandable-div :content="note.summary"> - <div v-html="markdown.makeHtml(note.summary)"></div> + <div v-html="markdown.makeHtml(note.summary)" /> </expandable-div> </div> <div class="meta"> <dangerous-button :class="['ui', {loading: isLoading}, 'basic borderless mini button']" - @confirm="remove(note)"> - <i class="trash icon"></i> - <translate translate-context="*/*/*/Verb">Delete</translate> - <p slot="modal-header"><translate translate-context="Popup/Moderation/Title">Delete this note?</translate></p> + @confirm="remove(note)" + > + <i class="trash icon" /> + <translate translate-context="*/*/*/Verb"> + Delete + </translate> + <p slot="modal-header"> + <translate translate-context="Popup/Moderation/Title"> + Delete this note? + </translate> + </p> <div slot="modal-content"> - <p><translate translate-context="Content/Moderation/Paragraph">The note will be removed. This action is irreversible.</translate></p> + <p> + <translate translate-context="Content/Moderation/Paragraph"> + The note will be removed. This action is irreversible. + </translate> + </p> </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> </div> </div> @@ -40,25 +62,25 @@ import showdown from 'showdown' export default { props: { - notes: {required: true}, + notes: { type: String, required: true } }, data () { - return { + return { markdown: new showdown.Converter(), - isLoading: false, + isLoading: false } }, methods: { remove (obj) { - let self = this + const self = this this.isLoading = true axios.delete(`manage/moderation/notes/${obj.uuid}/`).then((response) => { self.$emit('deleted', obj.uuid) self.isLoading = false - }, error => { + }, () => { self.isLoading = false }) - }, + } } } </script> diff --git a/front/src/components/manage/moderation/ReportCard.vue b/front/src/components/manage/moderation/ReportCard.vue index 5e6b1605bd37ed78896c29967717e4c576f8db16..d2dd0a54cad2347bbbd639dec4943672b1a2ad72 100644 --- a/front/src/components/manage/moderation/ReportCard.vue +++ b/front/src/components/manage/moderation/ReportCard.vue @@ -3,48 +3,69 @@ <div class="content"> <h4 class="header"> <router-link :to="{name: 'manage.moderation.reports.detail', params: {id: obj.uuid}}"> - <translate translate-context="Content/Moderation/Card/Short" :translate-params="{id: obj.uuid.substring(0, 8)}">Report %{ id }</translate> + <translate + translate-context="Content/Moderation/Card/Short" + :translate-params="{id: obj.uuid.substring(0, 8)}" + > + Report %{ id } + </translate> </router-link> - <collapse-link class="right floated" v-model="isCollapsed"></collapse-link> + <collapse-link + v-model="isCollapsed" + class="right floated" + /> </h4> <div class="content"> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <div class="ui stackable two column grid"> <div class="column"> <table class="ui very basic unstackable table"> <tbody> <tr> <td> - <translate translate-context="Content/Moderation/*">Submitted by</translate> + <translate translate-context="Content/Moderation/*"> + Submitted by + </translate> </td> <td> <div v-if="obj.submitter"> - <actor-link :admin="true" :actor="obj.submitter" /> + <actor-link + :admin="true" + :actor="obj.submitter" + /> </div> - <div v-else="obj.submitter_email"> + <div v-else-if="obj.submitter_email"> {{ obj.submitter_email }} </div> </td> </tr> <tr> <td> - <translate translate-context="*/*/*">Category</translate> + <translate translate-context="*/*/*"> + Category + </translate> </td> <td> <report-category-dropdown :value="obj.type" - @input="update({type: $event})"> + @input="update({type: $event})" + >   - <action-feedback :is-loading="updating.type"></action-feedback> + <action-feedback :is-loading="updating.type" /> </report-category-dropdown> </td> </tr> <tr> <td> - <translate translate-context="Content/*/*/Noun">Creation date</translate> + <translate translate-context="Content/*/*/Noun"> + Creation date + </translate> </td> <td> - <human-date :date="obj.creation_date" :icon="true"></human-date> + <human-date + :date="obj.creation_date" + :icon="true" + /> </td> </tr> </tbody> @@ -55,45 +76,72 @@ <tbody> <tr> <td> - <translate translate-context="*/*/*">Status</translate> + <translate translate-context="*/*/*"> + Status + </translate> </td> <td v-if="obj.is_handled"> <span v-if="obj.is_handled"> - <i class="success check icon"></i> + <i class="success check icon" /> <translate translate-context="Content/*/*/Short">Resolved</translate> </span> </td> <td v-else> - <i class="danger x icon"></i> - <translate translate-context="Content/*/*/Short">Unresolved</translate> + <i class="danger x icon" /> + <translate translate-context="Content/*/*/Short"> + Unresolved + </translate> </td> </tr> <tr> <td> - <translate translate-context="Content/Moderation/*">Assigned to</translate> + <translate translate-context="Content/Moderation/*"> + Assigned to + </translate> </td> <td> <div v-if="obj.assigned_to"> - <actor-link :admin="true" :actor="obj.assigned_to" /> + <actor-link + :admin="true" + :actor="obj.assigned_to" + /> </div> - <translate v-else translate-context="*/*/*">N/A</translate> + <translate + v-else + translate-context="*/*/*" + > + N/A + </translate> </td> </tr> <tr> <td> - <translate translate-context="Content/*/*/Noun">Resolution date</translate> + <translate translate-context="Content/*/*/Noun"> + Resolution date + </translate> </td> <td> - <human-date v-if="obj.handled_date" :date="obj.handled_date" :icon="true"></human-date> - <translate v-else translate-context="*/*/*">N/A</translate> + <human-date + v-if="obj.handled_date" + :date="obj.handled_date" + :icon="true" + /> + <translate + v-else + translate-context="*/*/*" + > + N/A + </translate> </td> </tr> <tr> <td> - <translate translate-context="Content/*/*/Noun">Internal notes</translate> + <translate translate-context="Content/*/*/Noun"> + Internal notes + </translate> </td> <td> - <i class="comment icon"></i> + <i class="comment icon" /> {{ obj.notes.length }} </td> </tr> @@ -103,95 +151,165 @@ </div> </div> </div> - <div class="main content" v-if="!isCollapsed"> + <div + v-if="!isCollapsed" + class="main content" + > <div class="ui stackable two column grid"> <div class="column"> <h3> - <translate translate-context="*/*/Field.Label/Noun">Message</translate> + <translate translate-context="*/*/Field.Label/Noun"> + Message + </translate> </h3> - <expandable-div v-if="obj.summary" class="summary" :content="obj.summary"> - <div v-html="markdown.makeHtml(obj.summary)"></div> + <expandable-div + v-if="obj.summary" + class="summary" + :content="obj.summary" + > + <div v-html="markdown.makeHtml(obj.summary)" /> </expandable-div> </div> <aside class="column"> <h3> - <translate translate-context="Content/*/*/Short">Reported object</translate> + <translate translate-context="Content/*/*/Short"> + Reported object + </translate> </h3> - <div v-if="!obj.target" role="alert" class="ui warning message"> - <translate translate-context="Content/Moderation/Message">The object associated with this report was deleted.</translate> + <div + v-if="!obj.target" + role="alert" + class="ui warning message" + > + <translate translate-context="Content/Moderation/Message"> + The object associated with this report was deleted. + </translate> </div> - <router-link class="ui basic button" v-if="target && configs[target.type].urls.getDetail" :to="configs[target.type].urls.getDetail(obj.target_state)"> - <i class="eye icon"></i> - <translate translate-context="Content/Moderation/Link">View public page</translate> + <router-link + v-if="target && configs[target.type].urls.getDetail" + class="ui basic button" + :to="configs[target.type].urls.getDetail(obj.target_state)" + > + <i class="eye icon" /> + <translate translate-context="Content/Moderation/Link"> + View public page + </translate> </router-link> - <router-link class="ui basic button" v-if="target && configs[target.type].urls.getAdminDetail" :to="configs[target.type].urls.getAdminDetail(obj.target_state)"> - <i class="wrench icon"></i> - <translate translate-context="Content/Moderation/Link">Open in moderation interface</translate> + <router-link + v-if="target && configs[target.type].urls.getAdminDetail" + class="ui basic button" + :to="configs[target.type].urls.getAdminDetail(obj.target_state)" + > + <i class="wrench icon" /> + <translate translate-context="Content/Moderation/Link"> + Open in moderation interface + </translate> </router-link> <table class="ui very basic unstackable table"> <tbody> <tr v-if="target"> <td> - <translate translate-context="Content/Track/Table.Label/Noun">Type</translate> + <translate translate-context="Content/Track/Table.Label/Noun"> + Type + </translate> </td> <td colspan="2"> - <i :class="[configs[target.type].icon, 'icon']"></i> + <i :class="[configs[target.type].icon, 'icon']" /> {{ configs[target.type].label }} </td> </tr> <tr v-if="obj.target_owner && (!target || target.type !== 'account')"> <td> - <translate translate-context="*/*/*">Owner</translate> + <translate translate-context="*/*/*"> + Owner + </translate> </td> <td> - <actor-link :admin="true" :actor="obj.target_owner"></actor-link> + <actor-link + :admin="true" + :actor="obj.target_owner" + /> </td> <td> <instance-policy-modal v-if="!obj.target_owner.is_local" - class="right floated mini basic" type="actor" :target="obj.target_owner.full_username" /> + class="right floated mini basic" + type="actor" + :target="obj.target_owner.full_username" + /> </td> </tr> <tr v-if="target && target.type === 'account'"> <td> - <translate translate-context="*/*/*/Noun">Account</translate> + <translate translate-context="*/*/*/Noun"> + Account + </translate> </td> <td> - <actor-link :admin="true" :actor="obj.target_owner"></actor-link> + <actor-link + :admin="true" + :actor="obj.target_owner" + /> </td> <td> <instance-policy-modal v-if="!obj.target_owner.is_local" - class="right floated mini basic" type="actor" :target="obj.target_owner.full_username" /> + class="right floated mini basic" + type="actor" + :target="obj.target_owner.full_username" + /> </td> </tr> <tr v-if="obj.target_state.is_local"> <td> - <translate translate-context="Content/Moderation/*/Noun">Domain</translate> + <translate translate-context="Content/Moderation/*/Noun"> + Domain + </translate> </td> <td colspan="2"> - <i class="home icon"></i> - <translate translate-context="Content/Moderation/*/Short, Noun">Local</translate> + <i class="home icon" /> + <translate translate-context="Content/Moderation/*/Short, Noun"> + Local + </translate> </td> </tr> <tr v-else-if="obj.target_state.domain"> <td> <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: obj.target_state.domain }}"> - <translate translate-context="Content/Moderation/*/Noun">Domain</translate> + <translate translate-context="Content/Moderation/*/Noun"> + Domain + </translate> </router-link> </td> <td> {{ obj.target_state.domain }} </td> <td> - <instance-policy-modal class="right floated mini basic" type="domain" :target="obj.target_state.domain" /> + <instance-policy-modal + class="right floated mini basic" + type="domain" + :target="obj.target_state.domain" + /> </td> </tr> - <tr v-for="field in targetFields" :key="field.id"> + <tr + v-for="field in targetFields" + :key="field.id" + > <td>{{ field.label }}</td> - <td colspan="2" v-if="field.repr">{{ field.repr }}</td> - <td colspan="2" v-else> - <translate translate-context="*/*/*">N/A</translate> + <td + v-if="field.repr" + colspan="2" + > + {{ field.repr }} + </td> + <td + v-else + colspan="2" + > + <translate translate-context="*/*/*"> + N/A + </translate> </td> </tr> </tbody> @@ -201,42 +319,66 @@ <div class="ui stackable two column grid"> <div class="column"> <h3> - <translate translate-context="Content/*/*/Noun">Internal notes</translate> + <translate translate-context="Content/*/*/Noun"> + Internal notes + </translate> </h3> - <notes-thread @deleted="handleRemovedNote($event)" :notes="obj.notes" /> - <note-form @created="obj.notes.push($event)" :target="{type: 'report', uuid: obj.uuid}" /> + <notes-thread + :notes="obj.notes" + @deleted="handleRemovedNote($event)" + /> + <note-form + :target="{type: 'report', uuid: obj.uuid}" + @created="obj.notes.push($event)" + /> </div> <div class="column"> <h3> - <translate translate-context="Content/*/*/Noun">Actions</translate> + <translate translate-context="Content/*/*/Noun"> + Actions + </translate> </h3> <div class="ui labelled icon basic buttons"> <button v-if="obj.is_handled === false" + :class="['ui', {loading: isLoading}, 'button']" @click="resolve(true)" - :class="['ui', {loading: isLoading}, 'button']"> - <i class="success check icon"></i> - <translate translate-context="Content/*/Button.Label/Verb">Resolve</translate> + > + <i class="success check icon" /> + <translate translate-context="Content/*/Button.Label/Verb"> + Resolve + </translate> </button> <button v-if="obj.is_handled === true" + :class="['ui', {loading: isLoading}, 'button']" @click="resolve(false)" - :class="['ui', {loading: isLoading}, 'button']"> - <i class="warning redo icon"></i> - <translate translate-context="Content/*/Button.Label">Unresolve</translate> + > + <i class="warning redo icon" /> + <translate translate-context="Content/*/Button.Label"> + Unresolve + </translate> </button> - <template v-for="action in actions"> + <template + v-for="(action, key) in actions" + > <dangerous-button v-if="action.dangerous && action.show(obj)" + :key="key" :class="['ui', {loading: isLoading}, 'button']" - :action="action.handler"> - <i :class="[action.iconColor, action.icon, 'icon']"></i> + :action="action.handler" + > + <i :class="[action.iconColor, action.icon, 'icon']" /> {{ action.label }} - <p slot="modal-header">{{ action.modalHeader}}</p> + <p slot="modal-header"> + {{ action.modalHeader }} + </p> <div slot="modal-content"> <p>{{ action.modalContent }}</p> </div> - <p slot="modal-confirm">{{ action.modalConfirmLabel }}</p> + <p slot="modal-confirm"> + {{ action.modalConfirmLabel }} + </p> </dangerous-button> </template> </div> @@ -248,16 +390,14 @@ <script> import axios from 'axios' -import { diffWordsWithSpace } from 'diff' import NoteForm from '@/components/manage/moderation/NoteForm' import NotesThread from '@/components/manage/moderation/NotesThread' import ReportCategoryDropdown from '@/components/moderation/ReportCategoryDropdown' import InstancePolicyModal from '@/components/manage/moderation/InstancePolicyModal' import entities from '@/entities' -import {setUpdate} from '@/utils' +import { setUpdate } from '@/utils' import showdown from 'showdown' - function castValue (value) { if (value === null || value === undefined) { return '' @@ -266,23 +406,24 @@ function castValue (value) { } export default { - props: { - obj: {required: true}, - currentState: {required: false} - }, components: { NoteForm, NotesThread, ReportCategoryDropdown, - InstancePolicyModal, + InstancePolicyModal + }, + props: { + initObj: { type: Object, required: true }, + currentState: { type: String, required: false, default: '' } }, data () { return { + obj: this.initObj, markdown: new showdown.Converter(), isLoading: false, isCollapsed: false, updating: { - type: false, + type: false } } }, @@ -303,7 +444,7 @@ export default { return '' } let namespace - let id = this.target.id + const id = this.target.id if (this.target.type === 'track') { namespace = 'library.tracks.edit.detail' } @@ -313,24 +454,23 @@ export default { if (this.target.type === 'artist') { namespace = 'library.artists.edit.detail' } - return this.$router.resolve({name: namespace, params: {id, editId: this.obj.uuid}}).href + return this.$router.resolve({ name: namespace, params: { id, editId: this.obj.uuid } }).href }, targetFields () { if (!this.target) { return [] } - let payload = this.obj.target_state - let fields = this.configs[this.target.type].moderatedFields - let self = this + const payload = this.obj.target_state + const fields = this.configs[this.target.type].moderatedFields return fields.map((fieldConfig) => { - let dummyRepr = (v) => { return v } - let getValueRepr = fieldConfig.getValueRepr || dummyRepr - let d = { + const dummyRepr = (v) => { return v } + const getValueRepr = fieldConfig.getValueRepr || dummyRepr + const d = { id: fieldConfig.id, label: fieldConfig.label, value: payload[fieldConfig.id], - repr: castValue(getValueRepr(payload[fieldConfig.id])), + repr: castValue(getValueRepr(payload[fieldConfig.id])) } return d }) @@ -346,11 +486,11 @@ export default { if (!this.target) { return [] } - let self = this - let actions = [] - let typeConfig = this.configs[this.target.type] + const self = this + const actions = [] + const typeConfig = this.configs[this.target.type] if (typeConfig.getDeleteUrl) { - let deleteUrl = typeConfig.getDeleteUrl(this.target) + const deleteUrl = typeConfig.getDeleteUrl(this.target) actions.push({ label: this.$pgettext('Content/Moderation/Button/Verb', 'Delete reported object'), modalHeader: this.$pgettext('Content/Moderation/Popup/Header', 'Delete reported object?'), @@ -365,7 +505,7 @@ export default { console.log('Target deleted') self.obj.target = null self.resolve(true) - }, error => { + }, () => { console.log('Error while deleting target') }) } @@ -376,8 +516,8 @@ export default { }, methods: { update (payload) { - let url = `manage/moderation/reports/${this.obj.uuid}/` - let self = this + const url = `manage/moderation/reports/${this.obj.uuid}/` + const self = this this.isLoading = true setUpdate(payload, this.updating, true) axios.patch(url, payload).then((response) => { @@ -385,16 +525,16 @@ export default { Object.assign(self.obj, payload) self.isLoading = false setUpdate(payload, self.updating, false) - }, error => { + }, () => { self.isLoading = false setUpdate(payload, self.updating, false) }) }, resolve (v) { - let url = `manage/moderation/reports/${this.obj.uuid}/` - let self = this + const url = `manage/moderation/reports/${this.obj.uuid}/` + const self = this this.isLoading = true - axios.patch(url, {is_handled: v}).then((response) => { + axios.patch(url, { is_handled: v }).then((response) => { self.$emit('handled', v) self.isLoading = false self.obj.is_handled = v @@ -405,16 +545,16 @@ export default { } else { increment = 1 } - self.$store.commit('ui/incrementNotifications', {count: increment, type: 'pendingReviewReports'}) - }, error => { + self.$store.commit('ui/incrementNotifications', { count: increment, type: 'pendingReviewReports' }) + }, () => { self.isLoading = false }) }, handleRemovedNote (uuid) { this.obj.notes = this.obj.notes.filter((note) => { - return note.uuid != uuid + return note.uuid !== uuid }) - }, + } } } </script> diff --git a/front/src/components/manage/moderation/UserRequestCard.vue b/front/src/components/manage/moderation/UserRequestCard.vue index 3528e0a56ff1a5633205507309ee0467ef6cccbd..8cf93a53b6318cd5575939b4c181fdddeb47f5ce 100644 --- a/front/src/components/manage/moderation/UserRequestCard.vue +++ b/front/src/components/manage/moderation/UserRequestCard.vue @@ -3,30 +3,48 @@ <div class="content"> <h4 class="header"> <router-link :to="{name: 'manage.moderation.requests.detail', params: {id: obj.uuid}}"> - <translate translate-context="Content/Moderation/Card/Short" :translate-params="{id: obj.uuid.substring(0, 8)}">Request %{ id }</translate> + <translate + translate-context="Content/Moderation/Card/Short" + :translate-params="{id: obj.uuid.substring(0, 8)}" + > + Request %{ id } + </translate> </router-link> - <collapse-link class="right floated" v-model="isCollapsed"></collapse-link> + <collapse-link + v-model="isCollapsed" + class="right floated" + /> </h4> <div class="content"> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <div class="ui stackable two column grid"> <div class="column"> <table class="ui very basic unstackable table"> <tbody> <tr> <td> - <translate translate-context="Content/Moderation/*">Submitted by</translate> + <translate translate-context="Content/Moderation/*"> + Submitted by + </translate> </td> <td> - <actor-link :admin="true" :actor="obj.submitter" /> + <actor-link + :admin="true" + :actor="obj.submitter" + /> </td> </tr> <tr> <td> - <translate translate-context="Content/*/*/Noun">Creation date</translate> + <translate translate-context="Content/*/*/Noun"> + Creation date + </translate> </td> <td> - <human-date :date="obj.creation_date" :icon="true"></human-date> + <human-date + :date="obj.creation_date" + :icon="true" + /> </td> </tr> </tbody> @@ -37,49 +55,80 @@ <tbody> <tr> <td> - <translate translate-context="*/*/*">Status</translate> + <translate translate-context="*/*/*"> + Status + </translate> </td> <td> <template v-if="obj.status === 'pending'"> - <i class="warning hourglass icon"></i> - <translate translate-context="Content/Library/*/Short">Pending</translate> + <i class="warning hourglass icon" /> + <translate translate-context="Content/Library/*/Short"> + Pending + </translate> </template> <template v-else-if="obj.status === 'refused'"> - <i class="danger x icon"></i> - <translate translate-context="Content/*/*/Short">Refused</translate> + <i class="danger x icon" /> + <translate translate-context="Content/*/*/Short"> + Refused + </translate> </template> <template v-else-if="obj.status === 'approved'"> - <i class="success check icon"></i> - <translate translate-context="Content/*/*/Short">Approved</translate> + <i class="success check icon" /> + <translate translate-context="Content/*/*/Short"> + Approved + </translate> </template> </td> </tr> <tr> <td> - <translate translate-context="Content/Moderation/*">Assigned to</translate> + <translate translate-context="Content/Moderation/*"> + Assigned to + </translate> </td> <td> <div v-if="obj.assigned_to"> - <actor-link :admin="true" :actor="obj.assigned_to" /> + <actor-link + :admin="true" + :actor="obj.assigned_to" + /> </div> - <translate v-else translate-context="*/*/*">N/A</translate> + <translate + v-else + translate-context="*/*/*" + > + N/A + </translate> </td> </tr> <tr> <td> - <translate translate-context="Content/*/*/Noun">Resolution date</translate> + <translate translate-context="Content/*/*/Noun"> + Resolution date + </translate> </td> <td> - <human-date v-if="obj.handled_date" :date="obj.handled_date" :icon="true"></human-date> - <translate v-else translate-context="*/*/*">N/A</translate> + <human-date + v-if="obj.handled_date" + :date="obj.handled_date" + :icon="true" + /> + <translate + v-else + translate-context="*/*/*" + > + N/A + </translate> </td> </tr> <tr> <td> - <translate translate-context="Content/*/*/Noun">Internal notes</translate> + <translate translate-context="Content/*/*/Noun"> + Internal notes + </translate> </td> <td> - <i class="comment icon"></i> + <i class="comment icon" /> {{ obj.notes.length }} </td> </tr> @@ -89,52 +138,85 @@ </div> </div> </div> - <div class="main content" v-if="!isCollapsed"> + <div + v-if="!isCollapsed" + class="main content" + > <div class="ui stackable two column grid"> <div class="column"> <h3> - <translate translate-context="*/*/Field.Label/Noun">Message</translate> + <translate translate-context="*/*/Field.Label/Noun"> + Message + </translate> </h3> <p> - <translate translate-context="Content/Moderation/Paragraph">This user wants to sign-up on your pod.</translate> + <translate translate-context="Content/Moderation/Paragraph"> + This user wants to sign-up on your pod. + </translate> </p> <template v-if="obj.metadata"> - <div class="ui hidden divider"></div> - <div v-for="k in Object.keys(obj.metadata)" :key="k"> + <div class="ui hidden divider" /> + <div + v-for="k in Object.keys(obj.metadata)" + :key="k" + > <h4>{{ k }}</h4> - <p v-if="obj.metadata[k] && obj.metadata[k].length">{{ obj.metadata[k] }}</p> - <translate v-else translate-context="*/*/*">N/A</translate> - <div class="ui hidden divider"></div> + <p v-if="obj.metadata[k] && obj.metadata[k].length"> + {{ obj.metadata[k] }} + </p> + <translate + v-else + translate-context="*/*/*" + > + N/A + </translate> + <div class="ui hidden divider" /> </div> </template> </div> <aside class="column"> <div v-if="obj.status != 'approved'"> <h3> - <translate translate-context="Content/*/*/Noun">Actions</translate> + <translate translate-context="Content/*/*/Noun"> + Actions + </translate> </h3> <div class="ui labelled icon basic buttons"> <button v-if="obj.status === 'pending' || obj.status === 'refused'" + :class="['ui', {loading: isLoading}, 'button']" @click="approve(true)" - :class="['ui', {loading: isLoading}, 'button']"> - <i class="success check icon"></i> - <translate translate-context="Content/*/Button.Label/Verb">Approve</translate> + > + <i class="success check icon" /> + <translate translate-context="Content/*/Button.Label/Verb"> + Approve + </translate> </button> <button v-if="obj.status === 'pending'" + :class="['ui', {loading: isLoading}, 'button']" @click="approve(false)" - :class="['ui', {loading: isLoading}, 'button']"> - <i class="danger x icon"></i> - <translate translate-context="Content/*/Button.Label">Refuse</translate> + > + <i class="danger x icon" /> + <translate translate-context="Content/*/Button.Label"> + Refuse + </translate> </button> </div> </div> <h3> - <translate translate-context="Content/*/*/Noun">Internal notes</translate> + <translate translate-context="Content/*/*/Noun"> + Internal notes + </translate> </h3> - <notes-thread @deleted="handleRemovedNote($event)" :notes="obj.notes" /> - <note-form @created="obj.notes.push($event)" :target="{type: 'request', uuid: obj.uuid}" /> + <notes-thread + :notes="obj.notes" + @deleted="handleRemovedNote($event)" + /> + <note-form + :target="{type: 'request', uuid: obj.uuid}" + @created="obj.notes.push($event)" + /> </aside> </div> </div> @@ -145,48 +227,47 @@ import axios from 'axios' import NoteForm from '@/components/manage/moderation/NoteForm' import NotesThread from '@/components/manage/moderation/NotesThread' -import {setUpdate} from '@/utils' import showdown from 'showdown' - export default { - props: { - obj: {required: true}, - }, components: { NoteForm, - NotesThread, + NotesThread + }, + props: { + initObj: { type: Object, required: true } }, data () { return { markdown: new showdown.Converter(), isLoading: false, isCollapsed: false, + obj: this.initObj } }, methods: { approve (v) { - let url = `manage/moderation/requests/${this.obj.uuid}/` - let self = this - let newStatus = v ? 'approved' : 'refused' + const url = `manage/moderation/requests/${this.obj.uuid}/` + const self = this + const newStatus = v ? 'approved' : 'refused' this.isLoading = true - axios.patch(url, {status: newStatus}).then((response) => { + axios.patch(url, { status: newStatus }).then((response) => { self.$emit('handled', newStatus) self.isLoading = false self.obj.status = newStatus if (v) { self.isCollapsed = true } - self.$store.commit('ui/incrementNotifications', {count: -1, type: 'pendingReviewRequests'}) - }, error => { + self.$store.commit('ui/incrementNotifications', { count: -1, type: 'pendingReviewRequests' }) + }, () => { self.isLoading = false }) }, handleRemovedNote (uuid) { this.obj.notes = this.obj.notes.filter((note) => { - return note.uuid != uuid + return note.uuid !== uuid }) - }, + } } } </script> diff --git a/front/src/components/manage/users/InvitationForm.vue b/front/src/components/manage/users/InvitationForm.vue index f8a0e88e222a03314225e468fdcd4bb021073016..72011a6aa6f419c415d41bad1d69da1cac44d7ab 100644 --- a/front/src/components/manage/users/InvitationForm.vue +++ b/front/src/components/manage/users/InvitationForm.vue @@ -1,41 +1,92 @@ <template> <div> - <form 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/Admin/Error message.Title">Error while creating invitation</translate></h4> + <form + 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/Admin/Error message.Title"> + Error while creating invitation + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> <div class="inline fields"> <div class="ui field"> <label for="invitation-code"><translate translate-context="Content/*/Input.Label">Invitation code</translate></label> - <input for="invitation-code" name="code" type="text" v-model="code" :placeholder="labels.placeholder" /> + <input + v-model="code" + for="invitation-code" + name="code" + type="text" + :placeholder="labels.placeholder" + > </div> <div class="ui field"> - <button :class="['ui', {loading: isLoading}, 'button']" :disabled="isLoading" type="submit"> - <translate translate-context="Content/Admin/Button.Label/Verb">Get a new invitation</translate> + <button + :class="['ui', {loading: isLoading}, 'button']" + :disabled="isLoading" + type="submit" + > + <translate translate-context="Content/Admin/Button.Label/Verb"> + Get a new invitation + </translate> </button> </div> </div> </form> <div v-if="invitations.length > 0"> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <table class="ui ui basic table"> <thead> <tr> - <th><translate translate-context="Content/Admin/Table.Label/Noun">Code</translate></th> - <th><translate translate-context="Content/Admin/Table.Label/Noun">Share link</translate></th> + <th> + <translate translate-context="Content/Admin/Table.Label/Noun"> + Code + </translate> + </th> + <th> + <translate translate-context="Content/Admin/Table.Label/Noun"> + Share link + </translate> + </th> </tr> </thead> <tbody> - <tr v-for="invitation in invitations" :key="invitation.code"> + <tr + v-for="invitation in invitations" + :key="invitation.code" + > <td>{{ invitation.code.toUpperCase() }}</td> - <td><a :href="getUrl(invitation.code)" target="_blank">{{ getUrl(invitation.code) }}</a></td> + <td> + <a + :href="getUrl(invitation.code)" + target="_blank" + >{{ getUrl(invitation.code) }}</a> + </td> </tr> </tbody> </table> - <button class="ui basic button" @click="invitations = []"><translate translate-context="Content/Library/Button.Label">Clear</translate></button> + <button + class="ui basic button" + @click="invitations = []" + > + <translate translate-context="Content/Library/Button.Label"> + Clear + </translate> + </button> </div> </div> </template> @@ -61,11 +112,11 @@ export default { }, methods: { submit () { - let self = this + const self = this this.isLoading = true this.errors = [] - let url = 'manage/users/invitations/' - let payload = { + const url = 'manage/users/invitations/' + const payload = { code: this.code } axios.post(url, payload).then((response) => { @@ -77,7 +128,7 @@ export default { }) }, getUrl (code) { - return this.$store.getters['instance/absoluteUrl'](this.$router.resolve({name: 'signup', query: {invitation: code.toUpperCase()}}).href) + return this.$store.getters['instance/absoluteUrl'](this.$router.resolve({ name: 'signup', query: { invitation: code.toUpperCase() } }).href) } } } diff --git a/front/src/components/manage/users/InvitationsTable.vue b/front/src/components/manage/users/InvitationsTable.vue index 62e5a4f05c6f7770c02a0f369f740868eea3a75b..289af6398005d89d253e9ef3b84bb6db18e92465 100644 --- a/front/src/components/manage/users/InvitationsTable.vue +++ b/front/src/components/manage/users/InvitationsTable.vue @@ -4,58 +4,126 @@ <div class="fields"> <div class="ui field"> <label for="invitations-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label> - <input id="invitations-search" name="search" type="text" v-model="search" :placeholder="labels.searchPlaceholder" /> + <input + id="invitations-search" + v-model="search" + name="search" + type="text" + :placeholder="labels.searchPlaceholder" + > </div> <div class="field"> <label for="invitations-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> - <select id="invitations-ordering" class="ui dropdown" v-model="ordering"> - <option v-for="option in orderingOptions" :value="option[0]"> + <select + id="invitations-ordering" + v-model="ordering" + class="ui dropdown" + > + <option + v-for="(option, key) in orderingOptions" + :key="key" + :value="option[0]" + > {{ sharedLabels.filters[option[1]] }} </option> </select> </div> <div class="field"> <label for="invitations-status"><translate translate-context="*/*/*">Status</translate></label> - <select id="invitations-status" class="ui dropdown" v-model="isOpen"> - <option :value="null"><translate translate-context="Content/*/Dropdown">All</translate></option> - <option :value="true"><translate translate-context="Content/Admin/Dropdown/Adjective">Open</translate></option> - <option :value="false"><translate translate-context="Content/Admin/Dropdown/Adjective">Expired/used</translate></option> + <select + id="invitations-status" + v-model="isOpen" + class="ui dropdown" + > + <option :value="null"> + <translate translate-context="Content/*/Dropdown"> + All + </translate> + </option> + <option :value="true"> + <translate translate-context="Content/Admin/Dropdown/Adjective"> + Open + </translate> + </option> + <option :value="false"> + <translate translate-context="Content/Admin/Dropdown/Adjective"> + Expired/used + </translate> + </option> </select> </div> </div> - </div> + </div> <div class="dimmable"> - <div v-if="isLoading" class="ui active inverted dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui active inverted dimmer" + > + <div class="ui loader" /> </div> <action-table v-if="result" - @action-launched="fetchData" :objects-data="result" :actions="actions" :action-url="'manage/users/invitations/action/'" - :filters="actionFilters"> + :filters="actionFilters" + @action-launched="fetchData" + > <template slot="header-cells"> - <th><translate translate-context="*/*/*">Owner</translate></th> - <th><translate translate-context="*/*/*">Status</translate></th> - <th><translate translate-context="Content/*/*/Noun">Creation date</translate></th> - <th><translate translate-context="Content/Admin/Table.Label/Noun">Expiration date</translate></th> - <th><translate translate-context="Content/Admin/Table.Label/Noun">Code</translate></th> + <th> + <translate translate-context="*/*/*"> + Owner + </translate> + </th> + <th> + <translate translate-context="*/*/*"> + Status + </translate> + </th> + <th> + <translate translate-context="Content/*/*/Noun"> + Creation date + </translate> + </th> + <th> + <translate translate-context="Content/Admin/Table.Label/Noun"> + Expiration date + </translate> + </th> + <th> + <translate translate-context="Content/Admin/Table.Label/Noun"> + Code + </translate> + </th> </template> - <template slot="row-cells" slot-scope="scope"> + <template + slot="row-cells" + slot-scope="scope" + > <td> - <router-link :to="{name: 'manage.users.users.detail', params: {id: scope.obj.id }}">{{ scope.obj.owner.username }}</router-link> + <router-link :to="{name: 'manage.users.users.detail', params: {id: scope.obj.id }}"> + {{ scope.obj.owner.username }} + </router-link> </td> <td> - <span v-if="scope.obj.users.length > 0" class="ui success basic label"><translate translate-context="Content/Admin/Table">Used</translate></span> - <span v-else-if="moment().isAfter(scope.obj.expiration_date)" class="ui danger basic label"><translate translate-context="Content/Admin/Table">Expired</translate></span> - <span v-else class="ui basic label"><translate translate-context="Content/Admin/Table">Not used</translate></span> + <span + v-if="scope.obj.users.length > 0" + class="ui success basic label" + ><translate translate-context="Content/Admin/Table">Used</translate></span> + <span + v-else-if="moment().isAfter(scope.obj.expiration_date)" + class="ui danger basic label" + ><translate translate-context="Content/Admin/Table">Expired</translate></span> + <span + v-else + class="ui basic label" + ><translate translate-context="Content/Admin/Table">Not used</translate></span> </td> <td> - <human-date :date="scope.obj.creation_date"></human-date> + <human-date :date="scope.obj.creation_date" /> </td> <td> - <human-date :date="scope.obj.expiration_date"></human-date> + <human-date :date="scope.obj.expiration_date" /> </td> <td> {{ scope.obj.code.toUpperCase() }} @@ -66,18 +134,20 @@ <div> <pagination v-if="result && result.count > paginateBy" - @page-changed="selectPage" :compact="true" :current="page" :paginate-by="paginateBy" :total="result.count" - ></pagination> + @page-changed="selectPage" + /> <span v-if="result && result.results.length > 0"> - <translate translate-context="Content/*/Paragraph" + <translate + translate-context="Content/*/Paragraph" translate-plural="Showing results %{ start } to %{ end } from %{ total }" :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}" - :translate-n="result.count"> + :translate-n="result.count" + > Showing one result </translate> </span> @@ -95,16 +165,16 @@ import OrderingMixin from '@/components/mixins/Ordering' import TranslationsMixin from '@/components/mixins/Translations' export default { - mixins: [OrderingMixin, TranslationsMixin], - props: { - filters: {type: Object, required: false} - }, components: { Pagination, ActionTable }, + mixins: [OrderingMixin, TranslationsMixin], + props: { + filters: { type: Object, required: false, default: function () { return {} } } + }, data () { - let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') + const defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') return { moment, isLoading: false, @@ -122,33 +192,6 @@ export default { } }, - created () { - this.fetchData() - }, - methods: { - fetchData () { - let params = _.merge({ - 'page': this.page, - 'page_size': this.paginateBy, - 'q': this.search, - 'is_open': this.isOpen, - 'ordering': this.getOrderingAsString() - }, this.filters) - let self = this - self.isLoading = true - self.checked = [] - axios.get('/manage/users/invitations/', {params: params}).then((response) => { - self.result = response.data - self.isLoading = false - }, error => { - self.isLoading = false - self.errors = error.backendErrors - }) - }, - selectPage: function (page) { - this.page = page - } - }, computed: { labels () { return { @@ -156,7 +199,7 @@ export default { } }, actionFilters () { - var currentFilters = { + const currentFilters = { q: this.search } if (this.filters) { @@ -166,7 +209,7 @@ export default { } }, actions () { - let deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete') + const deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete') return [ { name: 'delete', @@ -198,6 +241,33 @@ export default { this.page = 1 this.fetchData() } + }, + created () { + this.fetchData() + }, + methods: { + fetchData () { + const params = _.merge({ + page: this.page, + page_size: this.paginateBy, + q: this.search, + is_open: this.isOpen, + ordering: this.getOrderingAsString() + }, this.filters) + const self = this + self.isLoading = true + self.checked = [] + axios.get('/manage/users/invitations/', { params: params }).then((response) => { + self.result = response.data + self.isLoading = false + }, error => { + self.isLoading = false + self.errors = error.backendErrors + }) + }, + selectPage: function (page) { + this.page = page + } } } </script> diff --git a/front/src/components/manage/users/UsersTable.vue b/front/src/components/manage/users/UsersTable.vue index 3ad6210ca7a6c6c2f1ad6307094c4ffed112639e..ed1f1491bd46c165c982ee801b71f42904c2cdef 100644 --- a/front/src/components/manage/users/UsersTable.vue +++ b/front/src/components/manage/users/UsersTable.vue @@ -4,73 +4,172 @@ <div class="fields"> <div class="ui field"> <label for="users-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label> - <input id="users-search" name="search" type="text" v-model="search" :placeholder="labels.searchPlaceholder" /> + <input + id="users-search" + v-model="search" + name="search" + type="text" + :placeholder="labels.searchPlaceholder" + > </div> <div class="field"> <label for="users-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> - <select id="users-ordering" class="ui dropdown" v-model="ordering"> - <option v-for="option in orderingOptions" :value="option[0]"> + <select + id="users-ordering" + v-model="ordering" + class="ui dropdown" + > + <option + v-for="(option, key) in orderingOptions" + :key="key" + :value="option[0]" + > {{ sharedLabels.filters[option[1]] }} </option> </select> </div> <div class="field"> <label for="users-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Order</translate></label> - <select id="users-ordering-direction" class="ui dropdown" v-model="orderingDirection"> - <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option> - <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option> + <select + id="users-ordering-direction" + v-model="orderingDirection" + class="ui dropdown" + > + <option value="+"> + <translate translate-context="Content/Search/Dropdown"> + Ascending + </translate> + </option> + <option value="-"> + <translate translate-context="Content/Search/Dropdown"> + Descending + </translate> + </option> </select> </div> </div> - </div> + </div> <div class="dimmable"> - <div v-if="isLoading" class="ui active inverted dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui active inverted dimmer" + > + <div class="ui loader" /> </div> <action-table v-if="result" - @action-launched="fetchData" :objects-data="result" :actions="actions" :action-url="'manage/library/uploads/action/'" - :filters="actionFilters"> + :filters="actionFilters" + @action-launched="fetchData" + > <template slot="header-cells"> - <th><translate translate-context="Content/*/*">Username</translate></th> - <th><translate translate-context="Content/*/*/Noun">Email</translate></th> - <th><translate translate-context="Content/Admin/Table.Label/Short, Noun">Account status</translate></th> - <th><translate translate-context="Content/Admin/Table.Label/Short, Noun (Value is a date)">Sign-up</translate></th> - <th><translate translate-context="Content/Profile/Table.Label/Short, Noun (Value is a date)">Last activity</translate></th> - <th><translate translate-context="Content/*/*/Noun">Permissions</translate></th> - <th><translate translate-context="*/*/*">Status</translate></th> + <th> + <translate translate-context="Content/*/*"> + Username + </translate> + </th> + <th> + <translate translate-context="Content/*/*/Noun"> + Email + </translate> + </th> + <th> + <translate translate-context="Content/Admin/Table.Label/Short, Noun"> + Account status + </translate> + </th> + <th> + <translate translate-context="Content/Admin/Table.Label/Short, Noun (Value is a date)"> + Sign-up + </translate> + </th> + <th> + <translate translate-context="Content/Profile/Table.Label/Short, Noun (Value is a date)"> + Last activity + </translate> + </th> + <th> + <translate translate-context="Content/*/*/Noun"> + Permissions + </translate> + </th> + <th> + <translate translate-context="*/*/*"> + Status + </translate> + </th> </template> - <template slot="row-cells" slot-scope="scope"> + <template + slot="row-cells" + slot-scope="scope" + > <td> - <router-link v-if="scope.obj.actor" :to="{name: 'manage.moderation.accounts.detail', params: {id: scope.obj.actor.full_username }}">{{ scope.obj.username }}</router-link> - <router-link v-else :to="{name: 'manage.moderation.accounts.detail', params: {id: scope.obj.full_username }}">{{ scope.obj.username }}</router-link> + <router-link + v-if="scope.obj.actor" + :to="{name: 'manage.moderation.accounts.detail', params: {id: scope.obj.actor.full_username }}" + > + {{ scope.obj.username }} + </router-link> + <router-link + v-else + :to="{name: 'manage.moderation.accounts.detail', params: {id: scope.obj.full_username }}" + > + {{ scope.obj.username }} + </router-link> </td> <td> <span>{{ scope.obj.email }}</span> </td> <td> - <span v-if="scope.obj.is_active" class="ui basic success label"><translate translate-context="Content/Admin/Table">Active</translate></span> - <span v-else class="ui basic label"><translate translate-context="Content/Admin/Table">Inactive</translate></span> + <span + v-if="scope.obj.is_active" + class="ui basic success label" + ><translate translate-context="Content/Admin/Table">Active</translate></span> + <span + v-else + class="ui basic label" + ><translate translate-context="Content/Admin/Table">Inactive</translate></span> </td> <td> - <human-date :date="scope.obj.date_joined"></human-date> + <human-date :date="scope.obj.date_joined" /> </td> <td> - <human-date v-if="scope.obj.last_activity" :date="scope.obj.last_activity"></human-date> - <template v-else><translate translate-context="*/*/*">N/A</translate></template> + <human-date + v-if="scope.obj.last_activity" + :date="scope.obj.last_activity" + /> + <template v-else> + <translate translate-context="*/*/*"> + N/A + </translate> + </template> </td> <td> - <template v-for="p in permissions"> - <span class="ui basic tiny label" v-if="scope.obj.permissions[p.code]">{{ p.label }}</span> + <template + v-for="(p, key) in permissions" + > + <span + v-if="scope.obj.permissions[p.code]" + :key="key" + class="ui basic tiny label" + >{{ p.label }}</span> </template> </td> <td> - <span v-if="scope.obj.is_superuser" class="ui pink label"><translate translate-context="Content/Admin/Table.User role">Admin</translate></span> - <span v-else-if="scope.obj.is_staff" class="ui purple label"><translate translate-context="Content/Profile/User role">Staff member</translate></span> - <span v-else class="ui basic label"><translate translate-context="Content/Admin/Table, User role">Regular user</translate></span> + <span + v-if="scope.obj.is_superuser" + class="ui pink label" + ><translate translate-context="Content/Admin/Table.User role">Admin</translate></span> + <span + v-else-if="scope.obj.is_staff" + class="ui purple label" + ><translate translate-context="Content/Profile/User role">Staff member</translate></span> + <span + v-else + class="ui basic label" + ><translate translate-context="Content/Admin/Table, User role">Regular user</translate></span> </td> </template> </action-table> @@ -78,18 +177,20 @@ <div> <pagination v-if="result && result.count > paginateBy" - @page-changed="selectPage" :compact="true" :current="page" :paginate-by="paginateBy" :total="result.count" - ></pagination> + @page-changed="selectPage" + /> <span v-if="result && result.results.length > 0"> - <translate translate-context="Content/*/Paragraph" + <translate + translate-context="Content/*/Paragraph" translate-plural="Showing results %{ start } to %{ end } from %{ total }" :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}" - :translate-n="result.count"> + :translate-n="result.count" + > Showing one result </translate> </span> @@ -107,16 +208,16 @@ import OrderingMixin from '@/components/mixins/Ordering' import TranslationsMixin from '@/components/mixins/Translations' export default { - mixins: [OrderingMixin, TranslationsMixin], - props: { - filters: {type: Object, required: false} - }, components: { Pagination, ActionTable }, + mixins: [OrderingMixin, TranslationsMixin], + props: { + filters: { type: Object, required: false, default: function () { return {} } } + }, data () { - let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-date_joined') + const defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-date_joined') return { time, isLoading: false, @@ -134,32 +235,6 @@ export default { } }, - created () { - this.fetchData() - }, - methods: { - fetchData () { - let params = _.merge({ - 'page': this.page, - 'page_size': this.paginateBy, - 'q': this.search, - 'ordering': this.getOrderingAsString() - }, this.filters) - let self = this - self.isLoading = true - self.checked = [] - axios.get('/manage/users/users/', {params: params}).then((response) => { - self.result = response.data - self.isLoading = false - }, error => { - self.isLoading = false - self.errors = error.backendErrors - }) - }, - selectPage: function (page) { - this.page = page - } - }, computed: { labels () { return { @@ -172,21 +247,21 @@ export default { permissions () { return [ { - 'code': 'library', - 'label': this.$pgettext('*/*/*/Noun', 'Library') + code: 'library', + label: this.$pgettext('*/*/*/Noun', 'Library') }, { - 'code': 'moderation', - 'label': this.$pgettext('*/Moderation/*', 'Moderation') + code: 'moderation', + label: this.$pgettext('*/Moderation/*', 'Moderation') }, { - 'code': 'settings', - 'label': this.$pgettext('*/*/*/Noun', 'Settings') + code: 'settings', + label: this.$pgettext('*/*/*/Noun', 'Settings') } ] }, actionFilters () { - var currentFilters = { + const currentFilters = { q: this.search } if (this.filters) { @@ -219,6 +294,32 @@ export default { orderingDirection () { this.fetchData() } + }, + created () { + this.fetchData() + }, + methods: { + fetchData () { + const params = _.merge({ + page: this.page, + page_size: this.paginateBy, + q: this.search, + ordering: this.getOrderingAsString() + }, this.filters) + const self = this + self.isLoading = true + self.checked = [] + axios.get('/manage/users/users/', { params: params }).then((response) => { + self.result = response.data + self.isLoading = false + }, error => { + self.isLoading = false + self.errors = error.backendErrors + }) + }, + selectPage: function (page) { + this.page = page + } } } </script> diff --git a/front/src/components/mixins/Ordering.vue b/front/src/components/mixins/Ordering.vue index d91f28db54cbf49f926bd08dec55540d19aa7457..787519e91df4383b91d93d41db55aa86f7e0c78f 100644 --- a/front/src/components/mixins/Ordering.vue +++ b/front/src/components/mixins/Ordering.vue @@ -1,50 +1,50 @@ <script> export default { props: { - defaultOrdering: {type: String, required: false}, - orderingConfigName: {type: String, required: false}, + defaultOrdering: { type: String, required: false, default: '' }, + orderingConfigName: { type: String, required: false, default: '' } }, computed: { orderingConfig () { return this.$store.state.ui.routePreferences[this.orderingConfigName || this.$route.name] }, paginateBy: { - set(paginateBy) { + set (paginateBy) { this.$store.commit('ui/paginateBy', { route: this.$route.name, value: paginateBy }) }, - get() { + get () { return this.orderingConfig.paginateBy } }, ordering: { - set(ordering) { + set (ordering) { this.$store.commit('ui/ordering', { route: this.$route.name, value: ordering }) }, - get() { + get () { return this.orderingConfig.ordering } }, orderingDirection: { - set(orderingDirection) { + set (orderingDirection) { this.$store.commit('ui/orderingDirection', { route: this.$route.name, value: orderingDirection }) }, - get() { + get () { return this.orderingConfig.orderingDirection } - }, + } }, methods: { getOrderingFromString (s) { - let parts = s.split('-') + const parts = s.split('-') if (parts.length > 1) { return { direction: '-', diff --git a/front/src/components/mixins/Pagination.vue b/front/src/components/mixins/Pagination.vue index 532faaaa3bc52872a6be104d5577f94b8f58d30c..1bc7a05b9e8386ad9ab3438a0be3379c954aff3b 100644 --- a/front/src/components/mixins/Pagination.vue +++ b/front/src/components/mixins/Pagination.vue @@ -1,8 +1,8 @@ <script> export default { props: { - defaultPage: {required: false, default: 1}, - defaultPaginateBy: {required: false} + defaultPage: { type: Number, required: false, default: 1 }, + defaultPaginateBy: { type: Number, required: false, default: 1 } } } </script> diff --git a/front/src/components/mixins/PlayOptions.vue b/front/src/components/mixins/PlayOptions.vue index 2a9ed4345b65b3c9094f4e4d14df5e2823314f63..ebe1810ef17e3ab748fc1733f64339434ff2880f 100644 --- a/front/src/components/mixins/PlayOptions.vue +++ b/front/src/components/mixins/PlayOptions.vue @@ -1,9 +1,10 @@ <script> import axios from 'axios' +import jQuery from 'jquery' export default { computed: { - playable () { + playable () { if (this.isPlayable) { return true } @@ -34,27 +35,28 @@ export default { if (this.artist) { return this.artist } - }, + return null + } }, methods: { filterArtist () { - this.$store.dispatch('moderation/hide', {type: 'artist', target: this.filterableArtist}) + this.$store.dispatch('moderation/hide', { type: 'artist', target: this.filterableArtist }) }, - activateTrack(track, index) { + activateTrack (track, index) { if ( this.currentTrack && this.isPlaying && track.id === this.currentTrack.id ) { - this.pausePlayback(); + this.pausePlayback() } else if ( this.currentTrack && !this.isPlaying && track.id === this.currentTrack.id ) { - this.resumePlayback(); + this.resumePlayback() } else { - this.replacePlay(this.tracks, index); + this.replacePlay(this.tracks, index) } }, getTracksPage (page, params, resolve, tracks) { @@ -64,13 +66,13 @@ export default { } // when fetching artists/or album tracks, sometimes, we may have to fetch // multiple pages - let self = this - params['page_size'] = 100 - params['page'] = page - params['hidden'] = '' - params['playable'] = 'true' + const self = this + params.page_size = 100 + params.page = page + params.hidden = '' + params.playable = 'true' tracks = tracks || [] - axios.get('tracks/', {params: params}).then((response) => { + axios.get('tracks/', { params: params }).then((response) => { response.data.results.forEach(t => { tracks.push(t) }) @@ -82,9 +84,9 @@ export default { }) }, getPlayableTracks () { - let self = this + const self = this this.isLoading = true - let getTracks = new Promise((resolve, reject) => { + const getTracks = new Promise((resolve, reject) => { if (self.tracks) { resolve(self.tracks) } else if (self.track) { @@ -97,9 +99,9 @@ export default { resolve([self.track]) } } else if (self.playlist) { - let url = 'playlists/' + self.playlist.id + '/' + const url = 'playlists/' + self.playlist.id + '/' axios.get(url + 'tracks/').then((response) => { - let artistIds = self.$store.getters['moderation/artistFilters']().map((f) => { + const artistIds = self.$store.getters['moderation/artistFilters']().map((f) => { return f.target.id }) let tracks = response.data.results.map(plt => { @@ -108,21 +110,21 @@ export default { if (artistIds.length > 0) { // skip tracks from hidden artists tracks = tracks.filter((t) => { - let matchArtist = artistIds.indexOf(t.artist.id) > -1 - return !(matchArtist || t.album && artistIds.indexOf(t.album.artist.id) > -1) + const matchArtist = artistIds.indexOf(t.artist.id) > -1 + return !((matchArtist || t.album) && artistIds.indexOf(t.album.artist.id) > -1) }) } resolve(tracks) }) } else if (self.artist) { - let params = {'artist': self.artist.id, include_channels: 'true', 'ordering': 'album__release_date,disc_number,position'} + const params = { artist: self.artist.id, include_channels: 'true', ordering: 'album__release_date,disc_number,position' } self.getTracksPage(1, params, resolve) } else if (self.album) { - let params = {'album': self.album.id, include_channels: 'true', 'ordering': 'disc_number,position'} + const params = { album: self.album.id, include_channels: 'true', ordering: 'disc_number,position' } self.getTracksPage(1, params, resolve) } else if (self.library) { - let params = {'library': self.library.uuid, 'ordering': '-creation_date'} + const params = { library: self.library.uuid, ordering: '-creation_date' } self.getTracksPage(1, params, resolve) } }) @@ -136,17 +138,17 @@ export default { }) }, add () { - let self = this + const self = this this.getPlayableTracks().then((tracks) => { - self.$store.dispatch('queue/appendMany', {tracks: tracks}).then(() => self.addMessage(tracks)) + self.$store.dispatch('queue/appendMany', { tracks: tracks }).then(() => self.addMessage(tracks)) }) jQuery(self.$el).find('.ui.dropdown').dropdown('hide') }, replacePlay () { - let self = this + const self = this self.$store.dispatch('queue/clean') this.getPlayableTracks().then((tracks) => { - self.$store.dispatch('queue/appendMany', {tracks: tracks}).then(() => { + self.$store.dispatch('queue/appendMany', { tracks: tracks }).then(() => { if (self.track) { // set queue position to selected track const trackIndex = self.tracks.findIndex(track => track.id === self.track.id) @@ -158,11 +160,11 @@ export default { jQuery(self.$el).find('.ui.dropdown').dropdown('hide') }, addNext (next) { - let self = this - let wasEmpty = this.$store.state.queue.tracks.length === 0 + const self = this + const wasEmpty = this.$store.state.queue.tracks.length === 0 this.getPlayableTracks().then((tracks) => { - self.$store.dispatch('queue/appendMany', {tracks: tracks, index: self.$store.state.queue.currentIndex + 1}).then(() => self.addMessage(tracks)) - let goNext = next && !wasEmpty + self.$store.dispatch('queue/appendMany', { tracks: tracks, index: self.$store.state.queue.currentIndex + 1 }).then(() => self.addMessage(tracks)) + const goNext = next && !wasEmpty if (goNext) { self.$store.dispatch('queue/next') } @@ -173,12 +175,12 @@ export default { if (tracks.length < 1) { return } - let msg = this.$npgettext('*/Queue/Message', '%{ count } track was added to your queue', '%{ count } tracks were added to your queue', tracks.length) + const msg = this.$npgettext('*/Queue/Message', '%{ count } track was added to your queue', '%{ count } tracks were added to your queue', tracks.length) this.$store.commit('ui/addMessage', { - content: this.$gettextInterpolate(msg, {count: tracks.length}), + content: this.$gettextInterpolate(msg, { count: tracks.length }), date: new Date() }) - }, + } } } -</script> \ No newline at end of file +</script> diff --git a/front/src/components/mixins/Report.vue b/front/src/components/mixins/Report.vue index 403b89f243c2696d866f0da1f6404f2ab17dc9f0..66a49540b9aedca436cab4aa206d0dd16c6b7e89 100644 --- a/front/src/components/mixins/Report.vue +++ b/front/src/components/mixins/Report.vue @@ -1,18 +1,18 @@ <script> export default { methods: { - getReportableObjs ({track, album, artist, playlist, account, library, channel}) { - let reportableObjs = [] + getReportableObjs ({ track, album, artist, playlist, account, library, channel }) { + const reportableObjs = [] if (account) { - let accountLabel = this.$pgettext('*/Moderation/*/Verb', "Report @%{ username }…") + const accountLabel = this.$pgettext('*/Moderation/*/Verb', 'Report @%{ username }…') reportableObjs.push({ - label: this.$gettextInterpolate(accountLabel, {username: account.preferred_username}), + label: this.$gettextInterpolate(accountLabel, { username: account.preferred_username }), target: { type: 'account', _obj: account, full_username: account.full_username, label: account.full_username, - typeLabel: this.$pgettext("*/*/*/Noun", 'Account'), + typeLabel: this.$pgettext('*/*/*/Noun', 'Account') } }) if (track) { @@ -22,13 +22,13 @@ export default { } if (track) { reportableObjs.push({ - label: this.$pgettext('*/Moderation/*/Verb', "Report this track…"), + label: this.$pgettext('*/Moderation/*/Verb', 'Report this track…'), target: { type: 'track', id: track.id, _obj: track, label: track.title, - typeLabel: this.$pgettext("*/*/*/Noun", 'Track'), + typeLabel: this.$pgettext('*/*/*/Noun', 'Track') } }) album = track.album @@ -36,13 +36,13 @@ export default { } if (album) { reportableObjs.push({ - label: this.$pgettext('*/Moderation/*/Verb', "Report this album…"), + label: this.$pgettext('*/Moderation/*/Verb', 'Report this album…'), target: { type: 'album', id: album.id, label: album.title, _obj: album, - typeLabel: this.$pgettext("*/*/*", 'Album'), + typeLabel: this.$pgettext('*/*/*', 'Album') } }) if (!artist) { @@ -52,54 +52,53 @@ export default { if (channel) { reportableObjs.push({ - label: this.$pgettext('*/Moderation/*/Verb', "Report this channel…"), + label: this.$pgettext('*/Moderation/*/Verb', 'Report this channel…'), target: { type: 'channel', uuid: channel.uuid, label: channel.artist.name, _obj: channel, - typeLabel: this.$pgettext("*/*/*", 'Channel'), + typeLabel: this.$pgettext('*/*/*', 'Channel') } }) - } - else if (artist) { + } else if (artist) { reportableObjs.push({ - label: this.$pgettext('*/Moderation/*/Verb', "Report this artist…"), + label: this.$pgettext('*/Moderation/*/Verb', 'Report this artist…'), target: { type: 'artist', id: artist.id, label: artist.name, _obj: artist, - typeLabel: this.$pgettext("*/*/*/Noun", 'Artist'), + typeLabel: this.$pgettext('*/*/*/Noun', 'Artist') } }) } if (playlist) { reportableObjs.push({ - label: this.$pgettext('*/Moderation/*/Verb', "Report this playlist…"), + label: this.$pgettext('*/Moderation/*/Verb', 'Report this playlist…'), target: { type: 'playlist', id: playlist.id, label: playlist.name, _obj: playlist, - typeLabel: this.$pgettext("*/*/*", 'Playlist'), + typeLabel: this.$pgettext('*/*/*', 'Playlist') } }) } if (library) { reportableObjs.push({ - label: this.$pgettext('*/Moderation/*/Verb', "Report this library…"), + label: this.$pgettext('*/Moderation/*/Verb', 'Report this library…'), target: { type: 'library', uuid: library.uuid, label: library.name, _obj: library, - typeLabel: this.$pgettext("*/*/*/Noun", 'Library'), + typeLabel: this.$pgettext('*/*/*/Noun', 'Library') } }) } return reportableObjs - }, + } } } </script> diff --git a/front/src/components/mixins/SmartSearch.vue b/front/src/components/mixins/SmartSearch.vue index ef450603961a8cbdf61385d861698993a7ee5734..f662f8a303cebe2ed3f0fe4cb6c7a79c22f4c497 100644 --- a/front/src/components/mixins/SmartSearch.vue +++ b/front/src/components/mixins/SmartSearch.vue @@ -1,15 +1,39 @@ <script> -import {normalizeQuery, parseTokens, compileTokens} from '@/search' +import { normalizeQuery, parseTokens, compileTokens } from '@/search' export default { props: { - defaultQuery: {type: String, required: false}, - updateUrl: {type: Boolean, required: false, default: false}, + defaultQuery: { type: String, required: false, default: '' }, + updateUrl: { type: Boolean, required: false, default: false } + }, + watch: { + 'search.query' (newValue) { + this.search.tokens = parseTokens(normalizeQuery(newValue)) + }, + 'search.tokens': { + handler (newValue) { + const newQuery = compileTokens(newValue) + if (this.updateUrl) { + const params = {} + if (newQuery) { + params.q = newQuery + } + this.$router.replace({ + query: params + }) + } else { + this.search.query = newQuery + this.page = 1 + this.fetchData() + } + }, + deep: true + } }, methods: { getTokenValue (key, fallback) { - let matching = this.search.tokens.filter(t => { + const matching = this.search.tokens.filter(t => { return t.field === key }) if (matching.length > 0) { @@ -22,10 +46,10 @@ export default { if (!value) { // we remove existing matching tokens, if any this.search.tokens = this.search.tokens.filter(t => { - return t.field != key + return t.field !== key }) } else { - let existing = this.search.tokens.filter(t => { + const existing = this.search.tokens.filter(t => { return t.field === key }) if (existing.length > 0) { @@ -35,34 +59,10 @@ export default { }) } else { // we add a new token - this.search.tokens.push({field: key, value}) + this.search.tokens.push({ field: key, value }) } } - }, - }, - watch: { - 'search.query' (newValue) { - this.search.tokens = parseTokens(normalizeQuery(newValue)) - }, - 'search.tokens': { - handler (newValue) { - let newQuery = compileTokens(newValue) - if (this.updateUrl) { - let params = {} - if (newQuery) { - params.q = newQuery - } - this.$router.replace({ - query: params - }) - } else { - this.search.query = newQuery - this.page = 1 - this.fetchData() - } - }, - deep: true - }, + } } } </script> diff --git a/front/src/components/mixins/Translations.vue b/front/src/components/mixins/Translations.vue index 2d2631e8748d2bab5f40c9faad8164b4f693c18b..4380ae6eecc1b55449cd883740e47e3fb42fe3e0 100644 --- a/front/src/components/mixins/Translations.vue +++ b/front/src/components/mixins/Translations.vue @@ -10,12 +10,12 @@ export default { choices: { me: this.$pgettext('Content/Settings/Dropdown', 'Nobody except me'), instance: this.$pgettext('Content/Settings/Dropdown', 'Everyone on this instance'), - everyone: this.$pgettext('Content/Settings/Dropdown', 'Everyone, across all instances'), + everyone: this.$pgettext('Content/Settings/Dropdown', 'Everyone, across all instances') }, shortChoices: { me: this.$pgettext('Content/Settings/Dropdown/Short', 'Private'), instance: this.$pgettext('Content/Settings/Dropdown/Short', 'Instance'), - everyone: this.$pgettext('Content/Settings/Dropdown/Short', 'Everyone'), + everyone: this.$pgettext('Content/Settings/Dropdown/Short', 'Everyone') } }, import_status: { @@ -23,46 +23,46 @@ export default { choices: { skipped: { label: this.$pgettext('Content/Library/*', 'Skipped'), - help: this.$pgettext('Content/Library/Help text', 'This track is already present in one of your libraries'), + help: this.$pgettext('Content/Library/Help text', 'This track is already present in one of your libraries') }, draft: { label: this.$pgettext('Content/Library/*/Short', 'Draft'), - help: this.$pgettext('Content/Library/Help text', 'This track has been uploaded, but hasn\'t been scheduled for processing yet'), + help: this.$pgettext('Content/Library/Help text', 'This track has been uploaded, but hasn\'t been scheduled for processing yet') }, pending: { label: this.$pgettext('Content/Library/*/Short', 'Pending'), - help: this.$pgettext('Content/Library/Help text', 'This track has been uploaded, but hasn\'t been processed by the server yet'), + help: this.$pgettext('Content/Library/Help text', 'This track has been uploaded, but hasn\'t been processed by the server yet') }, errored: { label: this.$pgettext('Content/Library/Table/Short', 'Errored'), - help: this.$pgettext('Content/Library/Help text', 'This track could not be processed, please make sure it is tagged correctly'), + help: this.$pgettext('Content/Library/Help text', 'This track could not be processed, please make sure it is tagged correctly') }, finished: { label: this.$pgettext('Content/Library/*', 'Finished'), - help: this.$pgettext('Content/Library/Help text', 'Imported'), - }, + help: this.$pgettext('Content/Library/Help text', 'Imported') + } } }, report_type: { label: this.$pgettext('*/*/*', 'Category'), choices: { - takedown_request: this.$pgettext("Content/Moderation/Dropdown", "Takedown request"), - invalid_metadata: this.$pgettext("Popup/Import/Error.Label", "Invalid metadata"), - illegal_content: this.$pgettext("Content/Moderation/Dropdown", "Illegal content"), - offensive_content: this.$pgettext("Content/Moderation/Dropdown", "Offensive content"), - other: this.$pgettext("Content/Moderation/Dropdown", "Other"), - }, + takedown_request: this.$pgettext('Content/Moderation/Dropdown', 'Takedown request'), + invalid_metadata: this.$pgettext('Popup/Import/Error.Label', 'Invalid metadata'), + illegal_content: this.$pgettext('Content/Moderation/Dropdown', 'Illegal content'), + offensive_content: this.$pgettext('Content/Moderation/Dropdown', 'Offensive content'), + other: this.$pgettext('Content/Moderation/Dropdown', 'Other') + } }, summary: { - label: this.$pgettext('Content/Account/*', 'Bio'), + label: this.$pgettext('Content/Account/*', 'Bio') }, content_category: { label: this.$pgettext('Content/*/Dropdown.Label/Noun', 'Content category'), choices: { podcast: this.$pgettext('Content/*/Dropdown', 'Podcast'), music: this.$pgettext('*/*/*', 'Music'), - other: this.$pgettext('*/*/*', 'Other'), - }, + other: this.$pgettext('*/*/*', 'Other') + } } }, filters: { @@ -89,56 +89,56 @@ export default { users: this.$pgettext('*/*/*/Noun', 'Users'), received_messages: this.$pgettext('Content/Moderation/*/Noun', 'Received messages'), uploads: this.$pgettext('*/*/*', 'Uploads'), - followers: this.$pgettext('Content/Federation/*/Noun', 'Followers'), + followers: this.$pgettext('Content/Federation/*/Noun', 'Followers') }, scopes: { profile: { label: this.$pgettext('Content/OAuth Scopes/Label', 'Profile'), - description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to e-mail, username, and profile information'), + description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to e-mail, username, and profile information') }, libraries: { label: this.$pgettext('Content/OAuth Scopes/Label', 'Libraries and uploads'), - description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to audio files, libraries, artists, albums and tracks'), + description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to audio files, libraries, artists, albums and tracks') }, favorites: { label: this.$pgettext('Sidebar/Favorites/List item.Link/Noun', 'Favorites'), - description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to favorites'), + description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to favorites') }, listenings: { label: this.$pgettext('*/*/*/Noun', 'Listenings'), - description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to listening history'), + description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to listening history') }, follows: { label: this.$pgettext('Content/OAuth Scopes/Label', 'Follows'), - description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to follows'), + description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to follows') }, playlists: { label: this.$pgettext('*/*/*', 'Playlists'), - description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to playlists'), + description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to playlists') }, radios: { label: this.$pgettext('*/*/*', 'Radios'), - description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to radios'), + description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to radios') }, filters: { label: this.$pgettext('Content/Settings/Title/Noun', 'Content filters'), - description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to content filters'), + description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to content filters') }, notifications: { label: this.$pgettext('*/Notifications/*', 'Notifications'), - description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to notifications'), + description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to notifications') }, edits: { label: this.$pgettext('*/Admin/*/Noun', 'Edits'), - description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to edits'), + description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to edits') }, security: { label: this.$pgettext('*/Admin/*/Noun', 'Security'), - description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to security settings such as password and authorization'), + description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to security settings such as password and authorization') }, reports: { label: this.$pgettext('*/Moderation/*/Noun', 'Reports'), - description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to moderation reports'), + description: this.$pgettext('Content/OAuth Scopes/Paragraph', 'Access to moderation reports') } } } diff --git a/front/src/components/moderation/FilterModal.vue b/front/src/components/moderation/FilterModal.vue index f83e69741bd325d17ba095313634bc78788cf87d..981b82227b66730ebbaa39d8618ff4a89bdf7eef 100644 --- a/front/src/components/moderation/FilterModal.vue +++ b/front/src/components/moderation/FilterModal.vue @@ -1,19 +1,37 @@ <template> - <modal @update:show="update" :show="$store.state.moderation.showFilterModal"> + <modal + :show="$store.state.moderation.showFilterModal" + @update:show="update" + > <h4 class="header"> <translate v-if="type === 'artist'" key="1" translate-context="Popup/Moderation/Title/Verb" - :translate-params="{name: target.name}">Do you want to hide content from artist "%{ name }"?</translate> + :translate-params="{name: target.name}" + > + Do you want to hide content from artist "%{ name }"? + </translate> </h4> <div class="scrolling content"> <div class="description"> - - <div v-if="errors.length > 0" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Popup/Moderation/Error message">Error while creating filter</translate></h4> + <div + v-if="errors.length > 0" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Popup/Moderation/Error message"> + Error while creating filter + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> <template v-if="type === 'artist'"> @@ -23,10 +41,26 @@ </translate> </p> <ul> - <li><translate translate-context="Popup/Moderation/List item">In other users favorites and listening history</translate></li> - <li><translate translate-context="Popup/Moderation/List item">In "Recently added" widget</translate></li> - <li><translate translate-context="Popup/Moderation/List item">In artists and album listings</translate></li> - <li><translate translate-context="Popup/Moderation/List item">In radio suggestions</translate></li> + <li> + <translate translate-context="Popup/Moderation/List item"> + In other users favorites and listening history + </translate> + </li> + <li> + <translate translate-context="Popup/Moderation/List item"> + In "Recently added" widget + </translate> + </li> + <li> + <translate translate-context="Popup/Moderation/List item"> + In artists and album listings + </translate> + </li> + <li> + <translate translate-context="Popup/Moderation/List item"> + In radio suggestions + </translate> + </li> </ul> <p> <translate translate-context="Popup/Moderation/Paragraph"> @@ -37,23 +71,33 @@ </div> </div> <div class="actions"> - <button class="ui basic cancel button"><translate translate-context="*/*/Button.Label/Verb">Cancel</translate></button> - <button :class="['ui', 'success', {loading: isLoading}, 'button']" @click="hide"><translate translate-context="Popup/*/Button.Label">Hide content</translate></button> + <button class="ui basic cancel button"> + <translate translate-context="*/*/Button.Label/Verb"> + Cancel + </translate> + </button> + <button + :class="['ui', 'success', {loading: isLoading}, 'button']" + @click="hide" + > + <translate translate-context="Popup/*/Button.Label"> + Hide content + </translate> + </button> </div> </modal> </template> <script> -import _ from '@/lodash' import axios from 'axios' -import {mapState} from 'vuex' +import { mapState } from 'vuex' import logger from '@/logging' import Modal from '@/components/semantic/Modal' export default { components: { - Modal, + Modal }, data () { return { @@ -65,7 +109,7 @@ export default { computed: { ...mapState({ type: state => state.moderation.filterModalTarget.type, - target: state => state.moderation.filterModalTarget.target, + target: state => state.moderation.filterModalTarget.target }) }, methods: { @@ -74,12 +118,12 @@ export default { this.errors = [] }, hide () { - let self = this + const self = this self.isLoading = true - let payload = { + const payload = { target: { type: this.type, - id: this.target.id, + id: this.target.id } } return axios.post('moderation/content-filters/', payload).then(response => { @@ -87,7 +131,7 @@ export default { self.update(false) self.$store.commit('moderation/lastUpdate', new Date()) self.isLoading = false - let msg = this.$pgettext('*/Moderation/Message', 'Content filter successfully added') + const msg = this.$pgettext('*/Moderation/Message', 'Content filter successfully added') self.$store.commit('moderation/contentFilter', response.data) self.$store.commit('ui/addMessage', { content: msg, diff --git a/front/src/components/moderation/ReportCategoryDropdown.vue b/front/src/components/moderation/ReportCategoryDropdown.vue index 617f89b182a293aca78e96a270b0bbd0032530e1..abab26b27663cb5a65bbd9544d2d6c9852753f9a 100644 --- a/front/src/components/moderation/ReportCategoryDropdown.vue +++ b/front/src/components/moderation/ReportCategoryDropdown.vue @@ -1,11 +1,26 @@ <template> <div> <label v-if="label"><translate translate-context="*/*/*">Category</translate></label> - <select class="ui dropdown" :value="value" @change="$emit('input', $event.target.value)" :required="required"> - <option v-if="empty" disabled value=''></option> - <option :value="option.value" v-for="option in allCategories">{{ option.label }}</option> + <select + class="ui dropdown" + :value="value" + :required="required" + @change="$emit('input', $event.target.value)" + > + <option + v-if="empty" + disabled + value="" + /> + <option + v-for="(option, key) in allCategories" + :key="key" + :value="option.value" + > + {{ option.label }} + </option> </select> - <slot></slot> + <slot /> </div> </template> @@ -15,26 +30,26 @@ import lodash from '@/lodash' export default { mixins: [TranslationsMixin], props: { - value: {}, - all: {}, - label: {}, - empty: {}, - required: {}, - restrictTo: {default: () => { return [] }} + value: { type: String, required: true }, + all: { type: String, required: true }, + label: { type: String, required: true }, + empty: { type: String, required: true }, + required: { type: String, required: true }, + restrictTo: { type: Array, default: () => { return [] } } }, computed: { allCategories () { - let c = [] + const c = [] if (this.all) { c.push( { value: '', label: this.$pgettext('Content/*/Dropdown', 'All') - }, + } ) } let choices - if (this.restrictTo.length > 0) { + if (this.restrictTo.length > 0) { choices = this.restrictTo } else { choices = lodash.keys(this.sharedLabels.fields.report_type.choices) diff --git a/front/src/components/moderation/ReportModal.vue b/front/src/components/moderation/ReportModal.vue index bc308f5e251065776e0b31d8604d0304a2108080..a6fba132bc7f1707575fb639c541b6a81179e05f 100644 --- a/front/src/components/moderation/ReportModal.vue +++ b/front/src/components/moderation/ReportModal.vue @@ -1,39 +1,79 @@ <template> - <modal @update:show="update" :show="$store.state.moderation.showReportModal"> - <h2 class="ui header" v-if="target"> - <translate translate-context="Popup/Moderation/Title/Verb">Do you want to report this object?</translate> + <modal + :show="$store.state.moderation.showReportModal" + @update:show="update" + > + <h2 + v-if="target" + class="ui header" + > + <translate translate-context="Popup/Moderation/Title/Verb"> + Do you want to report this object? + </translate> <div class="ui sub header"> {{ target.typeLabel }} - {{ target.label }} </div> </h2> <div class="scrolling content"> <div class="description"> - <div v-if="errors.length > 0" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Popup/Moderation/Error message">Error while submitting report</translate></h4> + <div + v-if="errors.length > 0" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Popup/Moderation/Error message"> + Error while submitting report + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> </div> <p> - <translate translate-context="*/Moderation/Popup,Paragraph">Use this form to submit a report to our moderation team.</translate> + <translate translate-context="*/Moderation/Popup,Paragraph"> + Use this form to submit a report to our moderation team. + </translate> </p> - <form v-if="canSubmit" id="report-form" class="ui form" @submit.prevent="submit"> + <form + v-if="canSubmit" + id="report-form" + class="ui form" + @submit.prevent="submit" + > <div class="fields"> <report-category-dropdown - class="ui required eight wide field" v-model="category" + class="ui required eight wide field" :required="true" :empty="true" :restrict-to="allowedCategories" - :label="true"></report-category-dropdown> - <div v-if="!$store.state.auth.authenticated" class="ui eight wide required field"> + :label="true" + /> + <div + v-if="!$store.state.auth.authenticated" + class="ui eight wide required field" + > <label for="report-submitter-email"> <translate translate-context="Content/*/*/Noun">Email</translate> </label> - <input type="email" v-model="submitterEmail" name="report-submitter-email" id="report-submitter-email" required> + <input + id="report-submitter-email" + v-model="submitterEmail" + type="email" + name="report-submitter-email" + required + > <p> - <translate translate-context="*/*/Field,Help">We'll use this e-mail address if we need to contact you regarding this report.</translate> + <translate translate-context="*/*/Field,Help"> + We'll use this e-mail address if we need to contact you regarding this report. + </translate> </p> </div> </div> @@ -42,16 +82,32 @@ <translate translate-context="*/*/Field.Label/Noun">Message</translate> </label> <p> - <translate translate-context="*/*/Field,Help">Use this field to provide additional context to the moderator that will handle your report.</translate> + <translate translate-context="*/*/Field,Help"> + Use this field to provide additional context to the moderator that will handle your report. + </translate> </p> - <content-form field-id="report-summary" :rows="8" v-model="summary"></content-form> + <content-form + v-model="summary" + field-id="report-summary" + :rows="8" + /> </div> - <div class="ui field" v-if="!isLocal"> + <div + v-if="!isLocal" + class="ui field" + > <div class="ui checkbox"> - <input id="report-forward" v-model="forward" type="checkbox"> + <input + id="report-forward" + v-model="forward" + type="checkbox" + > <label for="report-forward"> <strong> - <translate :translate-params="{domain: targetDomain}" translate-context="*/*/Field.Label/Verb">Forward to %{ domain} </translate> + <translate + :translate-params="{domain: targetDomain}" + translate-context="*/*/Field.Label/Verb" + >Forward to %{ domain} </translate> </strong> <p> <translate translate-context="*/*/Field,Help">Forward an anonymized copy of your report to the server hosting this element.</translate> @@ -59,46 +115,58 @@ </label> </div> </div> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> </form> - <div v-else-if="isLoadingReportTypes" class="ui inline active loader"> - - </div> - <div v-else role="alert" class="ui warning message"> + <div + v-else-if="isLoadingReportTypes" + class="ui inline active loader" + /> + <div + v-else + role="alert" + class="ui warning message" + > <h4 class="header"> - <translate translate-context="Popup/Moderation/Error message">Anonymous reports are disabled, please sign-in to submit a report.</translate> + <translate translate-context="Popup/Moderation/Error message"> + Anonymous reports are disabled, please sign-in to submit a report. + </translate> </h4> </div> </div> <div class="actions"> - <button class="ui basic cancel button"><translate translate-context="*/*/Button.Label/Verb">Cancel</translate></button> + <button class="ui basic cancel button"> + <translate translate-context="*/*/Button.Label/Verb"> + Cancel + </translate> + </button> <button v-if="canSubmit" :class="['ui', 'success', {loading: isLoading}, 'button']" - type="submit" form="report-form"> - <translate translate-context="Popup/*/Button.Label">Submit report</translate> + type="submit" + form="report-form" + > + <translate translate-context="Popup/*/Button.Label"> + Submit report + </translate> </button> </div> </modal> </template> <script> -import _ from '@/lodash' import axios from 'axios' -import {mapState} from 'vuex' +import { mapState } from 'vuex' -import logger from '@/logging' - -function urlDomain(data) { - var a = document.createElement('a'); - a.href = data; - return a.hostname; +function urlDomain (data) { + const a = document.createElement('a') + a.href = data + return a.hostname } export default { components: { - ReportCategoryDropdown: () => import(/* webpackChunkName: "reports" */ "@/components/moderation/ReportCategoryDropdown"), - Modal: () => import(/* webpackChunkName: "modal" */ "@/components/semantic/Modal"), + ReportCategoryDropdown: () => import(/* webpackChunkName: "reports" */ '@/components/moderation/ReportCategoryDropdown'), + Modal: () => import(/* webpackChunkName: "modal" */ '@/components/semantic/Modal') }, data () { return { @@ -110,12 +178,12 @@ export default { submitterEmail: '', category: null, reportTypes: [], - forward: false, + forward: false } }, computed: { ...mapState({ - target: state => state.moderation.reportModalTarget, + target: state => state.moderation.reportModalTarget }), allowedCategories () { if (this.$store.state.auth.authenticated) { @@ -126,7 +194,6 @@ export default { }).map((c) => { return c.type }) - }, canSubmit () { if (this.$store.state.auth.authenticated) { @@ -140,7 +207,7 @@ export default { return } let fid = this.target._obj.fid - if (this.target.type === 'channel' && this.target._obj.actor ) { + if (this.target.type === 'channel' && this.target._obj.actor) { fid = this.target._obj.actor.fid } if (!fid) { @@ -152,19 +219,39 @@ export default { return this.$store.getters['instance/domain'] === this.targetDomain } }, + watch: { + '$store.state.moderation.showReportModal': function (v) { + if (!v || this.$store.state.auth.authenticated) { + return + } + + const self = this + self.isLoadingReportTypes = true + axios.get('instance/nodeinfo/2.0/').then(response => { + self.isLoadingReportTypes = false + self.reportTypes = response.data.metadata.reportTypes || [] + }, error => { + self.isLoadingReportTypes = false + self.$store.commit('ui/addMessage', { + content: 'Cannot fetch Node Info: ' + error, + date: new Date() + }) + }) + } + }, methods: { update (v) { this.$store.commit('moderation/showReportModal', v) this.errors = [] }, submit () { - let self = this + const self = this self.isLoading = true - let payload = { - target: {...this.target, _obj: null}, + const payload = { + target: { ...this.target, _obj: null }, summary: this.summary, type: this.category, - forward: this.forward, + forward: this.forward } if (!this.$store.state.auth.authenticated) { payload.submitter_email = this.submitterEmail @@ -172,7 +259,7 @@ export default { return axios.post('moderation/reports/', payload).then(response => { self.update(false) self.isLoading = false - let msg = this.$pgettext('*/Moderation/Message', 'Report successfully submitted, thank you') + const msg = this.$pgettext('*/Moderation/Message', 'Report successfully submitted, thank you') self.$store.commit('moderation/contentFilter', response.data) self.$store.commit('ui/addMessage', { content: msg, @@ -185,22 +272,6 @@ export default { self.isLoading = false }) } - }, - watch: { - '$store.state.moderation.showReportModal': function (v) { - if (!v || this.$store.state.auth.authenticated) { - return - } - - let self = this - self.isLoadingReportTypes = true - axios.get('instance/nodeinfo/2.0/').then(response => { - self.isLoadingReportTypes = false - self.reportTypes = response.data.metadata.reportTypes || [] - }, error => { - self.isLoadingReportTypes = false - }) - } } } </script> diff --git a/front/src/components/notifications/NotificationRow.vue b/front/src/components/notifications/NotificationRow.vue index 68a017d17650eea435caf91c2925ddd624f2eb16..3eaa16d965b1f151cfcf250a9875c759c550dc01 100644 --- a/front/src/components/notifications/NotificationRow.vue +++ b/front/src/components/notifications/NotificationRow.vue @@ -1,30 +1,67 @@ <template> <tr :class="[{'disabled-row': item.is_read}]"> <td> - <actor-link class="user" :actor="item.activity.actor" /> + <actor-link + class="user" + :actor="item.activity.actor" + /> </td> <td> - <router-link tag="span" class="link" v-if="notificationData.detailUrl" :to="notificationData.detailUrl" v-html="notificationData.message"> - - </router-link> - <template v-else v-html="notificationData.message"></template> - <template v-if="notificationData.acceptFollow"> - <button @click="handleAction(notificationData.acceptFollow.handler)" :class="['ui', 'basic', 'tiny', notificationData.acceptFollow.buttonClass || '', 'button']"> - <i v-if="notificationData.acceptFollow.icon" :class="[notificationData.acceptFollow.icon, 'icon']" /> + <router-link + v-if="notificationData.detailUrl" + tag="span" + class="link" + :to="notificationData.detailUrl" + v-html="notificationData.message" + /> + <template + v-else + v-html="notificationData.message" + /> + <template v-if="notificationData.acceptFollow"> + + <button + :class="['ui', 'basic', 'tiny', notificationData.acceptFollow.buttonClass || '', 'button']" + @click="handleAction(notificationData.acceptFollow.handler)" + > + <i + v-if="notificationData.acceptFollow.icon" + :class="[notificationData.acceptFollow.icon, 'icon']" + /> {{ notificationData.acceptFollow.label }} </button> - <button @click="handleAction(notificationData.rejectFollow.handler)" :class="['ui', 'basic', 'tiny', notificationData.rejectFollow.buttonClass || '', 'button']"> - <i v-if="notificationData.rejectFollow.icon" :class="[notificationData.rejectFollow.icon, 'icon']" /> + <button + :class="['ui', 'basic', 'tiny', notificationData.rejectFollow.buttonClass || '', 'button']" + @click="handleAction(notificationData.rejectFollow.handler)" + > + <i + v-if="notificationData.rejectFollow.icon" + :class="[notificationData.rejectFollow.icon, 'icon']" + /> {{ notificationData.rejectFollow.label }} </button> </template> </td> <td><human-date :date="item.activity.creation_date" /></td> <td class="read collapsing"> - <a href="" :aria-label="labels.markUnread" @click.prevent="markRead(false)" class="discrete link" v-if="item.is_read" :title="labels.markUnread"> + <a + v-if="item.is_read" + href="" + :aria-label="labels.markUnread" + class="discrete link" + :title="labels.markUnread" + @click.prevent="markRead(false)" + > <i class="redo icon" /> </a> - <a href="" :aria-label="labels.markRead" @click.prevent="markRead(true)" class="discrete link" v-else :title="labels.markRead"> + <a + v-else + href="" + :aria-label="labels.markRead" + class="discrete link" + :title="labels.markRead" + @click.prevent="markRead(true)" + > <i class="check icon" /> </a> </td> @@ -34,23 +71,28 @@ import axios from 'axios' export default { - props: ['item'], + props: { initialItem: { type: Object, required: true } }, + data: function () { + return { + item: this.initialItem + } + }, computed: { message () { return 'plop' }, labels () { - let libraryFollowMessage = this.$pgettext('Content/Notifications/Paragraph', '%{ username } followed your library "%{ library }"') - let libraryAcceptFollowMessage = this.$pgettext('Content/Notifications/Paragraph', '%{ username } accepted your follow on library "%{ library }"') - let libraryRejectMessage = this.$pgettext('Content/Notifications/Paragraph', 'You rejected %{ username }'s request to follow "%{ library }"') - let libraryPendingFollowMessage = this.$pgettext('Content/Notifications/Paragraph', '%{ username } wants to follow your library "%{ library }"') + const libraryFollowMessage = this.$pgettext('Content/Notifications/Paragraph', '%{ username } followed your library "%{ library }"') + const libraryAcceptFollowMessage = this.$pgettext('Content/Notifications/Paragraph', '%{ username } accepted your follow on library "%{ library }"') + const libraryRejectMessage = this.$pgettext('Content/Notifications/Paragraph', 'You rejected %{ username }'s request to follow "%{ library }"') + const libraryPendingFollowMessage = this.$pgettext('Content/Notifications/Paragraph', '%{ username } wants to follow your library "%{ library }"') return { libraryFollowMessage, libraryAcceptFollowMessage, libraryRejectMessage, libraryPendingFollowMessage, markRead: this.$pgettext('Content/Notifications/Button.Tooltip/Verb', 'Mark as read'), - markUnread: this.$pgettext('Content/Notifications/Button.Tooltip/Verb', 'Mark as unread'), + markUnread: this.$pgettext('Content/Notifications/Button.Tooltip/Verb', 'Mark as unread') } }, @@ -58,8 +100,8 @@ export default { return this.item.activity.actor.preferred_username }, notificationData () { - let self = this - let a = this.item.activity + const self = this + const a = this.item.activity if (a.type === 'Follow') { if (a.object && a.object.type === 'music.Library') { let acceptFollow = null @@ -72,7 +114,7 @@ export default { icon: 'check', label: this.$pgettext('Content/*/Button.Label/Verb', 'Approve'), handler: () => { self.approveLibraryFollow(a.related_object) } - }, + } rejectFollow = { buttonClass: 'danger', icon: 'x', @@ -87,10 +129,10 @@ export default { return { acceptFollow, rejectFollow, - detailUrl: {name: 'content.libraries.detail', params: {id: a.object.uuid}}, + detailUrl: { name: 'content.libraries.detail', params: { id: a.object.uuid } }, message: this.$gettextInterpolate( message, - {username: this.username, library: a.object.name} + { username: this.username, library: a.object.name } ) } } @@ -98,10 +140,10 @@ export default { if (a.type === 'Accept') { if (a.object && a.object.type === 'federation.LibraryFollow') { return { - detailUrl: {name: 'content.remote.index'}, + detailUrl: { name: 'content.remote.index' }, message: this.$gettextInterpolate( this.labels.libraryAcceptFollowMessage, - {username: this.username, library: a.related_object.name} + { username: this.username, library: a.related_object.name } ) } } @@ -116,30 +158,27 @@ export default { this.markRead(true) }, approveLibraryFollow (follow) { - let self = this - let action = 'accept' + const action = 'accept' axios.post(`federation/follows/library/${follow.uuid}/${action}/`).then((response) => { follow.isLoading = false follow.approved = true }) }, rejectLibraryFollow (follow) { - let self = this - let action = 'reject' + const action = 'reject' axios.post(`federation/follows/library/${follow.uuid}/${action}/`).then((response) => { follow.isLoading = false follow.approved = false }) }, markRead (value) { - let self = this - let action = 'accept' - axios.patch(`federation/inbox/${this.item.id}/`, {is_read: value}).then((response) => { + const self = this + axios.patch(`federation/inbox/${this.item.id}/`, { is_read: value }).then((response) => { self.item.is_read = value if (value) { - self.$store.commit('ui/incrementNotifications', {type: 'inbox', count: -1}) + self.$store.commit('ui/incrementNotifications', { type: 'inbox', count: -1 }) } else { - self.$store.commit('ui/incrementNotifications', {type: 'inbox', count: 1}) + self.$store.commit('ui/incrementNotifications', { type: 'inbox', count: 1 }) } }) } diff --git a/front/src/components/playlists/Card.vue b/front/src/components/playlists/Card.vue index c37f24289b90c7868fd4afe3e5c265d659abb156..d02aac56b75cb27fe95a15e2dc0925ce1d9862ec 100644 --- a/front/src/components/playlists/Card.vue +++ b/front/src/components/playlists/Card.vue @@ -1,24 +1,55 @@ <template> <div class="ui app-card card"> <div + :class="['ui', 'head-image', 'squares']" @click="$router.push({name: 'library.playlists.detail', params: {id: playlist.id }})" - :class="['ui', 'head-image', 'squares']"> - <img alt="" v-lazy="url" v-for="(url, idx) in images" :key="idx" /> - <play-button :icon-only="true" :is-playable="playlist.is_playable" :button-classes="['ui', 'circular', 'large', 'vibrant', 'icon', 'button']" :playlist="playlist"></play-button> + > + <img + v-for="(url, idx) in images" + :key="idx" + v-lazy="url" + alt="" + > + <play-button + :icon-only="true" + :is-playable="playlist.is_playable" + :button-classes="['ui', 'circular', 'large', 'vibrant', 'icon', 'button']" + :playlist="playlist" + /> </div> <div class="content"> <strong> - <router-link class="discrete link" :to="{name: 'library.playlists.detail', params: {id: playlist.id }}"> + <router-link + class="discrete link" + :to="{name: 'library.playlists.detail', params: {id: playlist.id }}" + > {{ playlist.name }} </router-link> </strong> <div class="description"> - <user-link :user="playlist.user" :avatar="false" class="left floated" /> + <user-link + :user="playlist.user" + :avatar="false" + class="left floated" + /> </div> </div> <div class="extra content"> - <translate translate-context="*/*/*" :translate-params="{count: playlist.tracks_count}" :translate-n="playlist.tracks_count" translate-plural="%{ count } tracks">%{ count } track</translate> - <play-button class="right floated basic icon" :dropdown-only="true" :is-playable="playlist.is_playable" :dropdown-icon-classes="['ellipsis', 'horizontal', 'large really discrete']" :playlist="playlist"></play-button> + <translate + translate-context="*/*/*" + :translate-params="{count: playlist.tracks_count}" + :translate-n="playlist.tracks_count" + translate-plural="%{ count } tracks" + > + %{ count } track + </translate> + <play-button + class="right floated basic icon" + :dropdown-only="true" + :is-playable="playlist.is_playable" + :dropdown-icon-classes="['ellipsis', 'horizontal', 'large really discrete']" + :playlist="playlist" + /> </div> </div> </template> @@ -27,14 +58,14 @@ import PlayButton from '@/components/audio/PlayButton' export default { - props: ['playlist'], components: { PlayButton }, + props: { playlist: { type: Object, required: true } }, computed: { images () { - let self = this - let urls = this.playlist.album_covers.map((url) => { + const self = this + const urls = this.playlist.album_covers.map((url) => { return self.$store.getters['instance/absoluteUrl'](url) }).slice(0, 4) while (urls.length < 4) { diff --git a/front/src/components/playlists/CardList.vue b/front/src/components/playlists/CardList.vue index fe9d916204911a3084835de7507b78043f416e33..e3c17e6366f1950f5eadc5851c5d43437a25d654 100644 --- a/front/src/components/playlists/CardList.vue +++ b/front/src/components/playlists/CardList.vue @@ -2,10 +2,10 @@ <div v-if="playlists.length > 0"> <div class="ui app-cards cards"> <playlist-card - :playlist="playlist" v-for="playlist in playlists" :key="playlist.id" - ></playlist-card> + :playlist="playlist" + /> </div> </div> </template> @@ -15,9 +15,9 @@ import PlaylistCard from '@/components/playlists/Card' export default { - props: ['playlists'], components: { PlaylistCard - } + }, + props: { playlists: { type: Array, required: true } } } </script> diff --git a/front/src/components/playlists/Editor.vue b/front/src/components/playlists/Editor.vue index a088e17372fe1178f6fdf83e1db778ed994d9daf..68ce6a10c996137b7fb867bb62e183f5481829ff 100644 --- a/front/src/components/playlists/Editor.vue +++ b/front/src/components/playlists/Editor.vue @@ -1,85 +1,172 @@ <template> <div class="ui text container component-playlist-editor"> - <playlist-form @updated="$emit('playlist-updated', $event)" :title="false" :playlist="playlist"></playlist-form> + <playlist-form + :title="false" + :playlist="playlist" + @updated="$emit('playlist-updated', $event)" + /> <h3 class="ui top attached header"> - <translate translate-context="Content/Playlist/Title">Playlist editor</translate> + <translate translate-context="Content/Playlist/Title"> + Playlist editor + </translate> </h3> <div class="ui attached segment"> <template v-if="status === 'loading'"> - <div class="ui active tiny inline loader"></div> - <translate translate-context="Content/Playlist/Paragraph">Syncing changes to server…</translate> + <div class="ui active tiny inline loader" /> + <translate translate-context="Content/Playlist/Paragraph"> + Syncing changes to server… + </translate> </template> <template v-else-if="status === 'errored'"> - <i class="dangerclose icon"></i> - <translate translate-context="Content/Playlist/Error message.Title">An error occurred while saving your changes</translate> - <div v-if="errors.length > 0" role="alert" class="ui negative message"> + <i class="dangerclose icon" /> + <translate translate-context="Content/Playlist/Error message.Title"> + An error occurred while saving your changes + </translate> + <div + v-if="errors.length > 0" + role="alert" + class="ui negative message" + > <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> </template> - <div v-else-if="status === 'confirmDuplicateAdd'" role="alert" class="ui warning message"> - <p translate-context="Content/Playlist/Paragraph" - v-translate="{playlist: playlist.name}">Some tracks in your queue are already in this playlist:</p> + <div + v-else-if="status === 'confirmDuplicateAdd'" + role="alert" + class="ui warning message" + > + <p + v-translate="{playlist: playlist.name}" + translate-context="Content/Playlist/Paragraph" + > + Some tracks in your queue are already in this playlist: + </p> <ul class="ui relaxed divided list duplicate-tracks-list"> - <li v-for="track in duplicateTrackAddInfo.tracks" class="ui item">{{ track }}</li> + <li + v-for="(track, key) in duplicateTrackAddInfo.tracks" + :key="key" + class="ui item" + > + {{ track }} + </li> </ul> <button class="ui small success button" - @click="insertMany(queueTracks, true)"><translate translate-context="*/Playlist/Button.Label/Verb">Add anyways</translate></button> + @click="insertMany(queueTracks, true)" + > + <translate translate-context="*/Playlist/Button.Label/Verb"> + Add anyways + </translate> + </button> </div> <template v-else-if="status === 'saved'"> - <i class="success check icon"></i> <translate translate-context="Content/Playlist/Paragraph">Changes synced with server</translate> + <i class="success check icon" /> <translate translate-context="Content/Playlist/Paragraph"> + Changes synced with server + </translate> </template> </div> <div class="ui bottom attached segment"> <button - @click="insertMany(queueTracks, false)" :disabled="queueTracks.length === 0" :class="['ui', {disabled: queueTracks.length === 0}, 'labeled', 'icon', 'button']" - :title="labels.copyTitle"> - <i class="plus icon"></i> - <translate translate-context="Content/Playlist/Button.Label/Verb" - translate-plural="Insert from queue (%{ count } tracks)" - :translate-n="queueTracks.length" - :translate-params="{count: queueTracks.length}"> - Insert from queue (%{ count } track) - </translate> - </button> + :title="labels.copyTitle" + @click="insertMany(queueTracks, false)" + > + <i class="plus icon" /> + <translate + translate-context="Content/Playlist/Button.Label/Verb" + translate-plural="Insert from queue (%{ count } tracks)" + :translate-n="queueTracks.length" + :translate-params="{count: queueTracks.length}" + > + Insert from queue (%{ count } track) + </translate> + </button> - <dangerous-button :disabled="plts.length === 0" class="ui labeled right floated danger icon button" :action="clearPlaylist"> - <i class="eraser icon"></i> <translate translate-context="*/Playlist/Button.Label/Verb">Clear playlist</translate> - <p slot="modal-header" v-translate="{playlist: playlist.name}" translate-context="Popup/Playlist/Title" :translate-params="{playlist: playlist.name}"> + <dangerous-button + :disabled="plts.length === 0" + class="ui labeled right floated danger icon button" + :action="clearPlaylist" + > + <i class="eraser icon" /> <translate translate-context="*/Playlist/Button.Label/Verb"> + Clear playlist + </translate> + <p + slot="modal-header" + v-translate="{playlist: playlist.name}" + translate-context="Popup/Playlist/Title" + :translate-params="{playlist: playlist.name}" + > Do you want to clear the playlist "%{ playlist }"? </p> - <p slot="modal-content"><translate translate-context="Popup/Playlist/Paragraph">This will remove all tracks from this playlist and cannot be undone.</translate></p> - <div slot="modal-confirm"><translate translate-context="*/Playlist/Button.Label/Verb">Clear playlist</translate></div> + <p slot="modal-content"> + <translate translate-context="Popup/Playlist/Paragraph"> + This will remove all tracks from this playlist and cannot be undone. + </translate> + </p> + <div slot="modal-confirm"> + <translate translate-context="*/Playlist/Button.Label/Verb"> + Clear playlist + </translate> + </div> </dangerous-button> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <template v-if="plts.length > 0"> - <p><translate translate-context="Content/Playlist/Paragraph/Call to action">Drag and drop rows to reorder tracks in the playlist</translate></p> + <p> + <translate translate-context="Content/Playlist/Paragraph/Call to action"> + Drag and drop rows to reorder tracks in the playlist + </translate> + </p> <div class="table-wrapper"> <table class="ui compact very basic unstackable table"> - <draggable v-model="plts" tag="tbody" @update="reorder"> - <tr v-for="(plt, index) in plts" :key="`${index}-${plt.track.id}`"> - <td class="left aligned">{{ plt.index + 1}}</td> + <draggable + v-model="plts" + tag="tbody" + @update="reorder" + > + <tr + v-for="(plt, index) in plts" + :key="`${index}-${plt.track.id}`" + > + <td class="left aligned"> + {{ plt.index + 1 }} + </td> <td class="center aligned"> - <img alt="" class="ui mini image" v-if="plt.track.album && plt.track.album.cover && plt.track.album.cover.urls.original" v-lazy="$store.getters['instance/absoluteUrl'](plt.track.album.cover.urls.medium_square_crop)"> - <img alt="" class="ui mini image" v-else src="../../assets/audio/default-cover.png"> + <img + v-if="plt.track.album && plt.track.album.cover && plt.track.album.cover.urls.original" + v-lazy="$store.getters['instance/absoluteUrl'](plt.track.album.cover.urls.medium_square_crop)" + alt="" + class="ui mini image" + > + <img + v-else + alt="" + class="ui mini image" + src="../../assets/audio/default-cover.png" + > </td> <td colspan="4"> - <strong>{{ plt.track.title }}</strong><br /> - {{ plt.track.artist.name }} + <strong>{{ plt.track.title }}</strong><br> + {{ plt.track.artist.name }} </td> <td class="right aligned"> <button class="ui circular danger basic icon button"> - <i @click.stop="removePlt(index)" class="trash icon"></i> + <i + class="trash icon" + @click.stop="removePlt(index)" + /> </button> </td> </tr> </draggable> </table> - </div> </template> </div> @@ -87,7 +174,7 @@ </template> <script> -import {mapState} from 'vuex' +import { mapState } from 'vuex' import axios from 'axios' import PlaylistForm from '@/components/playlists/Form' @@ -98,7 +185,10 @@ export default { draggable, PlaylistForm }, - props: ['playlist', 'playlistTracks'], + props: { + playlist: { type: Object, required: true }, + playlistTracks: { type: Array, required: true } + }, data () { return { plts: this.playlistTracks, @@ -108,6 +198,39 @@ export default { showDuplicateTrackAddConfirmation: false } }, + computed: { + ...mapState({ + queueTracks: state => state.queue.tracks + }), + labels () { + return { + copyTitle: this.$pgettext('Content/Playlist/Button.Tooltip/Verb', 'Copy the current queue to this playlist') + } + }, + status () { + if (this.isLoading) { + return 'loading' + } + if (this.errors.length > 0) { + return 'errored' + } + if (this.showDuplicateTrackAddConfirmation) { + return 'confirmDuplicateAdd' + } + return 'saved' + } + }, + watch: { + plts: { + handler (newValue) { + newValue.forEach((e, i) => { + e.index = i + }) + this.$emit('tracks-updated', newValue) + }, + deep: true + } + }, methods: { success () { this.isLoading = false @@ -116,19 +239,18 @@ export default { }, errored (errors) { this.isLoading = false - if (errors.length == 1 && errors[0].code == 'tracks_already_exist_in_playlist') { + if (errors.length === 1 && errors[0].code === 'tracks_already_exist_in_playlist') { this.duplicateTrackAddInfo = errors[0] this.showDuplicateTrackAddConfirmation = true } else { this.errors = errors } }, - reorder ({oldIndex, newIndex}) { - let self = this + reorder ({ oldIndex, newIndex }) { + const self = this self.isLoading = true - let plt = this.plts[newIndex] - let url = `playlists/${this.playlist.id}/move` - axios.post(url, {from: oldIndex, to: newIndex}).then((response) => { + const url = `playlists/${this.playlist.id}/move` + axios.post(url, { from: oldIndex, to: newIndex }).then((response) => { self.success() }, error => { self.errored(error.backendErrors) @@ -136,10 +258,10 @@ export default { }, removePlt (index) { this.plts.splice(index, 1) - let self = this + const self = this self.isLoading = true - let url = `playlists/${this.playlist.id}/remove` - axios.post(url, {index}).then((response) => { + const url = `playlists/${this.playlist.id}/remove` + axios.post(url, { index }).then((response) => { self.success() self.$store.dispatch('playlists/fetchOwn') }, error => { @@ -148,9 +270,9 @@ export default { }, clearPlaylist () { this.plts = [] - let self = this + const self = this self.isLoading = true - let url = 'playlists/' + this.playlist.id + '/clear' + const url = 'playlists/' + this.playlist.id + '/clear' axios.delete(url).then((response) => { self.success() self.$store.dispatch('playlists/fetchOwn') @@ -159,16 +281,16 @@ export default { }) }, insertMany (tracks, allowDuplicates) { - let self = this - let ids = tracks.map(t => { + const self = this + const ids = tracks.map(t => { return t.id }) - let payload = { + const payload = { tracks: ids, allow_duplicates: allowDuplicates } self.isLoading = true - let url = 'playlists/' + this.playlist.id + '/add/' + const url = 'playlists/' + this.playlist.id + '/add/' axios.post(url, payload).then((response) => { response.data.results.forEach(r => { self.plts.push(r) @@ -185,39 +307,6 @@ export default { } }) } - }, - computed: { - ...mapState({ - queueTracks: state => state.queue.tracks - }), - labels () { - return { - copyTitle: this.$pgettext('Content/Playlist/Button.Tooltip/Verb', 'Copy the current queue to this playlist') - } - }, - status () { - if (this.isLoading) { - return 'loading' - } - if (this.errors.length > 0) { - return 'errored' - } - if (this.showDuplicateTrackAddConfirmation) { - return 'confirmDuplicateAdd' - } - return 'saved' - } - }, - watch: { - plts: { - handler (newValue) { - newValue.forEach((e, i) => { - e.index = i - }) - this.$emit('tracks-updated', newValue) - }, - deep: true - } } } </script> diff --git a/front/src/components/playlists/Form.vue b/front/src/components/playlists/Form.vue index 757703dfd4879bfaa631405d92a20c43f689575d..9d53013883d6210c70780e678a993d446999545e 100644 --- a/front/src/components/playlists/Form.vue +++ b/front/src/components/playlists/Form.vue @@ -1,38 +1,96 @@ <template> - <form class="ui form" @submit.prevent="submit()"> - <h4 v-if="title" class="ui header"><translate translate-context="Popup/Playlist/Title/Verb">Create a new playlist</translate></h4> - <div v-if="success" class="ui positive message"> + <form + class="ui form" + @submit.prevent="submit()" + > + <h4 + v-if="title" + class="ui header" + > + <translate translate-context="Popup/Playlist/Title/Verb"> + Create a new playlist + </translate> + </h4> + <div + v-if="success" + class="ui positive message" + > <h4 class="header"> <template v-if="playlist"> - <translate translate-context="Content/Playlist/Message">Playlist updated</translate> + <translate translate-context="Content/Playlist/Message"> + Playlist updated + </translate> </template> <template v-else> - <translate translate-context="Content/Playlist/Message">Playlist created</translate> + <translate translate-context="Content/Playlist/Message"> + Playlist created + </translate> </template> </h4> </div> - <div v-if="errors.length > 0" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Content/Playlist/Error message.Title">The playlist could not be created</translate></h4> + <div + v-if="errors.length > 0" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Content/Playlist/Error message.Title"> + The playlist could not be created + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> <div class="three fields"> <div class="field"> <label for="playlist-name"><translate translate-context="Content/Playlist/Input.Label">Playlist name</translate></label> - <input id ="playlist-name" name="name" v-model="name" required type="text" :placeholder="labels.placeholder" /> + <input + id="playlist-name" + v-model="name" + name="name" + required + type="text" + :placeholder="labels.placeholder" + > </div> <div class="field"> <label for="playlist-visibility"><translate translate-context="Content/Playlist/Dropdown.Label">Playlist visibility</translate></label> - <select id="playlist-visibility" class="ui dropdown" v-model="privacyLevel"> - <option :value="c.value" v-for="c in privacyLevelChoices">{{ c.label }}</option> + <select + id="playlist-visibility" + v-model="privacyLevel" + class="ui dropdown" + > + <option + v-for="(c, key) in privacyLevelChoices" + :key="key" + :value="c.value" + > + {{ c.label }} + </option> </select> </div> <div class="field"> - <span id="updatePlaylistLabel"></span> - <button :class="['ui', 'fluid', {'loading': isLoading}, 'button']" type="submit"> - <template v-if="playlist"><translate translate-context="Content/Playlist/Button.Label/Verb">Update playlist</translate></template> - <template v-else><translate translate-context="Content/Playlist/Button.Label/Verb">Create playlist</translate></template> + <span id="updatePlaylistLabel" /> + <button + :class="['ui', 'fluid', {'loading': isLoading}, 'button']" + type="submit" + > + <template v-if="playlist"> + <translate translate-context="Content/Playlist/Button.Label/Verb"> + Update playlist + </translate> + </template> + <template v-else> + <translate translate-context="Content/Playlist/Button.Label/Verb"> + Create playlist + </translate> + </template> </button> </div> </div> @@ -42,21 +100,18 @@ <script> import $ from 'jquery' import axios from 'axios' -import TranslationsMixin from "@/components/mixins/Translations" +import TranslationsMixin from '@/components/mixins/Translations' import logger from '@/logging' export default { mixins: [TranslationsMixin], props: { - title: {type: Boolean, default: true}, - playlist: {type: Object, default: null} - }, - mounted () { - $(this.$el).find('.dropdown').dropdown() + title: { type: Boolean, default: true }, + playlist: { type: Object, default: null } }, data () { - let d = { + const d = { errors: [], success: false, isLoading: false @@ -80,26 +135,29 @@ export default { return [ { value: 'me', - label: this.sharedLabels.fields.privacy_level.choices['me'] + label: this.sharedLabels.fields.privacy_level.choices.me }, { value: 'instance', - label: this.sharedLabels.fields.privacy_level.choices['instance'] + label: this.sharedLabels.fields.privacy_level.choices.instance }, { value: 'everyone', - label: this.sharedLabels.fields.privacy_level.choices['everyone'] + label: this.sharedLabels.fields.privacy_level.choices.everyone } ] } }, + mounted () { + $(this.$el).find('.dropdown').dropdown() + }, methods: { submit () { this.isLoading = true this.success = false this.errors = [] - let self = this - let payload = { + const self = this + const payload = { name: this.name, privacy_level: this.privacyLevel } diff --git a/front/src/components/playlists/PlaylistModal.vue b/front/src/components/playlists/PlaylistModal.vue index ea63954b8ae543e8a920385351e2603552aa49b8..509138a99451721d3f5058027f83dcdde429ca87 100644 --- a/front/src/components/playlists/PlaylistModal.vue +++ b/front/src/components/playlists/PlaylistModal.vue @@ -1,80 +1,160 @@ <template> - <modal @update:show="update" :show="$store.state.playlists.showModal"> + <modal + :show="$store.state.playlists.showModal" + @update:show="update" + > <h4 class="header"> <template v-if="track"> <h2 class="ui header"> - <translate translate-context="Popup/Playlist/Title/Verb">Add to playlist</translate> + <translate translate-context="Popup/Playlist/Title/Verb"> + Add to playlist + </translate> <div + v-translate="{artist: track.artist.name, title: track.title}" class="ui sub header" translate-context="Popup/Playlist/Paragraph" - v-translate="{artist: track.artist.name, title: track.title}" - :translate-params="{artist: track.artist.name, title: track.title}"> + :translate-params="{artist: track.artist.name, title: track.title}" + > "%{ title }", by %{ artist } </div> </h2> </template> - <translate v-else translate-context="Popup/Playlist/Title/Verb">Manage playlists</translate> + <translate + v-else + translate-context="Popup/Playlist/Title/Verb" + > + Manage playlists + </translate> </h4> <div class="scrolling content"> - <playlist-form :key="formKey"></playlist-form> - <div class="ui divider"></div> + <playlist-form :key="formKey" /> + <div class="ui divider" /> <div v-if="playlists.length > 0"> - <div v-if="showDuplicateTrackAddConfirmation" role="alert" class="ui warning message"> - <p translate-context="Popup/Playlist/Paragraph" + <div + v-if="showDuplicateTrackAddConfirmation" + role="alert" + class="ui warning message" + > + <p v-translate="{track: track.title, playlist: duplicateTrackAddInfo.playlist_name}" - :translate-params="{track: track.title, playlist: duplicateTrackAddInfo.playlist_name}"><strong>%{ track }</strong> is already in <strong>%{ playlist }</strong>.</p> + translate-context="Popup/Playlist/Paragraph" + :translate-params="{track: track.title, playlist: duplicateTrackAddInfo.playlist_name}" + > + <strong>%{ track }</strong> is already in <strong>%{ playlist }</strong>. + </p> <button + class="ui small basic cancel button" @click="duplicateTrackAddConfirm(false)" - class="ui small basic cancel button"><translate translate-context="*/*/Button.Label/Verb">Cancel</translate> + > + <translate translate-context="*/*/Button.Label/Verb"> + Cancel + </translate> </button> <button class="ui small success button" - @click="addToPlaylist(lastSelectedPlaylist, true)"> - <translate translate-context="*/Playlist/Button.Label/Verb">Add anyways</translate></button> + @click="addToPlaylist(lastSelectedPlaylist, true)" + > + <translate translate-context="*/Playlist/Button.Label/Verb"> + Add anyways + </translate> + </button> </div> - <div v-if="errors.length > 0" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Popup/Playlist/Error message.Title">The track can't be added to a playlist</translate></h4> + <div + v-if="errors.length > 0" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Popup/Playlist/Error message.Title"> + The track can't be added to a playlist + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> - <h4 class="ui header"><translate translate-context="Popup/Playlist/Title">Available playlists</translate></h4> + <h4 class="ui header"> + <translate translate-context="Popup/Playlist/Title"> + Available playlists + </translate> + </h4> <div class="ui form"> <div class="fields"> <div class="field"> <label for="playlist-name-filter"><translate translate-context="Popup/Playlist/Label">Filter</translate></label> - <input id="playlist-name-filter" v-model="playlistNameFilter" type="text" class="inline" :placeholder="labels.filterPlaylistField" /> + <input + id="playlist-name-filter" + v-model="playlistNameFilter" + type="text" + class="inline" + :placeholder="labels.filterPlaylistField" + > </div> </div> </div> - <table v-if="sortedPlaylists.length > 0" class="ui unstackable very basic table"> + <table + v-if="sortedPlaylists.length > 0" + class="ui unstackable very basic table" + > <thead> <tr> <th><span class="visually-hidden"><translate translate-context="*/*/*/Verb">Edit</translate></span></th> - <th><translate translate-context="*/*/*/Noun">Name</translate></th> - <th class="sorted descending"><translate translate-context="Popup/Playlist/Table.Label/Short">Last modification</translate></th> - <th><translate translate-context="*/*/*">Tracks</translate></th> + <th> + <translate translate-context="*/*/*/Noun"> + Name + </translate> + </th> + <th class="sorted descending"> + <translate translate-context="Popup/Playlist/Table.Label/Short"> + Last modification + </translate> + </th> + <th> + <translate translate-context="*/*/*"> + Tracks + </translate> + </th> </tr> </thead> <tbody> - <tr v-for="playlist in sortedPlaylists"> + <tr + v-for="(playlist, key) in sortedPlaylists" + :key="key" + > <td> <router-link class="ui icon basic small button" - :to="{name: 'library.playlists.detail', params: {id: playlist.id }, query: {mode: 'edit'}}"><i class="ui pencil icon"></i> - <span class="visually-hidden"><translate translate-context="*/*/*/Verb">Edit</translate></span></router-link> + :to="{name: 'library.playlists.detail', params: {id: playlist.id }, query: {mode: 'edit'}}" + > + <i class="ui pencil icon" /> + <span class="visually-hidden"><translate translate-context="*/*/*/Verb">Edit</translate></span> + </router-link> </td> <td> - <router-link v-on:click.native="update(false)" :to="{name: 'library.playlists.detail', params: {id: playlist.id }}">{{ playlist.name }}</router-link></td> - <td><human-date :date="playlist.modification_date"></human-date></td> + <router-link + :to="{name: 'library.playlists.detail', params: {id: playlist.id }}" + @click.native="update(false)" + > + {{ playlist.name }} + </router-link> + </td> + <td><human-date :date="playlist.modification_date" /></td> <td>{{ playlist.tracks_count }}</td> <td> <button v-if="track" class="ui success icon basic small right floated button" :title="labels.addToPlaylist" - @click.prevent="addToPlaylist(playlist.id, false)"> - <i class="plus icon"></i> <translate translate-context="Popup/Playlist/Table.Button.Label/Verb">Add track</translate> + @click.prevent="addToPlaylist(playlist.id, false)" + > + <i class="plus icon" /> <translate translate-context="Popup/Playlist/Table.Button.Label/Verb"> + Add track + </translate> </button> </td> </tr> @@ -83,35 +163,41 @@ <template v-else> <div class="ui small placeholder segment component-placeholder"> <h4 class="ui header"> - <translate translate-context="Popup/Playlist/EmptyState">No results matching your filter</translate> - </h4> - </div> - </template> - <template v-else> - <div class="ui placeholder segment"> - <div class="ui icon header"> - <i class="list icon"></i> - <translate translate-context="Content/Home/Placeholder"> - No playlists have been created yet + <translate translate-context="Popup/Playlist/EmptyState"> + No results matching your filter </translate> - </div> + </h4> </div> </template> </div> + <template v-else> + <div class="ui placeholder segment"> + <div class="ui icon header"> + <i class="list icon" /> + <translate translate-context="Content/Home/Placeholder"> + No playlists have been created yet + </translate> + </div> + </div> + </template> </div> <div class="actions"> - <button class="ui basic cancel button"><translate translate-context="*/*/Button.Label/Verb">Cancel</translate></button> + <button class="ui basic cancel button"> + <translate translate-context="*/*/Button.Label/Verb"> + Cancel + </translate> + </button> </div> </modal> </template> <script> -import filter from "lodash/fp/filter"; -import sortBy from "lodash/fp/sortBy"; -import flow from "lodash/fp/flow"; +import filter from 'lodash/fp/filter' +import sortBy from 'lodash/fp/sortBy' +import flow from 'lodash/fp/flow' import axios from 'axios' -import {mapState} from 'vuex' +import { mapState } from 'vuex' import logger from '@/logging' import Modal from '@/components/semantic/Modal' @@ -129,38 +215,7 @@ export default { playlistNameFilter: '', duplicateTrackAddInfo: {}, showDuplicateTrackAddConfirmation: false, - lastSelectedPlaylist: -1, - } - }, - methods: { - update (v) { - this.$store.commit('playlists/showModal', v) - }, - addToPlaylist (playlistId, allowDuplicate) { - let self = this - let payload = { - tracks: [this.track.id], - allow_duplicates: allowDuplicate - } - - self.lastSelectedPlaylist = playlistId - - return axios.post(`playlists/${playlistId}/add`, payload).then(response => { - logger.default.info('Successfully added track to playlist') - self.update(false) - self.$store.dispatch('playlists/fetchOwn') - }, error => { - if (error.backendErrors.length == 1 && error.backendErrors[0].code == 'tracks_already_exist_in_playlist') { - self.duplicateTrackAddInfo = error.backendErrors[0] - self.showDuplicateTrackAddConfirmation = true - } else { - self.errors = error.backendErrors - self.showDuplicateTrackAddConfirmation = false - } - }) - }, - duplicateTrackAddConfirm (v) { - this.showDuplicateTrackAddConfirmation = v + lastSelectedPlaylist: -1 } }, computed: { @@ -175,10 +230,10 @@ export default { } }, sortedPlaylists () { - let regexp = new RegExp(this.playlistNameFilter, 'i'); - let p = flow( + const regexp = new RegExp(this.playlistNameFilter, 'i') + const p = flow( filter((e) => e.name.match(regexp) !== null), - sortBy((e) => { return e.modification_date }), + sortBy((e) => { return e.modification_date }) )(this.playlists) p.reverse() return p @@ -193,6 +248,37 @@ export default { this.formKey = String(new Date()) this.showDuplicateTrackAddConfirmation = false } + }, + methods: { + update (v) { + this.$store.commit('playlists/showModal', v) + }, + addToPlaylist (playlistId, allowDuplicate) { + const self = this + const payload = { + tracks: [this.track.id], + allow_duplicates: allowDuplicate + } + + self.lastSelectedPlaylist = playlistId + + return axios.post(`playlists/${playlistId}/add`, payload).then(response => { + logger.default.info('Successfully added track to playlist') + self.update(false) + self.$store.dispatch('playlists/fetchOwn') + }, error => { + if (error.backendErrors.length === 1 && error.backendErrors[0].code === 'tracks_already_exist_in_playlist') { + self.duplicateTrackAddInfo = error.backendErrors[0] + self.showDuplicateTrackAddConfirmation = true + } else { + self.errors = error.backendErrors + self.showDuplicateTrackAddConfirmation = false + } + }) + }, + duplicateTrackAddConfirm (v) { + this.showDuplicateTrackAddConfirmation = v + } } } </script> diff --git a/front/src/components/playlists/TrackPlaylistIcon.vue b/front/src/components/playlists/TrackPlaylistIcon.vue index aafdfe4daeeba8afc1adfd1f3bac506e9e0e0dcb..fbae04d2904983dd166853b5923c7926b8d78b4c 100644 --- a/front/src/components/playlists/TrackPlaylistIcon.vue +++ b/front/src/components/playlists/TrackPlaylistIcon.vue @@ -1,18 +1,22 @@ <template> <button - @click.stop="$store.commit('playlists/chooseTrack', track)" v-if="button" - :class="['ui', 'icon', 'labeled', 'button']"> - <i class="list icon"></i> - <translate translate-context="Sidebar/Player/Icon.Tooltip/Verb">Add to playlist…</translate> + :class="['ui', 'icon', 'labeled', 'button']" + @click.stop="$store.commit('playlists/chooseTrack', track)" + > + <i class="list icon" /> + <translate translate-context="Sidebar/Player/Icon.Tooltip/Verb"> + Add to playlist… + </translate> </button> <button v-else - @click.stop="$store.commit('playlists/chooseTrack', track)" :class="['ui', 'basic', 'circular', 'icon', {'really': !border}, 'button']" :aria-label="labels.addToPlaylist" - :title="labels.addToPlaylist"> - <i :class="['list', 'basic', 'icon']"></i> + :title="labels.addToPlaylist" + @click.stop="$store.commit('playlists/chooseTrack', track)" + > + <i :class="['list', 'basic', 'icon']" /> </button> </template> @@ -20,9 +24,9 @@ export default { props: { - track: {type: Object}, - button: {type: Boolean, default: false}, - border: {type: Boolean, default: false}, + track: { type: Object, default: function () { return {} } }, + button: { type: Boolean, default: false }, + border: { type: Boolean, default: false } }, data () { return { diff --git a/front/src/components/playlists/Widget.vue b/front/src/components/playlists/Widget.vue index d3bdf8f13a1cf992bb65ef9a071df21f3c6d4844..686c651eac92abf78998a310b76bc560ad8d3c1a 100644 --- a/front/src/components/playlists/Widget.vue +++ b/front/src/components/playlists/Widget.vue @@ -1,36 +1,58 @@ <template> <div> - <h3 v-if="!!this.$slots.title" class="ui header"> - <slot name="title"></slot> + <h3 + v-if="!!$slots.title" + class="ui header" + > + <slot name="title" /> </h3> - <div v-if="isLoading" class="ui inverted active dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui inverted active dimmer" + > + <div class="ui loader" /> </div> - <div v-if="playlistsExist" class="ui cards app-cards"> - <playlist-card v-for="playlist in objects" :key="playlist.id" :playlist="playlist"></playlist-card> + <div + v-if="playlistsExist" + class="ui cards app-cards" + > + <playlist-card + v-for="playlist in objects" + :key="playlist.id" + :playlist="playlist" + /> </div> - <div v-else class="ui placeholder segment"> + <div + v-else + class="ui placeholder segment" + > <div class="ui icon header"> - <i class="list icon"></i> + <i class="list icon" /> <translate translate-context="Content/Home/Placeholder"> No playlists have been created yet </translate> </div> <button v-if="$store.state.auth.authenticated" - @click="$store.commit('playlists/chooseTrack', null)" class="ui success icon labeled button" - > - <i class="list icon"></i> + @click="$store.commit('playlists/chooseTrack', null)" + > + <i class="list icon" /> <translate translate-context="Content/Home/CreatePlaylist"> Create Playlist </translate> </button> </div> <template v-if="nextPage"> - <div class="ui hidden divider"></div> - <button v-if="nextPage" @click="fetchData(nextPage)" :class="['ui', 'basic', 'button']"> - <translate translate-context="*/*/Button,Label">Show more</translate> + <div class="ui hidden divider" /> + <button + v-if="nextPage" + :class="['ui', 'basic', 'button']" + @click="fetchData(nextPage)" + > + <translate translate-context="*/*/Button,Label"> + Show more + </translate> </button> </template> </div> @@ -42,13 +64,13 @@ import axios from 'axios' import PlaylistCard from '@/components/playlists/Card' export default { - props: { - filters: {type: Object, required: true}, - url: {type: String, required: true} - }, components: { PlaylistCard }, + props: { + filters: { type: Object, required: true }, + url: { type: String, required: true } + }, data () { return { objects: [], @@ -59,25 +81,33 @@ export default { nextPage: null } }, - created () { - this.fetchData(this.url) - }, computed: { playlistsExist: function () { return this.objects.length > 0 } }, + watch: { + offset () { + this.fetchData() + }, + '$store.state.moderation.lastUpdate': function () { + this.fetchData(this.url) + } + }, + created () { + this.fetchData(this.url) + }, methods: { fetchData (url) { if (!url) { return } this.isLoading = true - let self = this - let params = _.clone(this.filters) + const self = this + const params = _.clone(this.filters) params.page_size = this.limit params.offset = this.offset - axios.get(url, {params: params}).then((response) => { + axios.get(url, { params: params }).then((response) => { self.previousPage = response.data.previous self.nextPage = response.data.next self.isLoading = false @@ -94,14 +124,6 @@ export default { this.offset = Math.max(this.offset - this.limit, 0) } } - }, - watch: { - offset () { - this.fetchData() - }, - "$store.state.moderation.lastUpdate": function () { - this.fetchData(this.url) - } } } </script> diff --git a/front/src/components/radios/Button.vue b/front/src/components/radios/Button.vue index 2e7bbec0894ac152d9280835c3f4573a214e1b4c..8c16795e89095411f5901360e33a6887a6a73f3c 100644 --- a/front/src/components/radios/Button.vue +++ b/front/src/components/radios/Button.vue @@ -1,8 +1,22 @@ <template> - <button @click="toggleRadio" :class="['ui', 'primary', {'inverted': running}, 'icon', 'labeled', 'button']"> - <i class="ui feed icon" role="button"></i> - <template v-if="running"><translate translate-context="*/Player/Button.Label/Short, Verb">Stop radio</translate></template> - <template v-else><translate translate-context="*/Queue/Button.Label/Short, Verb">Play radio</translate></template> + <button + :class="['ui', 'primary', {'inverted': running}, 'icon', 'labeled', 'button']" + @click="toggleRadio" + > + <i + class="ui feed icon" + role="button" + /> + <template v-if="running"> + <translate translate-context="*/Player/Button.Label/Short, Verb"> + Stop radio + </translate> + </template> + <template v-else> + <translate translate-context="*/Queue/Button.Label/Short, Verb"> + Play radio + </translate> + </template> </button> </template> @@ -11,10 +25,21 @@ import lodash from '@/lodash' export default { props: { - customRadioId: {required: false}, - type: {type: String, required: false}, - clientOnly: {type: Boolean, default: false}, - objectId: {default: null} + customRadioId: { type: Number, default: 0, required: false }, + type: { type: String, required: false, default: '' }, + clientOnly: { type: Boolean, default: false }, + objectId: { type: Number, default: null } + }, + computed: { + running () { + const state = this.$store.state.radios + const current = state.current + if (!state.running) { + return false + } else { + return current.type === this.type && lodash.isEqual(current.objectId, this.objectId) && current.customRadioId === this.customRadioId + } + } }, methods: { toggleRadio () { @@ -25,21 +50,10 @@ export default { type: this.type, objectId: this.objectId, customRadioId: this.customRadioId, - clientOnly: this.clientOnly, + clientOnly: this.clientOnly }) } } - }, - computed: { - running () { - let state = this.$store.state.radios - let current = state.current - if (!state.running) { - return false - } else { - return current.type === this.type && lodash.isEqual(current.objectId, this.objectId) && current.customRadioId === this.customRadioId - } - } } } </script> diff --git a/front/src/components/radios/Card.vue b/front/src/components/radios/Card.vue index 47bc0fe1a98fea02d2cf7e44b21fc95d8162367e..3b7f98a19b31ec3a573632b41ca68a440c358d33 100644 --- a/front/src/components/radios/Card.vue +++ b/front/src/components/radios/Card.vue @@ -1,44 +1,60 @@ <template> - <div class="ui card"> - <div class="content"> - <h4 class="header"> - <router-link v-if="radio.id" class="discrete link" :to="{name: 'library.radios.detail', params: {id: radio.id}}"> - {{ radio.name }} - </router-link> - <template v-else> - {{ radio.name }} - </template> - </h4> - <div class="description"> - {{ radio.description }} - </div> - </div> - <div class="extra content"> - <user-link v-if="radio.user" :user="radio.user" class="left floated" /> - <div class="ui hidden divider"></div> - <radio-button class="right floated button" :type="type" :custom-radio-id="customRadioId" :object-id="objectId"></radio-button> + <div class="ui card"> + <div class="content"> + <h4 class="header"> <router-link - class="ui success button right floated" - v-if="$store.state.auth.authenticated && type === 'custom' && radio.user.id === $store.state.auth.profile.id" - :to="{name: 'library.radios.edit', params: {id: customRadioId }}"> - <translate translate-context="Content/*/Button.Label/Verb">Edit</translate> + v-if="radio.id" + class="discrete link" + :to="{name: 'library.radios.detail', params: {id: radio.id}}" + > + {{ radio.name }} </router-link> + <template v-else> + {{ radio.name }} + </template> + </h4> + <div class="description"> + {{ radio.description }} </div> </div> + <div class="extra content"> + <user-link + v-if="radio.user" + :user="radio.user" + class="left floated" + /> + <div class="ui hidden divider" /> + <radio-button + class="right floated button" + :type="type" + :custom-radio-id="customRadioId" + :object-id="objectId" + /> + <router-link + v-if="$store.state.auth.authenticated && type === 'custom' && radio.user.id === $store.state.auth.profile.id" + class="ui success button right floated" + :to="{name: 'library.radios.edit', params: {id: customRadioId }}" + > + <translate translate-context="Content/*/Button.Label/Verb"> + Edit + </translate> + </router-link> + </div> + </div> </template> <script> import RadioButton from './Button' export default { - props: { - type: {type: String, required: true}, - customRadio: {required: false}, - objectId: {required: false}, - }, components: { RadioButton }, + props: { + type: { type: String, required: true, default: '' }, + customRadio: { type: Boolean, required: false }, + objectId: { type: Number, required: false, default: 0 } + }, computed: { radio () { if (this.customRadio) { diff --git a/front/src/components/tags/List.vue b/front/src/components/tags/List.vue index 0da76378e5d73ee68f73072d1276fb387129d35f..e9f695fcac7c43227ae68e0edd131bcc42bd215c 100644 --- a/front/src/components/tags/List.vue +++ b/front/src/components/tags/List.vue @@ -1,30 +1,43 @@ <template> <div class="component-tags-list"> <router-link + v-for="tag in toDisplay" + :key="tag" :to="{name: detailRoute, params: {id: tag}}" :class="['ui', 'circular', 'hashtag', 'label', labelClasses]" - v-for="tag in toDisplay" - :key="tag"> + > #{{ tag|truncate(truncateSize) }} </router-link> - <div role="button" @click.prevent="honorLimit = false" class="ui circular inverted accent label" v-if="showMore && toDisplay.length < tags.length"> - <translate translate-context="Content/*/Button/Label/Verb" :translate-params="{count: tags.length - toDisplay.length}" :translate-n="tags.length - toDisplay.length" translate-plural="Show %{ count } more tags">Show 1 more tag</translate> + <div + v-if="showMore && toDisplay.length < tags.length" + role="button" + class="ui circular inverted accent label" + @click.prevent="honorLimit = false" + > + <translate + translate-context="Content/*/Button/Label/Verb" + :translate-params="{count: tags.length - toDisplay.length}" + :translate-n="tags.length - toDisplay.length" + translate-plural="Show %{ count } more tags" + > + Show 1 more tag + </translate> </div> </div> </template> <script> export default { props: { - tags: {type: Array, required: true}, - showMore: {type: Boolean, default: true}, - truncateSize: {type: Number, default: 25}, - limit: {type: Number, default: 5}, - labelClasses: {type: String, default: ''}, - detailRoute: {type: String, default: 'library.tags.detail'}, + tags: { type: Array, required: true }, + showMore: { type: Boolean, default: true }, + truncateSize: { type: Number, default: 25 }, + limit: { type: Number, default: 5 }, + labelClasses: { type: String, default: '' }, + detailRoute: { type: String, default: 'library.tags.detail' } }, data () { return { - honorLimit: true, + honorLimit: true } }, computed: { diff --git a/front/src/components/utils/global-events.vue b/front/src/components/utils/global-events.vue index dd25865c902d1e0237a45149521279a403a11f66..6cc7f8472c90b72b3ed87c6d3e3268673ef6752c 100644 --- a/front/src/components/utils/global-events.vue +++ b/front/src/components/utils/global-events.vue @@ -18,13 +18,12 @@ function extractEventOptions (eventDescriptor) { } export default { - render: h => h(), mounted () { this._listeners = Object.create(null) Object.keys(this.$listeners).forEach(event => { const handler = this.$listeners[event] - let wrapper = function (event) { + const wrapper = function (event) { // we check here the event is not triggered from an input // to avoid collisions if (!$(event.target).is('.field, :input, [contenteditable]')) { @@ -47,6 +46,7 @@ export default { this._listeners[event] ) } - } + }, + render: h => h() } </script> diff --git a/front/src/edits.js b/front/src/edits.js index baa9bbb328ca5d990c748725bc3178a601c74507..637f28e46ef9a10a2b4215c02be559b1ad4f81cd 100644 --- a/front/src/edits.js +++ b/front/src/edits.js @@ -16,7 +16,7 @@ export default { type: 'content', required: true, label: this.$pgettext('*/*/*/Noun', 'Description'), - getValue: (obj) => { return obj.description || {text: null, content_type: 'text/markdown'}}, + getValue: (obj) => { return obj.description || { text: null, content_type: 'text/markdown' } }, getValueRepr: getContentValueRepr } const cover = { @@ -51,7 +51,7 @@ export default { label: this.$pgettext('*/*/*/Noun', 'Tags'), getValue: (obj) => { return obj.tags }, getValueRepr: getTagsValueRepr - }, + } ] }, album: { @@ -113,7 +113,7 @@ export default { type: 'license', required: false, label: this.$pgettext('Content/*/*/Noun', 'License'), - getValue: (obj) => { return obj.license }, + getValue: (obj) => { return obj.license } }, { id: 'tags', @@ -132,23 +132,23 @@ export default { return this.configs[this.objectType] }, getFieldConfig (configs, type, fieldId) { - let c = configs[type] + const c = configs[type] return c.fields.filter((f) => { - return f.id == fieldId + return f.id === fieldId })[0] }, getCurrentState () { - let self = this - let s = {} + const self = this + const s = {} this.config.fields.forEach(f => { - s[f.id] = {value: f.getValue(self.object)} + s[f.id] = { value: f.getValue(self.object) } }) return s }, getCurrentStateForObj (obj, config) { - let s = {} + const s = {} config.fields.forEach(f => { - s[f.id] = {value: f.getValue(obj)} + s[f.id] = { value: f.getValue(obj) } }) return s }, @@ -161,8 +161,8 @@ export default { return false } return ( - this.obj.created_by.full_username === this.$store.state.auth.fullUsername - || this.$store.state.auth.availablePermissions['library'] + this.obj.created_by.full_username === this.$store.state.auth.fullUsername || + this.$store.state.auth.availablePermissions.library ) }, getCanApprove () { @@ -172,20 +172,20 @@ export default { if (!this.$store.state.auth.authenticated) { return false } - return this.$store.state.auth.availablePermissions['library'] + return this.$store.state.auth.availablePermissions.library }, getCanEdit () { if (!this.$store.state.auth.authenticated) { return false } - let libraryPermission = this.$store.state.auth.availablePermissions['library'] - let objData = this.object || {} + const libraryPermission = this.$store.state.auth.availablePermissions.library + const objData = this.object || {} let isOwner = false if (objData.attributed_to) { isOwner = this.$store.state.auth.fullUsername === objData.attributed_to.full_username } return libraryPermission || isOwner - }, + } } diff --git a/front/src/embed.js b/front/src/embed.js index a079ee08507fd0db148a2221b704000a6f08abb6..dc3e866f3c48c71dd06d30af285de0b6035ca699 100644 --- a/front/src/embed.js +++ b/front/src/embed.js @@ -10,8 +10,8 @@ Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', + components: { EmbedFrame }, render (h) { return h('EmbedFrame') - }, - components: { EmbedFrame } + } }) diff --git a/front/src/entities.js b/front/src/entities.js index c13d7c6e2bc86d6f2f7953f8bb3953153af936e9..3bafa357bc5ef231045066aba69159a9587b31a6 100644 --- a/front/src/entities.js +++ b/front/src/entities.js @@ -15,8 +15,8 @@ export default { return `manage/library/artists/${obj.id}/` }, urls: { - getDetail: (obj) => { return {name: 'library.artists.detail', params: {id: obj.id}}}, - getAdminDetail: (obj) => { return {name: 'manage.library.artists.detail', params: {id: obj.id}}}, + getDetail: (obj) => { return { name: 'library.artists.detail', params: { id: obj.id } } }, + getAdminDetail: (obj) => { return { name: 'manage.library.artists.detail', params: { id: obj.id } } } }, moderatedFields: [ { @@ -40,7 +40,7 @@ export default { id: 'mbid', label: this.$pgettext('*/*/*/Noun', 'MusicBrainz ID'), getValue: (obj) => { return obj.mbid } - }, + } ] }, album: { @@ -50,8 +50,8 @@ export default { return `manage/library/albums/${obj.id}/` }, urls: { - getDetail: (obj) => { return {name: 'library.albums.detail', params: {id: obj.id}}}, - getAdminDetail: (obj) => { return {name: 'manage.library.albums.detail', params: {id: obj.id}}} + getDetail: (obj) => { return { name: 'library.albums.detail', params: { id: obj.id } } }, + getAdminDetail: (obj) => { return { name: 'manage.library.albums.detail', params: { id: obj.id } } } }, moderatedFields: [ { @@ -81,7 +81,7 @@ export default { id: 'mbid', label: this.$pgettext('*/*/*/Noun', 'MusicBrainz ID'), getValue: (obj) => { return obj.mbid } - }, + } ] }, track: { @@ -91,8 +91,8 @@ export default { return `manage/library/tracks/${obj.id}/` }, urls: { - getDetail: (obj) => { return {name: 'library.tracks.detail', params: {id: obj.id}}}, - getAdminDetail: (obj) => { return {name: 'manage.library.tracks.detail', params: {id: obj.id}}} + getDetail: (obj) => { return { name: 'library.tracks.detail', params: { id: obj.id } } }, + getAdminDetail: (obj) => { return { name: 'manage.library.tracks.detail', params: { id: obj.id } } } }, moderatedFields: [ { @@ -113,7 +113,7 @@ export default { { id: 'license', label: this.$pgettext('Content/*/*/Noun', 'License'), - getValue: (obj) => { return obj.license }, + getValue: (obj) => { return obj.license } }, { id: 'tags', @@ -125,7 +125,7 @@ export default { id: 'mbid', label: this.$pgettext('*/*/*/Noun', 'MusicBrainz ID'), getValue: (obj) => { return obj.mbid } - }, + } ] }, library: { @@ -135,7 +135,7 @@ export default { return `manage/library/libraries/${obj.uuid}/` }, urls: { - getAdminDetail: (obj) => { return {name: 'manage.library.libraries.detail', params: {id: obj.uuid}}} + getAdminDetail: (obj) => { return { name: 'manage.library.libraries.detail', params: { id: obj.uuid } } } }, moderatedFields: [ { @@ -152,14 +152,14 @@ export default { id: 'privacy_level', label: this.$pgettext('*/*/*', 'Visibility'), getValue: (obj) => { return obj.privacy_level } - }, + } ] }, playlist: { label: this.$pgettext('*/*/*', 'Playlist'), icon: 'list', urls: { - getDetail: (obj) => { return {name: 'library.playlists.detail', params: {id: obj.id}}}, + getDetail: (obj) => { return { name: 'library.playlists.detail', params: { id: obj.id } } } // getAdminDetail: (obj) => { return {name: 'manage.playlists.detail', params: {id: obj.id}}} }, moderatedFields: [ @@ -172,15 +172,15 @@ export default { id: 'privacy_level', label: this.$pgettext('*/*/*', 'Visibility'), getValue: (obj) => { return obj.privacy_level } - }, + } ] }, account: { label: this.$pgettext('*/*/*/Noun', 'Account'), icon: 'user', urls: { - getDetail: (obj) => { return {name: 'profile.full.overview', params: {username: obj.preferred_username, domain: obj.domain}}}, - getAdminDetail: (obj) => { return {name: 'manage.moderation.accounts.detail', params: {id: `${obj.preferred_username}@${obj.domain}`}}} + getDetail: (obj) => { return { name: 'profile.full.overview', params: { username: obj.preferred_username, domain: obj.domain } } }, + getAdminDetail: (obj) => { return { name: 'manage.moderation.accounts.detail', params: { id: `${obj.preferred_username}@${obj.domain}` } } } }, moderatedFields: [ { @@ -192,15 +192,15 @@ export default { id: 'summary', label: this.$pgettext('*/*/*/Noun', 'Bio'), getValue: (obj) => { return obj.summary } - }, + } ] }, channel: { label: this.$pgettext('*/*/*', 'Channel'), icon: 'stream', urls: { - getDetail: (obj) => { return {name: 'channels.detail', params: {id: obj.uuid}}}, - getAdminDetail: (obj) => { return {name: 'manage.channels.detail', params: {id: obj.uuid}}} + getDetail: (obj) => { return { name: 'channels.detail', params: { id: obj.uuid } } }, + getAdminDetail: (obj) => { return { name: 'manage.channels.detail', params: { id: obj.uuid } } } }, moderatedFields: [ { @@ -219,9 +219,9 @@ export default { label: this.$pgettext('*/*/*/Noun', 'Tags'), getValue: (obj) => { return obj.tags }, getValueRepr: getTagsValueRepr - }, + } ] - }, + } } }, @@ -229,17 +229,17 @@ export default { return this.configs[this.objectType] }, getFieldConfig (configs, type, fieldId) { - let c = configs[type] + const c = configs[type] return c.fields.filter((f) => { - return f.id == fieldId + return f.id === fieldId })[0] }, getCurrentStateForObj (obj, config) { - let s = {} + const s = {} config.fields.forEach(f => { - s[f.id] = {value: f.getValue(obj)} + s[f.id] = { value: f.getValue(obj) } }) return s - }, + } } diff --git a/front/src/filters.js b/front/src/filters.js index 030e50e830856c5325381f06a4aafcc911561c0d..75834ffbdc3b0086851790a3d28343924e601a51 100644 --- a/front/src/filters.js +++ b/front/src/filters.js @@ -14,14 +14,14 @@ export function truncate (str, max, ellipsis, middle) { return str } if (middle) { - var sepLen = 1, - charsToShow = max - sepLen, - frontChars = Math.ceil(charsToShow/2), - backChars = Math.floor(charsToShow/2); + const sepLen = 1 + const charsToShow = max - sepLen + const frontChars = Math.ceil(charsToShow / 2) + const backChars = Math.floor(charsToShow / 2) return str.substr(0, frontChars) + ellipsis + - str.substr(str.length - backChars); + str.substr(str.length - backChars) } else { return str.slice(0, max) + ellipsis } @@ -51,20 +51,20 @@ export function fromNow (date, locale) { relativeTime: { future: 'in %s', past: '%s ago', - s: 'seconds', + s: 'seconds', ss: '%ss', - m: 'a minute', + m: 'a minute', mm: '%dm', - h: 'an hour', + h: 'an hour', hh: '%dh', - d: 'a day', + d: 'a day', dd: '%dd', - M: 'a month', + M: 'a month', MM: '%dM', - y: 'a year', + y: 'a year', yy: '%dY' } - }); + }) const m = moment(date) m.locale(locale) return m.fromNow(true) @@ -73,7 +73,7 @@ export function fromNow (date, locale) { Vue.filter('fromNow', fromNow) export function secondsToObject (seconds) { - let m = moment.duration(seconds, 'seconds') + const m = moment.duration(seconds, 'seconds') return { seconds: m.seconds(), minutes: m.minutes(), @@ -84,9 +84,9 @@ export function secondsToObject (seconds) { Vue.filter('secondsToObject', secondsToObject) export function padDuration (duration) { - var s = String(duration); - while (s.length < 2) {s = "0" + s;} - return s; + let s = String(duration) + while (s.length < 2) { s = '0' + s } + return s } Vue.filter('padDuration', padDuration) @@ -117,15 +117,15 @@ export function capitalize (str) { Vue.filter('capitalize', capitalize) export function humanSize (bytes) { - let si = true - var thresh = si ? 1000 : 1024 + const si = true + const thresh = si ? 1000 : 1024 if (Math.abs(bytes) < thresh) { return bytes + ' B' } - var units = si + const units = si ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] - var u = -1 + let u = -1 do { bytes /= thresh ++u diff --git a/front/src/lodash.js b/front/src/lodash.js index 91bebd2e8d8a8cbe41c50d881299ed743cdd9b88..7204beee43846d4821b1d6b59d45364c674ad255 100644 --- a/front/src/lodash.js +++ b/front/src/lodash.js @@ -19,5 +19,5 @@ export default { sum: require('lodash/sum'), startCase: require('lodash/startCase'), tap: require('lodash/tap'), - trim: require('lodash/trim'), + trim: require('lodash/trim') } diff --git a/front/src/main.js b/front/src/main.js index 79505a707f47e2c1af27e6e58ab013b782a032a7..158dbe67bf3af18a09bed73fb808d3621d29504f 100644 --- a/front/src/main.js +++ b/front/src/main.js @@ -1,10 +1,7 @@ // The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. import logger from '@/logging' - -logger.default.info('Loading environment:', process.env.NODE_ENV) -logger.default.debug('Environment variables:', process.env) -import jQuery from "jquery" +import jQuery from 'jquery' import Vue from 'vue' import moment from 'moment' @@ -16,21 +13,24 @@ import store from './store' import GetTextPlugin from 'vue-gettext' import { sync } from 'vuex-router-sync' import locales from '@/locales' -import createAuthRefreshInterceptor from 'axios-auth-refresh'; +import createAuthRefreshInterceptor from 'axios-auth-refresh' import filters from '@/filters' // eslint-disable-line -import {parseAPIErrors} from '@/utils' +import { parseAPIErrors } from '@/utils' import globals from '@/components/globals' // eslint-disable-line import './registerServiceWorker' +logger.default.info('Loading environment:', process.env.NODE_ENV) +logger.default.debug('Environment variables:', process.env) + sync(store, router) window.$ = window.jQuery = require('jquery') require('./semantic.js') let APP = null -let availableLanguages = (function () { - let l = {} +const availableLanguages = (function () { + const l = {} locales.locales.forEach(c => { l[c.code] = c.label }) @@ -69,7 +69,7 @@ Vue.directive('dropdown', function (el, binding) { action: function (text, value, $el) { // used to ensure focusing the dropdown and clicking via keyboard // works as expected - let button = $el[0] + const button = $el[0] button.click() jQuery(el).find('.ui.dropdown').dropdown('hide') }, @@ -79,10 +79,9 @@ Vue.directive('dropdown', function (el, binding) { axios.defaults.xsrfCookieName = 'csrftoken' axios.defaults.xsrfHeaderName = 'X-CSRFToken' axios.interceptors.request.use(function (config) { - // Do something before request is sent if (store.state.auth.oauth.accessToken) { - config.headers['Authorization'] = store.getters['auth/header'] + config.headers.Authorization = store.getters['auth/header'] } return config }, function (error) { @@ -98,7 +97,7 @@ axios.interceptors.response.use(function (response) { if (store.state.auth.authenticated && !store.state.auth.oauth.accessToken && error.response.status === 401) { store.commit('auth/authenticated', false) logger.default.warn('Received 401 response from API, redirecting to login form', router.currentRoute.fullPath) - router.push({name: 'login', query: {next: router.currentRoute.fullPath}}) + router.push({ name: 'login', query: { next: router.currentRoute.fullPath } }) } if (error.response.status === 404) { error.backendErrors.push('Resource not found') @@ -106,28 +105,28 @@ axios.interceptors.response.use(function (response) { error.backendErrors.push('Permission denied') } else if (error.response.status === 429) { let message - let rateLimitStatus = { + const rateLimitStatus = { limit: error.response.headers['x-ratelimit-limit'], scope: error.response.headers['x-ratelimit-scope'], remaining: error.response.headers['x-ratelimit-remaining'], duration: error.response.headers['x-ratelimit-duration'], availableSeconds: error.response.headers['retry-after'], reset: error.response.headers['x-ratelimit-reset'], - resetSeconds: error.response.headers['x-ratelimit-resetseconds'], + resetSeconds: error.response.headers['x-ratelimit-resetseconds'] } if (rateLimitStatus.availableSeconds) { rateLimitStatus.availableSeconds = parseInt(rateLimitStatus.availableSeconds) - let tryAgain = moment().add(rateLimitStatus.availableSeconds, 's').toNow(true) + const tryAgain = moment().add(rateLimitStatus.availableSeconds, 's').toNow(true) message = APP.$pgettext('*/Error/Paragraph', 'You sent too many requests and have been rate limited, please try again in %{ delay }') - message = APP.$gettextInterpolate(message, {delay: tryAgain}) + message = APP.$gettextInterpolate(message, { delay: tryAgain }) } else { - message = APP.$pgettext('*/Error/Paragraph', 'You sent too many requests and have been rate limited, please try again later') + message = APP.$pgettext('*/Error/Paragraph', 'You sent too many requests and have been rate limited, please try again later') } error.backendErrors.push(message) - store.commit("ui/addMessage", { + store.commit('ui/addMessage', { content: message, date: new Date(), - class: 'error', + class: 'error' }) logger.default.error('This client is rate-limited!', rateLimitStatus) } else if (error.response.status === 500) { @@ -137,7 +136,7 @@ axios.interceptors.response.use(function (response) { error.backendErrors.push(error.response.data.detail) } else { error.rawPayload = error.response.data - let parsedErrors = parseAPIErrors(error.response.data) + const parsedErrors = parseAPIErrors(error.response.data) error.backendErrors = [...error.backendErrors, ...parsedErrors] } } @@ -153,15 +152,15 @@ const refreshAuth = (failedRequest) => { console.log('Failed request, refreshing auth…') // maybe the token was expired, let's try to refresh it return store.dispatch('auth/refreshOauthToken').then(() => { - failedRequest.response.config.headers['Authorization'] = store.getters["auth/header"]; - return Promise.resolve(); + failedRequest.response.config.headers.Authorization = store.getters['auth/header'] + return Promise.resolve() }) } else { - return Promise.resolve(); + return Promise.resolve() } } -createAuthRefreshInterceptor(axios, refreshAuth); +createAuthRefreshInterceptor(axios, refreshAuth) store.dispatch('instance/fetchFrontSettings').finally(() => { /* eslint-disable no-new */ @@ -169,26 +168,26 @@ store.dispatch('instance/fetchFrontSettings').finally(() => { el: '#app', router, store, - render (h) { - return h('App') - }, components: { App }, created () { APP = this window.addEventListener('resize', this.handleResize) - this.handleResize(); + this.handleResize() }, - destroyed() { + destroyed () { window.removeEventListener('resize', this.handleResize) }, methods: { - handleResize() { + handleResize () { this.$store.commit('ui/window', { width: window.innerWidth, - height: window.innerHeight, + height: window.innerHeight }) } }, + render (h) { + return h('App') + } }) logger.default.info('Everything loaded!') diff --git a/front/src/radios.js b/front/src/radios.js index 3c46b711bb2546f3c196d349af1b027d0cfee5fc..5650881faa3ebf33ebc6cdfdeae0561132f4ecad 100644 --- a/front/src/radios.js +++ b/front/src/radios.js @@ -1,4 +1,4 @@ -import axios from "axios" +import axios from 'axios' import logger from '@/logging' // import axios from 'axios' @@ -8,19 +8,19 @@ const RADIOS = { // method by hand account: { offset: 1, - populateQueue({current, dispatch, playNow}) { - let params = {scope: `actor:${current.objectId.fullUsername}`, ordering: '-creation_date', page_size: 1, page: this.offset} - axios.get('history/listenings', {params}).then((response) => { - let latest = response.data.results[0] + populateQueue ({ current, dispatch, playNow }) { + const params = { scope: `actor:${current.objectId.fullUsername}`, ordering: '-creation_date', page_size: 1, page: this.offset } + axios.get('history/listenings', { params }).then((response) => { + const latest = response.data.results[0] if (!latest) { logger.default.error('No more tracks') dispatch('stop') } this.offset += 1 - let append = dispatch('queue/append', {track: latest.track}, {root: true}) + const append = dispatch('queue/append', { track: latest.track }, { root: true }) if (playNow) { append.then(() => { - dispatch('queue/last', null, {root: true}) + dispatch('queue/last', null, { root: true }) }) } }, (error) => { @@ -36,7 +36,7 @@ const RADIOS = { if (event.actor.local_id === current.objectId.username) { axios.get(`tracks/${event.object.local_id}`).then((response) => { if (response.data.uploads.length > 0) { - store.dispatch('queue/append', {track: response.data}) + store.dispatch('queue/append', { track: response.data }) this.offset += 1 } }, (error) => { @@ -46,6 +46,6 @@ const RADIOS = { } } } -export function getClientOnlyRadio({type}) { +export function getClientOnlyRadio ({ type }) { return RADIOS[type] } diff --git a/front/src/registerServiceWorker.js b/front/src/registerServiceWorker.js index 0fdf6c9334f9d26e5ee03914bdf606c4f089e197..aa5d66df7a98f9c100ae020573b8f6d378df8b70 100644 --- a/front/src/registerServiceWorker.js +++ b/front/src/registerServiceWorker.js @@ -15,15 +15,15 @@ if (process.env.NODE_ENV === 'production') { registered (registration) { console.log('Service worker has been registered.') // check for updates every 2 hours - var checkInterval = 1000 * 60 * 60 * 2 + const checkInterval = 1000 * 60 * 60 * 2 // var checkInterval = 1000 * 5 setInterval(() => { console.log('Checking for service worker update…') - registration.update(); - }, checkInterval); - store.commit('ui/serviceWorker', {registration: registration}) + registration.update() + }, checkInterval) + store.commit('ui/serviceWorker', { registration: registration }) if (registration.active) { - registration.active.postMessage({command: 'serverChosen', serverUrl: store.state.instance.instanceUrl}) + registration.active.postMessage({ command: 'serverChosen', serverUrl: store.state.instance.instanceUrl }) } }, cached () { @@ -34,7 +34,7 @@ if (process.env.NODE_ENV === 'production') { }, updated (registration) { console.log('New content is available; please refresh!') - store.commit('ui/serviceWorker', {updateAvailable: true, registration: registration}) + store.commit('ui/serviceWorker', { updateAvailable: true, registration: registration }) }, offline () { console.log('No internet connection found. App is running in offline mode.') diff --git a/front/src/sanitize.js b/front/src/sanitize.js index d2a8d08091f3cbc2e864cdb58e547e0fbf72e1f8..f43a69875d41af5e38ce7ae3a53679c16fdd2de2 100644 --- a/front/src/sanitize.js +++ b/front/src/sanitize.js @@ -1,43 +1,43 @@ -import sanitizeHtml from "sanitize-html" +import sanitizeHtml from 'sanitize-html' const allowedTags = [ - "h3", - "h4", - "h5", - "h6", - "blockquote", - "p", - "a", - "ul", - "ol", - "nl", - "li", - "b", - "i", - "strong", - "em", - "strike", - "code", - "hr", - "br", - "div", - "table", - "thead", - "caption", - "tbody", - "tr", - "th", - "td", - "pre", + 'h3', + 'h4', + 'h5', + 'h6', + 'blockquote', + 'p', + 'a', + 'ul', + 'ol', + 'nl', + 'li', + 'b', + 'i', + 'strong', + 'em', + 'strike', + 'code', + 'hr', + 'br', + 'div', + 'table', + 'thead', + 'caption', + 'tbody', + 'tr', + 'th', + 'td', + 'pre' ] const allowedAttributes = { - a: ["href", "name", "target"], + a: ['href', 'name', 'target'], // We don't currently allow img itself by default, but this // would make sense if we did. You could add srcset here, // and if you do the URL is checked for safety - img: ["src"] + img: ['src'] } -export default function sanitize(input) { - return sanitizeHtml(input, {allowedAttributes, allowedTags}) +export default function sanitize (input) { + return sanitizeHtml(input, { allowedAttributes, allowedTags }) } diff --git a/front/src/search.js b/front/src/search.js index 5fc2a6b94401d70b2736e4512c26c12c57246cca..adb2e63fb02e6e9795a94130f8f0038631903261 100644 --- a/front/src/search.js +++ b/front/src/search.js @@ -1,61 +1,61 @@ export function normalizeQuery (query) { - // given a string such as 'this is "my query" go', returns - // an array of tokens like this: ['this', 'is', 'my query', 'go'] - if (!query) { - return [] - } - return query.match(/\\?.|^$/g).reduce((p, c) => { - if (c === '"'){ - p.quote ^= 1 - } else if (!p.quote && c === ' '){ - p.a.push('') - } else { - p.a[p.a.length-1] += c.replace(/\\(.)/,"$1") - } - return p - }, {a: ['']}).a + // given a string such as 'this is "my query" go', returns + // an array of tokens like this: ['this', 'is', 'my query', 'go'] + if (!query) { + return [] + } + return query.match(/\\?.|^$/g).reduce((p, c) => { + if (c === '"') { + p.quote ^= 1 + } else if (!p.quote && c === ' ') { + p.a.push('') + } else { + p.a[p.a.length - 1] += c.replace(/\\(.)/, '$1') + } + return p + }, { a: [''] }).a } export function parseTokens (tokens) { - // given an array of tokens as returned by normalizeQuery, - // returns a list of objects such as [ - // { - // field: 'status', - // value: 'pending' - // }, - // { - // field: null, - // value: 'hello' - // } - // ] - return tokens.map(t => { - // we split the token on ":" - let parts = t.split(/:(.+)/) - if (parts.length === 1) { - // no field specified - return {field: null, value: t} - } - // first item is the field, second is the value, possibly quoted - let field = parts[0] - let rawValue = parts[1] + // given an array of tokens as returned by normalizeQuery, + // returns a list of objects such as [ + // { + // field: 'status', + // value: 'pending' + // }, + // { + // field: null, + // value: 'hello' + // } + // ] + return tokens.map(t => { + // we split the token on ":" + const parts = t.split(/:(.+)/) + if (parts.length === 1) { + // no field specified + return { field: null, value: t } + } + // first item is the field, second is the value, possibly quoted + const field = parts[0] + let rawValue = parts[1] - // we remove surrounding quotes if any - if (rawValue[0] === '"') { - rawValue = rawValue.substring(1) - } - if (rawValue.slice(-1) === '"') { - rawValue = rawValue.substring(0, rawValue.length - 1); - } - return {field, value: rawValue} - }) + // we remove surrounding quotes if any + if (rawValue[0] === '"') { + rawValue = rawValue.substring(1) + } + if (rawValue.slice(-1) === '"') { + rawValue = rawValue.substring(0, rawValue.length - 1) + } + return { field, value: rawValue } + }) } export function compileTokens (tokens) { // given a list of tokens as returned by parseTokens, // returns a string query - let parts = tokens.map(t => { + const parts = tokens.map(t => { let v = t.value - let k = t.field + const k = t.field if (v.indexOf(' ') > -1) { v = `"${v}"` } diff --git a/front/src/service-worker.js b/front/src/service-worker.js index 1518363fdc3d87217ced032610a45efb9453b170..11f73ea72bd6568f30d38984083f211a2cc39f8f 100644 --- a/front/src/service-worker.js +++ b/front/src/service-worker.js @@ -1,32 +1,35 @@ +/* eslint no-undef: "off" */ + // This is the code piece that GenerateSW mode can't provide for us. // This code listens for the user's confirmation to update the app. -workbox.loadModule('workbox-routing'); -workbox.loadModule('workbox-strategies'); -workbox.loadModule('workbox-expiration'); +workbox.loadModule('workbox-routing') +workbox.loadModule('workbox-strategies') +workbox.loadModule('workbox-expiration') self.addEventListener('message', (e) => { if (!e.data) { - return; + return } console.log('[sw] received message', e.data) switch (e.data.command) { case 'skipWaiting': - self.skipWaiting(); - break; + self.skipWaiting() + break case 'serverChosen': self.registerServerRoutes(e.data.serverUrl) + break default: // NOOP - break; + break } -}); -workbox.core.clientsClaim(); +}) +workbox.core.clientsClaim() -const router = new workbox.routing.Router(); +const router = new workbox.routing.Router() router.addCacheListener() router.addFetchListener() -var registeredServerRoutes = [] +let registeredServerRoutes = [] self.registerServerRoutes = (serverUrl) => { console.log('[sw] Setting up API caching for', serverUrl) registeredServerRoutes.forEach((r) => { @@ -36,30 +39,30 @@ self.registerServerRoutes = (serverUrl) => { if (!serverUrl) { return } - var regexReadyServerUrl = serverUrl.replace('.', '\\.') + const regexReadyServerUrl = serverUrl.replace('.', '\\.') registeredServerRoutes = [] - var networkFirstPaths = [ + const networkFirstPaths = [ 'api/v1/', - 'media/', + 'media/' ] - var networkFirstExcludedPaths = [ + const networkFirstExcludedPaths = [ 'api/v1/listen' ] - var strategy = new workbox.strategies.NetworkFirst({ - cacheName: "api-cache:" + serverUrl, + const strategy = new workbox.strategies.NetworkFirst({ + cacheName: 'api-cache:' + serverUrl, plugins: [ new workbox.expiration.Plugin({ - maxAgeSeconds: 24 * 60 * 60 * 7, - }), + maxAgeSeconds: 24 * 60 * 60 * 7 + }) ] }) - var networkFirstRoutes = networkFirstPaths.map((path) => { - var regex = new RegExp(regexReadyServerUrl + path) + const networkFirstRoutes = networkFirstPaths.map((path) => { + const regex = new RegExp(regexReadyServerUrl + path) return new workbox.routing.RegExpRoute(regex, () => {}) }) - var matcher = ({url, event}) => { + const matcher = ({ url, event }) => { for (let index = 0; index < networkFirstExcludedPaths.length; index++) { - const blacklistedPath = networkFirstExcludedPaths[index]; + const blacklistedPath = networkFirstExcludedPaths[index] if (url.pathname.startsWith('/' + blacklistedPath)) { // the path is blacklisted, we don't cache it at all console.log('[sw] Path is blacklisted, not caching', url.pathname) @@ -68,8 +71,8 @@ self.registerServerRoutes = (serverUrl) => { } // we call other regex matchers for (let index = 0; index < networkFirstRoutes.length; index++) { - const route = networkFirstRoutes[index]; - let result = route.match({url, event}) + const route = networkFirstRoutes[index] + const result = route.match({ url, event }) if (result) { return result } @@ -77,14 +80,14 @@ self.registerServerRoutes = (serverUrl) => { return false } - var route = new workbox.routing.Route(matcher, strategy) + const route = new workbox.routing.Route(matcher, strategy) console.log('[sw] registering new API route...', route) router.registerRoute(route) registeredServerRoutes.push(route) } // The precaching code provided by Workbox. -self.__precacheManifest = [].concat(self.__precacheManifest || []); +self.__precacheManifest = [].concat(self.__precacheManifest || []) // workbox.precaching.suppressWarnings(); // Only used with Vue CLI 3 and Workbox v3. -workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); +workbox.precaching.precacheAndRoute(self.__precacheManifest, {}) diff --git a/front/src/store/auth.js b/front/src/store/auth.js index 7d7aa6fa6392277c74be957716162874019cdb18..6df59fd9d668cd7b923f996c85827852c56359db 100644 --- a/front/src/store/auth.js +++ b/front/src/store/auth.js @@ -133,7 +133,7 @@ export default { // Send a request to the login URL and save the returned JWT login ({ commit, dispatch }, { next, credentials, onError }) { const router = require('@/router').default - var form = new FormData() + const form = new FormData() Object.keys(credentials).forEach((k) => { form.set(k, credentials[k]) }) diff --git a/front/src/store/channels.js b/front/src/store/channels.js index d6c14f8357b54acf937fff8ebf6fd167c5b6ac2e..d5189fa7f12e28e2c9493acba1aa27595c1699fd 100644 --- a/front/src/store/channels.js +++ b/front/src/store/channels.js @@ -9,17 +9,17 @@ export default { showUploadModal: false, latestPublication: null, uploadModalConfig: { - channel: null, + channel: null } }, mutations: { - subscriptions: (state, {uuid, value}) => { + subscriptions: (state, { uuid, value }) => { if (value) { if (state.subscriptions.indexOf(uuid) === -1) { state.subscriptions.push(uuid) } } else { - let i = state.subscriptions.indexOf(uuid) + const i = state.subscriptions.indexOf(uuid) if (i > -1) { state.subscriptions.splice(i, 1) } @@ -38,11 +38,11 @@ export default { } } }, - publish (state, {uploads, channel}) { + publish (state, { uploads, channel }) { state.latestPublication = { date: new Date(), uploads, - channel, + channel } state.showUploadModal = false } @@ -53,32 +53,32 @@ export default { } }, actions: { - set ({commit, state}, {uuid, value}) { - commit('subscriptions', {uuid, value}) + set ({ commit, state }, { uuid, value }) { + commit('subscriptions', { uuid, value }) if (value) { return axios.post(`channels/${uuid}/subscribe/`).then((response) => { logger.default.info('Successfully subscribed to channel') }, (response) => { logger.default.info('Error while subscribing to channel') - commit('subscriptions', {uuid, value: !value}) + commit('subscriptions', { uuid, value: !value }) }) } else { return axios.post(`channels/${uuid}/unsubscribe/`).then((response) => { logger.default.info('Successfully unsubscribed from channel') }, (response) => { logger.default.info('Error while unsubscribing from channel') - commit('subscriptions', {uuid, value: !value}) + commit('subscriptions', { uuid, value: !value }) }) } }, - toggle ({getters, dispatch}, uuid) { - dispatch('set', {uuid, value: !getters['isSubscribed'](uuid)}) + toggle ({ getters, dispatch }, uuid) { + dispatch('set', { uuid, value: !getters.isSubscribed(uuid) }) }, - fetchSubscriptions ({dispatch, state, commit, rootState}, url) { - let promise = axios.get('subscriptions/all/') + fetchSubscriptions ({ dispatch, state, commit, rootState }, url) { + const promise = axios.get('subscriptions/all/') return promise.then((response) => { response.data.results.forEach(result => { - commit('subscriptions', {uuid: result.channel, value: true}) + commit('subscriptions', { uuid: result.channel, value: true }) }) }) } diff --git a/front/src/store/favorites.js b/front/src/store/favorites.js index 1d1302eb6942e6697111b35774207d9cfe49c142..7dede7b583cdb7483e5bef5bc5aa15da1d7210c2 100644 --- a/front/src/store/favorites.js +++ b/front/src/store/favorites.js @@ -8,13 +8,13 @@ export default { count: 0 }, mutations: { - track: (state, {id, value}) => { + track: (state, { id, value }) => { if (value) { if (state.tracks.indexOf(id) === -1) { state.tracks.push(id) } } else { - let i = state.tracks.indexOf(id) + const i = state.tracks.indexOf(id) if (i > -1) { state.tracks.splice(i, 1) } @@ -32,39 +32,39 @@ export default { } }, actions: { - set ({commit, state}, {id, value}) { - commit('track', {id, value}) + set ({ commit, state }, { id, value }) { + commit('track', { id, value }) if (value) { - return axios.post('favorites/tracks/', {'track': id}).then((response) => { + return axios.post('favorites/tracks/', { track: id }).then((response) => { logger.default.info('Successfully added track to favorites') }, (response) => { logger.default.info('Error while adding track to favorites') - commit('track', {id, value: !value}) + commit('track', { id, value: !value }) }) } else { - return axios.post('favorites/tracks/remove/', {'track': id}).then((response) => { + return axios.post('favorites/tracks/remove/', { track: id }).then((response) => { logger.default.info('Successfully removed track from favorites') }, (response) => { logger.default.info('Error while removing track from favorites') - commit('track', {id, value: !value}) + commit('track', { id, value: !value }) }) } }, - toggle ({getters, dispatch}, id) { - dispatch('set', {id, value: !getters['isFavorite'](id)}) + toggle ({ getters, dispatch }, id) { + dispatch('set', { id, value: !getters.isFavorite(id) }) }, - fetch ({dispatch, state, commit, rootState}, url) { + fetch ({ dispatch, state, commit, rootState }, url) { // will fetch favorites by batches from API to have them locally - let params = { + const params = { user: rootState.auth.profile.id, page_size: 50, ordering: '-creation_date' } - let promise = axios.get('favorites/tracks/all/', {params: params}) + const promise = axios.get('favorites/tracks/all/', { params: params }) return promise.then((response) => { logger.default.info('Fetched a batch of ' + response.data.results.length + ' favorites') response.data.results.forEach(result => { - commit('track', {id: result.track, value: true}) + commit('track', { id: result.track, value: true }) }) }) } diff --git a/front/src/store/index.js b/front/src/store/index.js index 33323749f6a78d10def462cbb0eb47799ded3b71..a7954420346599c335856b82980522dc6239730c 100644 --- a/front/src/store/index.js +++ b/front/src/store/index.js @@ -75,18 +75,18 @@ export default new Vuex.Store({ tracks: state.queue.tracks.map(track => { // we keep only valuable fields to make the cache lighter and avoid // cyclic value serialization errors - let artist = { + const artist = { id: track.artist.id, mbid: track.artist.mbid, name: track.artist.name } - let data = { + const data = { id: track.id, title: track.title, mbid: track.mbid, uploads: track.uploads, listen_url: track.listen_url, - artist: artist, + artist: artist } if (track.album) { data.album = { diff --git a/front/src/store/instance.js b/front/src/store/instance.js index 1f251bca9d00975654e8c5cfe887a7a7179cfa5e..c7e610a6f4e3d88430609fccb50cdc129d90ecb1 100644 --- a/front/src/store/instance.js +++ b/front/src/store/instance.js @@ -52,15 +52,15 @@ export default { }, moderation: { signup_approval_enabled: { - value: false, + value: false }, - signup_form_customization: {value: null} + signup_form_customization: { value: null } }, subsonic: { enabled: { value: true } - }, + } } }, mutations: { @@ -87,12 +87,12 @@ export default { value = value + '/' } state.instanceUrl = value - notifyServiceWorker(state.registration, {command: 'serverChosen', serverUrl: state.instanceUrl}) + notifyServiceWorker(state.registration, { command: 'serverChosen', serverUrl: state.instanceUrl }) // append the URL to the list (and remove existing one if needed) if (value) { - let index = state.knownInstances.indexOf(value); + const index = state.knownInstances.indexOf(value) if (index > -1) { - state.knownInstances.splice(index, 1); + state.knownInstances.splice(index, 1) } state.knownInstances.splice(0, 0, value) } @@ -100,7 +100,7 @@ export default { axios.defaults.baseURL = null return } - let suffix = 'api/v1/' + const suffix = 'api/v1/' axios.defaults.baseURL = state.instanceUrl + suffix } }, @@ -116,12 +116,12 @@ export default { relativeUrl = relativeUrl.slice(1) } - let instanceUrl = state.instanceUrl || getDefaultUrl() + const instanceUrl = state.instanceUrl || getDefaultUrl() return instanceUrl + relativeUrl }, domain: (state) => { - let url = state.instanceUrl - let parser = document.createElement("a") + const url = state.instanceUrl + const parser = document.createElement('a') parser.href = url return parser.hostname }, @@ -130,9 +130,9 @@ export default { } }, actions: { - setUrl ({commit, dispatch}, url) { + setUrl ({ commit, dispatch }, url) { commit('instanceUrl', url) - let modules = [ + const modules = [ 'auth', 'favorites', 'moderation', @@ -142,14 +142,14 @@ export default { 'radios' ] modules.forEach(m => { - commit(`${m}/reset`, null, {root: true}) + commit(`${m}/reset`, null, { root: true }) }) }, // Send a request to the login URL and save the returned JWT - fetchSettings ({commit}, payload) { + fetchSettings ({ commit }, payload) { return axios.get('instance/settings/').then(response => { logger.default.info('Successfully fetched instance settings') - let sections = {} + const sections = {} response.data.forEach(e => { sections[e.section] = {} }) @@ -164,7 +164,7 @@ export default { logger.default.error('Error while fetching settings', response.data) }) }, - fetchFrontSettings ({commit}) { + fetchFrontSettings ({ commit }) { return axios.get('/front/settings.json').then(response => { commit('frontSettings', response.data) }, response => { diff --git a/front/src/store/libraries.js b/front/src/store/libraries.js index 77632de8be1afbbb588db4ac3094db714dba0c4d..5e002f36c9274817d8517f6247b7b3f9713f39c0 100644 --- a/front/src/store/libraries.js +++ b/front/src/store/libraries.js @@ -6,18 +6,18 @@ export default { state: { followedLibraries: [], followsByLibrary: {}, - count: 0, + count: 0 }, mutations: { - follows: (state, {library, follow}) => { - let replacement = {...state.followsByLibrary} + follows: (state, { library, follow }) => { + const replacement = { ...state.followsByLibrary } if (follow) { if (state.followedLibraries.indexOf(library) === -1) { state.followedLibraries.push(library) replacement[library] = follow } } else { - let i = state.followedLibraries.indexOf(library) + const i = state.followedLibraries.indexOf(library) if (i > -1) { state.followedLibraries.splice(i, 1) replacement[library] = null @@ -30,7 +30,7 @@ export default { state.followedLibraries = [] state.followsByLibrary = {} state.count = 0 - }, + } }, getters: { follow: (state) => (library) => { @@ -38,34 +38,34 @@ export default { } }, actions: { - set ({commit, state}, {uuid, value}) { + set ({ commit, state }, { uuid, value }) { if (value) { - return axios.post(`federation/follows/library/`, {target: uuid}).then((response) => { + return axios.post('federation/follows/library/', { target: uuid }).then((response) => { logger.default.info('Successfully subscribed to library') - commit('follows', {library: uuid, follow: response.data}) + commit('follows', { library: uuid, follow: response.data }) }, (response) => { logger.default.info('Error while subscribing to library') - commit('follows', {library: uuid, follow: null}) + commit('follows', { library: uuid, follow: null }) }) } else { - let follow = state.followsByLibrary[uuid] + const follow = state.followsByLibrary[uuid] return axios.delete(`federation/follows/library/${follow.uuid}/`).then((response) => { logger.default.info('Successfully unsubscribed from library') - commit('follows', {library: uuid, follow: null}) + commit('follows', { library: uuid, follow: null }) }, (response) => { logger.default.info('Error while unsubscribing from library') - commit('follows', {library: uuid, follow: follow}) + commit('follows', { library: uuid, follow: follow }) }) } }, - toggle ({getters, dispatch}, uuid) { - dispatch('set', {uuid, value: !getters['follow'](uuid)}) + toggle ({ getters, dispatch }, uuid) { + dispatch('set', { uuid, value: !getters.follow(uuid) }) }, - fetchFollows ({dispatch, state, commit, rootState}, url) { - let promise = axios.get('federation/follows/library/all/') + fetchFollows ({ dispatch, state, commit, rootState }, url) { + const promise = axios.get('federation/follows/library/all/') return promise.then((response) => { response.data.results.forEach(result => { - commit('follows', {library: result.library, follow: result}) + commit('follows', { library: result.library, follow: result }) }) }) } diff --git a/front/src/store/moderation.js b/front/src/store/moderation.js index 16caf60aa4a5abca53b6fd06f4f4e7ff61f37c36..6bb499940d376b2e161940d4a94ea6c3c37da248 100644 --- a/front/src/store/moderation.js +++ b/front/src/store/moderation.js @@ -11,11 +11,11 @@ export default { lastUpdate: new Date(), filterModalTarget: { type: null, - target: null, + target: null }, reportModalTarget: { type: null, - target: null, + target: null } }, mutations: { @@ -39,7 +39,7 @@ export default { if (!value) { state.filterModalTarget = { type: null, - target: null, + target: null } } }, @@ -48,7 +48,7 @@ export default { if (!value) { state.reportModalTarget = { type: null, - target: null, + target: null } } }, @@ -61,41 +61,41 @@ export default { }, deleteContentFilter (state, uuid) { state.filters = state.filters.filter((e) => { - return e.uuid != uuid + return e.uuid !== uuid }) } }, getters: { artistFilters: (state) => () => { - let f = state.filters.filter((f) => { + const f = state.filters.filter((f) => { return f.target.type === 'artist' }) - let p = _.sortBy(f, [(e) => { return e.creation_date }]) + const p = _.sortBy(f, [(e) => { return e.creation_date }]) p.reverse() return p - }, + } }, actions: { - hide ({commit}, payload) { + hide ({ commit }, payload) { commit('filterModalTarget', payload) commit('showFilterModal', true) }, - report ({commit}, payload) { + report ({ commit }, payload) { commit('reportModalTarget', payload) commit('showReportModal', true) }, - fetchContentFilters ({dispatch, state, commit, rootState}, url) { + fetchContentFilters ({ dispatch, state, commit, rootState }, url) { let params = {} let promise if (url) { promise = axios.get(url) } else { - commit('empty') - params = { - page_size: 100, - ordering: '-creation_date' - } - promise = axios.get('moderation/content-filters/', {params: params}) + commit('empty') + params = { + page_size: 100, + ordering: '-creation_date' + } + promise = axios.get('moderation/content-filters/', { params: params }) } return promise.then((response) => { logger.default.info('Fetched a batch of ' + response.data.results.length + ' filters') @@ -107,8 +107,8 @@ export default { }) }) }, - deleteContentFilter ({commit}, uuid) { - return axios.delete(`moderation/content-filters/${ uuid }/`).then((response) => { + deleteContentFilter ({ commit }, uuid) { + return axios.delete(`moderation/content-filters/${uuid}/`).then((response) => { commit('deleteContentFilter', uuid) }) } diff --git a/front/src/store/playlists.js b/front/src/store/playlists.js index 6c91208dc93254a3b354996afa6af7008a4e4c27..1c55a71b5e8de5ac936082b37abed02a81560746 100644 --- a/front/src/store/playlists.js +++ b/front/src/store/playlists.js @@ -25,18 +25,17 @@ export default { } }, actions: { - async fetchOwn ({commit, rootState}) { - let userId = rootState.auth.profile.id + async fetchOwn ({ commit, rootState }) { + const userId = rootState.auth.profile.id if (!userId) { return } let playlists = [] let url = 'playlists/' while (url != null) { - let response = await axios.get(url, {params: {scope: "me"}}) + const response = await axios.get(url, { params: { scope: 'me' } }) playlists = [...playlists, ...response.data.results] url = response.data.next - } commit('playlists', playlists) } diff --git a/front/src/store/queue.js b/front/src/store/queue.js index e096a6f0adc2f5ef0324ca5afd9c19db00072017..86e4f644385493a1b2f4910c093aab8461928611 100644 --- a/front/src/store/queue.js +++ b/front/src/store/queue.js @@ -6,7 +6,7 @@ export default { state: { tracks: [], currentIndex: -1, - ended: true, + ended: true }, mutations: { reset (state) { @@ -20,16 +20,16 @@ export default { ended (state, value) { state.ended = value }, - splice (state, {start, size}) { + splice (state, { start, size }) { state.tracks.splice(start, size) }, tracks (state, value) { state.tracks = value }, - insert (state, {track, index}) { + insert (state, { track, index }) { state.tracks.splice(index, 0, track) }, - reorder (state, {tracks, oldIndex, newIndex}) { + reorder (state, { tracks, oldIndex, newIndex }) { // called when the user uses drag / drop to reorder // tracks in queue state.tracks = tracks @@ -60,18 +60,18 @@ export default { isEmpty: state => state.tracks.length === 0 }, actions: { - append ({commit, state, dispatch}, {track, index}) { + append ({ commit, state, dispatch }, { track, index }) { index = index || state.tracks.length if (index > state.tracks.length - 1) { // we simply push to the end - commit('insert', {track, index: state.tracks.length}) + commit('insert', { track, index: state.tracks.length }) } else { // we insert the track at given position - commit('insert', {track, index}) + commit('insert', { track, index }) } }, - appendMany ({state, commit, dispatch}, {tracks, index, callback}) { + appendMany ({ state, commit, dispatch }, { tracks, index, callback }) { logger.default.info('Appending many tracks to the queue', tracks.map(e => { return e.title })) let shouldPlay = false if (state.tracks.length === 0) { @@ -80,9 +80,9 @@ export default { } else { index = index || state.tracks.length } - let total = tracks.length + const total = tracks.length tracks.forEach((t, i) => { - let p = dispatch('append', {track: t, index: index}) + const p = dispatch('append', { track: t, index: index }) index += 1 if (callback && i + 1 === total) { p.then(callback) @@ -95,13 +95,13 @@ export default { }) }, - cleanTrack ({state, dispatch, commit}, index) { + cleanTrack ({ state, dispatch, commit }, index) { // are we removing current playin track const current = index === state.currentIndex if (current) { - dispatch('player/stop', null, {root: true}) + dispatch('player/stop', null, { root: true }) } - commit('splice', {start: index, size: 1}) + commit('splice', { start: index, size: 1 }) if (index < state.currentIndex) { commit('currentIndex', state.currentIndex - 1) } else if (index > 0 && index === state.tracks.length && current) { @@ -115,18 +115,18 @@ export default { commit('currentIndex', index) } if (state.currentIndex + 1 === state.tracks.length) { - dispatch('radios/populateQueue', null, {root: true}) + dispatch('radios/populateQueue', null, { root: true }) } }, - previous ({state, dispatch, rootState}) { + previous ({ state, dispatch, rootState }) { if (state.currentIndex > 0 && rootState.player.currentTime < 3) { dispatch('currentIndex', state.currentIndex - 1) } else { dispatch('currentIndex', state.currentIndex) } }, - next ({state, dispatch, commit, rootState}) { + next ({ state, dispatch, commit, rootState }) { if (rootState.player.looping === 2 && state.currentIndex >= state.tracks.length - 1) { logger.default.info('Going back to the beginning of the queue') return dispatch('currentIndex', 0) @@ -139,29 +139,29 @@ export default { } } }, - last ({state, dispatch}) { + last ({ state, dispatch }) { dispatch('currentIndex', state.tracks.length - 1) }, - currentIndex ({commit, state, rootState, dispatch}, index) { + currentIndex ({ commit, state, rootState, dispatch }, index) { commit('ended', false) - commit('player/currentTime', 0, {root: true}) + commit('player/currentTime', 0, { root: true }) commit('currentIndex', index) if (state.tracks.length - index <= 2 && rootState.radios.running) { - dispatch('radios/populateQueue', null, {root: true}) + dispatch('radios/populateQueue', null, { root: true }) } }, - clean ({dispatch, commit}) { - dispatch('radios/stop', null, {root: true}) - dispatch('player/stop', null, {root: true}) + clean ({ dispatch, commit }) { + dispatch('radios/stop', null, { root: true }) + dispatch('player/stop', null, { root: true }) commit('tracks', []) dispatch('currentIndex', -1) // so we replay automatically on next track append commit('ended', true) }, - async shuffle ({dispatch, commit, state}, callback) { - let shuffled = _.shuffle(state.tracks) + async shuffle ({ dispatch, commit, state }, callback) { + const shuffled = _.shuffle(state.tracks) commit('tracks', []) - let params = {tracks: shuffled} + const params = { tracks: shuffled } if (callback) { params.callback = callback } diff --git a/front/src/store/radios.js b/front/src/store/radios.js index 475b2457f21bd5494e46df6ec6beb701eb21c644..06f5ee2dabeac73d8a4c1541134f7c41e0871f66 100644 --- a/front/src/store/radios.js +++ b/front/src/store/radios.js @@ -1,7 +1,7 @@ import axios from 'axios' import logger from '@/logging' -import {getClientOnlyRadio} from '@/radios' +import { getClientOnlyRadio } from '@/radios' export default { namespaced: true, @@ -14,7 +14,7 @@ export default { return { 'actor-content': { name: 'Your content', - description: "Picks from your own libraries" + description: 'Picks from your own libraries' }, random: { name: 'Random', @@ -30,7 +30,7 @@ export default { }, 'recently-added': { name: 'Recently Added', - description: "Newest content on the network. Get some fresh air." + description: 'Newest content on the network. Get some fresh air.' } } } @@ -48,53 +48,53 @@ export default { } }, actions: { - start ({commit, dispatch}, {type, objectId, customRadioId, clientOnly}) { - var params = { + start ({ commit, dispatch }, { type, objectId, customRadioId, clientOnly }) { + const params = { radio_type: type, related_object_id: objectId, - custom_radio: customRadioId, + custom_radio: customRadioId } if (clientOnly) { - commit('current', {type, objectId, customRadioId, clientOnly}) + commit('current', { type, objectId, customRadioId, clientOnly }) commit('running', true) dispatch('populateQueue', true) return } return axios.post('radios/sessions/', params).then((response) => { logger.default.info('Successfully started radio ', type) - commit('current', {type, objectId, session: response.data.id, customRadioId}) + commit('current', { type, objectId, session: response.data.id, customRadioId }) commit('running', true) dispatch('populateQueue', true) }, (response) => { logger.default.error('Error while starting radio', type) }) }, - stop ({commit, state}) { + stop ({ commit, state }) { if (state.current && state.current.clientOnly) { getClientOnlyRadio(state.current).stop() } commit('current', null) commit('running', false) }, - populateQueue ({rootState, state, dispatch}, playNow) { + populateQueue ({ rootState, state, dispatch }, playNow) { if (!state.running) { return } if (rootState.player.errorCount >= rootState.player.maxConsecutiveErrors - 1) { return } - var params = { + const params = { session: state.current.session } if (state.current.clientOnly) { - return getClientOnlyRadio(state.current).populateQueue({current: state.current, dispatch, state, rootState, playNow}) + return getClientOnlyRadio(state.current).populateQueue({ current: state.current, dispatch, state, rootState, playNow }) } return axios.post('radios/tracks/', params).then((response) => { logger.default.info('Adding track to queue from radio') - let append = dispatch('queue/append', {track: response.data.track}, {root: true}) + const append = dispatch('queue/append', { track: response.data.track }, { root: true }) if (playNow) { append.then(() => { - dispatch('queue/last', null, {root: true}) + dispatch('queue/last', null, { root: true }) }) } }, (response) => { diff --git a/front/src/store/ui.js b/front/src/store/ui.js index 2646df3ee753b951a32eef2548da3fd6c89d08fb..5738f8ec7c54990abf420f6e78c57cb9d1fa8dfe 100644 --- a/front/src/store/ui.js +++ b/front/src/store/ui.js @@ -11,18 +11,18 @@ export default { lastDate: new Date(), maxMessages: 100, messageDisplayDuration: 5 * 1000, - supportedExtensions: ["flac", "ogg", "mp3", "opus", "aac", "m4a", "aiff", "aif"], + supportedExtensions: ['flac', 'ogg', 'mp3', 'opus', 'aac', 'm4a', 'aiff', 'aif'], messages: [], theme: 'light', window: { height: 0, - width: 0, + width: 0 }, notifications: { inbox: 0, pendingReviewEdits: 0, pendingReviewReports: 0, - pendingReviewRequests: 0, + pendingReviewRequests: 0 }, websocketEventsHandlers: { 'inbox.item_added': {}, @@ -31,95 +31,95 @@ export default { 'mutation.updated': {}, 'report.created': {}, 'user_request.created': {}, - 'Listen': {}, + Listen: {} }, pageTitle: null, routePreferences: { - "library.albums.browse": { + 'library.albums.browse': { paginateBy: 25, - orderingDirection: "-", - ordering: "creation_date", + orderingDirection: '-', + ordering: 'creation_date' }, - "library.artists.browse": { + 'library.artists.browse': { paginateBy: 30, - orderingDirection: "-", - ordering: "creation_date", + orderingDirection: '-', + ordering: 'creation_date' }, - "library.podcasts.browse": { + 'library.podcasts.browse': { paginateBy: 30, - orderingDirection: "-", - ordering: "creation_date", + orderingDirection: '-', + ordering: 'creation_date' }, - "library.radios.browse": { + 'library.radios.browse': { paginateBy: 12, - orderingDirection: "-", - ordering: "creation_date", + orderingDirection: '-', + ordering: 'creation_date' }, - "library.playlists.browse": { + 'library.playlists.browse': { paginateBy: 25, - orderingDirection: "-", - ordering: "creation_date", + orderingDirection: '-', + ordering: 'creation_date' }, - "library.albums.me": { + 'library.albums.me': { paginateBy: 25, - orderingDirection: "-", - ordering: "creation_date", + orderingDirection: '-', + ordering: 'creation_date' }, - "library.artists.me": { + 'library.artists.me': { paginateBy: 30, - orderingDirection: "-", - ordering: "creation_date", + orderingDirection: '-', + ordering: 'creation_date' }, - "library.radios.me": { + 'library.radios.me': { paginateBy: 12, - orderingDirection: "-", - ordering: "creation_date", + orderingDirection: '-', + ordering: 'creation_date' }, - "library.playlists.me": { + 'library.playlists.me': { paginateBy: 25, - orderingDirection: "-", - ordering: "creation_date", + orderingDirection: '-', + ordering: 'creation_date' }, - "content.libraries.files": { + 'content.libraries.files': { paginateBy: 50, - orderingDirection: "-", - ordering: "creation_date", + orderingDirection: '-', + ordering: 'creation_date' }, - "library.detail.upload": { + 'library.detail.upload': { paginateBy: 50, - orderingDirection: "-", - ordering: "creation_date", + orderingDirection: '-', + ordering: 'creation_date' }, - "library.detail.edit": { + 'library.detail.edit': { paginateBy: 50, - orderingDirection: "-", - ordering: "creation_date", + orderingDirection: '-', + ordering: 'creation_date' }, - "library.detail": { + 'library.detail': { paginateBy: 50, - orderingDirection: "-", - ordering: "creation_date", + orderingDirection: '-', + ordering: 'creation_date' }, - "favorites": { + favorites: { paginateBy: 50, - orderingDirection: "-", - ordering: "creation_date", + orderingDirection: '-', + ordering: 'creation_date' }, - "manage.moderation.requests.list": { + 'manage.moderation.requests.list': { paginateBy: 25, - orderingDirection: "-", - ordering: "creation_date", + orderingDirection: '-', + ordering: 'creation_date' }, - "manage.moderation.reports.list": { + 'manage.moderation.reports.list': { paginateBy: 25, - orderingDirection: "-", - ordering: "creation_date", - }, + orderingDirection: '-', + ordering: 'creation_date' + } }, serviceWorker: { refreshing: false, registration: null, - updateAvailable: false, + updateAvailable: false } }, getters: { @@ -130,7 +130,7 @@ export default { if (!rootState.instance.settings.instance.support_message.value) { return false } - let displayDate = rootState.auth.profile.instance_support_message_display_date + const displayDate = rootState.auth.profile.instance_support_message_display_date if (!displayDate) { return false } @@ -143,7 +143,7 @@ export default { if (!rootState.instance.settings.instance.funkwhale_support_message_enabled.value) { return false } - let displayDate = rootState.auth.profile.funkwhale_support_message_display_date + const displayDate = rootState.auth.profile.funkwhale_support_message_display_date if (!displayDate) { return false } @@ -163,21 +163,20 @@ export default { windowSize: (state, getters) => { // IMPORTANT: if you modify these breakpoints, also modify the values in // style/vendor/_media.scss - let width = state.window.width - let breakpoints = [ - {name: 'widedesktop', width: 1200}, - {name: 'desktop', width: 1024}, - {name: 'tablet', width: 768}, - {name: 'phone', width: 320}, + const width = state.window.width + const breakpoints = [ + { name: 'widedesktop', width: 1200 }, + { name: 'desktop', width: 1024 }, + { name: 'tablet', width: 768 }, + { name: 'phone', width: 320 } ] for (let index = 0; index < breakpoints.length; index++) { - const element = breakpoints[index]; + const element = breakpoints[index] if (width >= element.width) { return element.name } } return 'phone' - }, layoutVersion: (state, getters) => { if (['tablet', 'phone'].indexOf(getters.windowSize) > -1) { @@ -188,10 +187,10 @@ export default { } }, mutations: { - addWebsocketEventHandler: (state, {eventName, id, handler}) => { + addWebsocketEventHandler: (state, { eventName, id, handler }) => { state.websocketEventsHandlers[eventName][id] = handler }, - removeWebsocketEventHandler: (state, {eventName, id}) => { + removeWebsocketEventHandler: (state, { eventName, id }) => { delete state.websocketEventsHandlers[eventName][id] }, currentLanguage: (state, value) => { @@ -213,14 +212,14 @@ export default { state.theme = value }, addMessage (state, message) { - let finalMessage = { + const finalMessage = { displayTime: state.messageDisplayDuration, key: String(new Date()), - ...message, + ...message } - let key = finalMessage.key + const key = finalMessage.key state.messages = state.messages.filter((m) => { - return m.key != key + return m.key !== key }) state.messages.push(finalMessage) if (state.messages.length > state.maxMessages) { @@ -229,15 +228,15 @@ export default { }, removeMessage (state, key) { state.messages = state.messages.filter((m) => { - return m.key != key + return m.key !== key }) }, - notifications (state, {type, count}) { + notifications (state, { type, count }) { state.notifications[type] = count }, - incrementNotifications (state, {type, count, value}) { - if (value != undefined) { - state.notifications[type] = Math.max(0, value) + incrementNotifications (state, { type, count, value }) { + if (value !== undefined) { + state.notifications[type] = Math.max(0, value) } else { state.notifications[type] = Math.max(0, state.notifications[type] + count) } @@ -245,77 +244,77 @@ export default { pageTitle: (state, value) => { state.pageTitle = value }, - paginateBy: (state, {route, value}) => { + paginateBy: (state, { route, value }) => { state.routePreferences[route].paginateBy = value }, - ordering: (state, {route, value}) => { + ordering: (state, { route, value }) => { state.routePreferences[route].ordering = value }, - orderingDirection: (state, {route, value}) => { + orderingDirection: (state, { route, value }) => { state.routePreferences[route].orderingDirection = value }, serviceWorker: (state, value) => { - state.serviceWorker = {...state.serviceWorker, ...value} + state.serviceWorker = { ...state.serviceWorker, ...value } }, window: (state, value) => { state.window = value } }, actions: { - fetchUnreadNotifications ({commit}, payload) { - axios.get('federation/inbox/', {params: {is_read: false, page_size: 1}}).then((response) => { - commit('notifications', {type: 'inbox', count: response.data.count}) + fetchUnreadNotifications ({ commit }, payload) { + axios.get('federation/inbox/', { params: { is_read: false, page_size: 1 } }).then((response) => { + commit('notifications', { type: 'inbox', count: response.data.count }) }) }, - fetchPendingReviewEdits ({commit, rootState}, payload) { - axios.get('mutations/', {params: {is_approved: 'null', page_size: 1}}).then((response) => { - commit('notifications', {type: 'pendingReviewEdits', count: response.data.count}) + fetchPendingReviewEdits ({ commit, rootState }, payload) { + axios.get('mutations/', { params: { is_approved: 'null', page_size: 1 } }).then((response) => { + commit('notifications', { type: 'pendingReviewEdits', count: response.data.count }) }) }, - fetchPendingReviewReports ({commit, rootState}, payload) { - axios.get('manage/moderation/reports/', {params: {is_handled: 'false', page_size: 1}}).then((response) => { - commit('notifications', {type: 'pendingReviewReports', count: response.data.count}) + fetchPendingReviewReports ({ commit, rootState }, payload) { + axios.get('manage/moderation/reports/', { params: { is_handled: 'false', page_size: 1 } }).then((response) => { + commit('notifications', { type: 'pendingReviewReports', count: response.data.count }) }) }, - fetchPendingReviewRequests ({commit, rootState}, payload) { - axios.get('manage/moderation/requests/', {params: {status: 'pending', page_size: 1}}).then((response) => { - commit('notifications', {type: 'pendingReviewRequests', count: response.data.count}) + fetchPendingReviewRequests ({ commit, rootState }, payload) { + axios.get('manage/moderation/requests/', { params: { status: 'pending', page_size: 1 } }).then((response) => { + commit('notifications', { type: 'pendingReviewRequests', count: response.data.count }) }) }, - async currentLanguage ({state, commit, rootState}, value) { - commit("currentLanguage", value) + async currentLanguage ({ state, commit, rootState }, value) { + commit('currentLanguage', value) if (rootState.auth.authenticated) { - await axios.post("users/settings", {"language": value}) + await axios.post('users/settings', { language: value }) } }, - async theme ({state, commit, rootState}, value) { - commit("theme", value) + async theme ({ state, commit, rootState }, value) { + commit('theme', value) if (rootState.auth.authenticated) { - await axios.post("users/settings", {"theme": value}) + await axios.post('users/settings', { theme: value }) } }, - async initSettings ({commit}, settings) { + async initSettings ({ commit }, settings) { settings = settings || {} if (settings.language) { - commit("currentLanguage", settings.language) + commit('currentLanguage', settings.language) } if (settings.theme) { - commit("theme", settings.theme) + commit('theme', settings.theme) } }, - websocketEvent ({state}, event) { - let handlers = state.websocketEventsHandlers[event.type] + websocketEvent ({ state }, event) { + const handlers = state.websocketEventsHandlers[event.type] console.log('Dispatching websocket event', event, handlers) if (!handlers) { return } - let names = Object.keys(handlers) + const names = Object.keys(handlers) names.forEach((k) => { - let handler = handlers[k] + const handler = handlers[k] handler(event) }) } diff --git a/front/src/utils.js b/front/src/utils.js index 596ec07d53bdd33a11804e8e2345a453cf5ce284..11180497bce0671592b1508d68637d4eb2e57a18 100644 --- a/front/src/utils.js +++ b/front/src/utils.js @@ -1,17 +1,17 @@ import lodash from '@/lodash' -export function setUpdate(obj, statuses, value) { - let updatedKeys = lodash.keys(obj) +export function setUpdate (obj, statuses, value) { + const updatedKeys = lodash.keys(obj) updatedKeys.forEach((k) => { statuses[k] = value }) } -export function parseAPIErrors(responseData, parentField) { +export function parseAPIErrors (responseData, parentField) { let errors = [] - for (var field in responseData) { - if (responseData.hasOwnProperty(field)) { - let value = responseData[field] + for (const field in responseData) { + if (Object.prototype.hasOwnProperty.call(responseData, field)) { + const value = responseData[field] let fieldName = lodash.startCase(field.replace('_', ' ')) if (parentField) { fieldName = `${parentField} - ${fieldName}` @@ -26,7 +26,7 @@ export function parseAPIErrors(responseData, parentField) { }) } else if (typeof value === 'object') { // nested errors - let nestedErrors = parseAPIErrors(value, fieldName) + const nestedErrors = parseAPIErrors(value, fieldName) errors = [...errors, ...nestedErrors] } } @@ -34,13 +34,13 @@ export function parseAPIErrors(responseData, parentField) { return errors } -export function getCookie(name) { +export function getCookie (name) { return document.cookie - .split('; ') - .find(row => row.startsWith(name)) - .split('=')[1]; + .split('; ') + .find(row => row.startsWith(name)) + .split('=')[1] } -export function setCsrf(xhr) { +export function setCsrf (xhr) { if (getCookie('csrftoken')) { xhr.setRequestHeader('X-CSRFToken', getCookie('csrftoken')) } @@ -48,12 +48,12 @@ export function setCsrf(xhr) { export function checkRedirectToLogin (store, router) { if (!store.state.auth.authenticated) { - router.push({name: 'login', query: {next: router.currentRoute.fullPath}}) + router.push({ name: 'login', query: { next: router.currentRoute.fullPath } }) } } export function getDomain (url) { - let parser = document.createElement("a") + const parser = document.createElement('a') parser.href = url return parser.hostname -} \ No newline at end of file +} diff --git a/front/src/utils/color.js b/front/src/utils/color.js index 8066abd3c2d4a2c86fd5d51608626a96849556a1..b4ad6d8d273b63736fa20a697b3ed6f900a5f78c 100644 --- a/front/src/utils/color.js +++ b/front/src/utils/color.js @@ -1,12 +1,12 @@ export function hashCode (str) { // java String#hashCode - var hash = 0 - for (var i = 0; i < str.length; i++) { + let hash = 0 + for (let i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash) } return hash } export function intToRGB (i) { - var c = (i & 0x00FFFFFF).toString(16).toUpperCase() + const c = (i & 0x00FFFFFF).toString(16).toUpperCase() return '00000'.substring(0, 6 - c.length) + c } diff --git a/front/src/utils/time.js b/front/src/utils/time.js index 6c5770c12fdb7f331c11e3939e9a0f8ef52dff50..7a5f66ccf86a5c395949c5f18245cd0573cd0d94 100644 --- a/front/src/utils/time.js +++ b/front/src/utils/time.js @@ -9,7 +9,7 @@ function pad (val) { export default { parse: function (sec) { let min = 0 - let hours = Math.floor(sec/3600) + const hours = Math.floor(sec / 3600) if (hours >= 1) { sec = sec % 3600 } diff --git a/front/src/utils/url.js b/front/src/utils/url.js index 61a430988a4093852812d5aada507cdd14acf38b..2055ec675d90108c57faa741f7501b2b18163f1d 100644 --- a/front/src/utils/url.js +++ b/front/src/utils/url.js @@ -1,7 +1,7 @@ export default { updateQueryString (uri, key, value) { - var re = new RegExp('([?&])' + key + '=.*?(&|$)', 'i') - var separator = uri.indexOf('?') !== -1 ? '&' : '?' + const re = new RegExp('([?&])' + key + '=.*?(&|$)', 'i') + const separator = uri.indexOf('?') !== -1 ? '&' : '?' if (uri.match(re)) { return uri.replace(re, '$1' + key + '=' + value + '$2') } else { diff --git a/front/src/views/Notifications.vue b/front/src/views/Notifications.vue index eee0a5a02257735ba1c4c9895161bc14785dcb19..b97bedc71eb5f88ab382587fa89c6b810974f618 100644 --- a/front/src/views/Notifications.vue +++ b/front/src/views/Notifications.vue @@ -1,65 +1,151 @@ <template> - <main class="main pusher page-notifications" v-title="labels.title"> + <main + v-title="labels.title" + class="main pusher page-notifications" + > <section class="ui vertical aligned stripe segment"> <div class="ui container"> - <div class="ui container" v-if="additionalNotifications"> - <h1 class="ui header"><translate translate-context="Content/Notifications/Title">Your messages</translate></h1> + <div + v-if="additionalNotifications" + class="ui container" + > + <h1 class="ui header"> + <translate translate-context="Content/Notifications/Title"> + Your messages + </translate> + </h1> <div class="ui two column stackable grid"> - <div class="column" v-if="showInstanceSupportMessage"> + <div + v-if="showInstanceSupportMessage" + class="column" + > <div class="ui attached info message"> <h4 class="header"> - <translate translate-context="Content/Notifications/Header">Support this Funkwhale pod</translate> + <translate translate-context="Content/Notifications/Header"> + Support this Funkwhale pod + </translate> </h4> - <div v-html="markdown.makeHtml($store.state.instance.settings.instance.support_message.value)"></div> + <div v-html="markdown.makeHtml($store.state.instance.settings.instance.support_message.value)" /> </div> <div class="ui bottom attached segment"> - <form @submit.prevent="setDisplayDate('instance_support_message_display_date', instanceSupportMessageDelay)" class="ui inline form"> + <form + class="ui inline form" + @submit.prevent="setDisplayDate('instance_support_message_display_date', instanceSupportMessageDelay)" + > <div class="inline field"> <label for="instance-reminder-delay"> <translate translate-context="Content/Notifications/Label">Remind me in:</translate> </label> - <select id="instance-reminder-delay" v-model="instanceSupportMessageDelay"> - <option :value="30"><translate translate-context="*/*/*">30 days</translate></option> - <option :value="60"><translate translate-context="*/*/*">60 days</translate></option> - <option :value="90"><translate translate-context="*/*/*">90 days</translate></option> - <option :value="null"><translate translate-context="*/*/*">Never</translate></option> + <select + id="instance-reminder-delay" + v-model="instanceSupportMessageDelay" + > + <option :value="30"> + <translate translate-context="*/*/*"> + 30 days + </translate> + </option> + <option :value="60"> + <translate translate-context="*/*/*"> + 60 days + </translate> + </option> + <option :value="90"> + <translate translate-context="*/*/*"> + 90 days + </translate> + </option> + <option :value="null"> + <translate translate-context="*/*/*"> + Never + </translate> + </option> </select> - <button type="submit" class="ui right floated basic button"> - <translate translate-context="Content/Notifications/Button.Label">Got it!</translate> + <button + type="submit" + class="ui right floated basic button" + > + <translate translate-context="Content/Notifications/Button.Label"> + Got it! + </translate> </button> </div> </form> </div> </div> - <div class="column" v-if="showFunkwhaleSupportMessage"> + <div + v-if="showFunkwhaleSupportMessage" + class="column" + > <div class="ui info attached message"> <h4 class="header"> - <translate translate-context="Content/Notifications/Header">Do you like Funkwhale?</translate> + <translate translate-context="Content/Notifications/Header"> + Do you like Funkwhale? + </translate> </h4> <p> - <translate translate-context="Content/Notifications/Paragraph">We noticed you've been here for a while. If Funkwhale is useful to you, we could use your help to make it even better!</translate> + <translate translate-context="Content/Notifications/Paragraph"> + We noticed you've been here for a while. If Funkwhale is useful to you, we could use your help to make it even better! + </translate> </p> - <a href="https://funkwhale.audio/support-us" target="_blank" rel="noopener" class="ui primary inverted button"> + <a + href="https://funkwhale.audio/support-us" + target="_blank" + rel="noopener" + class="ui primary inverted button" + > <translate translate-context="Content/Notifications/Button.Label/Verb">Donate</translate> </a> - <a href="https://contribute.funkwhale.audio" target="_blank" rel="noopener" class="ui secondary inverted button"> + <a + href="https://contribute.funkwhale.audio" + target="_blank" + rel="noopener" + class="ui secondary inverted button" + > <translate translate-context="Content/Notifications/Button.Label/Verb">Discover other ways to help</translate> </a> </div> <div class="ui bottom attached segment"> - <form @submit.prevent="setDisplayDate('funkwhale_support_message_display_date', funkwhaleSupportMessageDelay)" class="ui inline form"> + <form + class="ui inline form" + @submit.prevent="setDisplayDate('funkwhale_support_message_display_date', funkwhaleSupportMessageDelay)" + > <div class="inline field"> <label for="funkwhale-reminder-delay"> <translate translate-context="Content/Notifications/Label">Remind me in:</translate> </label> - <select id="funkwhale-reminder-delay" v-model="funkwhaleSupportMessageDelay"> - <option :value="30"><translate translate-context="*/*/*">30 days</translate></option> - <option :value="60"><translate translate-context="*/*/*">60 days</translate></option> - <option :value="90"><translate translate-context="*/*/*">90 days</translate></option> - <option :value="null"><translate translate-context="*/*/*">Never</translate></option> + <select + id="funkwhale-reminder-delay" + v-model="funkwhaleSupportMessageDelay" + > + <option :value="30"> + <translate translate-context="*/*/*"> + 30 days + </translate> + </option> + <option :value="60"> + <translate translate-context="*/*/*"> + 60 days + </translate> + </option> + <option :value="90"> + <translate translate-context="*/*/*"> + 90 days + </translate> + </option> + <option :value="null"> + <translate translate-context="*/*/*"> + Never + </translate> + </option> </select> - <button type="submit" class="ui right floated basic button"> - <translate translate-context="Content/Notifications/Button.Label">Got it!</translate> + <button + type="submit" + class="ui right floated basic button" + > + <translate translate-context="Content/Notifications/Button.Label"> + Got it! + </translate> </button> </div> </form> @@ -67,31 +153,58 @@ </div> </div> </div> - <h1 class="ui header"><translate translate-context="Content/Notifications/Title">Your notifications</translate></h1> + <h1 class="ui header"> + <translate translate-context="Content/Notifications/Title"> + Your notifications + </translate> + </h1> <div class="ui toggle checkbox"> - <input id="show-read-notifications" v-model="filters.is_read" type="checkbox"> + <input + id="show-read-notifications" + v-model="filters.is_read" + type="checkbox" + > <label for="show-read-notifications"><translate translate-context="Content/Notifications/Form.Label/Verb">Show read notifications</translate></label> </div> <button v-if="filters.is_read === false && notifications.count > 0" + class="ui basic labeled icon right floated button" @click.prevent="markAllAsRead" - class="ui basic labeled icon right floated button"> + > <i class="ui check icon" /> - <translate translate-context="Content/Notifications/Button.Label/Verb">Mark all as read</translate> + <translate translate-context="Content/Notifications/Button.Label/Verb"> + Mark all as read + </translate> </button> <div class="ui hidden divider" /> - <div v-if="isLoading" :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']"> - <div class="ui text loader"><translate translate-context="Content/Notifications/Paragraph">Loading notifications…</translate></div> + <div + v-if="isLoading" + :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']" + > + <div class="ui text loader"> + <translate translate-context="Content/Notifications/Paragraph"> + Loading notifications… + </translate> + </div> </div> - <table v-else-if="notifications.count > 0" class="ui table"> + <table + v-else-if="notifications.count > 0" + class="ui table" + > <tbody> - <notification-row :item="item" v-for="item in notifications.results" :key="item.id" /> + <notification-row + v-for="item in notifications.results" + :key="item.id" + :item="item" + /> </tbody> </table> <p v-else-if="additionalNotifications === 0"> - <translate translate-context="Content/Notifications/Paragraph">No notification to show.</translate> + <translate translate-context="Content/Notifications/Paragraph"> + No notification to show. + </translate> </p> </div> </section> @@ -99,20 +212,22 @@ </template> <script> -import { mapState, mapGetters } from "vuex" -import axios from "axios" -import logger from "@/logging" +import { mapState, mapGetters } from 'vuex' +import axios from 'axios' import showdown from 'showdown' import moment from 'moment' -import NotificationRow from "@/components/notifications/NotificationRow" +import NotificationRow from '@/components/notifications/NotificationRow' export default { - data() { + components: { + NotificationRow + }, + data () { return { isLoading: false, markdown: new showdown.Converter(), - notifications: {count: 0, results: []}, + notifications: { count: 0, results: [] }, instanceSupportMessageDelay: 60, funkwhaleSupportMessageDelay: 60, filters: { @@ -120,23 +235,6 @@ export default { } } }, - components: { - NotificationRow - }, - created() { - this.fetch(this.filters) - this.$store.commit("ui/addWebsocketEventHandler", { - eventName: "inbox.item_added", - id: "notificationPage", - handler: this.handleNewNotification - }) - }, - destroyed() { - this.$store.commit("ui/removeWebsocketEventHandler", { - eventName: "inbox.item_added", - id: "notificationPage" - }) - }, computed: { ...mapState({ events: state => state.instance.events @@ -144,64 +242,78 @@ export default { ...mapGetters({ additionalNotifications: 'ui/additionalNotifications', showInstanceSupportMessage: 'ui/showInstanceSupportMessage', - showFunkwhaleSupportMessage: 'ui/showFunkwhaleSupportMessage', + showFunkwhaleSupportMessage: 'ui/showFunkwhaleSupportMessage' }), - labels() { + labels () { return { - title: this.$pgettext('*/Notifications/*', "Notifications") + title: this.$pgettext('*/Notifications/*', 'Notifications') } } }, + watch: { + 'filters.is_read' () { + this.fetch(this.filters) + } + }, + created () { + this.fetch(this.filters) + this.$store.commit('ui/addWebsocketEventHandler', { + eventName: 'inbox.item_added', + id: 'notificationPage', + handler: this.handleNewNotification + }) + }, + destroyed () { + this.$store.commit('ui/removeWebsocketEventHandler', { + eventName: 'inbox.item_added', + id: 'notificationPage' + }) + }, methods: { handleNewNotification (event) { this.notifications.count += 1 this.notifications.results.unshift(event.item) }, setDisplayDate (field, days) { - let payload = {} + const payload = {} let newDisplayDate if (days) { - newDisplayDate = moment().add({days}) + newDisplayDate = moment().add({ days }) } else { newDisplayDate = null } payload[field] = newDisplayDate - let self = this + const self = this axios.patch(`users/${this.$store.state.auth.username}/`, payload).then((response) => { self.$store.commit('auth/profilePartialUpdate', response.data) }) }, - fetch(params) { + fetch (params) { this.isLoading = true - let self = this - axios.get("federation/inbox/", { params: params }).then(response => { + const self = this + axios.get('federation/inbox/', { params: params }).then(response => { self.isLoading = false self.notifications = response.data }) }, - markAllAsRead() { - let self = this - let before = this.notifications.results[0].id - let payload = { - action: "read", - objects: "all", + markAllAsRead () { + const self = this + const before = this.notifications.results[0].id + const payload = { + action: 'read', + objects: 'all', filters: { is_read: false, before } } - axios.post("federation/inbox/action/", payload).then(response => { - self.$store.commit("ui/notifications", { type: "inbox", count: 0 }) + axios.post('federation/inbox/action/', payload).then(response => { + self.$store.commit('ui/notifications', { type: 'inbox', count: 0 }) self.notifications.results.forEach(n => { n.is_read = true }) }) } - }, - watch: { - "filters.is_read"() { - this.fetch(this.filters) - } } } </script> diff --git a/front/src/views/Search.vue b/front/src/views/Search.vue index e1a1fd43fe25d87676eeef549ecfa27af229f911..5fb6820eda4eaf7cd996e892a1f87eb27104c0d0 100644 --- a/front/src/views/Search.vue +++ b/front/src/views/Search.vue @@ -1,83 +1,136 @@ <template> - <main class="main pusher" v-title="labels.title"> + <main + v-title="labels.title" + class="main pusher" + > <section class="ui vertical stripe segment"> - <div class="ui small text container" v-if="initialId"> + <div + v-if="initialId" + class="ui small text container" + > <h2>{{ labels.title }}</h2> - <remote-search-form :initial-id="initialId" :type="initialType"></remote-search-form> + <remote-search-form + :initial-id="initialId" + :type="initialType" + /> </div> - <div class="ui container" v-else> + <div + v-else + class="ui container" + > <h2> <label for="query"> <translate translate-context="Content/Search/Input.Label/Noun">Search</translate> </label> </h2> - <form class="ui form" @submit.prevent="page = 1; search()"> + <form + class="ui form" + @submit.prevent="page = 1; search()" + > <div class="ui field"> <div class="ui action input"> - <input class="ui input" id="query" name="query" type="text" v-model="query"> - <button :aria-label="labels.submitSearch" type="submit" class="ui icon button"> - <i class="search icon"></i> + <input + id="query" + v-model="query" + class="ui input" + name="query" + type="text" + > + <button + :aria-label="labels.submitSearch" + type="submit" + class="ui icon button" + > + <i class="search icon" /> </button> </div> </div> </form> <div class="ui secondary pointing menu"> <a - :class="['item', {active: type === t.id}]" - @click.prevent="type = t.id" v-for="t in types" + :key="t.id" + :class="['item', {active: type === t.id}]" href="" - :key="t.id"> + @click.prevent="type = t.id" + > {{ t.label }} <span v-if="results[t.id]" - class="ui circular mini right floated label"> + class="ui circular mini right floated label" + > {{ results[t.id].count }}</span> - </a> + </a> </div> - <div v-if="isLoading" > - <div v-if="isLoading" class="ui inverted active dimmer"> - <div class="ui loader"></div> + <div v-if="isLoading"> + <div + v-if="isLoading" + class="ui inverted active dimmer" + > + <div class="ui loader" /> </div> </div> - - <empty-state v-else-if="!currentResults || currentResults.count === 0" @refresh="search" :refresh="true"></empty-state> - - <div v-else-if="type === 'artists' || type === 'podcasts'" class="ui five app-cards cards"> - <artist-card :artist="artist" v-for="artist in currentResults.results" :key="artist.id"></artist-card> + + <empty-state + v-else-if="!currentResults || currentResults.count === 0" + :refresh="true" + @refresh="search" + /> + + <div + v-else-if="type === 'artists' || type === 'podcasts'" + class="ui five app-cards cards" + > + <artist-card + v-for="artist in currentResults.results" + :key="artist.id" + :artist="artist" + /> </div> - - <div v-else-if="type === 'albums' || type === 'series'" class="ui five app-cards cards"> + + <div + v-else-if="type === 'albums' || type === 'series'" + class="ui five app-cards cards" + > <album-card v-for="album in currentResults.results" :key="album.id" - :album="album"></album-card> + :album="album" + /> </div> - <track-table v-else-if="type === 'tracks'" :tracks="currentResults.results"></track-table> - <playlist-card-list v-else-if="type === 'playlists'" :playlists="currentResults.results"></playlist-card-list> + <track-table + v-else-if="type === 'tracks'" + :tracks="currentResults.results" + /> + <playlist-card-list + v-else-if="type === 'playlists'" + :playlists="currentResults.results" + /> <div v-else-if="type === 'radios'" - class="ui cards"> + class="ui cards" + > <radio-card - type="custom" v-for="radio in currentResults.results" :key="radio.id" - :custom-radio="radio"></radio-card> + type="custom" + :custom-radio="radio" + /> </div> <tags-list v-else-if="type === 'tags'" :truncate-size="200" :limit="paginateBy" - :tags="currentResults.results.map(t => {return t.name })"></tags-list> - + :tags="currentResults.results.map(t => {return t.name })" + /> + <pagination v-if="currentResults && currentResults.count > paginateBy" - @page-changed="page = $event" :current="page" :paginate-by="paginateBy" :total="currentResults.count" - ></pagination> - + @page-changed="page = $event" + /> </div> </section> </main> @@ -85,23 +138,17 @@ <script> import RemoteSearchForm from '@/components/RemoteSearchForm' -import ArtistCard from "@/components/audio/artist/Card" -import AlbumCard from "@/components/audio/album/Card" -import TrackTable from "@/components/audio/track/Table" +import ArtistCard from '@/components/audio/artist/Card' +import AlbumCard from '@/components/audio/album/Card' +import TrackTable from '@/components/audio/track/Table' import Pagination from '@/components/Pagination' -import PlaylistCardList from "@/components/playlists/CardList" -import RadioCard from "@/components/radios/Card" -import TagsList from "@/components/tags/List" +import PlaylistCardList from '@/components/playlists/CardList' +import RadioCard from '@/components/radios/Card' +import TagsList from '@/components/tags/List' import axios from 'axios' export default { - props: { - initialId: { type: String, required: false}, - initialType: { type: String, required: false}, - initialQuery: { type: String, required: false}, - initialPage: { type: Number, required: false}, - }, components: { RemoteSearchForm, ArtistCard, @@ -110,7 +157,13 @@ export default { Pagination, PlaylistCardList, RadioCard, - TagsList, + TagsList + }, + props: { + initialId: { type: String, required: false, default: '' }, + initialType: { type: String, required: false, default: '' }, + initialQuery: { type: String, required: false, default: '' }, + initialPage: { type: Number, required: false, default: 0 } }, data () { return { @@ -125,84 +178,81 @@ export default { radios: null, tags: null, podcasts: null, - series: null, + series: null }, isLoading: false, - paginateBy: 25, + paginateBy: 25 } }, - created () { - this.search() - }, computed: { - labels() { - let submitSearch = this.$pgettext("Content/Search/Button.Label/Verb", "Submit Search Query") - let title = this.$pgettext("Content/Search/Input.Label/Noun", "Search") + labels () { + const submitSearch = this.$pgettext('Content/Search/Button.Label/Verb', 'Submit Search Query') + let title = this.$pgettext('Content/Search/Input.Label/Noun', 'Search') if (this.initialId) { - title = this.$pgettext('Head/Fetch/Title', "Search a remote object") - if (this.type === "rss") { - title = this.$pgettext('Head/Fetch/Title', "Subscribe to a podcast RSS feed") + title = this.$pgettext('Head/Fetch/Title', 'Search a remote object') + if (this.type === 'rss') { + title = this.$pgettext('Head/Fetch/Title', 'Subscribe to a podcast RSS feed') } - } + } return { title, submitSearch } }, - axiosParams() { - const params = new URLSearchParams(); - params.append('q', this.query); - params.append('page', this.page); - params.append('page_size', this.paginateBy); - if(this.currentType.contentCategory != undefined) {params.append('content_category', this.currentType.contentCategory)}; - if(this.currentType.includeChannels != undefined) {params.append('include_channels', this.currentType.includeChannels)}; - return params; + axiosParams () { + const params = new URLSearchParams() + params.append('q', this.query) + params.append('page', this.page) + params.append('page_size', this.paginateBy) + if (this.currentType.contentCategory !== undefined) { params.append('content_category', this.currentType.contentCategory) }; + if (this.currentType.includeChannels !== undefined) { params.append('include_channels', this.currentType.includeChannels) }; + return params }, types () { return [ { id: 'artists', - label: this.$pgettext("*/*/*/Noun", "Artists"), + label: this.$pgettext('*/*/*/Noun', 'Artists'), includeChannels: true, - contentCategory: 'music', + contentCategory: 'music' }, { id: 'albums', - label: this.$pgettext("*/*/*", "Albums"), + label: this.$pgettext('*/*/*', 'Albums'), includeChannels: true, - contentCategory: 'music', + contentCategory: 'music' }, { id: 'tracks', - label: this.$pgettext("*/*/*", "Tracks"), + label: this.$pgettext('*/*/*', 'Tracks') }, { id: 'playlists', - label: this.$pgettext("*/*/*", "Playlists"), + label: this.$pgettext('*/*/*', 'Playlists') }, { id: 'radios', - label: this.$pgettext("*/*/*", "Radios"), - endpoint: 'radios/radios', + label: this.$pgettext('*/*/*', 'Radios'), + endpoint: 'radios/radios' }, { id: 'tags', - label: this.$pgettext("*/*/*", "Tags"), + label: this.$pgettext('*/*/*', 'Tags') }, { id: 'podcasts', - label: this.$pgettext("*/*/*", "Podcasts"), + label: this.$pgettext('*/*/*', 'Podcasts'), endpoint: '/artists', contentCategory: 'podcast', - includeChannels: true, + includeChannels: true }, { id: 'series', - label: this.$pgettext("*/*/*", "Series"), + label: this.$pgettext('*/*/*', 'Series'), endpoint: '/albums', includeChannels: true, - contentCategory: 'podcast', - }, + contentCategory: 'podcast' + } ] }, currentType () { @@ -214,6 +264,25 @@ export default { return this.results[this.currentType.id] } }, + watch: { + async type () { + this.page = 1 + this.updateQueryString() + await this.search() + }, + async page () { + this.updateQueryString() + await this.search() + }, + '$route.query.q': async function (v) { + this.query = v + this.updateQueryString() + await this.search() + } + }, + created () { + this.search() + }, methods: { async search () { this.updateQueryString() @@ -224,52 +293,38 @@ export default { return } this.isLoading = true - let response = await axios.get( + const response = await axios.get( this.currentType.endpoint || this.currentType.id, - {params: this.axiosParams} + { params: this.axiosParams } ) this.results[this.currentType.id] = response.data this.isLoading = false this.types.forEach(t => { - if (t.id != this.currentType.id) { - axios.get(t.endpoint || t.id, {params: { - q: this.query, + if (t.id !== this.currentType.id) { + axios.get(t.endpoint || t.id, { + params: { + q: this.query, page_size: 1, content_category: t.contentCategory, - include_channels: t.includeChannels, - }}).then(response => { + include_channels: t.includeChannels + } + }).then(response => { this.results[t.id] = response.data }) } }) }, - updateQueryString: function() { + updateQueryString: function () { history.pushState( {}, null, this.$route.path + '?' + new URLSearchParams( { - q: this.query, - page: this.page, - type: this.type, - }).toString() + q: this.query, + page: this.page, + type: this.type + }).toString() ) - }, - }, - watch: { - async type () { - this.page = 1 - this.updateQueryString() - await this.search() - }, - async page () { - this.updateQueryString() - await this.search() - }, - "$route.query.q": async function (v) { - this.query = v - this.updateQueryString() - await this.search() } } } diff --git a/front/src/views/admin/ChannelDetail.vue b/front/src/views/admin/ChannelDetail.vue index 3b923478587af8ddae754fecf355253697efdec5..4f83947fe137f2b2c7f407d961d7f799fbad3685 100644 --- a/front/src/views/admin/ChannelDetail.vue +++ b/front/src/views/admin/ChannelDetail.vue @@ -1,22 +1,36 @@ <template> <main> - <div v-if="isLoading" class="ui vertical segment"> - <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> + <div + v-if="isLoading" + class="ui vertical segment" + > + <div :class="['ui', 'centered', 'active', 'inline', 'loader']" /> </div> <template v-if="object"> - <section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="object.artist.name"> + <section + v-title="object.artist.name" + :class="['ui', 'head', 'vertical', 'stripe', 'segment']" + > <div class="ui stackable one column grid"> <div class="ui column"> <div class="segment-content"> <h2 class="ui header"> - <img alt="" v-if="object.artist.cover && object.artist.cover.urls.medium_square_crop" v-lazy="$store.getters['instance/absoluteUrl'](object.artist.cover.urls.medium_square_crop)"> - <img alt="" v-else src="../../assets/audio/default-cover.png"> + <img + v-if="object.artist.cover && object.artist.cover.urls.medium_square_crop" + v-lazy="$store.getters['instance/absoluteUrl'](object.artist.cover.urls.medium_square_crop)" + alt="" + > + <img + v-else + alt="" + src="../../assets/audio/default-cover.png" + > <div class="content"> {{ object.artist.name | truncate(100) }} <div class="sub header"> <template v-if="object.artist.is_local"> <span class="ui tiny accent label"> - <i class="home icon"></i> + <i class="home icon" /> <translate translate-context="Content/Moderation/*/Short, Noun">Local</translate> </span> @@ -25,34 +39,59 @@ </div> </h2> <template v-if="object.artist.tags && object.artist.tags.length > 0"> - <tags-list :limit="5" detail-route="manage.library.tags.detail" :tags="object.artist.tags"></tags-list> - <div class="ui hidden divider"></div> + <tags-list + :limit="5" + detail-route="manage.library.tags.detail" + :tags="object.artist.tags" + /> + <div class="ui hidden divider" /> </template> <div class="header-buttons"> - <div class="ui icon buttons"> - <router-link class="ui labeled icon button" :to="{name: 'channels.detail', params: {id: object.uuid }}"> - <i class="info icon"></i> - <translate translate-context="Content/Moderation/Link/Verb">Open local profile</translate> + <router-link + class="ui labeled icon button" + :to="{name: 'channels.detail', params: {id: object.uuid }}" + > + <i class="info icon" /> + <translate translate-context="Content/Moderation/Link/Verb"> + Open local profile + </translate> </router-link> - <button class="ui floating dropdown icon button" v-dropdown> - <i class="dropdown icon"></i> + <button + v-dropdown + class="ui floating dropdown icon button" + > + <i class="dropdown icon" /> <div class="menu"> <a v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser" class="basic item" :href="$store.getters['instance/absoluteUrl'](`/api/admin/audio/channel/${object.id}`)" - target="_blank" rel="noopener noreferrer"> - <i class="wrench icon"></i> + target="_blank" + rel="noopener noreferrer" + > + <i class="wrench icon" /> <translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate> </a> - <fetch-button @refresh="fetchData" v-if="!object.actor.is_local" class="basic item" :url="`channels/${object.uuid}/fetches/`"> - <i class="refresh icon"></i> - <translate translate-context="Content/Moderation/Button/Verb">Refresh from remote server</translate> + <fetch-button + v-if="!object.actor.is_local" + class="basic item" + :url="`channels/${object.uuid}/fetches/`" + @refresh="fetchData" + > + <i class="refresh icon" /> + <translate translate-context="Content/Moderation/Button/Verb"> + Refresh from remote server + </translate> </fetch-button> - <a class="basic item" :href="object.actor.url || object.actor.fid" target="_blank" rel="noopener noreferrer"> - <i class="external icon"></i> + <a + class="basic item" + :href="object.actor.url || object.actor.fid" + target="_blank" + rel="noopener noreferrer" + > + <i class="external icon" /> <translate translate-context="Content/Moderation/Link/Verb">Open remote profile</translate> </a> </div> @@ -61,13 +100,28 @@ <div class="ui buttons"> <dangerous-button :class="['ui', {loading: isLoading}, 'basic danger button']" - :action="remove"> - <translate translate-context="*/*/*/Verb">Delete</translate> - <p slot="modal-header"><translate translate-context="Popup/Library/Title">Delete this channel?</translate></p> + :action="remove" + > + <translate translate-context="*/*/*/Verb"> + Delete + </translate> + <p slot="modal-header"> + <translate translate-context="Popup/Library/Title"> + Delete this channel? + </translate> + </p> <div slot="modal-content"> - <p><translate translate-context="Content/Moderation/Paragraph">The channel will be removed, as well as associated uploads, tracks, and albums. This action is irreversible.</translate></p> + <p> + <translate translate-context="Content/Moderation/Paragraph"> + The channel will be removed, as well as associated uploads, tracks, and albums. This action is irreversible. + </translate> + </p> </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> </div> </div> @@ -80,16 +134,20 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="info icon"></i> + <i class="info icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Channel data</translate> + <translate translate-context="Content/Moderation/Title"> + Channel data + </translate> </div> </h3> <table class="ui very basic table"> <tbody> <tr> <td> - <translate translate-context="*/*/*/Noun">Name</translate> + <translate translate-context="*/*/*/Noun"> + Name + </translate> </td> <td> {{ object.artist.name }} @@ -98,7 +156,9 @@ <tr> <td> <router-link :to="{name: 'manage.channels', query: {q: getQuery('category', object.artist.content_category) }}"> - <translate translate-context="*/*/*">Category</translate> + <translate translate-context="*/*/*"> + Category + </translate> </router-link> </td> <td> @@ -108,7 +168,9 @@ <tr> <td> <router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: object.attributed_to.full_username }}"> - <translate translate-context="*/*/*/Noun">Account</translate> + <translate translate-context="*/*/*/Noun"> + Account + </translate> </router-link> </td> <td> @@ -118,7 +180,9 @@ <tr v-if="!object.actor.is_local"> <td> <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.actor.domain }}"> - <translate translate-context="Content/Moderation/*/Noun">Domain</translate> + <translate translate-context="Content/Moderation/*/Noun"> + Domain + </translate> </router-link> </td> <td> @@ -127,24 +191,38 @@ </tr> <tr v-if="object.artist.description"> <td> - <translate translate-context="'*/*/*/Noun">Description</translate> + <translate translate-context="'*/*/*/Noun"> + Description + </translate> </td> - <td v-html="object.artist.description.html"></td> + <td v-html="object.artist.description.html" /> </tr> <tr v-if="object.actor.url"> <td> - <translate translate-context="'Content/*/*/Noun">URL</translate> + <translate translate-context="'Content/*/*/Noun"> + URL + </translate> </td> <td> - <a :href="object.actor.url" rel="noreferrer noopener" target="_blank">{{ object.actor.url }}</a> + <a + :href="object.actor.url" + rel="noreferrer noopener" + target="_blank" + >{{ object.actor.url }}</a> </td> </tr> <tr v-if="object.rss_url"> <td> - <translate translate-context="'*/*/*">RSS Feed</translate> + <translate translate-context="'*/*/*"> + RSS Feed + </translate> </td> <td> - <a :href="object.rss_url" rel="noreferrer noopener" target="_blank">{{ object.rss_url }}</a> + <a + :href="object.rss_url" + rel="noreferrer noopener" + target="_blank" + >{{ object.rss_url }}</a> </td> </tr> </tbody> @@ -154,32 +232,43 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="feed icon"></i> + <i class="feed icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Activity</translate> - <span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span> - + <translate translate-context="Content/Moderation/Title"> + Activity + </translate> + <span :data-tooltip="labels.statsWarning"><i class="question circle icon" /></span> </div> </h3> - <div v-if="isLoadingStats" class="ui placeholder"> - <div class="full line"></div> - <div class="short line"></div> - <div class="medium line"></div> - <div class="long line"></div> + <div + v-if="isLoadingStats" + class="ui placeholder" + > + <div class="full line" /> + <div class="short line" /> + <div class="medium line" /> + <div class="long line" /> </div> - <table v-else class="ui very basic table"> + <table + v-else + class="ui very basic table" + > <tbody> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate> + <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)"> + First seen + </translate> </td> <td> - <human-date :date="object.creation_date"></human-date> + <human-date :date="object.creation_date" /> </td> </tr> <tr> <td> - <translate translate-context="*/*/*/Noun">Listenings</translate> + <translate translate-context="*/*/*/Noun"> + Listenings + </translate> </td> <td> {{ stats.listenings }} @@ -187,7 +276,9 @@ </tr> <tr> <td> - <translate translate-context="*/*/*">Favorited tracks</translate> + <translate translate-context="*/*/*"> + Favorited tracks + </translate> </td> <td> {{ stats.track_favorites }} @@ -195,7 +286,9 @@ </tr> <tr> <td> - <translate translate-context="*/*/*">Playlists</translate> + <translate translate-context="*/*/*"> + Playlists + </translate> </td> <td> {{ stats.playlists }} @@ -204,7 +297,9 @@ <tr> <td> <router-link :to="{name: 'manage.moderation.reports.list', query: {q: getQuery('target', `channel:${object.uuid}`) }}"> - <translate translate-context="Content/Moderation/Table.Label/Noun">Linked reports</translate> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Linked reports + </translate> </router-link> </td> <td> @@ -214,7 +309,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.edits', query: {q: getQuery('target', 'artist ' + object.artist.id)}}"> - <translate translate-context="*/Admin/*/Noun">Edits</translate> + <translate translate-context="*/Admin/*/Noun"> + Edits + </translate> </router-link> </td> <td> @@ -228,25 +325,33 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="music icon"></i> + <i class="music icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Audio content</translate> - <span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span> - + <translate translate-context="Content/Moderation/Title"> + Audio content + </translate> + <span :data-tooltip="labels.statsWarning"><i class="question circle icon" /></span> </div> </h3> - <div v-if="isLoadingStats" class="ui placeholder"> - <div class="full line"></div> - <div class="short line"></div> - <div class="medium line"></div> - <div class="long line"></div> + <div + v-if="isLoadingStats" + class="ui placeholder" + > + <div class="full line" /> + <div class="short line" /> + <div class="medium line" /> + <div class="long line" /> </div> - <table v-else class="ui very basic table"> + <table + v-else + class="ui very basic table" + > <tbody> - <tr> <td> - <translate translate-context="Content/Moderation/Table.Label/Noun">Cached size</translate> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Cached size + </translate> </td> <td> {{ stats.media_downloaded_size | humanSize }} @@ -254,7 +359,9 @@ </tr> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label">Total size</translate> + <translate translate-context="Content/Moderation/Table.Label"> + Total size + </translate> </td> <td> {{ stats.media_total_size | humanSize }} @@ -263,7 +370,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('channel_id', object.uuid) }}"> - <translate translate-context="*/*/*">Uploads</translate> + <translate translate-context="*/*/*"> + Uploads + </translate> </router-link> </td> <td> @@ -273,7 +382,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.albums', query: {q: getQuery('channel_id', object.uuid) }}"> - <translate translate-context="*/*/*">Albums</translate> + <translate translate-context="*/*/*"> + Albums + </translate> </router-link> </td> <td> @@ -283,7 +394,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.tracks', query: {q: getQuery('channel_id', object.uuid) }}"> - <translate translate-context="*/*/*">Tracks</translate> + <translate translate-context="*/*/*"> + Tracks + </translate> </router-link> </td> <td> @@ -292,78 +405,75 @@ </tr> </tbody> </table> - </section> </div> </div> </div> - </template> </main> </template> <script> -import axios from "axios" -import logger from "@/logging" +import axios from 'axios' -import TagsList from "@/components/tags/List" -import FetchButton from "@/components/federation/FetchButton" +import TagsList from '@/components/tags/List' +import FetchButton from '@/components/federation/FetchButton' export default { - props: ["id"], components: { FetchButton, TagsList }, - data() { + props: { id: { type: Number, required: true } }, + data () { return { isLoading: true, isLoadingStats: false, object: null, - stats: null, + stats: null } }, - created() { + computed: { + labels () { + return { + statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object') + } + } + }, + created () { this.fetchData() this.fetchStats() }, methods: { - fetchData() { - var self = this + fetchData () { + const self = this this.isLoading = true - let url = `manage/channels/${this.id}/` + const url = `manage/channels/${this.id}/` axios.get(url).then(response => { self.object = response.data self.isLoading = false }) }, - fetchStats() { - var self = this + fetchStats () { + const self = this this.isLoadingStats = true - let url = `manage/channels/${this.id}/stats/` + const url = `manage/channels/${this.id}/stats/` axios.get(url).then(response => { self.stats = response.data self.isLoadingStats = false }) }, remove () { - var self = this + const self = this this.isLoading = true - let url = `manage/channels/${this.id}/` + const url = `manage/channels/${this.id}/` axios.delete(url).then(response => { - self.$router.push({name: 'manage.channels'}) + self.$router.push({ name: 'manage.channels' }) }) }, getQuery (field, value) { return `${field}:"${value}"` } - }, - computed: { - labels() { - return { - statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object'), - } - }, } } </script> diff --git a/front/src/views/admin/ChannelsList.vue b/front/src/views/admin/ChannelsList.vue index 74cfb56ccfd912f25d7108faed22d5049b910f8d..2a86598efd8c7092b75984de72e1f8d1d0c6d882 100644 --- a/front/src/views/admin/ChannelsList.vue +++ b/front/src/views/admin/ChannelsList.vue @@ -1,25 +1,30 @@ <template> <main v-title="labels.title"> <section class="ui vertical stripe segment"> - <h2 class="ui header">{{ labels.title }}</h2> - <div class="ui hidden divider"></div> - <channels-table :update-url="true" :default-query="defaultQuery"></channels-table> + <h2 class="ui header"> + {{ labels.title }} + </h2> + <div class="ui hidden divider" /> + <channels-table + :update-url="true" + :default-query="defaultQuery" + /> </section> </main> </template> <script> -import ChannelsTable from "@/components/manage/ChannelsTable" +import ChannelsTable from '@/components/manage/ChannelsTable' export default { components: { ChannelsTable }, props: { - defaultQuery: {type: String, required: false}, + defaultQuery: { type: String, required: false, default: '' } }, computed: { - labels() { + labels () { return { title: this.$pgettext('*/*/*', 'Channels') } diff --git a/front/src/views/admin/Settings.vue b/front/src/views/admin/Settings.vue index 30f79f829386f0948f71dbcaff141edc6b79d66c..ade695e93fb48264e277cca6ba5c56930804d27b 100644 --- a/front/src/views/admin/Settings.vue +++ b/front/src/views/admin/Settings.vue @@ -1,203 +1,216 @@ <template> - <main class="main pusher" v-title="labels.settings"> + <main + v-title="labels.settings" + class="main pusher" + > <div class="ui vertical stripe segment"> <div class="ui text container"> - <div :class="['ui', {'loading': isLoading}, 'form']"></div> - <div id="settings-grid" v-if="settingsData" class="ui grid"> + <div :class="['ui', {'loading': isLoading}, 'form']" /> + <div + v-if="settingsData" + id="settings-grid" + class="ui grid" + > <div class="twelve wide stretched column"> <settings-group + v-for="group in groups" + :key="group.title" :settings-data="settingsData" :group="group" - :key="group.title" - v-for="group in groups" /> + /> </div> <div class="four wide column"> <div class="ui sticky vertical secondary menu"> - <div class="header item"><translate translate-context="Content/Admin/Menu.Title">Sections</translate></div> - <a :class="['menu', {active: group.id === current}, 'item']" - @click.prevent="scrollTo(group.id)" + <div class="header item"> + <translate translate-context="Content/Admin/Menu.Title"> + Sections + </translate> + </div> + <a + v-for="(group, key) in groups" + :key="key" + :class="['menu', {active: group.id === current}, 'item']" :href="'#' + group.id" - v-for="group in groups">{{ group.label }}</a> + @click.prevent="scrollTo(group.id)" + >{{ group.label }}</a> </div> </div> </div> - </div> </div> </main> </template> <script> -import axios from "axios" -import $ from "jquery" +import axios from 'axios' +import $ from 'jquery' -import SettingsGroup from "@/components/admin/SettingsGroup" +import SettingsGroup from '@/components/admin/SettingsGroup' export default { components: { SettingsGroup }, - data() { + data () { return { isLoading: false, settingsData: null, current: null } }, - created() { - let self = this - this.fetchSettings().then(r => { - self.$nextTick(() => { - if (self.$store.state.route.hash) { - self.scrollTo(self.$store.state.route.hash.substr(1)) - } - $("select.dropdown").dropdown() - }) - }) - }, - methods: { - scrollTo(id) { - this.current = id - document.getElementById(id).scrollIntoView() - }, - fetchSettings() { - let self = this - self.isLoading = true - return axios.get("instance/admin/settings/").then(response => { - self.settingsData = response.data - self.isLoading = false - }) - } - }, computed: { - labels() { + labels () { return { settings: this.$pgettext('Head/Admin/Title', 'Instance settings') } }, - groups() { + groups () { // somehow, extraction fails if in the return block directly - let instanceLabel = this.$pgettext('Content/Admin/Menu','Instance information') - let signupsLabel = this.$pgettext('*/*/*/Noun', 'Sign-ups') - let securityLabel = this.$pgettext('*/*/*/Noun', 'Security') - let musicLabel = this.$pgettext('*/*/*/Noun', 'Music') - let channelsLabel = this.$pgettext('*/*/*', 'Channels') - let playlistsLabel = this.$pgettext('*/*/*', 'Playlists') - let federationLabel = this.$pgettext('*/*/*', 'Federation') - let moderationLabel = this.$pgettext('*/Moderation/*', 'Moderation') - let subsonicLabel = this.$pgettext('Content/Admin/Menu', 'Subsonic') - let statisticsLabel = this.$pgettext('Content/Home/Header', 'Statistics') - let uiLabel = this.$pgettext('Content/Admin/Menu', 'User Interface') - let errorLabel = this.$pgettext('Content/Admin/Menu', 'Error reporting') + const instanceLabel = this.$pgettext('Content/Admin/Menu', 'Instance information') + const signupsLabel = this.$pgettext('*/*/*/Noun', 'Sign-ups') + const securityLabel = this.$pgettext('*/*/*/Noun', 'Security') + const musicLabel = this.$pgettext('*/*/*/Noun', 'Music') + const channelsLabel = this.$pgettext('*/*/*', 'Channels') + const playlistsLabel = this.$pgettext('*/*/*', 'Playlists') + const federationLabel = this.$pgettext('*/*/*', 'Federation') + const moderationLabel = this.$pgettext('*/Moderation/*', 'Moderation') + const subsonicLabel = this.$pgettext('Content/Admin/Menu', 'Subsonic') + const statisticsLabel = this.$pgettext('Content/Home/Header', 'Statistics') + const uiLabel = this.$pgettext('Content/Admin/Menu', 'User Interface') return [ { label: instanceLabel, - id: "instance", + id: 'instance', settings: [ - {name: "instance__name"}, - {name: "instance__short_description"}, - {name: "instance__long_description", fieldType: 'markdown', fieldParams: {charLimit: null, permissive: true}}, - {name: "instance__contact_email"}, - {name: "instance__rules", fieldType: 'markdown', fieldParams: {charLimit: null, permissive: true}}, - {name: "instance__terms", fieldType: 'markdown', fieldParams: {charLimit: null, permissive: true}}, - {name: "instance__banner"}, - {name: "instance__support_message", fieldType: 'markdown', fieldParams: {charLimit: null, permissive: true}}, + { name: 'instance__name' }, + { name: 'instance__short_description' }, + { name: 'instance__long_description', fieldType: 'markdown', fieldParams: { charLimit: null, permissive: true } }, + { name: 'instance__contact_email' }, + { name: 'instance__rules', fieldType: 'markdown', fieldParams: { charLimit: null, permissive: true } }, + { name: 'instance__terms', fieldType: 'markdown', fieldParams: { charLimit: null, permissive: true } }, + { name: 'instance__banner' }, + { name: 'instance__support_message', fieldType: 'markdown', fieldParams: { charLimit: null, permissive: true } } ] }, { label: signupsLabel, - id: "signup", + id: 'signup', settings: [ - {name: "users__registration_enabled"}, - {name: "moderation__signup_approval_enabled"}, - {name: "moderation__signup_form_customization", fieldType: 'formBuilder'}, + { name: 'users__registration_enabled' }, + { name: 'moderation__signup_approval_enabled' }, + { name: 'moderation__signup_form_customization', fieldType: 'formBuilder' } ] }, { label: securityLabel, - id: "security", + id: 'security', settings: [ - {name: "common__api_authentication_required"}, - {name: "users__default_permissions"}, - {name: "users__upload_quota"}, + { name: 'common__api_authentication_required' }, + { name: 'users__default_permissions' }, + { name: 'users__upload_quota' } ] }, { label: musicLabel, - id: "music", + id: 'music', settings: [ - {name: "music__transcoding_enabled"}, - {name: "music__transcoding_cache_duration"}, + { name: 'music__transcoding_enabled' }, + { name: 'music__transcoding_cache_duration' } ] }, { label: channelsLabel, - id: "channels", + id: 'channels', settings: [ - {name: "audio__channels_enabled"}, - {name: "audio__max_channels"}, + { name: 'audio__channels_enabled' }, + { name: 'audio__max_channels' } ] }, { label: playlistsLabel, - id: "playlists", + id: 'playlists', settings: [ - {name: "playlists__max_tracks"}, + { name: 'playlists__max_tracks' } ] }, { label: moderationLabel, - id: "moderation", + id: 'moderation', settings: [ - {name: "moderation__allow_list_enabled"}, - {name: "moderation__allow_list_public"}, - {name: "moderation__unauthenticated_report_types"}, + { name: 'moderation__allow_list_enabled' }, + { name: 'moderation__allow_list_public' }, + { name: 'moderation__unauthenticated_report_types' } ] }, { label: federationLabel, - id: "federation", + id: 'federation', settings: [ - {name: "federation__enabled"}, - {name: "federation__public_index"}, - {name: "federation__collection_page_size"}, - {name: "federation__music_cache_duration"}, - {name: "federation__actor_fetch_delay"}, + { name: 'federation__enabled' }, + { name: 'federation__public_index' }, + { name: 'federation__collection_page_size' }, + { name: 'federation__music_cache_duration' }, + { name: 'federation__actor_fetch_delay' } ] }, { label: subsonicLabel, - id: "subsonic", + id: 'subsonic', settings: [ - {name: "subsonic__enabled"}, + { name: 'subsonic__enabled' } ] }, { label: uiLabel, - id: "ui", + id: 'ui', settings: [ - {name: "ui__custom_css"}, - {name: "instance__funkwhale_support_message_enabled"}, + { name: 'ui__custom_css' }, + { name: 'instance__funkwhale_support_message_enabled' } ] }, { label: statisticsLabel, - id: "statistics", + id: 'statistics', settings: [ - {name: "instance__nodeinfo_stats_enabled"}, - {name: "instance__nodeinfo_private"}, + { name: 'instance__nodeinfo_stats_enabled' }, + { name: 'instance__nodeinfo_private' } ] } ] } }, watch: { - settingsData() { - let self = this + settingsData () { + const self = this this.$nextTick(() => { $(self.$el) - .find(".sticky") - .sticky({ context: "#settings-grid" }) + .find('.sticky') + .sticky({ context: '#settings-grid' }) + }) + } + }, + created () { + const self = this + this.fetchSettings().then(r => { + self.$nextTick(() => { + if (self.$store.state.route.hash) { + self.scrollTo(self.$store.state.route.hash.substr(1)) + } + $('select.dropdown').dropdown() + }) + }) + }, + methods: { + scrollTo (id) { + this.current = id + document.getElementById(id).scrollIntoView() + }, + fetchSettings () { + const self = this + self.isLoading = true + return axios.get('instance/admin/settings/').then(response => { + self.settingsData = response.data + self.isLoading = false }) } } diff --git a/front/src/views/admin/library/AlbumDetail.vue b/front/src/views/admin/library/AlbumDetail.vue index 33f8808a45156d5e12465f3a7f121b3044ef8c9d..50cba8da70470033d7414ccb97cc693788bc33e9 100644 --- a/front/src/views/admin/library/AlbumDetail.vue +++ b/front/src/views/admin/library/AlbumDetail.vue @@ -1,22 +1,36 @@ <template> <main> - <div v-if="isLoading" class="ui vertical segment"> - <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> + <div + v-if="isLoading" + class="ui vertical segment" + > + <div :class="['ui', 'centered', 'active', 'inline', 'loader']" /> </div> <template v-if="object"> - <section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="object.title"> + <section + v-title="object.title" + :class="['ui', 'head', 'vertical', 'stripe', 'segment']" + > <div class="ui stackable one column grid"> <div class="ui column"> <div class="segment-content"> <h2 class="ui header"> - <img alt="" v-if="object.cover && object.cover.urls.original" v-lazy="$store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)"> - <img alt="" v-else src="../../../assets/audio/default-cover.png"> + <img + v-if="object.cover && object.cover.urls.original" + v-lazy="$store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)" + alt="" + > + <img + v-else + alt="" + src="../../../assets/audio/default-cover.png" + > <div class="content"> {{ object.title | truncate(100) }} <div class="sub header"> <template v-if="object.is_local"> <span class="ui tiny accent label"> - <i class="home icon"></i> + <i class="home icon" /> <translate translate-context="Content/Moderation/*/Short, Noun">Local</translate> </span> @@ -26,38 +40,69 @@ </h2> <template v-if="object.tags && object.tags.length > 0"> - <tags-list :limit="5" detail-route="manage.library.tags.detail" :tags="object.tags"></tags-list> - <div class="ui hidden divider"></div> + <tags-list + :limit="5" + detail-route="manage.library.tags.detail" + :tags="object.tags" + /> + <div class="ui hidden divider" /> </template> <div class="header-buttons"> - <div class="ui icon buttons"> - <router-link class="ui labeled icon button" :to="{name: 'library.albums.detail', params: {id: object.id }}"> - <i class="info icon"></i> - <translate translate-context="Content/Moderation/Link/Verb">Open local profile</translate> + <router-link + class="ui labeled icon button" + :to="{name: 'library.albums.detail', params: {id: object.id }}" + > + <i class="info icon" /> + <translate translate-context="Content/Moderation/Link/Verb"> + Open local profile + </translate> </router-link> - <button class="ui floating dropdown icon button" v-dropdown> - <i class="dropdown icon"></i> + <button + v-dropdown + class="ui floating dropdown icon button" + > + <i class="dropdown icon" /> <div class="menu"> <a v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser" class="basic item" :href="$store.getters['instance/absoluteUrl'](`/api/admin/music/album/${object.id}`)" - target="_blank" rel="noopener noreferrer"> - <i class="wrench icon"></i> + target="_blank" + rel="noopener noreferrer" + > + <i class="wrench icon" /> <translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate> </a> - <a class="basic item" v-if="object.mbid" :href="`https://musicbrainz.org/release/${object.mbid}`" target="_blank" rel="noopener noreferrer"> - <i class="external icon"></i> + <a + v-if="object.mbid" + class="basic item" + :href="`https://musicbrainz.org/release/${object.mbid}`" + target="_blank" + rel="noopener noreferrer" + > + <i class="external icon" /> <translate translate-context="Content/Moderation/Link/Verb">Open on MusicBrainz</translate> </a> - <fetch-button @refresh="fetchData" v-if="!object.is_local" class="basic item" :url="`albums/${object.id}/fetches/`"> - <i class="refresh icon"></i> - <translate translate-context="Content/Moderation/Button/Verb">Refresh from remote server</translate> + <fetch-button + v-if="!object.is_local" + class="basic item" + :url="`albums/${object.id}/fetches/`" + @refresh="fetchData" + > + <i class="refresh icon" /> + <translate translate-context="Content/Moderation/Button/Verb"> + Refresh from remote server + </translate> </fetch-button> - <a class="basic item" :href="object.url || object.fid" target="_blank" rel="noopener noreferrer"> - <i class="external icon"></i> + <a + class="basic item" + :href="object.url || object.fid" + target="_blank" + rel="noopener noreferrer" + > + <i class="external icon" /> <translate translate-context="Content/Moderation/Link/Verb">Open remote profile</translate> </a> </div> @@ -67,21 +112,39 @@ <router-link v-if="object.is_local" :to="{name: 'library.albums.edit', params: {id: object.id }}" - class="ui labeled icon button"> - <i class="edit icon"></i> - <translate translate-context="Content/*/Button.Label/Verb">Edit</translate> + class="ui labeled icon button" + > + <i class="edit icon" /> + <translate translate-context="Content/*/Button.Label/Verb"> + Edit + </translate> </router-link> </div> <div class="ui buttons"> <dangerous-button :class="['ui', {loading: isLoading}, 'basic danger button']" - :action="remove"> - <translate translate-context="*/*/*/Verb">Delete</translate> - <p slot="modal-header"><translate translate-context="Popup/Library/Title">Delete this album?</translate></p> + :action="remove" + > + <translate translate-context="*/*/*/Verb"> + Delete + </translate> + <p slot="modal-header"> + <translate translate-context="Popup/Library/Title"> + Delete this album? + </translate> + </p> <div slot="modal-content"> - <p><translate translate-context="Content/Moderation/Paragraph">The album will be removed, as well as associated uploads, tracks, favorites and listening history. This action is irreversible.</translate></p> + <p> + <translate translate-context="Content/Moderation/Paragraph"> + The album will be removed, as well as associated uploads, tracks, favorites and listening history. This action is irreversible. + </translate> + </p> </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> </div> </div> @@ -94,16 +157,20 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="info icon"></i> + <i class="info icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Album data</translate> + <translate translate-context="Content/Moderation/Title"> + Album data + </translate> </div> </h3> <table class="ui very basic table"> <tbody> <tr> <td> - <translate translate-context="*/*/*/Noun">Title</translate> + <translate translate-context="*/*/*/Noun"> + Title + </translate> </td> <td> {{ object.title }} @@ -112,7 +179,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.artists.detail', params: {id: object.artist.id }}"> - <translate translate-context="*/*/*/Noun">Artist</translate> + <translate translate-context="*/*/*/Noun"> + Artist + </translate> </router-link> </td> <td> @@ -122,7 +191,9 @@ <tr v-if="!object.is_local"> <td> <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}"> - <translate translate-context="Content/Moderation/*/Noun">Domain</translate> + <translate translate-context="Content/Moderation/*/Noun"> + Domain + </translate> </router-link> </td> <td> @@ -131,9 +202,11 @@ </tr> <tr v-if="object.description"> <td> - <translate translate-context="'*/*/*/Noun">Description</translate> + <translate translate-context="'*/*/*/Noun"> + Description + </translate> </td> - <td v-html="object.description.html"></td> + <td v-html="object.description.html" /> </tr> </tbody> </table> @@ -142,32 +215,43 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="feed icon"></i> + <i class="feed icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Activity</translate> - <span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span> - + <translate translate-context="Content/Moderation/Title"> + Activity + </translate> + <span :data-tooltip="labels.statsWarning"><i class="question circle icon" /></span> </div> </h3> - <div v-if="isLoadingStats" class="ui placeholder"> - <div class="full line"></div> - <div class="short line"></div> - <div class="medium line"></div> - <div class="long line"></div> + <div + v-if="isLoadingStats" + class="ui placeholder" + > + <div class="full line" /> + <div class="short line" /> + <div class="medium line" /> + <div class="long line" /> </div> - <table v-else class="ui very basic table"> + <table + v-else + class="ui very basic table" + > <tbody> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate> + <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)"> + First seen + </translate> </td> <td> - <human-date :date="object.creation_date"></human-date> + <human-date :date="object.creation_date" /> </td> </tr> <tr> <td> - <translate translate-context="*/*/*/Noun">Listenings</translate> + <translate translate-context="*/*/*/Noun"> + Listenings + </translate> </td> <td> {{ stats.listenings }} @@ -175,7 +259,9 @@ </tr> <tr> <td> - <translate translate-context="*/*/*">Favorited tracks</translate> + <translate translate-context="*/*/*"> + Favorited tracks + </translate> </td> <td> {{ stats.track_favorites }} @@ -183,7 +269,9 @@ </tr> <tr> <td> - <translate translate-context="*/*/*">Playlists</translate> + <translate translate-context="*/*/*"> + Playlists + </translate> </td> <td> {{ stats.playlists }} @@ -192,7 +280,9 @@ <tr> <td> <router-link :to="{name: 'manage.moderation.reports.list', query: {q: getQuery('target', `album:${object.id}`) }}"> - <translate translate-context="Content/Moderation/Table.Label/Noun">Linked reports</translate> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Linked reports + </translate> </router-link> </td> <td> @@ -202,7 +292,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.edits', query: {q: getQuery('target', 'album ' + object.id)}}"> - <translate translate-context="*/Admin/*/Noun">Edits</translate> + <translate translate-context="*/Admin/*/Noun"> + Edits + </translate> </router-link> </td> <td> @@ -216,25 +308,33 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="music icon"></i> + <i class="music icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Audio content</translate> - <span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span> - + <translate translate-context="Content/Moderation/Title"> + Audio content + </translate> + <span :data-tooltip="labels.statsWarning"><i class="question circle icon" /></span> </div> </h3> - <div v-if="isLoadingStats" class="ui placeholder"> - <div class="full line"></div> - <div class="short line"></div> - <div class="medium line"></div> - <div class="long line"></div> + <div + v-if="isLoadingStats" + class="ui placeholder" + > + <div class="full line" /> + <div class="short line" /> + <div class="medium line" /> + <div class="long line" /> </div> - <table v-else class="ui very basic table"> + <table + v-else + class="ui very basic table" + > <tbody> - <tr> <td> - <translate translate-context="Content/Moderation/Table.Label/Noun">Cached size</translate> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Cached size + </translate> </td> <td> {{ stats.media_downloaded_size | humanSize }} @@ -242,7 +342,9 @@ </tr> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label">Total size</translate> + <translate translate-context="Content/Moderation/Table.Label"> + Total size + </translate> </td> <td> {{ stats.media_total_size | humanSize }} @@ -252,7 +354,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('album_id', object.id) }}"> - <translate translate-context="*/*/*/Noun">Libraries</translate> + <translate translate-context="*/*/*/Noun"> + Libraries + </translate> </router-link> </td> <td> @@ -262,7 +366,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('album_id', object.id) }}"> - <translate translate-context="*/*/*">Uploads</translate> + <translate translate-context="*/*/*"> + Uploads + </translate> </router-link> </td> <td> @@ -272,7 +378,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.tracks', query: {q: getQuery('album_id', object.id) }}"> - <translate translate-context="*/*/*">Tracks</translate> + <translate translate-context="*/*/*"> + Tracks + </translate> </router-link> </td> <td> @@ -281,77 +389,74 @@ </tr> </tbody> </table> - </section> </div> </div> </div> - </template> </main> </template> <script> -import axios from "axios" -import logger from "@/logging" -import FetchButton from "@/components/federation/FetchButton" -import TagsList from "@/components/tags/List" +import axios from 'axios' +import FetchButton from '@/components/federation/FetchButton' +import TagsList from '@/components/tags/List' export default { - props: ["id"], components: { FetchButton, TagsList }, - data() { + props: { id: { type: Number, required: true } }, + data () { return { isLoading: true, isLoadingStats: false, object: null, - stats: null, + stats: null + } + }, + computed: { + labels () { + return { + statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object') + } } }, - created() { + created () { this.fetchData() this.fetchStats() }, methods: { - fetchData() { - var self = this + fetchData () { + const self = this this.isLoading = true - let url = `manage/library/albums/${this.id}/` + const url = `manage/library/albums/${this.id}/` axios.get(url).then(response => { self.object = response.data self.isLoading = false }) }, - fetchStats() { - var self = this + fetchStats () { + const self = this this.isLoadingStats = true - let url = `manage/library/albums/${this.id}/stats/` + const url = `manage/library/albums/${this.id}/stats/` axios.get(url).then(response => { self.stats = response.data self.isLoadingStats = false }) }, remove () { - var self = this + const self = this this.isLoading = true - let url = `manage/library/albums/${this.id}/` + const url = `manage/library/albums/${this.id}/` axios.delete(url).then(response => { - self.$router.push({name: 'manage.library.albums'}) + self.$router.push({ name: 'manage.library.albums' }) }) }, getQuery (field, value) { return `${field}:"${value}"` } - }, - computed: { - labels() { - return { - statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object'), - } - }, } } </script> diff --git a/front/src/views/admin/library/AlbumsList.vue b/front/src/views/admin/library/AlbumsList.vue index 650b4d69389aadd05ae8fd2e7a385ed02c2a5945..419449959b4565f7304b4643c18a209fbcdb27ab 100644 --- a/front/src/views/admin/library/AlbumsList.vue +++ b/front/src/views/admin/library/AlbumsList.vue @@ -1,25 +1,30 @@ <template> <main v-title="labels.title"> <section class="ui vertical stripe segment"> - <h2 class="ui header">{{ labels.title }}</h2> - <div class="ui hidden divider"></div> - <albums-table :update-url="true" :default-query="defaultQuery"></albums-table> + <h2 class="ui header"> + {{ labels.title }} + </h2> + <div class="ui hidden divider" /> + <albums-table + :update-url="true" + :default-query="defaultQuery" + /> </section> </main> </template> <script> -import AlbumsTable from "@/components/manage/library/AlbumsTable" +import AlbumsTable from '@/components/manage/library/AlbumsTable' export default { components: { AlbumsTable }, props: { - defaultQuery: {type: String, required: false}, + defaultQuery: { type: String, required: false, default: '' } }, computed: { - labels() { + labels () { return { title: this.$pgettext('*/*/*', 'Albums') } diff --git a/front/src/views/admin/library/ArtistDetail.vue b/front/src/views/admin/library/ArtistDetail.vue index e3e11c5178dd0bafe512acd29f79f43d0c87d74e..8da0906cc007e4ddb35de875f7f74549000fd3a6 100644 --- a/front/src/views/admin/library/ArtistDetail.vue +++ b/front/src/views/admin/library/ArtistDetail.vue @@ -1,22 +1,36 @@ <template> <main> - <div v-if="isLoading" class="ui vertical segment"> - <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> + <div + v-if="isLoading" + class="ui vertical segment" + > + <div :class="['ui', 'centered', 'active', 'inline', 'loader']" /> </div> <template v-if="object"> - <section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="object.name"> + <section + v-title="object.name" + :class="['ui', 'head', 'vertical', 'stripe', 'segment']" + > <div class="ui stackable one column grid"> <div class="ui column"> <div class="segment-content"> <h2 class="ui header"> - <img alt="" v-if="object.cover && object.cover.urls.medium_square_crop" v-lazy="$store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)"> - <img alt="" v-else src="../../../assets/audio/default-cover.png"> + <img + v-if="object.cover && object.cover.urls.medium_square_crop" + v-lazy="$store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)" + alt="" + > + <img + v-else + alt="" + src="../../../assets/audio/default-cover.png" + > <div class="content"> {{ object.name | truncate(100) }} <div class="sub header"> <template v-if="object.is_local"> <span class="ui tiny accent label"> - <i class="home icon"></i> + <i class="home icon" /> <translate translate-context="Content/Moderation/*/Short, Noun">Local</translate> </span> @@ -25,38 +39,69 @@ </div> </h2> <template v-if="object.tags && object.tags.length > 0"> - <tags-list :limit="5" detail-route="manage.library.tags.detail" :tags="object.tags"></tags-list> - <div class="ui hidden divider"></div> + <tags-list + :limit="5" + detail-route="manage.library.tags.detail" + :tags="object.tags" + /> + <div class="ui hidden divider" /> </template> <div class="header-buttons"> - <div class="ui icon buttons"> - <router-link class="ui labeled icon button" :to="{name: 'library.artists.detail', params: {id: object.id }}"> - <i class="info icon"></i> - <translate translate-context="Content/Moderation/Link/Verb">Open local profile</translate> + <router-link + class="ui labeled icon button" + :to="{name: 'library.artists.detail', params: {id: object.id }}" + > + <i class="info icon" /> + <translate translate-context="Content/Moderation/Link/Verb"> + Open local profile + </translate> </router-link> - <button class="ui floating dropdown icon button" v-dropdown> - <i class="dropdown icon"></i> + <button + v-dropdown + class="ui floating dropdown icon button" + > + <i class="dropdown icon" /> <div class="menu"> <a v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser" class="basic item" :href="$store.getters['instance/absoluteUrl'](`/api/admin/music/artist/${object.id}`)" - target="_blank" rel="noopener noreferrer"> - <i class="wrench icon"></i> + target="_blank" + rel="noopener noreferrer" + > + <i class="wrench icon" /> <translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate> </a> - <a class="basic item" v-if="object.mbid" :href="`https://musicbrainz.org/artist/${object.mbid}`" target="_blank" rel="noopener noreferrer"> - <i class="external icon"></i> + <a + v-if="object.mbid" + class="basic item" + :href="`https://musicbrainz.org/artist/${object.mbid}`" + target="_blank" + rel="noopener noreferrer" + > + <i class="external icon" /> <translate translate-context="Content/Moderation/Link/Verb">Open on MusicBrainz</translate> </a> - <fetch-button @refresh="fetchData" v-if="!object.is_local" class="basic item" :url="`artists/${object.id}/fetches/`"> - <i class="refresh icon"></i> - <translate translate-context="Content/Moderation/Button/Verb">Refresh from remote server</translate> + <fetch-button + v-if="!object.is_local" + class="basic item" + :url="`artists/${object.id}/fetches/`" + @refresh="fetchData" + > + <i class="refresh icon" /> + <translate translate-context="Content/Moderation/Button/Verb"> + Refresh from remote server + </translate> </fetch-button> - <a class="basic item" :href="object.url || object.fid" target="_blank" rel="noopener noreferrer"> - <i class="external icon"></i> + <a + class="basic item" + :href="object.url || object.fid" + target="_blank" + rel="noopener noreferrer" + > + <i class="external icon" /> <translate translate-context="Content/Moderation/Link/Verb">Open remote profile</translate> </a> </div> @@ -66,21 +111,39 @@ <router-link v-if="object.is_local" :to="{name: 'library.artists.edit', params: {id: object.id }}" - class="ui labeled icon button"> - <i class="edit icon"></i> - <translate translate-context="Content/*/Button.Label/Verb">Edit</translate> + class="ui labeled icon button" + > + <i class="edit icon" /> + <translate translate-context="Content/*/Button.Label/Verb"> + Edit + </translate> </router-link> </div> <div class="ui buttons"> <dangerous-button :class="['ui', {loading: isLoading}, 'basic danger button']" - :action="remove"> - <translate translate-context="*/*/*/Verb">Delete</translate> - <p slot="modal-header"><translate translate-context="Popup/Library/Title">Delete this artist?</translate></p> + :action="remove" + > + <translate translate-context="*/*/*/Verb"> + Delete + </translate> + <p slot="modal-header"> + <translate translate-context="Popup/Library/Title"> + Delete this artist? + </translate> + </p> <div slot="modal-content"> - <p><translate translate-context="Content/Moderation/Paragraph">The artist will be removed, as well as associated uploads, tracks, albums, favorites and listening history. This action is irreversible.</translate></p> + <p> + <translate translate-context="Content/Moderation/Paragraph"> + The artist will be removed, as well as associated uploads, tracks, albums, favorites and listening history. This action is irreversible. + </translate> + </p> </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> </div> </div> @@ -93,16 +156,20 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="info icon"></i> + <i class="info icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Artist data</translate> + <translate translate-context="Content/Moderation/Title"> + Artist data + </translate> </div> </h3> <table class="ui very basic table"> <tbody> <tr> <td> - <translate translate-context="*/*/*/Noun">Name</translate> + <translate translate-context="*/*/*/Noun"> + Name + </translate> </td> <td> {{ object.name }} @@ -111,7 +178,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.artists', query: {q: getQuery('category', object.content_category) }}"> - <translate translate-context="*/*/*">Category</translate> + <translate translate-context="*/*/*"> + Category + </translate> </router-link> </td> <td> @@ -121,7 +190,9 @@ <tr v-if="!object.is_local"> <td> <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}"> - <translate translate-context="Content/Moderation/*/Noun">Domain</translate> + <translate translate-context="Content/Moderation/*/Noun"> + Domain + </translate> </router-link> </td> <td> @@ -130,9 +201,11 @@ </tr> <tr v-if="object.description"> <td> - <translate translate-context="'*/*/*/Noun">Description</translate> + <translate translate-context="'*/*/*/Noun"> + Description + </translate> </td> - <td v-html="object.description.html"></td> + <td v-html="object.description.html" /> </tr> </tbody> </table> @@ -141,32 +214,43 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="feed icon"></i> + <i class="feed icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Activity</translate> - <span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span> - + <translate translate-context="Content/Moderation/Title"> + Activity + </translate> + <span :data-tooltip="labels.statsWarning"><i class="question circle icon" /></span> </div> </h3> - <div v-if="isLoadingStats" class="ui placeholder"> - <div class="full line"></div> - <div class="short line"></div> - <div class="medium line"></div> - <div class="long line"></div> + <div + v-if="isLoadingStats" + class="ui placeholder" + > + <div class="full line" /> + <div class="short line" /> + <div class="medium line" /> + <div class="long line" /> </div> - <table v-else class="ui very basic table"> + <table + v-else + class="ui very basic table" + > <tbody> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate> + <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)"> + First seen + </translate> </td> <td> - <human-date :date="object.creation_date"></human-date> + <human-date :date="object.creation_date" /> </td> </tr> <tr> <td> - <translate translate-context="*/*/*/Noun">Listenings</translate> + <translate translate-context="*/*/*/Noun"> + Listenings + </translate> </td> <td> {{ stats.listenings }} @@ -174,7 +258,9 @@ </tr> <tr> <td> - <translate translate-context="*/*/*">Favorited tracks</translate> + <translate translate-context="*/*/*"> + Favorited tracks + </translate> </td> <td> {{ stats.track_favorites }} @@ -182,7 +268,9 @@ </tr> <tr> <td> - <translate translate-context="*/*/*">Playlists</translate> + <translate translate-context="*/*/*"> + Playlists + </translate> </td> <td> {{ stats.playlists }} @@ -191,7 +279,9 @@ <tr> <td> <router-link :to="{name: 'manage.moderation.reports.list', query: {q: getQuery('target', `artist:${object.id}`) }}"> - <translate translate-context="Content/Moderation/Table.Label/Noun">Linked reports</translate> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Linked reports + </translate> </router-link> </td> <td> @@ -201,7 +291,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.edits', query: {q: getQuery('target', 'artist ' + object.id)}}"> - <translate translate-context="*/Admin/*/Noun">Edits</translate> + <translate translate-context="*/Admin/*/Noun"> + Edits + </translate> </router-link> </td> <td> @@ -215,25 +307,33 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="music icon"></i> + <i class="music icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Audio content</translate> - <span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span> - + <translate translate-context="Content/Moderation/Title"> + Audio content + </translate> + <span :data-tooltip="labels.statsWarning"><i class="question circle icon" /></span> </div> </h3> - <div v-if="isLoadingStats" class="ui placeholder"> - <div class="full line"></div> - <div class="short line"></div> - <div class="medium line"></div> - <div class="long line"></div> + <div + v-if="isLoadingStats" + class="ui placeholder" + > + <div class="full line" /> + <div class="short line" /> + <div class="medium line" /> + <div class="long line" /> </div> - <table v-else class="ui very basic table"> + <table + v-else + class="ui very basic table" + > <tbody> - <tr> <td> - <translate translate-context="Content/Moderation/Table.Label/Noun">Cached size</translate> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Cached size + </translate> </td> <td> {{ stats.media_downloaded_size | humanSize }} @@ -241,7 +341,9 @@ </tr> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label">Total size</translate> + <translate translate-context="Content/Moderation/Table.Label"> + Total size + </translate> </td> <td> {{ stats.media_total_size | humanSize }} @@ -251,7 +353,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('artist_id', object.id) }}"> - <translate translate-context="*/*/*/Noun">Libraries</translate> + <translate translate-context="*/*/*/Noun"> + Libraries + </translate> </router-link> </td> <td> @@ -261,7 +365,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('artist_id', object.id) }}"> - <translate translate-context="*/*/*">Uploads</translate> + <translate translate-context="*/*/*"> + Uploads + </translate> </router-link> </td> <td> @@ -271,7 +377,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.albums', query: {q: getQuery('artist_id', object.id) }}"> - <translate translate-context="*/*/*">Albums</translate> + <translate translate-context="*/*/*"> + Albums + </translate> </router-link> </td> <td> @@ -281,7 +389,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.tracks', query: {q: getQuery('artist_id', object.id) }}"> - <translate translate-context="*/*/*">Tracks</translate> + <translate translate-context="*/*/*"> + Tracks + </translate> </router-link> </td> <td> @@ -290,82 +400,79 @@ </tr> </tbody> </table> - </section> </div> </div> </div> - </template> </main> </template> <script> -import axios from "axios" -import logger from "@/logging" +import axios from 'axios' -import TagsList from "@/components/tags/List" -import FetchButton from "@/components/federation/FetchButton" +import TagsList from '@/components/tags/List' +import FetchButton from '@/components/federation/FetchButton' export default { - props: ["id"], components: { FetchButton, TagsList }, - data() { + props: { id: { type: Number, required: true } }, + data () { return { isLoading: true, isLoadingStats: false, object: null, - stats: null, + stats: null + } + }, + computed: { + labels () { + return { + statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object') + } } }, - created() { + created () { this.fetchData() this.fetchStats() }, methods: { - fetchData() { - var self = this + fetchData () { + const self = this this.isLoading = true - let url = `manage/library/artists/${this.id}/` + const url = `manage/library/artists/${this.id}/` axios.get(url).then(response => { if (response.data.channel) { - self.$router.push({name: "manage.channels.detail", params: {id: response.data.channel}}) + self.$router.push({ name: 'manage.channels.detail', params: { id: response.data.channel } }) } else { self.object = response.data self.isLoading = false } }) }, - fetchStats() { - var self = this + fetchStats () { + const self = this this.isLoadingStats = true - let url = `manage/library/artists/${this.id}/stats/` + const url = `manage/library/artists/${this.id}/stats/` axios.get(url).then(response => { self.stats = response.data self.isLoadingStats = false }) }, remove () { - var self = this + const self = this this.isLoading = true - let url = `manage/library/artists/${this.id}/` + const url = `manage/library/artists/${this.id}/` axios.delete(url).then(response => { - self.$router.push({name: 'manage.library.artists'}) + self.$router.push({ name: 'manage.library.artists' }) }) }, getQuery (field, value) { return `${field}:"${value}"` } - }, - computed: { - labels() { - return { - statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object'), - } - }, } } </script> diff --git a/front/src/views/admin/library/ArtistsList.vue b/front/src/views/admin/library/ArtistsList.vue index dae856be445f84b938e40c1a972e2fcb59f13852..7e07353e018b66eada2caed031d6f736d20af003 100644 --- a/front/src/views/admin/library/ArtistsList.vue +++ b/front/src/views/admin/library/ArtistsList.vue @@ -1,25 +1,30 @@ <template> <main v-title="labels.title"> <section class="ui vertical stripe segment"> - <h2 class="ui header">{{ labels.title }}</h2> - <div class="ui hidden divider"></div> - <artists-table :update-url="true" :default-query="defaultQuery"></artists-table> + <h2 class="ui header"> + {{ labels.title }} + </h2> + <div class="ui hidden divider" /> + <artists-table + :update-url="true" + :default-query="defaultQuery" + /> </section> </main> </template> <script> -import ArtistsTable from "@/components/manage/library/ArtistsTable" +import ArtistsTable from '@/components/manage/library/ArtistsTable' export default { components: { ArtistsTable }, props: { - defaultQuery: {type: String, required: false}, + defaultQuery: { type: String, required: false, default: '' } }, computed: { - labels() { + labels () { return { title: this.$pgettext('*/*/*/Noun', 'Artists') } diff --git a/front/src/views/admin/library/Base.vue b/front/src/views/admin/library/Base.vue index 8b2587d22bbf26e22bd3791c154c1e87709cfbd2..97f9013e7b6cfd9c7e811a784b3fa339a139f167 100644 --- a/front/src/views/admin/library/Base.vue +++ b/front/src/views/admin/library/Base.vue @@ -1,41 +1,88 @@ <template> - <div class="main pusher page-admin-library" v-title="labels.title"> - <nav class="ui secondary pointing menu" role="navigation" :aria-label="labels.secondaryMenu"> + <div + v-title="labels.title" + class="main pusher page-admin-library" + > + <nav + class="ui secondary pointing menu" + role="navigation" + :aria-label="labels.secondaryMenu" + > <router-link class="ui item" - :to="{name: 'manage.library.edits'}"><translate translate-context="*/Admin/*/Noun">Edits</translate></router-link> + :to="{name: 'manage.library.edits'}" + > + <translate translate-context="*/Admin/*/Noun"> + Edits + </translate> + </router-link> <router-link class="ui item" - :to="{name: 'manage.channels'}"><translate translate-context="*/*/*">Channels</translate></router-link> + :to="{name: 'manage.channels'}" + > + <translate translate-context="*/*/*"> + Channels + </translate> + </router-link> <router-link class="ui item" - :to="{name: 'manage.library.artists'}"><translate translate-context="*/*/*/Noun">Artists</translate></router-link> + :to="{name: 'manage.library.artists'}" + > + <translate translate-context="*/*/*/Noun"> + Artists + </translate> + </router-link> <router-link class="ui item" - :to="{name: 'manage.library.albums'}"><translate translate-context="*/*/*">Albums</translate></router-link> + :to="{name: 'manage.library.albums'}" + > + <translate translate-context="*/*/*"> + Albums + </translate> + </router-link> <router-link class="ui item" - :to="{name: 'manage.library.tracks'}"><translate translate-context="*/*/*">Tracks</translate></router-link> + :to="{name: 'manage.library.tracks'}" + > + <translate translate-context="*/*/*"> + Tracks + </translate> + </router-link> <router-link class="ui item" - :to="{name: 'manage.library.libraries'}"><translate translate-context="*/*/*/Noun">Libraries</translate></router-link> + :to="{name: 'manage.library.libraries'}" + > + <translate translate-context="*/*/*/Noun"> + Libraries + </translate> + </router-link> <router-link class="ui item" - :to="{name: 'manage.library.uploads'}"><translate translate-context="*/*/*">Uploads</translate></router-link> + :to="{name: 'manage.library.uploads'}" + > + <translate translate-context="*/*/*"> + Uploads + </translate> + </router-link> <router-link class="ui item" - :to="{name: 'manage.library.tags'}"><translate translate-context="*/*/*/Noun">Tags</translate></router-link> + :to="{name: 'manage.library.tags'}" + > + <translate translate-context="*/*/*/Noun"> + Tags + </translate> + </router-link> </nav> - <router-view :key="$route.fullPath"></router-view> + <router-view :key="$route.fullPath" /> </div> </template> <script> export default { computed: { - labels() { - let title = this.$pgettext('Head/Admin/Title', 'Manage library') - let secondaryMenu = this.$pgettext('Menu/*/Hidden text', 'Secondary menu') + labels () { + const title = this.$pgettext('Head/Admin/Title', 'Manage library') + const secondaryMenu = this.$pgettext('Menu/*/Hidden text', 'Secondary menu') return { title, secondaryMenu diff --git a/front/src/views/admin/library/EditsList.vue b/front/src/views/admin/library/EditsList.vue index 8af28804c5b350db21233dc57f18e141c8839fb2..2b67c69b6121f85c6b4fa716eceda6ae178beb34 100644 --- a/front/src/views/admin/library/EditsList.vue +++ b/front/src/views/admin/library/EditsList.vue @@ -1,25 +1,32 @@ <template> <main v-title="labels.title"> <section class="ui vertical stripe segment"> - <edits-card-list :update-url="true" :default-query="defaultQuery"> - <h2 class="ui header"><translate translate-context="Content/Admin/Title/Noun">Library edits</translate></h2> + <edits-card-list + :update-url="true" + :default-query="defaultQuery" + > + <h2 class="ui header"> + <translate translate-context="Content/Admin/Title/Noun"> + Library edits + </translate> + </h2> </edits-card-list> </section> </main> </template> <script> -import EditsCardList from "@/components/manage/library/EditsCardList" +import EditsCardList from '@/components/manage/library/EditsCardList' export default { - props: { - defaultQuery: {type: String, required: false}, - }, components: { EditsCardList }, + props: { + defaultQuery: { type: String, required: false, default: '' } + }, computed: { - labels() { + labels () { return { title: this.$pgettext('*/Admin/*/Noun', 'Edits') } diff --git a/front/src/views/admin/library/LibrariesList.vue b/front/src/views/admin/library/LibrariesList.vue index 479008bdb1f88939fe13daeae071244d6cc7aa89..7e06cf21cdcef8df86720402d4d5be7c9fa3d296 100644 --- a/front/src/views/admin/library/LibrariesList.vue +++ b/front/src/views/admin/library/LibrariesList.vue @@ -1,25 +1,30 @@ <template> <main v-title="labels.title"> <section class="ui vertical stripe segment"> - <h2 class="ui header">{{ labels.title }}</h2> - <div class="ui hidden divider"></div> - <libraries-table :update-url="true" :default-query="defaultQuery"></libraries-table> + <h2 class="ui header"> + {{ labels.title }} + </h2> + <div class="ui hidden divider" /> + <libraries-table + :update-url="true" + :default-query="defaultQuery" + /> </section> </main> </template> <script> -import LibrariesTable from "@/components/manage/library/LibrariesTable" +import LibrariesTable from '@/components/manage/library/LibrariesTable' export default { components: { LibrariesTable }, props: { - defaultQuery: {type: String, required: false}, + defaultQuery: { type: String, required: false, default: '' } }, computed: { - labels() { + labels () { return { title: this.$pgettext('*/*/*/Noun', 'Libraries') } diff --git a/front/src/views/admin/library/LibraryDetail.vue b/front/src/views/admin/library/LibraryDetail.vue index 6076063df908427fa1841d1f6e3c1314a750ac35..ee58f13fad7f4c90aac1176161d175ec8196e9ac 100644 --- a/front/src/views/admin/library/LibraryDetail.vue +++ b/front/src/views/admin/library/LibraryDetail.vue @@ -1,21 +1,27 @@ <template> <main> - <div v-if="isLoading" class="ui vertical segment"> - <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> + <div + v-if="isLoading" + class="ui vertical segment" + > + <div :class="['ui', 'centered', 'active', 'inline', 'loader']" /> </div> <template v-if="object"> - <section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="object.name"> + <section + v-title="object.name" + :class="['ui', 'head', 'vertical', 'stripe', 'segment']" + > <div class="ui stackable one column grid"> <div class="ui column"> <div class="segment-content"> <h2 class="ui header"> - <i class="circular inverted book icon"></i> + <i class="circular inverted book icon" /> <div class="content"> {{ object.name | truncate(100) }} <div class="sub header"> <template v-if="object.is_local"> <span class="ui tiny accent label"> - <i class="home icon"></i> + <i class="home icon" /> <translate translate-context="Content/Moderation/*/Short, Noun">Local</translate> </span> @@ -24,29 +30,40 @@ </div> </h2> <div class="header-buttons"> - <div class="ui icon buttons"> <a v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser" class="ui labeled icon button" :href="$store.getters['instance/absoluteUrl'](`/api/admin/music/library/${object.id}`)" - target="_blank" rel="noopener noreferrer"> - <i class="wrench icon"></i> + target="_blank" + rel="noopener noreferrer" + > + <i class="wrench icon" /> <translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate> </a> - <button class="ui floating dropdown icon button" v-dropdown> - <i class="dropdown icon"></i> + <button + v-dropdown + class="ui floating dropdown icon button" + > + <i class="dropdown icon" /> <div class="menu"> <a v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser" class="basic item" :href="$store.getters['instance/absoluteUrl'](`/api/admin/music/library/${object.id}`)" - target="_blank" rel="noopener noreferrer"> - <i class="wrench icon"></i> + target="_blank" + rel="noopener noreferrer" + > + <i class="wrench icon" /> <translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate> </a> - <a class="basic item" :href="object.url || object.fid" target="_blank" rel="noopener noreferrer"> - <i class="external icon"></i> + <a + class="basic item" + :href="object.url || object.fid" + target="_blank" + rel="noopener noreferrer" + > + <i class="external icon" /> <translate translate-context="Content/Moderation/Link/Verb">Open remote profile</translate> </a> </div> @@ -55,13 +72,28 @@ <div class="ui buttons"> <dangerous-button :class="['ui', {loading: isLoading}, 'basic danger button']" - :action="remove"> - <translate translate-context="*/*/*/Verb">Delete</translate> - <p slot="modal-header"><translate translate-context="Popup/Library/Title">Delete this library?</translate></p> + :action="remove" + > + <translate translate-context="*/*/*/Verb"> + Delete + </translate> + <p slot="modal-header"> + <translate translate-context="Popup/Library/Title"> + Delete this library? + </translate> + </p> <div slot="modal-content"> - <p><translate translate-context="Content/Moderation/Paragraph">The library will be removed, as well as associated uploads, and follows. This action is irreversible.</translate></p> + <p> + <translate translate-context="Content/Moderation/Paragraph"> + The library will be removed, as well as associated uploads, and follows. This action is irreversible. + </translate> + </p> </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> </div> </div> @@ -74,16 +106,20 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="info icon"></i> + <i class="info icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Library data</translate> + <translate translate-context="Content/Moderation/Title"> + Library data + </translate> </div> </h3> <table class="ui very basic table"> <tbody> <tr> <td> - <translate translate-context="*/*/*/Noun">Name</translate> + <translate translate-context="*/*/*/Noun"> + Name + </translate> </td> <td> {{ object.name }} @@ -92,26 +128,39 @@ <tr> <td> <router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('privacy_level', object.privacy_level) }}"> - <translate translate-context="*/*/*">Visibility</translate> + <translate translate-context="*/*/*"> + Visibility + </translate> </router-link> </td> <td> <select - v-dropdown v-if="object.is_local" - @change="updateObj('privacy_level')" v-model="object.privacy_level" + v-dropdown + class="ui search selection dropdown" - class="ui search selection dropdown"> - <option v-for="p in ['me', 'instance', 'everyone']" :value="p">{{ sharedLabels.fields.privacy_level.shortChoices[p] }}</option> + @change="updateObj('privacy_level')" + > + <option + v-for="(p, key) in ['me', 'instance', 'everyone']" + :key="key" + :value="p" + > + {{ sharedLabels.fields.privacy_level.shortChoices[p] }} + </option> </select> - <template v-else>{{ sharedLabels.fields.privacy_level.shortChoices[object.privacy_level] }}</template> + <template v-else> + {{ sharedLabels.fields.privacy_level.shortChoices[object.privacy_level] }} + </template> </td> </tr> <tr> <td> <router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: object.actor.full_username }}"> - <translate translate-context="*/*/*/Noun">Account</translate> + <translate translate-context="*/*/*/Noun"> + Account + </translate> </router-link> </td> <td> @@ -121,7 +170,9 @@ <tr v-if="!object.is_local"> <td> <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}"> - <translate translate-context="Content/Moderation/*/Noun">Domain</translate> + <translate translate-context="Content/Moderation/*/Noun"> + Domain + </translate> </router-link> </td> <td> @@ -130,7 +181,9 @@ </tr> <tr> <td> - <translate translate-context="*/*/*/Noun">Description</translate> + <translate translate-context="*/*/*/Noun"> + Description + </translate> </td> <td> {{ object.description }} @@ -143,32 +196,43 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="feed icon"></i> + <i class="feed icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Activity</translate> - <span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span> - + <translate translate-context="Content/Moderation/Title"> + Activity + </translate> + <span :data-tooltip="labels.statsWarning"><i class="question circle icon" /></span> </div> </h3> - <div v-if="isLoadingStats" class="ui placeholder"> - <div class="full line"></div> - <div class="short line"></div> - <div class="medium line"></div> - <div class="long line"></div> + <div + v-if="isLoadingStats" + class="ui placeholder" + > + <div class="full line" /> + <div class="short line" /> + <div class="medium line" /> + <div class="long line" /> </div> - <table v-else class="ui very basic table"> + <table + v-else + class="ui very basic table" + > <tbody> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate> + <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)"> + First seen + </translate> </td> <td> - <human-date :date="object.creation_date"></human-date> + <human-date :date="object.creation_date" /> </td> </tr> <tr> <td> - <translate translate-context="Content/Federation/*/Noun">Followers</translate> + <translate translate-context="Content/Federation/*/Noun"> + Followers + </translate> </td> <td> {{ stats.followers }} @@ -177,7 +241,9 @@ <tr> <td> <router-link :to="{name: 'manage.moderation.reports.list', query: {q: getQuery('target', `library:${object.uuid}`) }}"> - <translate translate-context="Content/Moderation/Table.Label/Noun">Linked reports</translate> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Linked reports + </translate> </router-link> </td> <td> @@ -191,25 +257,33 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="music icon"></i> + <i class="music icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Audio content</translate> - <span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span> - + <translate translate-context="Content/Moderation/Title"> + Audio content + </translate> + <span :data-tooltip="labels.statsWarning"><i class="question circle icon" /></span> </div> </h3> - <div v-if="isLoadingStats" class="ui placeholder"> - <div class="full line"></div> - <div class="short line"></div> - <div class="medium line"></div> - <div class="long line"></div> + <div + v-if="isLoadingStats" + class="ui placeholder" + > + <div class="full line" /> + <div class="short line" /> + <div class="medium line" /> + <div class="long line" /> </div> - <table v-else class="ui very basic table"> + <table + v-else + class="ui very basic table" + > <tbody> - <tr> <td> - <translate translate-context="Content/Moderation/Table.Label/Noun">Cached size</translate> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Cached size + </translate> </td> <td> {{ stats.media_downloaded_size | humanSize }} @@ -217,7 +291,9 @@ </tr> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label">Total size</translate> + <translate translate-context="Content/Moderation/Table.Label"> + Total size + </translate> </td> <td> {{ stats.media_total_size | humanSize }} @@ -226,7 +302,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.artists', query: {q: getQuery('library_id', object.id) }}"> - <translate translate-context="*/*/*/Noun">Artists</translate> + <translate translate-context="*/*/*/Noun"> + Artists + </translate> </router-link> </td> <td> @@ -236,7 +314,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.albums', query: {q: getQuery('library_id', object.id) }}"> - <translate translate-context="*/*/*">Albums</translate> + <translate translate-context="*/*/*"> + Albums + </translate> </router-link> </td> <td> @@ -246,7 +326,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.tracks', query: {q: getQuery('library_id', object.id) }}"> - <translate translate-context="*/*/*">Tracks</translate> + <translate translate-context="*/*/*"> + Tracks + </translate> </router-link> </td> <td> @@ -256,7 +338,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('library_id', object.id) }}"> - <translate translate-context="*/*/*">Uploads</translate> + <translate translate-context="*/*/*"> + Uploads + </translate> </router-link> </td> <td> @@ -265,75 +349,79 @@ </tr> </tbody> </table> - </section> </div> </div> </div> - </template> </main> </template> <script> -import axios from "axios" -import logger from "@/logging" -import TranslationsMixin from "@/components/mixins/Translations" - +import axios from 'axios' +import logger from '@/logging' +import TranslationsMixin from '@/components/mixins/Translations' export default { - props: ["id"], mixins: [ TranslationsMixin ], - data() { + props: { id: { type: Number, required: true } }, + data () { return { isLoading: true, isLoadingStats: false, object: null, - stats: null, + stats: null } }, - created() { + computed: { + labels () { + return { + statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object') + } + } + }, + created () { this.fetchData() this.fetchStats() }, methods: { - fetchData() { - var self = this + fetchData () { + const self = this this.isLoading = true - let url = `manage/library/libraries/${this.id}/` + const url = `manage/library/libraries/${this.id}/` axios.get(url).then(response => { self.object = response.data self.isLoading = false }) }, - fetchStats() { - var self = this + fetchStats () { + const self = this this.isLoadingStats = true - let url = `manage/library/libraries/${this.id}/stats/` + const url = `manage/library/libraries/${this.id}/stats/` axios.get(url).then(response => { self.stats = response.data self.isLoadingStats = false }) }, remove () { - var self = this + const self = this this.isLoading = true - let url = `manage/library/libraries/${this.id}/` + const url = `manage/library/libraries/${this.id}/` axios.delete(url).then(response => { - self.$router.push({name: 'manage.library.libraries'}) + self.$router.push({ name: 'manage.library.libraries' }) }) }, getQuery (field, value) { return `${field}:"${value}"` }, - updateObj(attr, toNull) { + updateObj (attr, toNull) { let newValue = this.object[attr] if (toNull && !newValue) { newValue = null } - let params = {} + const params = {} params[attr] = newValue axios.patch(`manage/library/libraries/${this.id}/`, params).then( response => { @@ -348,14 +436,7 @@ export default { ) } ) - }, - }, - computed: { - labels() { - return { - statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object'), - } - }, + } } } </script> diff --git a/front/src/views/admin/library/TagDetail.vue b/front/src/views/admin/library/TagDetail.vue index 856478d79db03fededeaf865f2ff35bf224d7c10..b6becc0463ce8856f5336cc2bef11116655ddc4e 100644 --- a/front/src/views/admin/library/TagDetail.vue +++ b/front/src/views/admin/library/TagDetail.vue @@ -1,35 +1,50 @@ <template> <main> - <div v-if="isLoading" class="ui vertical segment"> - <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> + <div + v-if="isLoading" + class="ui vertical segment" + > + <div :class="['ui', 'centered', 'active', 'inline', 'loader']" /> </div> <template v-if="object"> - <section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="object.name"> + <section + v-title="object.name" + :class="['ui', 'head', 'vertical', 'stripe', 'segment']" + > <div class="ui stackable one column grid"> <div class="ui column"> <div class="segment-content"> <h2 class="ui header"> - <i class="circular inverted hashtag icon"></i> + <i class="circular inverted hashtag icon" /> <div class="content"> {{ object.name | truncate(100) }} </div> </h2> <div class="header-buttons"> - <div class="ui icon buttons"> - <router-link class="ui labeled icon button" :to="{name: 'library.tags.detail', params: {id: object.name }}"> - <i class="info icon"></i> - <translate translate-context="Content/Moderation/Link/Verb">Open local profile</translate> + <router-link + class="ui labeled icon button" + :to="{name: 'library.tags.detail', params: {id: object.name }}" + > + <i class="info icon" /> + <translate translate-context="Content/Moderation/Link/Verb"> + Open local profile + </translate> </router-link> - <button class="ui floating dropdown icon button" v-dropdown> - <i class="dropdown icon"></i> + <button + v-dropdown + class="ui floating dropdown icon button" + > + <i class="dropdown icon" /> <div class="menu"> <a v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser" class="basic item" :href="$store.getters['instance/absoluteUrl'](`/api/admin/tags/tag/${object.id}`)" - target="_blank" rel="noopener noreferrer"> - <i class="wrench icon"></i> + target="_blank" + rel="noopener noreferrer" + > + <i class="wrench icon" /> <translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate> </a> </div> @@ -38,13 +53,28 @@ <div class="ui buttons"> <dangerous-button :class="['ui', {loading: isLoading}, 'basic danger button']" - :action="remove"> - <translate translate-context="*/*/*/Verb">Delete</translate> - <p slot="modal-header"><translate translate-context="Popup/Library/Title">Delete this tag?</translate></p> + :action="remove" + > + <translate translate-context="*/*/*/Verb"> + Delete + </translate> + <p slot="modal-header"> + <translate translate-context="Popup/Library/Title"> + Delete this tag? + </translate> + </p> <div slot="modal-content"> - <p><translate translate-context="Content/Moderation/Paragraph">The tag will be removed and unlinked from any existing entity. This action is irreversible.</translate></p> + <p> + <translate translate-context="Content/Moderation/Paragraph"> + The tag will be removed and unlinked from any existing entity. This action is irreversible. + </translate> + </p> </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> </div> </div> @@ -57,16 +87,20 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="info icon"></i> + <i class="info icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Tag data</translate> + <translate translate-context="Content/Moderation/Title"> + Tag data + </translate> </div> </h3> <table class="ui very basic table"> <tbody> <tr> <td> - <translate translate-context="*/*/*/Noun">Name</translate> + <translate translate-context="*/*/*/Noun"> + Name + </translate> </td> <td> {{ object.name }} @@ -79,27 +113,36 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="feed icon"></i> + <i class="feed icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Activity</translate> - <span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span> - + <translate translate-context="Content/Moderation/Title"> + Activity + </translate> + <span :data-tooltip="labels.statsWarning"><i class="question circle icon" /></span> </div> </h3> - <div v-if="isLoadingStats" class="ui placeholder"> - <div class="full line"></div> - <div class="short line"></div> - <div class="medium line"></div> - <div class="long line"></div> + <div + v-if="isLoadingStats" + class="ui placeholder" + > + <div class="full line" /> + <div class="short line" /> + <div class="medium line" /> + <div class="long line" /> </div> - <table v-else class="ui very basic table"> + <table + v-else + class="ui very basic table" + > <tbody> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate> + <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)"> + First seen + </translate> </td> <td> - <human-date :date="object.creation_date"></human-date> + <human-date :date="object.creation_date" /> </td> </tr> </tbody> @@ -109,11 +152,12 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="music icon"></i> + <i class="music icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Audio content</translate> - <span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span> - + <translate translate-context="Content/Moderation/Title"> + Audio content + </translate> + <span :data-tooltip="labels.statsWarning"><i class="question circle icon" /></span> </div> </h3> <table class="ui very basic table"> @@ -121,7 +165,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.artists', query: {q: getQuery('tag', object.name) }}"> - <translate translate-context="*/*/*/Noun">Artists</translate> + <translate translate-context="*/*/*/Noun"> + Artists + </translate> </router-link> </td> <td> @@ -131,7 +177,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.albums', query: {q: getQuery('tag', object.name) }}"> - <translate translate-context="*/*/*">Albums</translate> + <translate translate-context="*/*/*"> + Albums + </translate> </router-link> </td> <td> @@ -141,7 +189,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.tracks', query: {q: getQuery('tag', object.name) }}"> - <translate translate-context="*/*/*">Tracks</translate> + <translate translate-context="*/*/*"> + Tracks + </translate> </router-link> </td> <td> @@ -150,66 +200,58 @@ </tr> </tbody> </table> - </section> </div> </div> </div> - </template> </main> </template> <script> -import axios from "axios" -import logger from "@/logging" - -import FetchButton from "@/components/federation/FetchButton" +import axios from 'axios' export default { - props: ["id"], - components: { - FetchButton - }, - data() { + props: { id: { type: Number, required: true } }, + data () { return { isLoading: true, isLoadingStats: false, object: null, - stats: null, + stats: null } }, - created() { + computed: { + labels () { + return { + statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object') + } + } + }, + created () { this.fetchData() }, methods: { - fetchData() { - var self = this + fetchData () { + const self = this this.isLoading = true - let url = `manage/tags/${this.id}/` + const url = `manage/tags/${this.id}/` axios.get(url).then(response => { self.object = response.data self.isLoading = false }) }, remove () { - var self = this + const self = this this.isLoading = true - let url = `manage/tags/${this.id}/` + const url = `manage/tags/${this.id}/` axios.delete(url).then(response => { - self.$router.push({name: 'manage.library.tags'}) + self.$router.push({ name: 'manage.library.tags' }) }) }, getQuery (field, value) { return `${field}:"${value}"` } - }, - computed: { - labels() { - return { - statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object'), - } - }, } } </script> diff --git a/front/src/views/admin/library/TagsList.vue b/front/src/views/admin/library/TagsList.vue index 81e892a401bb03d8d8c78f49303a5fa56c958395..8f388cdf05c7e82b37386abdca0c3b0d3becf44f 100644 --- a/front/src/views/admin/library/TagsList.vue +++ b/front/src/views/admin/library/TagsList.vue @@ -1,25 +1,30 @@ <template> <main v-title="labels.title"> <section class="ui vertical stripe segment"> - <h2 class="ui header">{{ labels.title }}</h2> - <div class="ui hidden divider"></div> - <tags-table :update-url="true" :default-query="defaultQuery"></tags-table> + <h2 class="ui header"> + {{ labels.title }} + </h2> + <div class="ui hidden divider" /> + <tags-table + :update-url="true" + :default-query="defaultQuery" + /> </section> </main> </template> <script> -import TagsTable from "@/components/manage/library/TagsTable" +import TagsTable from '@/components/manage/library/TagsTable' export default { components: { TagsTable }, props: { - defaultQuery: {type: String, required: false}, + defaultQuery: { type: String, required: false, default: '' } }, computed: { - labels() { + labels () { return { title: this.$pgettext('*/*/*/Noun', 'Tags') } diff --git a/front/src/views/admin/library/TrackDetail.vue b/front/src/views/admin/library/TrackDetail.vue index 822942598ff6ab4172baa54d230ac416ca978a4b..4a7789387bacae439059e1a2c826229734341db8 100644 --- a/front/src/views/admin/library/TrackDetail.vue +++ b/front/src/views/admin/library/TrackDetail.vue @@ -1,22 +1,36 @@ <template> <main> - <div v-if="isLoading" class="ui vertical segment"> - <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> + <div + v-if="isLoading" + class="ui vertical segment" + > + <div :class="['ui', 'centered', 'active', 'inline', 'loader']" /> </div> <template v-if="object"> - <section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="object.title"> + <section + v-title="object.title" + :class="['ui', 'head', 'vertical', 'stripe', 'segment']" + > <div class="ui stackable one column grid"> <div class="ui column"> <div class="segment-content"> <h2 class="ui header"> - <img alt="" v-if="object.cover && object.cover.urls.medium_square_crop" v-lazy="$store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)"> - <img alt="" v-else src="../../../assets/audio/default-cover.png"> + <img + v-if="object.cover && object.cover.urls.medium_square_crop" + v-lazy="$store.getters['instance/absoluteUrl'](object.cover.urls.medium_square_crop)" + alt="" + > + <img + v-else + alt="" + src="../../../assets/audio/default-cover.png" + > <div class="content"> {{ object.title | truncate(100) }} <div class="sub header"> <template v-if="object.is_local"> <span class="ui tiny accent label"> - <i class="home icon"></i> + <i class="home icon" /> <translate translate-context="Content/Moderation/*/Short, Noun">Local</translate> </span> @@ -26,38 +40,69 @@ </h2> <template v-if="object.tags && object.tags.length > 0"> - <tags-list :limit="5" detail-route="manage.library.tags.detail" :tags="object.tags"></tags-list> - <div class="ui hidden divider"></div> + <tags-list + :limit="5" + detail-route="manage.library.tags.detail" + :tags="object.tags" + /> + <div class="ui hidden divider" /> </template> <div class="header-buttons"> - <div class="ui icon buttons"> - <router-link class="ui icon labeled button" :to="{name: 'library.tracks.detail', params: {id: object.id }}"> - <i class="info icon"></i> - <translate translate-context="Content/Moderation/Link/Verb">Open local profile</translate> + <router-link + class="ui icon labeled button" + :to="{name: 'library.tracks.detail', params: {id: object.id }}" + > + <i class="info icon" /> + <translate translate-context="Content/Moderation/Link/Verb"> + Open local profile + </translate> </router-link> - <button class="ui floating dropdown icon button" v-dropdown> - <i class="dropdown icon"></i> + <button + v-dropdown + class="ui floating dropdown icon button" + > + <i class="dropdown icon" /> <div class="menu"> <a v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser" class="basic item" :href="$store.getters['instance/absoluteUrl'](`/api/admin/music/track/${object.id}`)" - target="_blank" rel="noopener noreferrer"> - <i class="wrench icon"></i> + target="_blank" + rel="noopener noreferrer" + > + <i class="wrench icon" /> <translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate> </a> - <a class="basic item" v-if="object.mbid" :href="`https://musicbrainz.org/recording/${object.mbid}`" target="_blank" rel="noopener noreferrer"> - <i class="external icon"></i> + <a + v-if="object.mbid" + class="basic item" + :href="`https://musicbrainz.org/recording/${object.mbid}`" + target="_blank" + rel="noopener noreferrer" + > + <i class="external icon" /> <translate translate-context="Content/Moderation/Link/Verb">Open on MusicBrainz</translate> </a> - <fetch-button @refresh="fetchData" v-if="!object.is_local" class="basic item" :url="`tracks/${object.id}/fetches/`"> - <i class="refresh icon"></i> - <translate translate-context="Content/Moderation/Button/Verb">Refresh from remote server</translate> + <fetch-button + v-if="!object.is_local" + class="basic item" + :url="`tracks/${object.id}/fetches/`" + @refresh="fetchData" + > + <i class="refresh icon" /> + <translate translate-context="Content/Moderation/Button/Verb"> + Refresh from remote server + </translate> </fetch-button> - <a class="basic item" :href="object.url || object.fid" target="_blank" rel="noopener noreferrer"> - <i class="external icon"></i> + <a + class="basic item" + :href="object.url || object.fid" + target="_blank" + rel="noopener noreferrer" + > + <i class="external icon" /> <translate translate-context="Content/Moderation/Link/Verb">Open remote profile</translate> </a> </div> @@ -67,21 +112,39 @@ <router-link v-if="object.is_local" :to="{name: 'library.tracks.edit', params: {id: object.id }}" - class="ui labeled icon button"> - <i class="edit icon"></i> - <translate translate-context="Content/*/Button.Label/Verb">Edit</translate> + class="ui labeled icon button" + > + <i class="edit icon" /> + <translate translate-context="Content/*/Button.Label/Verb"> + Edit + </translate> </router-link> </div> <div class="ui buttons"> <dangerous-button :class="['ui', {loading: isLoading}, 'basic danger button']" - :action="remove"> - <translate translate-context="*/*/*/Verb">Delete</translate> - <p slot="modal-header"><translate translate-context="Popup/Library/Title">Delete this track?</translate></p> + :action="remove" + > + <translate translate-context="*/*/*/Verb"> + Delete + </translate> + <p slot="modal-header"> + <translate translate-context="Popup/Library/Title"> + Delete this track? + </translate> + </p> <div slot="modal-content"> - <p><translate translate-context="Content/Moderation/Paragraph">The track will be removed, as well as associated uploads, favorites and listening history. This action is irreversible.</translate></p> + <p> + <translate translate-context="Content/Moderation/Paragraph"> + The track will be removed, as well as associated uploads, favorites and listening history. This action is irreversible. + </translate> + </p> </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> </div> </div> @@ -94,16 +157,20 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="info icon"></i> + <i class="info icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Track data</translate> + <translate translate-context="Content/Moderation/Title"> + Track data + </translate> </div> </h3> <table class="ui very basic table"> <tbody> <tr> <td> - <translate translate-context="*/*/*/Noun">Title</translate> + <translate translate-context="*/*/*/Noun"> + Title + </translate> </td> <td> {{ object.title }} @@ -112,7 +179,9 @@ <tr v-if="object.album"> <td> <router-link :to="{name: 'manage.library.albums.detail', params: {id: object.album.id }}"> - <translate translate-context="*/*/*">Album</translate> + <translate translate-context="*/*/*"> + Album + </translate> </router-link> </td> <td> @@ -123,7 +192,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.artists.detail', params: {id: object.artist.id }}"> - <translate translate-context="*/*/*/Noun">Artist</translate> + <translate translate-context="*/*/*/Noun"> + Artist + </translate> </router-link> </td> <td> @@ -133,7 +204,9 @@ <tr v-if="object.album"> <td> <router-link :to="{name: 'manage.library.artists.detail', params: {id: object.album.artist.id }}"> - <translate translate-context="*/*/*/Noun">Album artist</translate> + <translate translate-context="*/*/*/Noun"> + Album artist + </translate> </router-link> </td> <td> @@ -142,7 +215,9 @@ </tr> <tr> <td> - <translate translate-context="*/*/*/Short, Noun">Position</translate> + <translate translate-context="*/*/*/Short, Noun"> + Position + </translate> </td> <td> {{ object.position }} @@ -150,7 +225,9 @@ </tr> <tr v-if="object.disc_number"> <td> - <translate translate-context="*/*/*/Noun">Disc number</translate> + <translate translate-context="*/*/*/Noun"> + Disc number + </translate> </td> <td> {{ object.disc_number }} @@ -158,13 +235,17 @@ </tr> <tr v-if="object.copyright"> <td> - <translate translate-context="Content/Track/*/Noun">Copyright</translate> + <translate translate-context="Content/Track/*/Noun"> + Copyright + </translate> </td> <td>{{ object.copyright }}</td> </tr> <tr v-if="object.license"> <td> - <translate translate-context="Content/*/*/Noun">License</translate> + <translate translate-context="Content/*/*/Noun"> + License + </translate> </td> <td> <router-link :to="{name: 'manage.library.tracks', query: {q: getQuery('license', object.license)}}"> @@ -175,7 +256,9 @@ <tr v-if="!object.is_local"> <td> <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}"> - <translate translate-context="Content/Moderation/*/Noun">Domain</translate> + <translate translate-context="Content/Moderation/*/Noun"> + Domain + </translate> </router-link> </td> <td> @@ -184,9 +267,11 @@ </tr> <tr v-if="object.description"> <td> - <translate translate-context="'*/*/*/Noun">Description</translate> + <translate translate-context="'*/*/*/Noun"> + Description + </translate> </td> - <td v-html="object.description.html"></td> + <td v-html="object.description.html" /> </tr> </tbody> </table> @@ -195,32 +280,43 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="feed icon"></i> + <i class="feed icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Activity</translate> - <span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span> - + <translate translate-context="Content/Moderation/Title"> + Activity + </translate> + <span :data-tooltip="labels.statsWarning"><i class="question circle icon" /></span> </div> </h3> - <div v-if="isLoadingStats" class="ui placeholder"> - <div class="full line"></div> - <div class="short line"></div> - <div class="medium line"></div> - <div class="long line"></div> + <div + v-if="isLoadingStats" + class="ui placeholder" + > + <div class="full line" /> + <div class="short line" /> + <div class="medium line" /> + <div class="long line" /> </div> - <table v-else class="ui very basic table"> + <table + v-else + class="ui very basic table" + > <tbody> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate> + <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)"> + First seen + </translate> </td> <td> - <human-date :date="object.creation_date"></human-date> + <human-date :date="object.creation_date" /> </td> </tr> <tr> <td> - <translate translate-context="*/*/*/Noun">Listenings</translate> + <translate translate-context="*/*/*/Noun"> + Listenings + </translate> </td> <td> {{ stats.listenings }} @@ -228,7 +324,9 @@ </tr> <tr> <td> - <translate translate-context="*/*/*">Favorited tracks</translate> + <translate translate-context="*/*/*"> + Favorited tracks + </translate> </td> <td> {{ stats.track_favorites }} @@ -236,7 +334,9 @@ </tr> <tr> <td> - <translate translate-context="*/*/*">Playlists</translate> + <translate translate-context="*/*/*"> + Playlists + </translate> </td> <td> {{ stats.playlists }} @@ -245,7 +345,9 @@ <tr> <td> <router-link :to="{name: 'manage.moderation.reports.list', query: {q: getQuery('target', `track:${object.id}`) }}"> - <translate translate-context="Content/Moderation/Table.Label/Noun">Linked reports</translate> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Linked reports + </translate> </router-link> </td> <td> @@ -255,7 +357,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.edits', query: {q: getQuery('target', 'track ' + object.id)}}"> - <translate translate-context="*/Admin/*/Noun">Edits</translate> + <translate translate-context="*/Admin/*/Noun"> + Edits + </translate> </router-link> </td> <td> @@ -269,25 +373,33 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="music icon"></i> + <i class="music icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Audio content</translate> - <span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span> - + <translate translate-context="Content/Moderation/Title"> + Audio content + </translate> + <span :data-tooltip="labels.statsWarning"><i class="question circle icon" /></span> </div> </h3> - <div v-if="isLoadingStats" class="ui placeholder"> - <div class="full line"></div> - <div class="short line"></div> - <div class="medium line"></div> - <div class="long line"></div> + <div + v-if="isLoadingStats" + class="ui placeholder" + > + <div class="full line" /> + <div class="short line" /> + <div class="medium line" /> + <div class="long line" /> </div> - <table v-else class="ui very basic table"> + <table + v-else + class="ui very basic table" + > <tbody> - <tr> <td> - <translate translate-context="Content/Moderation/Table.Label/Noun">Cached size</translate> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Cached size + </translate> </td> <td> {{ stats.media_downloaded_size | humanSize }} @@ -295,7 +407,9 @@ </tr> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label">Total size</translate> + <translate translate-context="Content/Moderation/Table.Label"> + Total size + </translate> </td> <td> {{ stats.media_total_size | humanSize }} @@ -305,7 +419,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('track_id', object.id) }}"> - <translate translate-context="*/*/*/Noun">Libraries</translate> + <translate translate-context="*/*/*/Noun"> + Libraries + </translate> </router-link> </td> <td> @@ -315,7 +431,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('track_id', object.id) }}"> - <translate translate-context="*/*/*">Uploads</translate> + <translate translate-context="*/*/*"> + Uploads + </translate> </router-link> </td> <td> @@ -324,78 +442,74 @@ </tr> </tbody> </table> - </section> </div> </div> </div> - </template> </main> </template> <script> -import axios from "axios" -import logger from "@/logging" -import FetchButton from "@/components/federation/FetchButton" -import TagsList from "@/components/tags/List" - +import axios from 'axios' +import FetchButton from '@/components/federation/FetchButton' +import TagsList from '@/components/tags/List' export default { - props: ["id"], components: { FetchButton, TagsList }, - data() { + props: { id: { type: Number, required: true } }, + data () { return { isLoading: true, isLoadingStats: false, object: null, - stats: null, + stats: null + } + }, + computed: { + labels () { + return { + statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object') + } } }, - created() { + created () { this.fetchData() this.fetchStats() }, methods: { - fetchData() { - var self = this + fetchData () { + const self = this this.isLoading = true - let url = `manage/library/tracks/${this.id}/` + const url = `manage/library/tracks/${this.id}/` axios.get(url).then(response => { self.object = response.data self.isLoading = false }) }, - fetchStats() { - var self = this + fetchStats () { + const self = this this.isLoadingStats = true - let url = `manage/library/tracks/${this.id}/stats/` + const url = `manage/library/tracks/${this.id}/stats/` axios.get(url).then(response => { self.stats = response.data self.isLoadingStats = false }) }, remove () { - var self = this + const self = this this.isLoading = true - let url = `manage/library/tracks/${this.id}/` + const url = `manage/library/tracks/${this.id}/` axios.delete(url).then(response => { - self.$router.push({name: 'manage.library.tracks'}) + self.$router.push({ name: 'manage.library.tracks' }) }) }, getQuery (field, value) { return `${field}:"${value}"` } - }, - computed: { - labels() { - return { - statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object'), - } - }, } } </script> diff --git a/front/src/views/admin/library/TracksList.vue b/front/src/views/admin/library/TracksList.vue index 3aefc86060af1a12aa006136f49c224f27b2b7af..4588a4e5b976aa2e9c2c811f9d6d212046dec15e 100644 --- a/front/src/views/admin/library/TracksList.vue +++ b/front/src/views/admin/library/TracksList.vue @@ -1,25 +1,30 @@ <template> <main v-title="labels.title"> <section class="ui vertical stripe segment"> - <h2 class="ui header">{{ labels.title }}</h2> - <div class="ui hidden divider"></div> - <tracks-table :update-url="true" :default-query="defaultQuery"></tracks-table> + <h2 class="ui header"> + {{ labels.title }} + </h2> + <div class="ui hidden divider" /> + <tracks-table + :update-url="true" + :default-query="defaultQuery" + /> </section> </main> </template> <script> -import TracksTable from "@/components/manage/library/TracksTable" +import TracksTable from '@/components/manage/library/TracksTable' export default { components: { TracksTable }, props: { - defaultQuery: {type: String, required: false}, + defaultQuery: { type: String, required: false, default: '' } }, computed: { - labels() { + labels () { return { title: this.$pgettext('*/*/*', 'Tracks') } diff --git a/front/src/views/admin/library/UploadDetail.vue b/front/src/views/admin/library/UploadDetail.vue index b5a8c522b9d65b2a340d799aa23446c9afb0df09..b332c0ec334469bc59466cb12e6d59b92365eed2 100644 --- a/front/src/views/admin/library/UploadDetail.vue +++ b/front/src/views/admin/library/UploadDetail.vue @@ -1,22 +1,31 @@ <template> <main> - <div v-if="isLoading" class="ui vertical segment"> - <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> + <div + v-if="isLoading" + class="ui vertical segment" + > + <div :class="['ui', 'centered', 'active', 'inline', 'loader']" /> </div> <template v-if="object"> - <import-status-modal :upload="object" :show.sync="showUploadDetailModal" /> - <section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="displayName(object)"> + <import-status-modal + :upload="object" + :show.sync="showUploadDetailModal" + /> + <section + v-title="displayName(object)" + :class="['ui', 'head', 'vertical', 'stripe', 'segment']" + > <div class="ui stackable one column grid"> <div class="ui column"> <div class="segment-content"> <h2 class="ui header"> - <i class="circular inverted file icon"></i> + <i class="circular inverted file icon" /> <div class="content"> {{ displayName(object) | truncate(100) }} <div class="sub header"> <template v-if="object.is_local"> <span class="ui tiny accent label"> - <i class="home icon"></i> + <i class="home icon" /> <translate translate-context="Content/Moderation/*/Short, Noun">Local</translate> </span> @@ -25,50 +34,82 @@ </div> </h2> <div class="header-buttons"> - <div class="ui icon buttons"> <a v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser" class="ui labeled icon button" :href="$store.getters['instance/absoluteUrl'](`/api/admin/music/upload/${object.id}`)" - target="_blank" rel="noopener noreferrer"> - <i class="wrench icon"></i> + target="_blank" + rel="noopener noreferrer" + > + <i class="wrench icon" /> <translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate> </a> - <button class="ui floating dropdown icon button" v-dropdown> - <i class="dropdown icon"></i> + <button + v-dropdown + class="ui floating dropdown icon button" + > + <i class="dropdown icon" /> <div class="menu"> <a v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser" class="basic item" :href="$store.getters['instance/absoluteUrl'](`/api/admin/music/upload/${object.id}`)" - target="_blank" rel="noopener noreferrer"> - <i class="wrench icon"></i> + target="_blank" + rel="noopener noreferrer" + > + <i class="wrench icon" /> <translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate> </a> - <a class="basic item" :href="object.url || object.fid" target="_blank" rel="noopener noreferrer"> - <i class="external icon"></i> + <a + class="basic item" + :href="object.url || object.fid" + target="_blank" + rel="noopener noreferrer" + > + <i class="external icon" /> <translate translate-context="Content/Moderation/Link/Verb">Open remote profile</translate> </a> </div> </button> </div> <div class="ui buttons"> - <a class="ui labeled icon button" v-if="object.audio_file" :href="$store.getters['instance/absoluteUrl'](object.audio_file)" target="_blank" rel="noopener noreferrer"> - <i class="download icon"></i> + <a + v-if="object.audio_file" + class="ui labeled icon button" + :href="$store.getters['instance/absoluteUrl'](object.audio_file)" + target="_blank" + rel="noopener noreferrer" + > + <i class="download icon" /> <translate translate-context="Content/Track/Link/Verb">Download</translate> </a> </div> <div class="ui buttons"> <dangerous-button :class="['ui', {loading: isLoading}, 'basic danger button']" - :action="remove"> - <translate translate-context="*/*/*/Verb">Delete</translate> - <p slot="modal-header"><translate translate-context="Popup/Library/Title">Delete this upload?</translate></p> + :action="remove" + > + <translate translate-context="*/*/*/Verb"> + Delete + </translate> + <p slot="modal-header"> + <translate translate-context="Popup/Library/Title"> + Delete this upload? + </translate> + </p> <div slot="modal-content"> - <p><translate translate-context="Content/Moderation/Paragraph">The upload will be removed. This action is irreversible.</translate></p> + <p> + <translate translate-context="Content/Moderation/Paragraph"> + The upload will be removed. This action is irreversible. + </translate> + </p> </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> </div> </div> @@ -81,16 +122,20 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="info icon"></i> + <i class="info icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Upload data</translate> + <translate translate-context="Content/Moderation/Title"> + Upload data + </translate> </div> </h3> <table class="ui very basic table"> <tbody> <tr> <td> - <translate translate-context="*/*/*/Noun">Name</translate> + <translate translate-context="*/*/*/Noun"> + Name + </translate> </td> <td> {{ displayName(object) }} @@ -99,7 +144,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('privacy_level', object.library.privacy_level) }}"> - <translate translate-context="*/*/*">Visibility</translate> + <translate translate-context="*/*/*"> + Visibility + </translate> </router-link> </td> <td> @@ -109,7 +156,9 @@ <tr> <td> <router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: object.library.actor.full_username }}"> - <translate translate-context="*/*/*/Noun">Account</translate> + <translate translate-context="*/*/*/Noun"> + Account + </translate> </router-link> </td> <td> @@ -119,7 +168,9 @@ <tr v-if="!object.is_local"> <td> <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}"> - <translate translate-context="Content/Moderation/*/Noun">Domain</translate> + <translate translate-context="Content/Moderation/*/Noun"> + Domain + </translate> </router-link> </td> <td> @@ -129,20 +180,28 @@ <tr> <td> <router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('status', object.import_status) }}"> - <translate translate-context="Content/*/*/Noun">Import status</translate> + <translate translate-context="Content/*/*/Noun"> + Import status + </translate> </router-link> </td> <td> {{ sharedLabels.fields.import_status.choices[object.import_status].label }} - <button class="ui tiny basic icon button" :title="sharedLabels.fields.import_status.detailTitle" @click="detailedUpload = object; showUploadDetailModal = true"> - <i class="question circle outline icon"></i> + <button + class="ui tiny basic icon button" + :title="sharedLabels.fields.import_status.detailTitle" + @click="detailedUpload = object; showUploadDetailModal = true" + > + <i class="question circle outline icon" /> </button> </td> </tr> <tr> <td> <router-link :to="{name: 'manage.library.libraries.detail', params: {id: object.library.uuid }}"> - <translate translate-context="*/*/*/Noun">Library</translate> + <translate translate-context="*/*/*/Noun"> + Library + </translate> </router-link> </td> <td> @@ -156,28 +215,42 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="feed icon"></i> + <i class="feed icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Activity</translate> + <translate translate-context="Content/Moderation/Title"> + Activity + </translate> </div> </h3> <table class="ui very basic table"> <tbody> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate> + <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)"> + First seen + </translate> </td> <td> - <human-date :date="object.creation_date"></human-date> + <human-date :date="object.creation_date" /> </td> </tr> <tr> <td> - <translate translate-context="Content/*/*/Noun">Accessed date</translate> + <translate translate-context="Content/*/*/Noun"> + Accessed date + </translate> </td> <td> - <human-date v-if="object.accessed_date" :date="object.accessed_date"></human-date> - <translate v-else translate-context="*/*/*">N/A</translate> + <human-date + v-if="object.accessed_date" + :date="object.accessed_date" + /> + <translate + v-else + translate-context="*/*/*" + > + N/A + </translate> </td> </tr> </tbody> @@ -187,9 +260,11 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="music icon"></i> + <i class="music icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Audio content</translate> + <translate translate-context="Content/Moderation/Title"> + Audio content + </translate> </div> </h3> <table class="ui very basic table"> @@ -197,7 +272,9 @@ <tr v-if="object.track"> <td> <router-link :to="{name: 'manage.library.tracks.detail', params: {id: object.track.id }}"> - <translate translate-context="*/*/*/Noun">Track</translate> + <translate translate-context="*/*/*/Noun"> + Track + </translate> </router-link> </td> <td> @@ -206,18 +283,27 @@ </tr> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label/Noun">Cached size</translate> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Cached size + </translate> </td> <td> <template v-if="object.audio_file"> {{ object.size | humanSize }} </template> - <translate v-else translate-context="*/*/*">N/A</translate> + <translate + v-else + translate-context="*/*/*" + > + N/A + </translate> </td> </tr> <tr> <td> - <translate translate-context="Content/*/*/Noun">Size</translate> + <translate translate-context="Content/*/*/Noun"> + Size + </translate> </td> <td> {{ object.size | humanSize }} @@ -225,37 +311,58 @@ </tr> <tr> <td> - <translate translate-context="Content/Track/*/Noun">Bitrate</translate> + <translate translate-context="Content/Track/*/Noun"> + Bitrate + </translate> </td> <td> <template v-if="object.bitrate"> {{ object.bitrate | humanSize }}/s </template> - <translate v-else translate-context="*/*/*">N/A</translate> + <translate + v-else + translate-context="*/*/*" + > + N/A + </translate> </td> </tr> <tr> <td> - <translate translate-context="Content/*/*">Duration</translate> + <translate translate-context="Content/*/*"> + Duration + </translate> </td> <td> <template v-if="object.duration"> {{ object.duration | duration }} </template> - <translate v-else translate-context="*/*/*">N/A</translate> + <translate + v-else + translate-context="*/*/*" + > + N/A + </translate> </td> </tr> <tr> <td> <router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('type', object.mimetype) }}"> - <translate translate-context="Content/Track/Table.Label/Noun">Type</translate> + <translate translate-context="Content/Track/Table.Label/Noun"> + Type + </translate> </router-link> </td> <td> <template v-if="object.mimetype"> {{ object.mimetype }} </template> - <translate v-else translate-context="*/*/*">N/A</translate> + <translate + v-else + translate-context="*/*/*" + > + N/A + </translate> </td> </tr> </tbody> @@ -264,56 +371,60 @@ </div> </div> </div> - </template> </main> </template> <script> -import axios from "axios" -import logger from "@/logging" -import TranslationsMixin from "@/components/mixins/Translations" +import axios from 'axios' +import TranslationsMixin from '@/components/mixins/Translations' import ImportStatusModal from '@/components/library/ImportStatusModal' import time from '@/utils/time' - export default { - props: ["id"], - mixins: [ - TranslationsMixin, - ], components: { ImportStatusModal }, - data() { + mixins: [ + TranslationsMixin + ], + props: { id: { type: Number, required: true } }, + data () { return { time, detailedUpload: null, showUploadDetailModal: false, isLoading: true, object: null, - stats: null, + stats: null } }, - created() { + computed: { + labels () { + return { + statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object') + } + } + }, + created () { this.fetchData() }, methods: { - fetchData() { - var self = this + fetchData () { + const self = this this.isLoading = true - let url = `manage/library/uploads/${this.id}/` + const url = `manage/library/uploads/${this.id}/` axios.get(url).then(response => { self.object = response.data self.isLoading = false }) }, remove () { - var self = this + const self = this this.isLoading = true - let url = `manage/library/uploads/${this.id}/` + const url = `manage/library/uploads/${this.id}/` axios.delete(url).then(response => { - self.$router.push({name: 'manage.library.uploads'}) + self.$router.push({ name: 'manage.library.uploads' }) }) }, getQuery (field, value) { @@ -328,13 +439,6 @@ export default { } return upload.uuid } - }, - computed: { - labels() { - return { - statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object'), - } - }, } } </script> diff --git a/front/src/views/admin/library/UploadsList.vue b/front/src/views/admin/library/UploadsList.vue index 0d4d7b5e3c2975914ebbb05f3a5d3e26e044cd6a..2e2a5d77812b86852b2b35843800ce084bb993a7 100644 --- a/front/src/views/admin/library/UploadsList.vue +++ b/front/src/views/admin/library/UploadsList.vue @@ -1,25 +1,30 @@ <template> <main v-title="labels.title"> <section class="ui vertical stripe segment"> - <h2 class="ui header">{{ labels.title }}</h2> - <div class="ui hidden divider"></div> - <uploads-table :update-url="true" :default-query="defaultQuery"></uploads-table> + <h2 class="ui header"> + {{ labels.title }} + </h2> + <div class="ui hidden divider" /> + <uploads-table + :update-url="true" + :default-query="defaultQuery" + /> </section> </main> </template> <script> -import UploadsTable from "@/components/manage/library/UploadsTable" +import UploadsTable from '@/components/manage/library/UploadsTable' export default { components: { UploadsTable }, props: { - defaultQuery: {type: String, required: false}, + defaultQuery: { type: String, required: false, default: '' } }, computed: { - labels() { + labels () { return { title: this.$pgettext('*/*/*', 'Uploads') } diff --git a/front/src/views/admin/moderation/AccountsDetail.vue b/front/src/views/admin/moderation/AccountsDetail.vue index 0a78f345adbf061efdb14c8779a321a44423ef74..03d6d8ec0465923538aae0ff95a1a3f0cf617694 100644 --- a/front/src/views/admin/moderation/AccountsDetail.vue +++ b/front/src/views/admin/moderation/AccountsDetail.vue @@ -1,28 +1,38 @@ <template> <main class="page-admin-account-detail"> - <div v-if="isLoading" class="ui vertical segment"> - <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> + <div + v-if="isLoading" + class="ui vertical segment" + > + <div :class="['ui', 'centered', 'active', 'inline', 'loader']" /> </div> <template v-if="object"> - <section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="object.full_username"> + <section + v-title="object.full_username" + :class="['ui', 'head', 'vertical', 'stripe', 'segment']" + > <div class="ui stackable two column grid"> <div class="ui column"> <div class="segment-content"> <h2 class="ui header"> - <i class="circular inverted user icon"></i> + <i class="circular inverted user icon" /> <div class="content"> {{ object.full_username }} <div class="sub header"> <template v-if="object.user"> <span class="ui tiny accent label"> - <i class="home icon"></i> + <i class="home icon" /> <translate translate-context="Content/Moderation/*/Short, Noun">Local account</translate> </span> </template> - <a :href="object.url || object.fid" target="_blank" rel="noopener noreferrer"> + <a + :href="object.url || object.fid" + target="_blank" + rel="noopener noreferrer" + > <translate translate-context="Content/Moderation/Link/Verb">Open profile</translate> - <i class="external icon"></i> + <i class="external icon" /> </a> </div> </div> @@ -33,23 +43,35 @@ v-if="object.user && $store.state.auth.profile && $store.state.auth.profile.is_superuser" class="ui labeled icon button" :href="$store.getters['instance/absoluteUrl'](`/api/admin/users/user/${object.user.id}`)" - target="_blank" rel="noopener noreferrer"> - <i class="wrench icon"></i> + target="_blank" + rel="noopener noreferrer" + > + <i class="wrench icon" /> <translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate> </a> <a v-else-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser" class="ui labeled icon button" :href="$store.getters['instance/absoluteUrl'](`/api/admin/federation/actor/${object.id}`)" - target="_blank" rel="noopener noreferrer"> - <i class="wrench icon"></i> + target="_blank" + rel="noopener noreferrer" + > + <i class="wrench icon" /> <translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate> </a> - <button class="ui floating dropdown icon button" v-dropdown> - <i class="dropdown icon"></i> + <button + v-dropdown + class="ui floating dropdown icon button" + > + <i class="dropdown icon" /> <div class="menu"> - <a class="basic item" :href="object.url || object.fid" target="_blank" rel="noopener noreferrer"> - <i class="external icon"></i> + <a + class="basic item" + :href="object.url || object.fid" + target="_blank" + rel="noopener noreferrer" + > + <i class="external icon" /> <translate translate-context="Content/Moderation/Link/Verb">Open remote profile</translate> </a> </div> @@ -59,41 +81,64 @@ </div> </div> <div class="ui column"> - <div v-if="!object.user" class="ui compact clearing placeholder segment component-placeholder"> + <div + v-if="!object.user" + class="ui compact clearing placeholder segment component-placeholder" + > <template v-if="isLoadingPolicy"> <div class="paragraph"> - <div class="line"></div> - <div class="line"></div> - <div class="line"></div> - <div class="line"></div> - <div class="line"></div> + <div class="line" /> + <div class="line" /> + <div class="line" /> + <div class="line" /> + <div class="line" /> </div> </template> <template v-else-if="!policy && !showPolicyForm"> <header class="ui header"> <h3> - <i class="shield icon"></i> - <translate translate-context="Content/Moderation/Card.Title">You don't have any rule in place for this account.</translate> + <i class="shield icon" /> + <translate translate-context="Content/Moderation/Card.Title"> + You don't have any rule in place for this account. + </translate> </h3> </header> - <p><translate translate-context="Content/Moderation/Card.Paragraph">Moderation policies help you control how your instance interact with a given domain or account.</translate></p> - <button @click="showPolicyForm = true" class="ui primary button"><translate translate-context="Content/Moderation/Button/Verb">Add a moderation policy</translate></button> + <p> + <translate translate-context="Content/Moderation/Card.Paragraph"> + Moderation policies help you control how your instance interact with a given domain or account. + </translate> + </p> + <button + class="ui primary button" + @click="showPolicyForm = true" + > + <translate translate-context="Content/Moderation/Button/Verb"> + Add a moderation policy + </translate> + </button> </template> - <instance-policy-card v-else-if="policy && !showPolicyForm" :object="policy" @update="showPolicyForm = true"> + <instance-policy-card + v-else-if="policy && !showPolicyForm" + :object="policy" + @update="showPolicyForm = true" + > <header class="ui header"> <h3> - <translate translate-context="Content/Moderation/Card.Title">This domain is subject to specific moderation rules</translate> + <translate translate-context="Content/Moderation/Card.Title"> + This domain is subject to specific moderation rules + </translate> </h3> </header> </instance-policy-card> <instance-policy-form v-else-if="showPolicyForm" + :object="policy" + type="actor" + :target="object.full_username" @cancel="showPolicyForm = false" @save="updatePolicy" @delete="policy = null; showPolicyForm = false" - :object="policy" - type="actor" - :target="object.full_username" /> + /> </div> </div> </div> @@ -103,16 +148,20 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="info icon"></i> + <i class="info icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Account data</translate> + <translate translate-context="Content/Moderation/Title"> + Account data + </translate> </div> </h3> <table class="ui very basic table"> <tbody> <tr> <td> - <translate translate-context="Content/*/*">Username</translate> + <translate translate-context="Content/*/*"> + Username + </translate> </td> <td> {{ object.preferred_username }} @@ -121,7 +170,9 @@ <tr v-if="!object.user"> <td> <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}"> - <translate translate-context="Content/Moderation/*/Noun">Domain</translate> + <translate translate-context="Content/Moderation/*/Noun"> + Domain + </translate> </router-link> </td> <td> @@ -130,7 +181,9 @@ </tr> <tr> <td> - <translate translate-context="'Content/*/*/Noun'">Display name</translate> + <translate translate-context="'Content/*/*/Noun'"> + Display name + </translate> </td> <td> {{ object.name }} @@ -138,7 +191,9 @@ </tr> <tr v-if="object.user"> <td> - <translate translate-context="Content/*/*">Email address</translate> + <translate translate-context="Content/*/*"> + Email address + </translate> </td> <td> {{ object.user.email }} @@ -146,41 +201,79 @@ </tr> <tr v-if="object.user"> <td> - <translate translate-context="Content/*/*/Noun">Login status</translate> + <translate translate-context="Content/*/*/Noun"> + Login status + </translate> </td> <td> - <div class="ui toggle checkbox" v-if="object.user.username != $store.state.auth.profile.username"> + <div + v-if="object.user.username != $store.state.auth.profile.username" + class="ui toggle checkbox" + > <input id="is-active" + v-model="object.user.is_active" + type="checkbox" @change="updateUser('is_active')" - v-model="object.user.is_active" type="checkbox"> + > <label for="is-active"> - <translate v-if="object.user.is_active" key="1" translate-context="*/*/*/State of feature">Enabled</translate> - <translate v-else key="2" translate-context="*/*/*/State of feature">Disabled</translate> + <translate + v-if="object.user.is_active" + key="1" + translate-context="*/*/*/State of feature" + >Enabled</translate> + <translate + v-else + key="2" + translate-context="*/*/*/State of feature" + >Disabled</translate> </label> </div> - <translate v-else-if="object.user.is_active" key="1" translate-context="*/*/*/State of feature">Enabled</translate> - <translate v-else key="2" translate-context="*/*/*/State of feature">Disabled</translate> + <translate + v-else-if="object.user.is_active" + key="1" + translate-context="*/*/*/State of feature" + > + Enabled + </translate> + <translate + v-else + key="2" + translate-context="*/*/*/State of feature" + > + Disabled + </translate> </td> </tr> <tr v-if="object.user"> <td> - <translate translate-context="Content/*/*/Noun">Permissions</translate> + <translate translate-context="Content/*/*/Noun"> + Permissions + </translate> </td> <td> <select - @change="updateUser('permissions')" v-model="permissions" multiple - class="ui search selection dropdown"> - <option v-for="p in allPermissions" :value="p.code">{{ p.label }}</option> + class="ui search selection dropdown" + @change="updateUser('permissions')" + > + <option + v-for="(p, key) in allPermissions" + :key="key" + :value="p.code" + > + {{ p.label }} + </option> </select> - <action-feedback :is-loading="updating.permissions"></action-feedback> + <action-feedback :is-loading="updating.permissions" /> </td> </tr> <tr> <td> - <translate translate-context="Content/Track/Table.Label/Noun">Type</translate> + <translate translate-context="Content/Track/Table.Label/Noun"> + Type + </translate> </td> <td> {{ object.type }} @@ -188,27 +281,41 @@ </tr> <tr v-if="!object.user"> <td> - <translate translate-context="Content/*/Table.Label">Last checked</translate> + <translate translate-context="Content/*/Table.Label"> + Last checked + </translate> </td> <td> - <human-date v-if="object.last_fetch_date" :date="object.last_fetch_date"></human-date> - <translate v-else translate-context="*/*/*">N/A</translate> + <human-date + v-if="object.last_fetch_date" + :date="object.last_fetch_date" + /> + <translate + v-else + translate-context="*/*/*" + > + N/A + </translate> </td> </tr> <tr v-if="object.user"> <td> - <translate translate-context="Content/Admin/Table.Label/Noun">Sign-up date</translate> + <translate translate-context="Content/Admin/Table.Label/Noun"> + Sign-up date + </translate> </td> <td> - <human-date :date="object.user.date_joined"></human-date> + <human-date :date="object.user.date_joined" /> </td> </tr> <tr v-if="object.user"> <td> - <translate translate-context="Content/Profile/Table.Label/Short, Noun (Value is a date)">Last activity</translate> + <translate translate-context="Content/Profile/Table.Label/Short, Noun (Value is a date)"> + Last activity + </translate> </td> <td> - <human-date :date="object.user.last_activity"></human-date> + <human-date :date="object.user.last_activity" /> </td> </tr> </tbody> @@ -218,57 +325,74 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="feed icon"></i> + <i class="feed icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Activity</translate> - <span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span> - + <translate translate-context="Content/Moderation/Title"> + Activity + </translate> + <span :data-tooltip="labels.statsWarning"><i class="question circle icon" /></span> </div> </h3> - <div v-if="isLoadingStats" class="ui placeholder"> - <div class="full line"></div> - <div class="short line"></div> - <div class="medium line"></div> - <div class="long line"></div> + <div + v-if="isLoadingStats" + class="ui placeholder" + > + <div class="full line" /> + <div class="short line" /> + <div class="medium line" /> + <div class="long line" /> </div> - <table v-else class="ui very basic table"> + <table + v-else + class="ui very basic table" + > <tbody> <tr v-if="!object.user"> <td> - <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate> + <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)"> + First seen + </translate> </td> <td> - <human-date :date="object.creation_date"></human-date> + <human-date :date="object.creation_date" /> </td> </tr> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label/Noun">Emitted messages</translate> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Emitted messages + </translate> </td> <td> - {{ stats.outbox_activities}} + {{ stats.outbox_activities }} </td> </tr> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label/Noun">Received library follows</translate> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Received library follows + </translate> </td> <td> - {{ stats.received_library_follows}} + {{ stats.received_library_follows }} </td> </tr> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label/Noun">Emitted library follows</translate> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Emitted library follows + </translate> </td> <td> - {{ stats.emitted_library_follows}} + {{ stats.emitted_library_follows }} </td> </tr> <tr> <td> <router-link :to="{name: 'manage.moderation.reports.list', query: {q: getQuery('target', `account:${object.full_username}`) }}"> - <translate translate-context="Content/Moderation/Table.Label/Noun">Linked reports</translate> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Linked reports + </translate> </router-link> </td> <td> @@ -278,7 +402,9 @@ <tr> <td> <router-link :to="{name: 'manage.moderation.requests.list', query: {q: getQuery('submitter', `${object.full_username}`) }}"> - <translate translate-context="Content/Moderation/Table.Label/Noun">Requests</translate> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Requests + </translate> </router-link> </td> <td> @@ -292,25 +418,33 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="music icon"></i> + <i class="music icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Audio content</translate> - <span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span> - + <translate translate-context="Content/Moderation/Title"> + Audio content + </translate> + <span :data-tooltip="labels.statsWarning"><i class="question circle icon" /></span> </div> </h3> - <div v-if="isLoadingStats" class="ui placeholder"> - <div class="full line"></div> - <div class="short line"></div> - <div class="medium line"></div> - <div class="long line"></div> + <div + v-if="isLoadingStats" + class="ui placeholder" + > + <div class="full line" /> + <div class="short line" /> + <div class="medium line" /> + <div class="long line" /> </div> - <table v-else class="ui very basic table"> + <table + v-else + class="ui very basic table" + > <tbody> - <tr v-if="!object.user"> <td> - <translate translate-context="Content/Moderation/Table.Label/Noun">Cached size</translate> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Cached size + </translate> </td> <td> {{ stats.media_downloaded_size | humanSize }} @@ -318,27 +452,38 @@ </tr> <tr v-if="object.user"> <td> - <translate translate-context="*/*/*" >Upload quota</translate> - <span :data-tooltip="labels.uploadQuota"><i class="question circle icon"></i></span> + <translate translate-context="*/*/*"> + Upload quota + </translate> + <span :data-tooltip="labels.uploadQuota"><i class="question circle icon" /></span> </td> <td> <div class="ui right labeled input"> <input - @change="updateUser('upload_quota', true)" v-model.number="object.user.upload_quota" step="100" name="quota" - type="number" /> + type="number" + @change="updateUser('upload_quota', true)" + > <div class="ui basic label"> - <translate translate-context="Content/*/*/Unit">MB</translate>  + <translate translate-context="Content/*/*/Unit"> + MB + </translate>  </div> - <action-feedback class="ui basic label" size="tiny" :is-loading="updating.upload_quota"></action-feedback> + <action-feedback + class="ui basic label" + size="tiny" + :is-loading="updating.upload_quota" + /> </div> </td> </tr> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label">Total size</translate> + <translate translate-context="Content/Moderation/Table.Label"> + Total size + </translate> </td> <td> {{ stats.media_total_size | humanSize }} @@ -347,7 +492,9 @@ <tr> <td> <router-link :to="{name: 'manage.channels', query: {q: getQuery('account', object.full_username) }}"> - <translate translate-context="*/*/*">Channels</translate> + <translate translate-context="*/*/*"> + Channels + </translate> </router-link> </td> <td> @@ -357,7 +504,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('account', object.full_username) }}"> - <translate translate-context="*/*/*/Noun">Libraries</translate> + <translate translate-context="*/*/*/Noun"> + Libraries + </translate> </router-link> </td> <td> @@ -367,7 +516,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('account', object.full_username) }}"> - <translate translate-context="*/*/*">Uploads</translate> + <translate translate-context="*/*/*"> + Uploads + </translate> </router-link> </td> <td> @@ -376,7 +527,9 @@ </tr> <tr> <td> - <translate translate-context="*/*/*/Noun">Artists</translate> + <translate translate-context="*/*/*/Noun"> + Artists + </translate> </td> <td> {{ stats.artists }} @@ -384,15 +537,19 @@ </tr> <tr> <td> - <translate translate-context="*/*/*">Albums</translate> + <translate translate-context="*/*/*"> + Albums + </translate> </td> <td> - {{ stats.albums}} + {{ stats.albums }} </td> </tr> <tr> <td> - <translate translate-context="*/*/*">Tracks</translate> + <translate translate-context="*/*/*"> + Tracks + </translate> </td> <td> {{ stats.tracks }} @@ -400,32 +557,30 @@ </tr> </tbody> </table> - </section> </div> </div> </div> - </template> </main> </template> <script> -import axios from "axios" -import logger from "@/logging" +import axios from 'axios' +import logger from '@/logging' import lodash from '@/lodash' -import $ from "jquery" +import $ from 'jquery' -import InstancePolicyForm from "@/components/manage/moderation/InstancePolicyForm" -import InstancePolicyCard from "@/components/manage/moderation/InstancePolicyCard" +import InstancePolicyForm from '@/components/manage/moderation/InstancePolicyForm' +import InstancePolicyCard from '@/components/manage/moderation/InstancePolicyCard' export default { - props: ["id"], components: { InstancePolicyForm, - InstancePolicyCard, + InstancePolicyCard }, - data() { + props: { id: { type: Number, required: true } }, + data () { return { lodash, isLoading: true, @@ -437,19 +592,50 @@ export default { permissions: [], updating: { permissions: false, - upload_quota: false, + upload_quota: false } } }, - created() { + computed: { + labels () { + return { + statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this account'), + uploadQuota: this.$pgettext('Content/Moderation/Help text', 'Determine how much content the user can upload. Leave empty to use the default value of the instance.') + } + }, + allPermissions () { + return [ + { + code: 'library', + label: this.$pgettext('*/*/*/Noun', 'Library') + }, + { + code: 'moderation', + label: this.$pgettext('*/Moderation/*', 'Moderation') + }, + { + code: 'settings', + label: this.$pgettext('*/*/*/Noun', 'Settings') + } + ] + } + }, + watch: { + object () { + this.$nextTick(() => { + $(this.$el).find('select.dropdown').dropdown() + }) + } + }, + created () { this.fetchData() this.fetchStats() }, methods: { - fetchData() { - var self = this + fetchData () { + const self = this this.isLoading = true - let url = "manage/accounts/" + this.id + "/" + const url = 'manage/accounts/' + this.id + '/' axios.get(url).then(response => { self.object = response.data self.isLoading = false @@ -465,19 +651,19 @@ export default { } }) }, - fetchPolicy(id) { - var self = this + fetchPolicy (id) { + const self = this this.isLoadingPolicy = true - let url = `manage/moderation/instance-policies/${id}/` + const url = `manage/moderation/instance-policies/${id}/` axios.get(url).then(response => { self.policy = response.data self.isLoadingPolicy = false }) }, - fetchStats() { - var self = this + fetchStats () { + const self = this this.isLoadingStats = true - let url = "manage/accounts/" + this.id + "/stats/" + const url = 'manage/accounts/' + this.id + '/stats/' axios.get(url).then(response => { self.stats = response.data self.isLoadingStats = false @@ -488,18 +674,18 @@ export default { this.object.nodeinfo_fetch_date = new Date() }, - updateUser(attr, toNull) { + updateUser (attr, toNull) { let newValue = this.object.user[attr] if (toNull && !newValue) { newValue = null } - let self = this + const self = this this.updating[attr] = true - let params = {} - if (attr === "permissions") { - params["permissions"] = {} + const params = {} + if (attr === 'permissions') { + params.permissions = {} this.allPermissions.forEach(p => { - params["permissions"][p.code] = this.permissions.indexOf(p.code) > -1 + params.permissions[p.code] = this.permissions.indexOf(p.code) > -1 }) } else { params[attr] = newValue @@ -523,37 +709,6 @@ export default { getQuery (field, value) { return `${field}:"${value}"` } - }, - computed: { - labels() { - return { - statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this account'), - uploadQuota: this.$pgettext('Content/Moderation/Help text', 'Determine how much content the user can upload. Leave empty to use the default value of the instance.'), - } - }, - allPermissions() { - return [ - { - code: "library", - label: this.$pgettext('*/*/*/Noun', "Library") - }, - { - code: "moderation", - label: this.$pgettext('*/Moderation/*', "Moderation") - }, - { - code: "settings", - label: this.$pgettext('*/*/*/Noun', "Settings") - } - ] - } - }, - watch: { - object () { - this.$nextTick(() => { - $(this.$el).find("select.dropdown").dropdown() - }) - } } } </script> diff --git a/front/src/views/admin/moderation/AccountsList.vue b/front/src/views/admin/moderation/AccountsList.vue index 1fbc57ae61661bd75f7c8def517a19ef94f17b28..559694464534154abe4092ec99286a54e583770a 100644 --- a/front/src/views/admin/moderation/AccountsList.vue +++ b/front/src/views/admin/moderation/AccountsList.vue @@ -1,27 +1,34 @@ <template> <main v-title="labels.accounts"> <section class="ui vertical stripe segment"> - <h2 class="ui header"><translate translate-context="*/Moderation/Title">Accounts</translate></h2> - <div class="ui hidden divider"></div> - <accounts-table :update-url="true" :default-query="defaultQuery"></accounts-table> + <h2 class="ui header"> + <translate translate-context="*/Moderation/Title"> + Accounts + </translate> + </h2> + <div class="ui hidden divider" /> + <accounts-table + :update-url="true" + :default-query="defaultQuery" + /> </section> </main> </template> <script> -import AccountsTable from "@/components/manage/moderation/AccountsTable" +import AccountsTable from '@/components/manage/moderation/AccountsTable' export default { components: { AccountsTable }, props: { - defaultQuery: {type: String, required: false}, + defaultQuery: { type: String, required: false, default: '' } }, computed: { - labels() { + labels () { return { - accounts: this.$pgettext('*/Moderation/Title', "Accounts") + accounts: this.$pgettext('*/Moderation/Title', 'Accounts') } } } diff --git a/front/src/views/admin/moderation/Base.vue b/front/src/views/admin/moderation/Base.vue index 9bf5468b086adcac49192a3e146a9bd38e83c43d..8d52dc71786b027be1ec53ad5fa29d1a9f075ee4 100644 --- a/front/src/views/admin/moderation/Base.vue +++ b/front/src/views/admin/moderation/Base.vue @@ -1,31 +1,62 @@ <template> - <div class="main pusher" v-title="labels.moderation"> - <nav class="ui secondary pointing menu" role="navigation" :aria-label="labels.secondaryMenu"> + <div + v-title="labels.moderation" + class="main pusher" + > + <nav + class="ui secondary pointing menu" + role="navigation" + :aria-label="labels.secondaryMenu" + > <router-link class="ui item" - :to="{name: 'manage.moderation.reports.list', query: {q: 'resolved:no'}}"> - <translate translate-context="*/Moderation/*/Noun">Reports</translate> + :to="{name: 'manage.moderation.reports.list', query: {q: 'resolved:no'}}" + > + <translate translate-context="*/Moderation/*/Noun"> + Reports + </translate> <div v-if="$store.state.ui.notifications.pendingReviewReports > 0" - :class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']">{{ $store.state.ui.notifications.pendingReviewReports }}</div> + :class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']" + > + {{ $store.state.ui.notifications.pendingReviewReports }} + </div> </router-link> <router-link class="ui item" - :to="{name: 'manage.moderation.requests.list', query: {q: 'status:pending'}}"> - <translate translate-context="*/Moderation/*/Noun">User Requests</translate> + :to="{name: 'manage.moderation.requests.list', query: {q: 'status:pending'}}" + > + <translate translate-context="*/Moderation/*/Noun"> + User Requests + </translate> <div v-if="$store.state.ui.notifications.pendingReviewRequests > 0" - :class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']">{{ $store.state.ui.notifications.pendingReviewRequests }}</div> + :class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']" + > + {{ $store.state.ui.notifications.pendingReviewRequests }} + </div> </router-link> <router-link class="ui item" - :to="{name: 'manage.moderation.domains.list'}"><translate translate-context="*/Moderation/*/Noun">Domains</translate></router-link> + :to="{name: 'manage.moderation.domains.list'}" + > + <translate translate-context="*/Moderation/*/Noun"> + Domains + </translate> + </router-link> <router-link class="ui item" - :to="{name: 'manage.moderation.accounts.list'}"><translate translate-context="*/Moderation/Title">Accounts</translate></router-link> - + :to="{name: 'manage.moderation.accounts.list'}" + > + <translate translate-context="*/Moderation/Title"> + Accounts + </translate> + </router-link> </nav> - <router-view :allow-list-enabled="allowListEnabled" :key="$route.fullPath"></router-view> + <router-view + :key="$route.fullPath" + :allow-list-enabled="allowListEnabled" + /> </div> </template> @@ -39,24 +70,24 @@ export default { allowListEnabled: false } }, + computed: { + labels () { + return { + moderation: this.$pgettext('*/Moderation/*', 'Moderation'), + secondaryMenu: this.$pgettext('Menu/*/Hidden text', 'Secondary menu') + } + } + }, created () { this.fetchNodeInfo() }, methods: { fetchNodeInfo () { - let self = this + const self = this axios.get('instance/nodeinfo/2.0/').then(response => { self.allowListEnabled = _.get(response.data, 'metadata.allowList.enabled', false) }) - }, - }, - computed: { - labels() { - return { - moderation: this.$pgettext('*/Moderation/*', "Moderation"), - secondaryMenu: this.$pgettext('Menu/*/Hidden text', "Secondary menu") - } } - }, + } } </script> diff --git a/front/src/views/admin/moderation/DomainsDetail.vue b/front/src/views/admin/moderation/DomainsDetail.vue index 38915b87d5a8459fb74bcb2513d01112d1415ea6..17293512d3c9536b75f6aff8add005815547e4d4 100644 --- a/front/src/views/admin/moderation/DomainsDetail.vue +++ b/front/src/views/admin/moderation/DomainsDetail.vue @@ -1,21 +1,32 @@ <template> <main class="page-admin-domain-detail"> - <div v-if="isLoading" class="ui vertical segment"> - <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> + <div + v-if="isLoading" + class="ui vertical segment" + > + <div :class="['ui', 'centered', 'active', 'inline', 'loader']" /> </div> <template v-if="object"> - <section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="object.name"> + <section + v-title="object.name" + :class="['ui', 'head', 'vertical', 'stripe', 'segment']" + > <div class="ui stackable two column grid"> <div class="ui column"> <div class="segment-content"> <h2 class="ui header"> - <i class="circular inverted cloud icon"></i> + <i class="circular inverted cloud icon" /> <div class="content"> {{ object.name }} <div class="sub header"> - <a :href="externalUrl" target="_blank" rel="noopener noreferrer" class="logo-wrapper"> + <a + :href="externalUrl" + target="_blank" + rel="noopener noreferrer" + class="logo-wrapper" + > <translate translate-context="Content/Moderation/Link/Verb">Open website</translate> - <i class="external icon"></i> + <i class="external icon" /> </a> </div> </div> @@ -26,25 +37,36 @@ v-if="$store.state.auth.profile.is_superuser" class="ui labeled icon button" :href="$store.getters['instance/absoluteUrl'](`/api/admin/federation/domain/${object.name}`)" - target="_blank" rel="noopener noreferrer"> - <i class="wrench icon"></i> + target="_blank" + rel="noopener noreferrer" + > + <i class="wrench icon" /> <translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate> </a> </div> - <div v-if="allowListEnabled" class="ui icon buttons"> + <div + v-if="allowListEnabled" + class="ui icon buttons" + > <button v-if="object.allowed" + :class="['ui', 'labeled', {loading: isLoadingAllowList}, 'icon', 'button']" @click.prevent="setAllowList(false)" - :class="['ui', 'labeled', {loading: isLoadingAllowList}, 'icon', 'button']"> - <i class="x icon"></i> - <translate translate-context="Content/Moderation/Action/Verb">Remove from allow-list</translate> + > + <i class="x icon" /> + <translate translate-context="Content/Moderation/Action/Verb"> + Remove from allow-list + </translate> </button> <button v-else + :class="['ui', 'labeled', {loading: isLoadingAllowList}, 'icon', 'button']" @click.prevent="setAllowList(true)" - :class="['ui', 'labeled', {loading: isLoadingAllowList}, 'icon', 'button']"> - <i class="check icon"></i> - <translate translate-context="Content/Moderation/Action/Verb">Add to allow-list</translate> + > + <i class="check icon" /> + <translate translate-context="Content/Moderation/Action/Verb"> + Add to allow-list + </translate> </button> </div> </div> @@ -54,38 +76,56 @@ <div class="ui compact clearing placeholder segment component-placeholder"> <template v-if="isLoadingPolicy"> <div class="paragraph"> - <div class="line"></div> - <div class="line"></div> - <div class="line"></div> - <div class="line"></div> - <div class="line"></div> + <div class="line" /> + <div class="line" /> + <div class="line" /> + <div class="line" /> + <div class="line" /> </div> </template> <template v-else-if="!policy && !showPolicyForm"> <header class="ui header"> <h3> - <i class="shield icon"></i> - <translate translate-context="Content/Moderation/Card.Title">You don't have any rule in place for this domain.</translate> + <i class="shield icon" /> + <translate translate-context="Content/Moderation/Card.Title"> + You don't have any rule in place for this domain. + </translate> </h3> </header> - <p><translate translate-context="Content/Moderation/Card.Paragraph">Moderation policies help you control how your instance interact with a given domain or account.</translate></p> - <button @click="showPolicyForm = true" class="ui primary button">Add a moderation policy</button> + <p> + <translate translate-context="Content/Moderation/Card.Paragraph"> + Moderation policies help you control how your instance interact with a given domain or account. + </translate> + </p> + <button + class="ui primary button" + @click="showPolicyForm = true" + > + Add a moderation policy + </button> </template> - <instance-policy-card v-else-if="policy && !showPolicyForm" :object="policy" @update="showPolicyForm = true"> + <instance-policy-card + v-else-if="policy && !showPolicyForm" + :object="policy" + @update="showPolicyForm = true" + > <header class="ui header"> <h3> - <translate translate-context="Content/Moderation/Card.Title">This domain is subject to specific moderation rules</translate> + <translate translate-context="Content/Moderation/Card.Title"> + This domain is subject to specific moderation rules + </translate> </h3> </header> </instance-policy-card> <instance-policy-form v-else-if="showPolicyForm" + :object="policy" + type="domain" + :target="object.name" @cancel="showPolicyForm = false" @save="updatePolicy" @delete="policy = null; showPolicyForm = false" - :object="policy" - type="domain" - :target="object.name" /> + /> </div> </div> </div> @@ -95,36 +135,62 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="info icon"></i> + <i class="info icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Instance data</translate> + <translate translate-context="Content/Moderation/Title"> + Instance data + </translate> </div> </h3> <table class="ui very basic table"> <tbody> <tr v-if="allowListEnabled"> <td> - <translate translate-context="Content/Moderation/*/Adjective">Is present on allow-list</translate> + <translate translate-context="Content/Moderation/*/Adjective"> + Is present on allow-list + </translate> </td> <td> - <translate v-if="object.allowed" translate-context="*/*/*">Yes</translate> - <translate v-else translate-context="*/*/*">No</translate> + <translate + v-if="object.allowed" + translate-context="*/*/*" + > + Yes + </translate> + <translate + v-else + translate-context="*/*/*" + > + No + </translate> </td> </tr> <tr> <td> - <translate translate-context="Content/*/Table.Label">Last checked</translate> + <translate translate-context="Content/*/Table.Label"> + Last checked + </translate> </td> <td> - <human-date v-if="object.nodeinfo_fetch_date" :date="object.nodeinfo_fetch_date"></human-date> - <translate v-else translate-context="*/*/*">N/A</translate> + <human-date + v-if="object.nodeinfo_fetch_date" + :date="object.nodeinfo_fetch_date" + /> + <translate + v-else + translate-context="*/*/*" + > + N/A + </translate> </td> </tr> <template v-if="object.nodeinfo && object.nodeinfo.status === 'ok'"> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label">Software</translate> + <translate translate-context="Content/Moderation/Table.Label"> + Software + </translate> </td> <td> {{ lodash.get(object, 'nodeinfo.payload.software.name', $pgettext('*/*/*', 'N/A')) }} ({{ lodash.get(object, 'nodeinfo.payload.software.version', $pgettext('*/*/*', 'N/A')) }}) @@ -132,7 +198,9 @@ </tr> <tr> <td> - <translate translate-context="*/*/*/Noun">Name</translate> + <translate translate-context="*/*/*/Noun"> + Name + </translate> </td> <td> {{ lodash.get(object, 'nodeinfo.payload.metadata.nodeName', $pgettext('*/*/*', 'N/A')) }} @@ -140,7 +208,9 @@ </tr> <tr> <td> - <translate translate-context="Content/*/*">Total users</translate> + <translate translate-context="Content/*/*"> + Total users + </translate> </td> <td> {{ lodash.get(object, 'nodeinfo.payload.usage.users.total', $pgettext('*/*/*', 'N/A')) }} @@ -150,55 +220,76 @@ <template v-if="object.nodeinfo && object.nodeinfo.status === 'error'"> <tr> <td> - <translate translate-context="*/*/*">Status</translate> + <translate translate-context="*/*/*"> + Status + </translate> </td> <td> - <translate translate-context="Content/Moderation/Table">Error while fetching node info</translate> + <translate translate-context="Content/Moderation/Table"> + Error while fetching node info + </translate> - <span :data-tooltip="object.nodeinfo.error"><i class="question circle icon"></i></span> + <span :data-tooltip="object.nodeinfo.error"><i class="question circle icon" /></span> </td> </tr> </template> </tbody> </table> - <ajax-button @action-done="refreshNodeInfo" method="get" :url="'manage/federation/domains/' + object.name + '/nodeinfo/'"> - <translate translate-context="Content/Moderation/Button.Label/Verb">Refresh node info</translate> + <ajax-button + method="get" + :url="'manage/federation/domains/' + object.name + '/nodeinfo/'" + @action-done="refreshNodeInfo" + > + <translate translate-context="Content/Moderation/Button.Label/Verb"> + Refresh node info + </translate> </ajax-button> </section> </div> <div class="column"> <section> <h3 class="ui header"> - <i class="feed icon"></i> + <i class="feed icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Activity</translate> - <span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span> - + <translate translate-context="Content/Moderation/Title"> + Activity + </translate> + <span :data-tooltip="labels.statsWarning"><i class="question circle icon" /></span> </div> </h3> - <div v-if="isLoadingStats" class="ui placeholder"> - <div class="full line"></div> - <div class="short line"></div> - <div class="medium line"></div> - <div class="long line"></div> + <div + v-if="isLoadingStats" + class="ui placeholder" + > + <div class="full line" /> + <div class="short line" /> + <div class="medium line" /> + <div class="long line" /> </div> - <table v-else class="ui very basic table"> + <table + v-else + class="ui very basic table" + > <tbody> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate> + <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)"> + First seen + </translate> </td> <td> - <human-date :date="object.creation_date"></human-date> + <human-date :date="object.creation_date" /> </td> </tr> <tr> <td> <router-link - :to="{name: 'manage.moderation.accounts.list', query: {q: 'domain:' + object.name }}"> - <translate translate-context="Content/Moderation/Table.Label.Link">Known accounts</translate> - </router-link> - + :to="{name: 'manage.moderation.accounts.list', query: {q: 'domain:' + object.name }}" + > + <translate translate-context="Content/Moderation/Table.Label.Link"> + Known accounts + </translate> + </router-link> </td> <td> {{ stats.actors }} @@ -206,26 +297,32 @@ </tr> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label/Noun">Emitted messages</translate> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Emitted messages + </translate> </td> <td> - {{ stats.outbox_activities}} + {{ stats.outbox_activities }} </td> </tr> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label/Noun">Received library follows</translate> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Received library follows + </translate> </td> <td> - {{ stats.received_library_follows}} + {{ stats.received_library_follows }} </td> </tr> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label/Noun">Emitted library follows</translate> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Emitted library follows + </translate> </td> <td> - {{ stats.emitted_library_follows}} + {{ stats.emitted_library_follows }} </td> </tr> </tbody> @@ -235,24 +332,33 @@ <div class="column"> <section> <h3 class="ui header"> - <i class="music icon"></i> + <i class="music icon" /> <div class="content"> - <translate translate-context="Content/Moderation/Title">Audio content</translate> - <span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span> - + <translate translate-context="Content/Moderation/Title"> + Audio content + </translate> + <span :data-tooltip="labels.statsWarning"><i class="question circle icon" /></span> </div> </h3> - <div v-if="isLoadingStats" class="ui placeholder"> - <div class="full line"></div> - <div class="short line"></div> - <div class="medium line"></div> - <div class="long line"></div> + <div + v-if="isLoadingStats" + class="ui placeholder" + > + <div class="full line" /> + <div class="short line" /> + <div class="medium line" /> + <div class="long line" /> </div> - <table v-else class="ui very basic table"> + <table + v-else + class="ui very basic table" + > <tbody> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label/Noun">Cached size</translate> + <translate translate-context="Content/Moderation/Table.Label/Noun"> + Cached size + </translate> </td> <td> {{ stats.media_downloaded_size | humanSize }} @@ -260,7 +366,9 @@ </tr> <tr> <td> - <translate translate-context="Content/Moderation/Table.Label">Total size</translate> + <translate translate-context="Content/Moderation/Table.Label"> + Total size + </translate> </td> <td> {{ stats.media_total_size | humanSize }} @@ -269,7 +377,9 @@ <tr> <td> <router-link :to="{name: 'manage.channels', query: {q: getQuery('domain', object.name) }}"> - <translate translate-context="*/*/*">Channels</translate> + <translate translate-context="*/*/*"> + Channels + </translate> </router-link> </td> <td> @@ -279,7 +389,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('domain', object.name) }}"> - <translate translate-context="*/*/*/Noun">Libraries</translate> + <translate translate-context="*/*/*/Noun"> + Libraries + </translate> </router-link> </td> <td> @@ -289,7 +401,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('domain', object.name) }}"> - <translate translate-context="*/*/*">Uploads</translate> + <translate translate-context="*/*/*"> + Uploads + </translate> </router-link> </td> <td> @@ -299,7 +413,9 @@ <tr> <td> <router-link :to="{name: 'manage.library.artists', query: {q: getQuery('domain', object.name) }}"> - <translate translate-context="*/*/*/Noun">Artists</translate> + <translate translate-context="*/*/*/Noun"> + Artists + </translate> </router-link> </td> <td> @@ -309,17 +425,21 @@ <tr> <td> <router-link :to="{name: 'manage.library.albums', query: {q: getQuery('domain', object.name) }}"> - <translate translate-context="*/*/*">Albums</translate> + <translate translate-context="*/*/*"> + Albums + </translate> </router-link> </td> <td> - {{ stats.albums}} + {{ stats.albums }} </td> </tr> <tr> <td> <router-link :to="{name: 'manage.library.tracks', query: {q: getQuery('domain', object.name) }}"> - <translate translate-context="*/*/*">Tracks</translate> + <translate translate-context="*/*/*"> + Tracks + </translate> </router-link> </td> <td> @@ -328,31 +448,28 @@ </tr> </tbody> </table> - </section> </div> </div> </div> - </template> </main> </template> <script> -import axios from "axios" -import logger from "@/logging" +import axios from 'axios' import lodash from '@/lodash' -import InstancePolicyForm from "@/components/manage/moderation/InstancePolicyForm" -import InstancePolicyCard from "@/components/manage/moderation/InstancePolicyCard" +import InstancePolicyForm from '@/components/manage/moderation/InstancePolicyForm' +import InstancePolicyCard from '@/components/manage/moderation/InstancePolicyCard' export default { - props: ["id", "allowListEnabled"], components: { InstancePolicyForm, - InstancePolicyCard, + InstancePolicyCard }, - data() { + props: { id: { type: Number, required: true }, allowListEnabled: { type: Boolean, required: true } }, + data () { return { lodash, isLoading: true, @@ -363,18 +480,28 @@ export default { object: null, stats: null, showPolicyForm: false, - permissions: [], + permissions: [] } }, - created() { + computed: { + labels () { + return { + statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this domain') + } + }, + externalUrl () { + return `https://${this.object.name}` + } + }, + created () { this.fetchData() this.fetchStats() }, methods: { - fetchData() { - var self = this + fetchData () { + const self = this this.isLoading = true - let url = "manage/federation/domains/" + this.id + "/" + const url = 'manage/federation/domains/' + this.id + '/' axios.get(url).then(response => { self.object = response.data self.isLoading = false @@ -383,29 +510,29 @@ export default { } }) }, - fetchStats() { - var self = this + fetchStats () { + const self = this this.isLoadingStats = true - let url = "manage/federation/domains/" + this.id + "/stats/" + const url = 'manage/federation/domains/' + this.id + '/stats/' axios.get(url).then(response => { self.stats = response.data self.isLoadingStats = false }) }, - fetchPolicy(id) { - var self = this + fetchPolicy (id) { + const self = this this.isLoadingPolicy = true - let url = `manage/moderation/instance-policies/${id}/` + const url = `manage/moderation/instance-policies/${id}/` axios.get(url).then(response => { self.policy = response.data self.isLoadingPolicy = false }) }, - setAllowList(value) { - var self = this + setAllowList (value) { + const self = this this.isLoadingAllowList = true - let url = `manage/federation/domains/${this.id}/` - axios.patch(url, {allowed: value}).then(response => { + const url = `manage/federation/domains/${this.id}/` + axios.patch(url, { allowed: value }).then(response => { self.object = response.data self.isLoadingAllowList = false }) @@ -421,16 +548,6 @@ export default { getQuery (field, value) { return `${field}:"${value}"` } - }, - computed: { - labels() { - return { - statsWarning: this.$pgettext('Content/Moderation/Help text', "Statistics are computed from known activity and content on your instance, and do not reflect general activity for this domain") - } - }, - externalUrl () { - return `https://${this.object.name}` - } } } </script> diff --git a/front/src/views/admin/moderation/DomainsList.vue b/front/src/views/admin/moderation/DomainsList.vue index 34bff4d92a47ce08b9639fe7a9e4f675ed8037af..0182961e9a70793be36677bd7461757c631f4511 100644 --- a/front/src/views/admin/moderation/DomainsList.vue +++ b/front/src/views/admin/moderation/DomainsList.vue @@ -1,32 +1,71 @@ <template> <main v-title="labels.domains"> <section class="ui vertical stripe segment"> - <h2 class="ui left floated header"><translate translate-context="*/Moderation/*/Noun">Domains</translate></h2> - <form class="ui right floated form" @submit.prevent="createDomain"> - <div v-if="errors && errors.length > 0" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Content/Moderation/Message.Title">Error while creating domain</translate></h4> + <h2 class="ui left floated header"> + <translate translate-context="*/Moderation/*/Noun"> + Domains + </translate> + </h2> + <form + class="ui right floated form" + @submit.prevent="createDomain" + > + <div + v-if="errors && errors.length > 0" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Content/Moderation/Message.Title"> + Error while creating domain + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> <div class="inline fields"> <div class="field"> <label for="add-domain"><translate translate-context="Content/Moderation/Form.Label/Verb">Add a domain</translate></label> - <input type="text" name="domain" id="add-domain" v-model="domainName"> + <input + id="add-domain" + v-model="domainName" + type="text" + name="domain" + > </div> - <div class="field" v-if="allowListEnabled"> - <input type="checkbox" name="allowed" id="allowed" v-model="domainAllowed"> + <div + v-if="allowListEnabled" + class="field" + > + <input + id="allowed" + v-model="domainAllowed" + type="checkbox" + name="allowed" + > <label for="allowed"><translate translate-context="Content/Moderation/Action/Verb">Add to allow-list</translate></label> </div> <div class="field"> - <button :class="['ui', {'loading': isCreating}, 'success', 'button']" type="submit" :disabled="isCreating"> - <translate translate-context="Content/Moderation/Button/Verb">Add</translate> + <button + :class="['ui', {'loading': isCreating}, 'success', 'button']" + type="submit" + :disabled="isCreating" + > + <translate translate-context="Content/Moderation/Button/Verb"> + Add + </translate> </button> </div> </div> </form> - <div class="ui clearing hidden divider"></div> - <domains-table :allow-list-enabled="allowListEnabled"></domains-table> + <div class="ui clearing hidden divider" /> + <domains-table :allow-list-enabled="allowListEnabled" /> </section> </main> </template> @@ -34,12 +73,12 @@ <script> import axios from 'axios' -import DomainsTable from "@/components/manage/moderation/DomainsTable" +import DomainsTable from '@/components/manage/moderation/DomainsTable' export default { - props: ['allowListEnabled'], components: { DomainsTable }, + props: { allowListEnabled: { type: Boolean, required: true } }, data () { return { domainName: '', @@ -49,22 +88,22 @@ export default { } }, computed: { - labels() { + labels () { return { - domains: this.$pgettext('*/Moderation/*/Noun', "Domains") + domains: this.$pgettext('*/Moderation/*/Noun', 'Domains') } } }, methods: { createDomain () { - let self = this + const self = this this.isCreating = true this.errors = [] - axios.post('manage/federation/domains/', {name: this.domainName, allowed: this.domainAllowed}).then((response) => { + axios.post('manage/federation/domains/', { name: this.domainName, allowed: this.domainAllowed }).then((response) => { this.isCreating = false this.$router.push({ - name: "manage.moderation.domains.detail", - params: {'id': response.data.name} + name: 'manage.moderation.domains.detail', + params: { id: response.data.name } }) }, (error) => { self.isCreating = false diff --git a/front/src/views/admin/moderation/ReportDetail.vue b/front/src/views/admin/moderation/ReportDetail.vue index ab2bd3e42440f313146c7d303b521a42aea71866..37bdbbac7a7c5008a1c1f031f80e52913bed8b85 100644 --- a/front/src/views/admin/moderation/ReportDetail.vue +++ b/front/src/views/admin/moderation/ReportDetail.vue @@ -1,46 +1,48 @@ <template> <main> - <div v-if="isLoading" class="ui vertical segment"> - <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> + <div + v-if="isLoading" + class="ui vertical segment" + > + <div :class="['ui', 'centered', 'active', 'inline', 'loader']" /> </div> <template v-if="object"> - <div class="ui vertical stripe segment"> - <report-card :obj="object"></report-card> + <report-card :obj="object" /> </div> </template> </main> </template> <script> -import axios from "axios" +import axios from 'axios' -import ReportCard from "@/components/manage/moderation/ReportCard" +import ReportCard from '@/components/manage/moderation/ReportCard' export default { - props: ["id"], components: { - ReportCard, + ReportCard }, - data() { + props: { id: { type: Number, required: true } }, + data () { return { isLoading: true, - object: null, + object: null } }, - created() { + created () { this.fetchData() }, methods: { - fetchData() { - var self = this + fetchData () { + const self = this this.isLoading = true - let url = `manage/moderation/reports/${this.id}/` + const url = `manage/moderation/reports/${this.id}/` axios.get(url).then(response => { self.object = response.data self.isLoading = false }) - }, - }, + } + } } </script> diff --git a/front/src/views/admin/moderation/ReportsList.vue b/front/src/views/admin/moderation/ReportsList.vue index 5660c01d5ce68d9a788c27fe5f3f1197befefdb7..54675070d2c79a2fc1032aab10b331650ef2b3d5 100644 --- a/front/src/views/admin/moderation/ReportsList.vue +++ b/front/src/views/admin/moderation/ReportsList.vue @@ -1,70 +1,124 @@ <template> <main v-title="labels.reports"> <section class="ui vertical stripe segment"> - <h2 class="ui header"><translate translate-context="*/Moderation/*/Noun">Reports</translate></h2> - <div class="ui hidden divider"></div> + <h2 class="ui header"> + <translate translate-context="*/Moderation/*/Noun"> + Reports + </translate> + </h2> + <div class="ui hidden divider" /> <div class="ui inline form"> <div class="fields"> <div class="ui field"> <label for="reports-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label> <form @submit.prevent="search.query = $refs.search.value"> - <input id="reports-search" name="search" ref="search" type="text" :value="search.query" :placeholder="labels.searchPlaceholder" /> + <input + id="reports-search" + ref="search" + name="search" + type="text" + :value="search.query" + :placeholder="labels.searchPlaceholder" + > </form> </div> <div class="field"> <label for="reports-status"><translate translate-context="*/*/*">Status</translate></label> - <select id="reports-status" class="ui dropdown" @change="addSearchToken('resolved', $event.target.value)" :value="getTokenValue('resolved', '')"> + <select + id="reports-status" + class="ui dropdown" + :value="getTokenValue('resolved', '')" + @change="addSearchToken('resolved', $event.target.value)" + > <option value=""> - <translate translate-context="Content/*/Dropdown">All</translate> + <translate translate-context="Content/*/Dropdown"> + All + </translate> </option> <option value="yes"> - <translate translate-context="Content/*/*/Short">Resolved</translate> + <translate translate-context="Content/*/*/Short"> + Resolved + </translate> </option> <option value="no"> - <translate translate-context="Content/*/*/Short">Unresolved</translate> + <translate translate-context="Content/*/*/Short"> + Unresolved + </translate> </option> </select> </div> <report-category-dropdown class="field" - @input="addSearchToken('category', $event)" :all="true" :label="true" - :value="getTokenValue('category', '')"></report-category-dropdown> + :value="getTokenValue('category', '')" + @input="addSearchToken('category', $event)" + /> <div class="field"> <label for="reports-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> - <select id="reports-ordering" class="ui dropdown" v-model="ordering"> - <option v-for="option in orderingOptions" :value="option[0]"> + <select + id="reports-ordering" + v-model="ordering" + class="ui dropdown" + > + <option + v-for="(option, key) in orderingOptions" + :key="key" + :value="option[0]" + > {{ sharedLabels.filters[option[1]] }} </option> </select> </div> <div class="field"> <label for="reports-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Order</translate></label> - <select id="reports-ordering-direction" class="ui dropdown" v-model="orderingDirection"> - <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option> - <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option> + <select + id="reports-ordering-direction" + v-model="orderingDirection" + class="ui dropdown" + > + <option value="+"> + <translate translate-context="Content/Search/Dropdown"> + Ascending + </translate> + </option> + <option value="-"> + <translate translate-context="Content/Search/Dropdown"> + Descending + </translate> + </option> </select> </div> </div> </div> - <div v-if="isLoading" class="ui active inverted dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui active inverted dimmer" + > + <div class="ui loader" /> </div> <div v-else-if="!result || result.count === 0"> - <empty-state @refresh="fetchData()" :refresh="true"></empty-state> + <empty-state + :refresh="true" + @refresh="fetchData()" + /> </div> <div v-else-if="mode === 'card'"> - <report-card @handled="fetchData" :obj="obj" v-for="obj in result.results" :key="obj.uuid" /> + <report-card + v-for="obj in result.results" + :key="obj.uuid" + :obj="obj" + @handled="fetchData" + /> </div> <div class="ui center aligned basic segment"> <pagination v-if="result && result.count > paginateBy" - @page-changed="selectPage" :current="page" :paginate-by="paginateBy" :total="result.count" - ></pagination> + @page-changed="selectPage" + /> </div> </section> </main> @@ -72,7 +126,6 @@ <script> - import axios from 'axios' import _ from '@/lodash' import time from '@/utils/time' @@ -81,22 +134,21 @@ import OrderingMixin from '@/components/mixins/Ordering' import TranslationsMixin from '@/components/mixins/Translations' import ReportCard from '@/components/manage/moderation/ReportCard' import ReportCategoryDropdown from '@/components/moderation/ReportCategoryDropdown' -import {normalizeQuery, parseTokens} from '@/search' +import { normalizeQuery, parseTokens } from '@/search' import SmartSearchMixin from '@/components/mixins/SmartSearch' - export default { - mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], components: { Pagination, ReportCard, - ReportCategoryDropdown, + ReportCategoryDropdown }, + mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], props: { - mode: {default: 'card'}, + mode: { type: String, default: 'card' } }, data () { - let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') + const defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') return { time, isLoading: false, @@ -111,72 +163,62 @@ export default { ordering: defaultOrdering.field, orderingOptions: [ ['creation_date', 'creation_date'], - ['applied_date', 'applied_date'], + ['applied_date', 'applied_date'] ], targets: { track: {} } } }, + computed: { + labels () { + return { + searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by account, summary, domain…'), + reports: this.$pgettext('*/Moderation/*/Noun', 'Reports') + } + } + }, + watch: { + search (newValue) { + this.page = 1 + this.fetchData() + }, + page () { + this.fetchData() + }, + ordering () { + this.fetchData() + }, + orderingDirection () { + this.fetchData() + } + }, created () { this.fetchData() }, methods: { fetchData () { - let params = _.merge({ - 'page': this.page, - 'page_size': this.paginateBy, - 'q': this.search.query, - 'ordering': this.getOrderingAsString() + const params = _.merge({ + page: this.page, + page_size: this.paginateBy, + q: this.search.query, + ordering: this.getOrderingAsString() }, this.filters) - let self = this + const self = this self.isLoading = true this.result = null - axios.get('manage/moderation/reports/', {params: params}).then((response) => { + axios.get('manage/moderation/reports/', { params: params }).then((response) => { self.result = response.data self.isLoading = false if (self.search.query === 'resolved:no') { console.log('Refreshing sidebar notifications') - self.$store.commit('ui/incrementNotifications', {type: 'pendingReviewReports', value: response.data.count}) + self.$store.commit('ui/incrementNotifications', { type: 'pendingReviewReports', value: response.data.count }) } }, error => { self.isLoading = false self.errors = error.backendErrors }) }, - fetchTargets () { - // we request target data via the API so we can display previous state - // additionnal data next to the edit card - let self = this - let typesAndIds = { - track: { - url: 'tracks/', - ids: [], - } - } - this.result.results.forEach((m) => { - if (!m.target || !typesAndIds[m.target.type]) { - return - } - typesAndIds[m.target.type]['ids'].push(m.target.id) - }) - Object.keys(typesAndIds).forEach((k) => { - let config = typesAndIds[k] - if (config.ids.length === 0) { - return - } - axios.get(config.url, {params: {id: _.uniq(config.ids), hidden: 'null'}}).then((response) => { - response.data.results.forEach((e) => { - self.$set(self.targets[k], e.id, { - payload: e, - currentState: edits.getCurrentStateForObj(e, edits.getConfigs.bind(self)()[k]) - }) - }) - }, error => { - self.errors = error.backendErrors - }) - }) - }, selectPage: function (page) { this.page = page }, @@ -200,29 +242,6 @@ export default { } return {} } - }, - computed: { - labels () { - return { - searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by account, summary, domain…'), - reports: this.$pgettext('*/Moderation/*/Noun', "Reports"), - } - }, - }, - watch: { - search (newValue) { - this.page = 1 - this.fetchData() - }, - page () { - this.fetchData() - }, - ordering () { - this.fetchData() - }, - orderingDirection () { - this.fetchData() - } } } diff --git a/front/src/views/admin/moderation/RequestDetail.vue b/front/src/views/admin/moderation/RequestDetail.vue index b8f9e57c9b634423c18c3b8af6e4b48377cd3d51..ca089ad8f0d0f240fd80a88d036e6dd92589e878 100644 --- a/front/src/views/admin/moderation/RequestDetail.vue +++ b/front/src/views/admin/moderation/RequestDetail.vue @@ -1,46 +1,48 @@ <template> <main> - <div v-if="isLoading" class="ui vertical segment"> - <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> + <div + v-if="isLoading" + class="ui vertical segment" + > + <div :class="['ui', 'centered', 'active', 'inline', 'loader']" /> </div> <template v-if="object"> - <div class="ui vertical stripe segment"> - <user-request-card :obj="object"></user-request-card> + <user-request-card :obj="object" /> </div> </template> </main> </template> <script> -import axios from "axios" +import axios from 'axios' -import UserRequestCard from "@/components/manage/moderation/UserRequestCard" +import UserRequestCard from '@/components/manage/moderation/UserRequestCard' export default { - props: ["id"], components: { - UserRequestCard, + UserRequestCard }, - data() { + props: { id: { type: Number, required: true } }, + data () { return { isLoading: true, - object: null, + object: null } }, - created() { + created () { this.fetchData() }, methods: { - fetchData() { - var self = this + fetchData () { + const self = this this.isLoading = true - let url = `manage/moderation/requests/${this.id}/` + const url = `manage/moderation/requests/${this.id}/` axios.get(url).then(response => { self.object = response.data self.isLoading = false }) - }, - }, + } + } } </script> diff --git a/front/src/views/admin/moderation/RequestsList.vue b/front/src/views/admin/moderation/RequestsList.vue index 614694fd2f0d8340899a2dd84be9607f50288963..9e77b63406a5fa7a3f2eed49eb2eba65c65b9ad7 100644 --- a/front/src/views/admin/moderation/RequestsList.vue +++ b/front/src/views/admin/moderation/RequestsList.vue @@ -1,68 +1,122 @@ <template> <main v-title="labels.reports"> <section class="ui vertical stripe segment"> - <h2 class="ui header"><translate translate-context="*/Moderation/*/Noun">User Requests</translate></h2> - <div class="ui hidden divider"></div> + <h2 class="ui header"> + <translate translate-context="*/Moderation/*/Noun"> + User Requests + </translate> + </h2> + <div class="ui hidden divider" /> <div class="ui inline form"> <div class="fields"> <div class="ui field"> <label for="requests-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label> <form @submit.prevent="search.query = $refs.search.value"> - <input id="requests-search" name="search" ref="search" type="text" :value="search.query" :placeholder="labels.searchPlaceholder" /> + <input + id="requests-search" + ref="search" + name="search" + type="text" + :value="search.query" + :placeholder="labels.searchPlaceholder" + > </form> </div> <div class="field"> <label for="requests-status"><translate translate-context="*/*/*">Status</translate></label> - <select id="requests-status" class="ui dropdown" @change="addSearchToken('status', $event.target.value)" :value="getTokenValue('status', '')"> + <select + id="requests-status" + class="ui dropdown" + :value="getTokenValue('status', '')" + @change="addSearchToken('status', $event.target.value)" + > <option value=""> - <translate translate-context="Content/*/Dropdown">All</translate> + <translate translate-context="Content/*/Dropdown"> + All + </translate> </option> <option value="pending"> - <translate translate-context="Content/Library/*/Short">Pending</translate> + <translate translate-context="Content/Library/*/Short"> + Pending + </translate> </option> <option value="approved"> - <translate translate-context="Content/*/*/Short">Approved</translate> + <translate translate-context="Content/*/*/Short"> + Approved + </translate> </option> <option value="refused"> - <translate translate-context="Content/*/*/Short">Refused</translate> + <translate translate-context="Content/*/*/Short"> + Refused + </translate> </option> </select> </div> <div class="field"> <label for="requests-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> - <select id="requests-ordering" class="ui dropdown" v-model="ordering"> - <option v-for="option in orderingOptions" :value="option[0]"> + <select + id="requests-ordering" + v-model="ordering" + class="ui dropdown" + > + <option + v-for="(option, key) in orderingOptions" + :key="key" + :value="option[0]" + > {{ sharedLabels.filters[option[1]] }} </option> </select> </div> <div class="field"> <label for="requests-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Order</translate></label> - <select id="requests-ordering-direction" class="ui dropdown" v-model="orderingDirection"> - <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option> - <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option> + <select + id="requests-ordering-direction" + v-model="orderingDirection" + class="ui dropdown" + > + <option value="+"> + <translate translate-context="Content/Search/Dropdown"> + Ascending + </translate> + </option> + <option value="-"> + <translate translate-context="Content/Search/Dropdown"> + Descending + </translate> + </option> </select> </div> </div> </div> - <div v-if="isLoading" class="ui active inverted dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui active inverted dimmer" + > + <div class="ui loader" /> </div> <div v-else-if="!result || result.count === 0"> - <empty-state @refresh="fetchData()" :refresh="true"></empty-state> + <empty-state + :refresh="true" + @refresh="fetchData()" + /> </div> <template v-else> - <user-request-card @handled="fetchData" :obj="obj" v-for="obj in result.results" :key="obj.uuid" /> + <user-request-card + v-for="obj in result.results" + :key="obj.uuid" + :obj="obj" + @handled="fetchData" + /> <div class="ui center aligned basic segment"> <pagination v-if="result.count > paginateBy" - @page-changed="selectPage" :current="page" :paginate-by="paginateBy" :total="result.count" - ></pagination> + @page-changed="selectPage" + /> </div> - </template> </section> </main> @@ -70,7 +124,6 @@ <script> - import axios from 'axios' import _ from '@/lodash' import time from '@/utils/time' @@ -78,16 +131,15 @@ import Pagination from '@/components/Pagination' import OrderingMixin from '@/components/mixins/Ordering' import TranslationsMixin from '@/components/mixins/Translations' import UserRequestCard from '@/components/manage/moderation/UserRequestCard' -import {normalizeQuery, parseTokens} from '@/search' +import { normalizeQuery, parseTokens } from '@/search' import SmartSearchMixin from '@/components/mixins/SmartSearch' - export default { - mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], components: { Pagination, - UserRequestCard, + UserRequestCard }, + mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], data () { return { time, @@ -100,49 +152,20 @@ export default { }, orderingOptions: [ ['creation_date', 'creation_date'], - ['handled_date', 'handled_date'], + ['handled_date', 'handled_date'] ], targets: { track: {} } } }, - created () { - this.fetchData() - }, - methods: { - fetchData () { - let params = _.merge({ - 'page': this.page, - 'page_size': this.paginateBy, - 'q': this.search.query, - 'ordering': this.getOrderingAsString() - }, this.filters) - let self = this - self.isLoading = true - this.result = null - axios.get('manage/moderation/requests/', {params: params}).then((response) => { - self.result = response.data - self.isLoading = false - if (self.search.query === 'status:pending') { - self.$store.commit('ui/incrementNotifications', {type: 'pendingReviewRequests', value: response.data.count}) - } - }, error => { - self.isLoading = false - self.errors = error.backendErrors - }) - }, - selectPage: function (page) { - this.page = page - }, - }, computed: { labels () { return { searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by username…'), - reports: this.$pgettext('*/Moderation/*/Noun', "User Requests"), + reports: this.$pgettext('*/Moderation/*/Noun', 'User Requests') } - }, + } }, watch: { search (newValue) { @@ -158,6 +181,35 @@ export default { orderingDirection () { this.fetchData() } + }, + created () { + this.fetchData() + }, + methods: { + fetchData () { + const params = _.merge({ + page: this.page, + page_size: this.paginateBy, + q: this.search.query, + ordering: this.getOrderingAsString() + }, this.filters) + const self = this + self.isLoading = true + this.result = null + axios.get('manage/moderation/requests/', { params: params }).then((response) => { + self.result = response.data + self.isLoading = false + if (self.search.query === 'status:pending') { + self.$store.commit('ui/incrementNotifications', { type: 'pendingReviewRequests', value: response.data.count }) + } + }, error => { + self.isLoading = false + self.errors = error.backendErrors + }) + }, + selectPage: function (page) { + this.page = page + } } } diff --git a/front/src/views/admin/users/Base.vue b/front/src/views/admin/users/Base.vue index 84d70bebb6cbc5c9afbaebbd3b6e0c25d90dd38d..96e9e44a554858e63f61b620d2fdf780769ec6a1 100644 --- a/front/src/views/admin/users/Base.vue +++ b/front/src/views/admin/users/Base.vue @@ -1,24 +1,41 @@ <template> - <div class="main pusher" v-title="labels.manageUsers"> - <nav class="ui secondary pointing menu" role="navigation" :aria-label="labels.secondaryMenu"> + <div + v-title="labels.manageUsers" + class="main pusher" + > + <nav + class="ui secondary pointing menu" + role="navigation" + :aria-label="labels.secondaryMenu" + > <router-link class="ui item" - :to="{name: 'manage.users.users.list'}"><translate translate-context="*/*/*/Noun">Users</translate></router-link> + :to="{name: 'manage.users.users.list'}" + > + <translate translate-context="*/*/*/Noun"> + Users + </translate> + </router-link> <router-link class="ui item" - :to="{name: 'manage.users.invitations.list'}"><translate translate-context="*/Admin/*/Noun">Invitations</translate></router-link> + :to="{name: 'manage.users.invitations.list'}" + > + <translate translate-context="*/Admin/*/Noun"> + Invitations + </translate> + </router-link> </nav> - <router-view :key="$route.fullPath"></router-view> + <router-view :key="$route.fullPath" /> </div> </template> <script> export default { computed: { - labels() { + labels () { return { manageUsers: this.$pgettext('Head/Admin/Title', 'Manage users'), - secondaryMenu: this.$pgettext('Menu/*/Hidden text','Secondary menu') + secondaryMenu: this.$pgettext('Menu/*/Hidden text', 'Secondary menu') } } } diff --git a/front/src/views/admin/users/InvitationsList.vue b/front/src/views/admin/users/InvitationsList.vue index 96738983fb25575a516c83741d99e6347768c2c1..d95b387da405c3255ada08a1a58ae5ec766c0ebc 100644 --- a/front/src/views/admin/users/InvitationsList.vue +++ b/front/src/views/admin/users/InvitationsList.vue @@ -1,17 +1,19 @@ <template> <main v-title="labels.invitations"> <section class="ui vertical stripe segment"> - <h2 class="ui header">{{ labels.invitations }}</h2> - <invitation-form></invitation-form> - <div class="ui hidden divider"></div> - <invitations-table></invitations-table> + <h2 class="ui header"> + {{ labels.invitations }} + </h2> + <invitation-form /> + <div class="ui hidden divider" /> + <invitations-table /> </section> </main> </template> <script> -import InvitationForm from "@/components/manage/users/InvitationForm" -import InvitationsTable from "@/components/manage/users/InvitationsTable" +import InvitationForm from '@/components/manage/users/InvitationForm' +import InvitationsTable from '@/components/manage/users/InvitationsTable' export default { components: { @@ -19,7 +21,7 @@ export default { InvitationsTable }, computed: { - labels() { + labels () { return { invitations: this.$pgettext('*/Admin/*/Noun', 'Invitations') } diff --git a/front/src/views/admin/users/UsersList.vue b/front/src/views/admin/users/UsersList.vue index e54194ca1c58f3e26c2ca077dae44e041dcdb8cc..29141850f1a782589919f93af3e92ce1ca276f95 100644 --- a/front/src/views/admin/users/UsersList.vue +++ b/front/src/views/admin/users/UsersList.vue @@ -1,22 +1,24 @@ <template> <main v-title="labels.users"> <section class="ui vertical stripe segment"> - <h2 class="ui header">{{ labels.users }}</h2> - <div class="ui hidden divider"></div> - <users-table></users-table> + <h2 class="ui header"> + {{ labels.users }} + </h2> + <div class="ui hidden divider" /> + <users-table /> </section> </main> </template> <script> -import UsersTable from "@/components/manage/users/UsersTable" +import UsersTable from '@/components/manage/users/UsersTable' export default { components: { UsersTable }, computed: { - labels() { + labels () { return { users: this.$pgettext('*/*/*/Noun', 'Users') } diff --git a/front/src/views/auth/Callback.vue b/front/src/views/auth/Callback.vue index 4a64eb1e1a7218b8ba7e1a7e5436b08a709b4814..b16acae8c678588ee348b54a8444500225cb413c 100644 --- a/front/src/views/auth/Callback.vue +++ b/front/src/views/auth/Callback.vue @@ -2,10 +2,14 @@ <main class="main pusher"> <section class="ui vertical stripe segment"> <div class="ui small text container"> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <div class="ui active inverted dimmer"> <div class="ui text loader"> - <h2><translate translate-context="*/Login/*">Logging in…</translate></h2> + <h2> + <translate translate-context="*/Login/*"> + Logging in… + </translate> + </h2> </div> </div> </div> @@ -16,7 +20,10 @@ <script> export default { - props: ["state", "code"], + props: { + state: { type: String, required: true }, + code: { type: String, required: true } + }, async mounted () { await this.$store.dispatch('auth/handleOauthCallback', this.code) this.$router.push(this.state || '/library') diff --git a/front/src/views/auth/EmailConfirm.vue b/front/src/views/auth/EmailConfirm.vue index aec373d143ef7afc936945723550d3714ef7d62b..61952a274435ee540ae2306c40b167d03301ad42 100644 --- a/front/src/views/auth/EmailConfirm.vue +++ b/front/src/views/auth/EmailConfirm.vue @@ -1,30 +1,75 @@ <template> - <main class="main pusher" v-title="labels.confirm"> + <main + v-title="labels.confirm" + class="main pusher" + > <section class="ui vertical stripe segment"> <div class="ui small text container"> <h2>{{ labels.confirm }}</h2> - <form v-if="!success" 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/Signup/Paragraph">Could not confirm your e-mail address</translate></h4> + <form + v-if="!success" + 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/Signup/Paragraph"> + Could not confirm your e-mail address + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, i) in errors" + :key="i" + > + {{ error }} + </li> </ul> </div> <div class="field"> <label for="confirmation-code"><translate translate-context="Content/Signup/Form.Label">Confirmation code</translate></label> - <input id="confirmation-code" name="confirmation-code" type="text" required v-model="key" /> + <input + id="confirmation-code" + v-model="key" + name="confirmation-code" + type="text" + required + > </div> <router-link :to="{path: '/login'}"> - <translate translate-context="Content/Signup/Link/Verb">Return to login</translate> + <translate translate-context="Content/Signup/Link/Verb"> + Return to login + </translate> </router-link> - <button :class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']" type="submit"> - {{ labels.confirm }}</button> + <button + :class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']" + type="submit" + > + {{ labels.confirm }} + </button> </form> - <div v-else class="ui positive message"> - <h4 class="header"><translate translate-context="Content/Signup/Message">E-mail address confirmed</translate></h4> - <p><translate translate-context="Content/Signup/Paragraph">You can now use the service without limitations.</translate></p> + <div + v-else + class="ui positive message" + > + <h4 class="header"> + <translate translate-context="Content/Signup/Message"> + E-mail address confirmed + </translate> + </h4> + <p> + <translate translate-context="Content/Signup/Paragraph"> + You can now use the service without limitations. + </translate> + </p> <router-link :to="{name: 'login'}"> - <translate translate-context="Content/Signup/Link/Verb">Proceed to login</translate> + <translate translate-context="Content/Signup/Link/Verb"> + Proceed to login + </translate> </router-link> </div> </div> @@ -33,11 +78,11 @@ </template> <script> -import axios from "axios" +import axios from 'axios' export default { - props: ["defaultKey"], - data() { + props: { defaultKey: { type: String, required: true } }, + data () { return { isLoading: false, errors: [], @@ -46,9 +91,9 @@ export default { } }, computed: { - labels() { + labels () { return { - confirm: this.$pgettext('Head/Signup/Title', "Confirm your e-mail address") + confirm: this.$pgettext('Head/Signup/Title', 'Confirm your e-mail address') } } }, @@ -58,14 +103,14 @@ export default { } }, methods: { - submit() { - let self = this + submit () { + const self = this self.isLoading = true self.errors = [] - let payload = { + const payload = { key: this.key } - return axios.post("auth/registration/verify-email/", payload).then( + return axios.post('auth/registration/verify-email/', payload).then( response => { self.isLoading = false self.success = true diff --git a/front/src/views/auth/Login.vue b/front/src/views/auth/Login.vue index a63716555497fba8a9a9bd5da98c5f380f6fcf31..8938115bb375ab3a4c5ac7272942c221d47a6d59 100644 --- a/front/src/views/auth/Login.vue +++ b/front/src/views/auth/Login.vue @@ -1,9 +1,16 @@ <template> - <main class="main pusher" v-title="labels.title"> + <main + v-title="labels.title" + class="main pusher" + > <section class="ui vertical stripe segment"> <div class="ui small text container"> - <h2><translate translate-context="Content/Login/Title/Verb">Log in to your Funkwhale account</translate></h2> - <login-form :next="redirectTo"></login-form> + <h2> + <translate translate-context="Content/Login/Title/Verb"> + Log in to your Funkwhale account + </translate> + </h2> + <login-form :next="redirectTo" /> </div> </section> </main> @@ -13,6 +20,9 @@ import LoginForm from '@/components/auth/LoginForm' export default { + components: { + LoginForm + }, props: { next: { type: String, default: '/library' } }, @@ -21,8 +31,13 @@ export default { redirectTo: this.next } }, - components: { - LoginForm + computed: { + labels () { + const title = this.$pgettext('Head/Login/Title', 'Log In') + return { + title + } + } }, created () { const resolved = this.$router.resolve(this.redirectTo) @@ -33,14 +48,6 @@ export default { if (this.$store.state.auth.authenticated) { this.$router.push(this.redirectTo) } - }, - computed: { - labels () { - const title = this.$pgettext('Head/Login/Title', 'Log In') - return { - title - } - } } } </script> diff --git a/front/src/views/auth/PasswordReset.vue b/front/src/views/auth/PasswordReset.vue index 380e95a8f7c30dcd5abbb7515275ac7eca91b6c6..47ea5490b20509cb9061a348a2c21f22e84ed4bb 100644 --- a/front/src/views/auth/PasswordReset.vue +++ b/front/src/views/auth/PasswordReset.vue @@ -1,33 +1,69 @@ <template> - <main class="main pusher" v-title="labels.reset"> + <main + v-title="labels.reset" + class="main pusher" + > <section class="ui vertical stripe segment"> <div class="ui small text container"> - <h2><translate translate-context="*/Login/*/Verb">Reset your password</translate></h2> - <form 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/Signup/Card.Title">Error while asking for a password reset</translate></h4> + <h2> + <translate translate-context="*/Login/*/Verb"> + Reset your password + </translate> + </h2> + <form + 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/Signup/Card.Title"> + Error while asking for a password reset + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> - <p><translate translate-context="Content/Signup/Paragraph">Use this form to request a password reset. We will send an e-mail to the given address with instructions to reset your password.</translate></p> + <p> + <translate translate-context="Content/Signup/Paragraph"> + Use this form to request a password reset. We will send an e-mail to the given address with instructions to reset your password. + </translate> + </p> <div class="field"> <label for="account-email"><translate translate-context="Content/Signup/Input.Label">Account's e-mail address</translate></label> <input id="account-email" - required ref="email" + v-model="email" + required type="email" name="email" autofocus :placeholder="labels.placeholder" - v-model="email"> + > </div> <router-link :to="{path: '/login'}"> - <translate translate-context="Content/Signup/Link">Back to login</translate> + <translate translate-context="Content/Signup/Link"> + Back to login + </translate> </router-link> - <button :class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']" type="submit"> - <translate translate-context="Content/Signup/Button.Label/Verb">Ask for a password reset</translate></button> + <button + :class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']" + type="submit" + > + <translate translate-context="Content/Signup/Button.Label/Verb"> + Ask for a password reset + </translate> + </button> </form> </div> </section> @@ -35,24 +71,21 @@ </template> <script> -import axios from "axios" +import axios from 'axios' export default { - props: ["defaultEmail"], - data() { + props: { defaultEmail: { type: String, required: true } }, + data () { return { email: this.defaultEmail, isLoading: false, errors: [] } }, - mounted() { - this.$refs.email.focus() - }, computed: { - labels() { - let reset = this.$pgettext('*/Login/*/Verb', "Reset your password") - let placeholder = this.$pgettext('Content/Signup/Input.Placeholder', "Enter the e-mail address linked to your account" + labels () { + const reset = this.$pgettext('*/Login/*/Verb', 'Reset your password') + const placeholder = this.$pgettext('Content/Signup/Input.Placeholder', 'Enter the e-mail address linked to your account' ) return { reset, @@ -60,19 +93,22 @@ export default { } } }, + mounted () { + this.$refs.email.focus() + }, methods: { - submit() { - let self = this + submit () { + const self = this self.isLoading = true self.errors = [] - let payload = { + const payload = { email: this.email } - return axios.post("auth/password/reset/", payload).then( + return axios.post('auth/password/reset/', payload).then( response => { self.isLoading = false self.$router.push({ - name: "auth.password-reset-confirm" + name: 'auth.password-reset-confirm' }) }, error => { diff --git a/front/src/views/auth/PasswordResetConfirm.vue b/front/src/views/auth/PasswordResetConfirm.vue index 7d0f4b84fdd7c4288eeb421a2d2942b0599229cd..3653135a0b3edd2d6878c331850600af43124d43 100644 --- a/front/src/views/auth/PasswordResetConfirm.vue +++ b/front/src/views/auth/PasswordResetConfirm.vue @@ -1,35 +1,83 @@ <template> - <main class="main pusher" v-title="labels.changePassword"> + <main + v-title="labels.changePassword" + class="main pusher" + > <section class="ui vertical stripe segment"> <div class="ui small text container"> <h2>{{ labels.changePassword }}</h2> - <form v-if="!success" 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/Signup/Card.Title">Error while changing your password</translate></h4> + <form + v-if="!success" + 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/Signup/Card.Title"> + Error while changing your password + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> <template v-if="token && uid"> <div class="field"> <label for="password-field"><translate translate-context="Content/Settings/Input.Label">New password</translate></label> - <password-input field-id="password-field" v-model="newPassword" /> + <password-input + v-model="newPassword" + field-id="password-field" + /> </div> <router-link :to="{path: '/login'}"> - <translate translate-context="Content/Signup/Link">Back to login</translate> + <translate translate-context="Content/Signup/Link"> + Back to login + </translate> </router-link> - <button :class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']" type="submit"> - <translate translate-context="Content/Signup/Button.Label">Update your password</translate></button> + <button + :class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']" + type="submit" + > + <translate translate-context="Content/Signup/Button.Label"> + Update your password + </translate> + </button> </template> <template v-else> - <p><translate translate-context="Content/Signup/Paragraph">If the e-mail address provided in the previous step is valid and linked to a user account, you should receive an e-mail with reset instructions in the next couple of minutes.</translate></p> + <p> + <translate translate-context="Content/Signup/Paragraph"> + If the e-mail address provided in the previous step is valid and linked to a user account, you should receive an e-mail with reset instructions in the next couple of minutes. + </translate> + </p> </template> </form> - <div v-else class="ui positive message"> - <h4 class="header"><translate translate-context="Content/Signup/Card.Title">Password updated successfully</translate></h4> - <p><translate translate-context="Content/Signup/Card.Paragraph">Your password has been updated successfully.</translate></p> + <div + v-else + class="ui positive message" + > + <h4 class="header"> + <translate translate-context="Content/Signup/Card.Title"> + Password updated successfully + </translate> + </h4> + <p> + <translate translate-context="Content/Signup/Card.Paragraph"> + Your password has been updated successfully. + </translate> + </p> <router-link :to="{name: 'login'}"> - <translate translate-context="Content/Signup/Link/Verb">Proceed to login</translate> + <translate translate-context="Content/Signup/Link/Verb"> + Proceed to login + </translate> </router-link> </div> </div> @@ -38,17 +86,20 @@ </template> <script> -import axios from "axios" -import PasswordInput from "@/components/forms/PasswordInput" +import axios from 'axios' +import PasswordInput from '@/components/forms/PasswordInput' export default { - props: ["defaultToken", "defaultUid"], components: { PasswordInput }, - data() { + props: { + defaultToken: { type: String, required: true }, + defaultUid: { type: String, required: true } + }, + data () { return { - newPassword: "", + newPassword: '', isLoading: false, errors: [], token: this.defaultToken, @@ -57,24 +108,24 @@ export default { } }, computed: { - labels() { + labels () { return { - changePassword: this.$pgettext('*/Signup/Title', "Change your password") + changePassword: this.$pgettext('*/Signup/Title', 'Change your password') } } }, methods: { - submit() { - let self = this + submit () { + const self = this self.isLoading = true self.errors = [] - let payload = { + const payload = { uid: this.uid, token: this.token, new_password1: this.newPassword, new_password2: this.newPassword } - return axios.post("auth/password/reset/confirm/", payload).then( + return axios.post('auth/password/reset/confirm/', payload).then( response => { self.isLoading = false self.success = true diff --git a/front/src/views/auth/Plugins.vue b/front/src/views/auth/Plugins.vue index a03dc978a770e5e774e37c6ddeb756e800747e42..6e6400ffd0a8142b6efc0932847c1ff9b1e8d0a0 100644 --- a/front/src/views/auth/Plugins.vue +++ b/front/src/views/auth/Plugins.vue @@ -1,19 +1,27 @@ <template> - <main class="main pusher" v-title="labels.title"> + <main + v-title="labels.title" + class="main pusher" + > <section class="ui vertical stripe segment"> <div class="ui small text container"> <h2>{{ labels.title }}</h2> - <div v-if="isLoading" class="ui inverted active dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui inverted active dimmer" + > + <div class="ui loader" /> </div> - <plugin-form - v-if="plugins && plugins.length > 0" - v-for="plugin in plugins" - :plugin="plugin" - :libraries="libraries" - :key="plugin.name"></plugin-form> - <empty-state v-else></empty-state> + <template v-if="plugins && plugins.length > 0"> + <plugin-form + v-for="plugin in plugins" + :key="plugin.name" + :plugin="plugin" + :libraries="libraries" + /> + </template> + <empty-state v-else /> </div> </section> </main> @@ -31,26 +39,26 @@ export default { return { isLoading: true, plugins: null, - libraries: null, + libraries: null } }, - async created () { - await this.fetchData() - }, computed: { - labels() { - let title = this.$pgettext('Head/Login/Title', "Manage plugins") + labels () { + const title = this.$pgettext('Head/Login/Title', 'Manage plugins') return { title } } }, + async created () { + await this.fetchData() + }, methods: { async fetchData () { this.isLoading = true let response = await axios.get('plugins') this.plugins = response.data - response = await axios.get('libraries', {paramis: {scope: 'me', page_size: 50}}) + response = await axios.get('libraries', { paramis: { scope: 'me', page_size: 50 } }) this.libraries = response.data.results this.isLoading = false } diff --git a/front/src/views/auth/ProfileActivity.vue b/front/src/views/auth/ProfileActivity.vue index d378547d6841539a8ceafb0374019bfc7d84189b..433bd657058ad46c6c840ac9892ef3ecd56bb26e 100644 --- a/front/src/views/auth/ProfileActivity.vue +++ b/front/src/views/auth/ProfileActivity.vue @@ -1,48 +1,65 @@ <template> <section> <div> - <radio-button v-if="recentActivity > 0" class="right floated" type="account" :object-id="{username: object.preferred_username, fullUsername: object.full_username}" :client-only="true"></radio-button> + <radio-button + v-if="recentActivity > 0" + class="right floated" + type="account" + :object-id="{username: object.preferred_username, fullUsername: object.full_username}" + :client-only="true" + /> <h2 class="ui header"> - <translate translate-context="Content/Home/Title">Recently listened</translate> + <translate translate-context="Content/Home/Title"> + Recently listened + </translate> </h2> - <div class="ui divider"></div> + <div class="ui divider" /> <track-widget - @count="recentActivity = $event" :url="'history/listenings/'" - :filters="{scope: `actor:${object.full_username}`, ordering: '-creation_date'}"> - </track-widget> + :filters="{scope: `actor:${object.full_username}`, ordering: '-creation_date'}" + @count="recentActivity = $event" + /> </div> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <div> <h2 class="ui header"> - <translate translate-context="Content/Home/Title">Recently favorited</translate> + <translate translate-context="Content/Home/Title"> + Recently favorited + </translate> </h2> - <div class="ui divider"></div> - <track-widget :url="'favorites/tracks/'" :filters="{scope: `actor:${object.full_username}`, ordering: '-creation_date'}"></track-widget> + <div class="ui divider" /> + <track-widget + :url="'favorites/tracks/'" + :filters="{scope: `actor:${object.full_username}`, ordering: '-creation_date'}" + /> </div> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <div> <h2 class="ui header"> - <translate translate-context="*/*/*">Playlists</translate> + <translate translate-context="*/*/*"> + Playlists + </translate> </h2> - <div class="ui divider"></div> - <playlist-widget :url="'playlists/'" :filters="{scope: `actor:${object.full_username}`, playable: true, ordering: '-modification_date'}"> - </playlist-widget> + <div class="ui divider" /> + <playlist-widget + :url="'playlists/'" + :filters="{scope: `actor:${object.full_username}`, playable: true, ordering: '-modification_date'}" + /> </div> </section> </template> <script> -import TrackWidget from "@/components/audio/track/Widget" -import PlaylistWidget from "@/components/playlists/Widget" -import RadioButton from "@/components/radios/Button" +import TrackWidget from '@/components/audio/track/Widget' +import PlaylistWidget from '@/components/playlists/Widget' +import RadioButton from '@/components/radios/Button' export default { - props: ['object'], - components: {TrackWidget, PlaylistWidget, RadioButton}, + components: { TrackWidget, PlaylistWidget, RadioButton }, + props: { object: { type: Object, required: true } }, data () { return { - recentActivity: 0, + recentActivity: 0 } } } diff --git a/front/src/views/auth/ProfileBase.vue b/front/src/views/auth/ProfileBase.vue index f776322bf14c1f0590178a774e7812f6d3d46663..bf525640c2faaadfdf910d181954c3570c9c64f7 100644 --- a/front/src/views/auth/ProfileBase.vue +++ b/front/src/views/auth/ProfileBase.vue @@ -1,79 +1,133 @@ <template> - <main class="main pusher page-profile" v-title="labels.usernameProfile"> - <div v-if="isLoading" class="ui vertical segment"> - <div class="ui centered active inline loader"></div> + <main + v-title="labels.usernameProfile" + class="main pusher page-profile" + > + <div + v-if="isLoading" + class="ui vertical segment" + > + <div class="ui centered active inline loader" /> </div> <div class="ui head vertical stripe segment container"> - <div class="ui stackable grid" v-if="object"> + <div + v-if="object" + class="ui stackable grid" + > <div class="ui five wide column"> - <button class="ui pointing dropdown icon small basic right floated button" ref="dropdown" v-dropdown="{direction: 'downward'}" style="position: absolute; right: 1em; top: 1em;"> - <i class="ellipsis vertical icon"></i> + <button + ref="dropdown" + v-dropdown="{direction: 'downward'}" + class="ui pointing dropdown icon small basic right floated button" + style="position: absolute; right: 1em; top: 1em;" + > + <i class="ellipsis vertical icon" /> <div class="menu"> <a - :href="object.fid" v-if="object.domain != $store.getters['instance/domain']" + :href="object.fid" target="_blank" - class="basic item"> - <i class="external icon"></i> - <translate :translate-params="{domain: object.domain}" translate-context="Content/*/Button.Label/Verb">View on %{ domain }</translate> + class="basic item" + > + <i class="external icon" /> + <translate + :translate-params="{domain: object.domain}" + translate-context="Content/*/Button.Label/Verb" + >View on %{ domain }</translate> </a> <div - role="button" - class="basic item" v-for="obj in getReportableObjs({account: object})" :key="obj.target.type + obj.target.id" - @click.stop.prevent="$store.dispatch('moderation/report', obj.target)"> + role="button" + class="basic item" + @click.stop.prevent="$store.dispatch('moderation/report', obj.target)" + > <i class="share icon" /> {{ obj.label }} </div> - <div class="divider"></div> - <router-link class="basic item" v-if="$store.state.auth.availablePermissions['moderation']" :to="{name: 'manage.moderation.accounts.detail', params: {id: object.full_username}}"> - <i class="wrench icon"></i> - <translate translate-context="Content/Moderation/Link">Open in moderation interface</translate> + <div class="divider" /> + <router-link + v-if="$store.state.auth.availablePermissions['moderation']" + class="basic item" + :to="{name: 'manage.moderation.accounts.detail', params: {id: object.full_username}}" + > + <i class="wrench icon" /> + <translate translate-context="Content/Moderation/Link"> + Open in moderation interface + </translate> </router-link> </div> </button> <h1 class="ui center aligned icon header"> - <i v-if="!object.icon" class="circular inverted user success icon"></i> - <img alt="" class="ui big circular image" v-else v-lazy="$store.getters['instance/absoluteUrl'](object.icon.urls.medium_square_crop)" /> + <i + v-if="!object.icon" + class="circular inverted user success icon" + /> + <img + v-else + v-lazy="$store.getters['instance/absoluteUrl'](object.icon.urls.medium_square_crop)" + alt="" + class="ui big circular image" + > <div class="ellispsis content"> - <div class="ui very small hidden divider"></div> + <div class="ui very small hidden divider" /> <span>{{ displayName }}</span> - <div class="ui very small hidden divider"></div> - <div class="sub header ellipsis" :title="object.full_username"> + <div class="ui very small hidden divider" /> + <div + class="sub header ellipsis" + :title="object.full_username" + > {{ object.full_username }} </div> </div> - <template v-if="object.full_username === $store.state.auth.fullUsername"> - <div class="ui very small hidden divider"></div> + <template v-if="object.full_username === $store.state.auth.fullUsername"> + <div class="ui very small hidden divider" /> <div class="ui basic success label"> - <translate translate-context="Content/Profile/Button.Paragraph">This is you!</translate> + <translate translate-context="Content/Profile/Button.Paragraph"> + This is you! + </translate> </div> </template> </h1> - <div class="ui small hidden divider"></div> + <div class="ui small hidden divider" /> <div v-if="$store.getters['ui/layoutVersion'] === 'large'"> <rendered-description - @updated="$emit('updated', $event)" :content="object.summary" :field-name="'summary'" :update-url="`users/${$store.state.auth.username}/`" - :can-update="$store.state.auth.authenticated && object.full_username === $store.state.auth.fullUsername"></rendered-description> + :can-update="$store.state.auth.authenticated && object.full_username === $store.state.auth.fullUsername" + @updated="$emit('updated', $event)" + /> </div> </div> <div class="ui eleven wide column"> <div class="ui head vertical stripe segment"> <div class="ui container"> <div class="ui secondary pointing center aligned menu"> - <router-link class="item" :exact="true" :to="{name: 'profile.overview', params: routerParams}"> - <translate translate-context="Content/Profile/Link">Overview</translate> + <router-link + class="item" + :exact="true" + :to="{name: 'profile.overview', params: routerParams}" + > + <translate translate-context="Content/Profile/Link"> + Overview + </translate> </router-link> - <router-link class="item" :exact="true" :to="{name: 'profile.activity', params: routerParams}"> - <translate translate-context="Content/Profile/*">Activity</translate> + <router-link + class="item" + :exact="true" + :to="{name: 'profile.activity', params: routerParams}" + > + <translate translate-context="Content/Profile/*"> + Activity + </translate> </router-link> </div> - <div class="ui hidden divider"></div> - <router-view @updated="fetch" :object="object"></router-view> + <div class="ui hidden divider" /> + <router-view + :object="object" + @updated="fetch" + /> </div> </div> </div> @@ -83,50 +137,30 @@ </template> <script> -import { mapState } from "vuex" import axios from 'axios' import ReportMixin from '@/components/mixins/Report' export default { mixins: [ReportMixin], + beforeRouteUpdate (to, from, next) { + to.meta.preserveScrollPosition = true + next() + }, props: { - username: {type: String, required: true}, - domain: {type: String, required: false, default: null}, + username: { type: String, required: true }, + domain: { type: String, required: false, default: null } }, data () { return { object: null, - isLoading: false, - } - }, - created() { - let authenticated = this.$store.state.auth.authenticated - if (!authenticated && this.domain && this.$store.getters['instance/domain'] != this.domain) { - this.$router.push({name: 'login', query: {next: this.$route.fullPath}}) - } else { - this.fetch() - } - }, - beforeRouteUpdate (to, from, next) { - to.meta.preserveScrollPosition = true - next() - }, - methods: { - fetch () { - let self = this - self.object = null - self.isLoading = true - axios.get(`federation/actors/${this.fullUsername}/`).then((response) => { - self.object = response.data - self.isLoading = false - }) + isLoading: false } }, computed: { - labels() { - let msg = this.$pgettext('Head/Profile/Title', "%{ username }'s profile") - let usernameProfile = this.$gettextInterpolate(msg, { + labels () { + const msg = this.$pgettext('Head/Profile/Title', "%{ username }'s profile") + const usernameProfile = this.$gettextInterpolate(msg, { username: this.username }) return { @@ -142,9 +176,9 @@ export default { }, routerParams () { if (this.domain) { - return {username: this.username, domain: this.domain} + return { username: this.username, domain: this.domain } } else { - return {username: this.username} + return { username: this.username } } }, displayName () { @@ -158,6 +192,25 @@ export default { username () { this.fetch() } + }, + created () { + const authenticated = this.$store.state.auth.authenticated + if (!authenticated && this.domain && this.$store.getters['instance/domain'] !== this.domain) { + this.$router.push({ name: 'login', query: { next: this.$route.fullPath } }) + } else { + this.fetch() + } + }, + methods: { + fetch () { + const self = this + self.object = null + self.isLoading = true + axios.get(`federation/actors/${this.fullUsername}/`).then((response) => { + self.object = response.data + self.isLoading = false + }) + } } } </script> diff --git a/front/src/views/auth/ProfileOverview.vue b/front/src/views/auth/ProfileOverview.vue index 3c0f89eca6f1418ba1b82292d1b6476e0a0537aa..43c1428f2e9cd1974409bb0c992e784b15e1a444 100644 --- a/front/src/views/auth/ProfileOverview.vue +++ b/front/src/views/auth/ProfileOverview.vue @@ -2,45 +2,87 @@ <section> <div v-if="$store.getters['ui/layoutVersion'] === 'small'"> <rendered-description - @updated="$emit('updated', $event)" :content="object.summary" :field-name="'summary'" :update-url="`users/${$store.state.auth.username}/`" - :can-update="$store.state.auth.authenticated && object.full_username === $store.state.auth.fullUsername"></rendered-description> - <div class="ui hidden divider"></div> + :can-update="$store.state.auth.authenticated && object.full_username === $store.state.auth.fullUsername" + @updated="$emit('updated', $event)" + /> + <div class="ui hidden divider" /> </div> <div> <h2 class="ui with-actions header"> - <translate translate-context="*/*/*">Channels</translate> - <div class="actions" v-if="$store.state.auth.authenticated && object.full_username === $store.state.auth.fullUsername"> - <a @click.stop.prevent="showCreateModal = true" href=""> - <i class="plus icon"></i> + <translate translate-context="*/*/*"> + Channels + </translate> + <div + v-if="$store.state.auth.authenticated && object.full_username === $store.state.auth.fullUsername" + class="actions" + > + <a + href="" + @click.stop.prevent="showCreateModal = true" + > + <i class="plus icon" /> <translate translate-context="Content/Profile/Button">Add new</translate> </a> </div> </h2> - <channels-widget :filters="{scope: `actor:${object.full_username}`}"></channels-widget> + <channels-widget :filters="{scope: `actor:${object.full_username}`}" /> <h2 class="ui with-actions header"> - <translate translate-context="Content/Profile/Header">User Libraries</translate> - <div class="actions" v-if="$store.state.auth.authenticated && object.full_username === $store.state.auth.fullUsername"> + <translate translate-context="Content/Profile/Header"> + User Libraries + </translate> + <div + v-if="$store.state.auth.authenticated && object.full_username === $store.state.auth.fullUsername" + class="actions" + > <router-link :to="{name: 'content.libraries.index'}"> - <i class="plus icon"></i> - <translate translate-context="Content/Profile/Button">Add new</translate> + <i class="plus icon" /> + <translate translate-context="Content/Profile/Button"> + Add new + </translate> </router-link> </div> </h2> <library-widget :url="`federation/actors/${object.full_username}/libraries/`"> - <translate translate-context="Content/Profile/Paragraph" slot="title">This user shared the following libraries</translate> + <translate + slot="title" + translate-context="Content/Profile/Paragraph" + > + This user shared the following libraries + </translate> </library-widget> </div> <modal :show.sync="showCreateModal"> <h4 class="header"> - <translate v-if="step === 1" key="1" translate-context="Content/Channel/*/Verb">Create channel</translate> - <translate v-else-if="category === 'podcast'" key="2" translate-context="Content/Channel/*">Podcast channel</translate> - <translate v-else key="3" translate-context="Content/Channel/*">Artist channel</translate> + <translate + v-if="step === 1" + key="1" + translate-context="Content/Channel/*/Verb" + > + Create channel + </translate> + <translate + v-else-if="category === 'podcast'" + key="2" + translate-context="Content/Channel/*" + > + Podcast channel + </translate> + <translate + v-else + key="3" + translate-context="Content/Channel/*" + > + Artist channel + </translate> </h4> - <div class="scrolling content" ref="modalContent"> + <div + ref="modalContent" + class="scrolling content" + > <channel-form ref="createForm" :object="null" @@ -49,44 +91,69 @@ @submittable="submittable = $event" @category="category = $event" @errored="$refs.modalContent.scrollTop = 0" - @created="$router.push({name: 'channels.detail', params: {id: $event.actor.preferred_username}})"></channel-form> - <div class="ui hidden divider"></div> + @created="$router.push({name: 'channels.detail', params: {id: $event.actor.preferred_username}})" + /> + <div class="ui hidden divider" /> </div> <div class="actions"> - <button v-if="step === 1" class="ui basic deny button"> - <translate translate-context="*/*/Button.Label/Verb">Cancel</translate> + <button + v-if="step === 1" + class="ui basic deny button" + > + <translate translate-context="*/*/Button.Label/Verb"> + Cancel + </translate> </button> - <button v-if="step > 1" class="ui basic button" @click.stop.prevent="step -= 1"> - <translate translate-context="*/*/Button.Label/Verb">Previous step</translate> + <button + v-if="step > 1" + class="ui basic button" + @click.stop.prevent="step -= 1" + > + <translate translate-context="*/*/Button.Label/Verb"> + Previous step + </translate> </button> - <button v-if="step === 1" class="ui primary button" @click.stop.prevent="step += 1"> - <translate translate-context="*/*/Button.Label">Next step</translate> + <button + v-if="step === 1" + class="ui primary button" + @click.stop.prevent="step += 1" + > + <translate translate-context="*/*/Button.Label"> + Next step + </translate> </button> - <button v-if="step === 2" :class="['ui', 'primary button', {loading: isLoading}]" type="submit" @click.prevent.stop="$refs.createForm.submit" :disabled="!submittable && !isLoading"> - <translate translate-context="*/Channels/Button.Label">Create channel</translate> + <button + v-if="step === 2" + :class="['ui', 'primary button', {loading: isLoading}]" + type="submit" + :disabled="!submittable && !isLoading" + @click.prevent.stop="$refs.createForm.submit" + > + <translate translate-context="*/Channels/Button.Label"> + Create channel + </translate> </button> </div> </modal> - </section> </template> <script> import Modal from '@/components/semantic/Modal' -import LibraryWidget from "@/components/federation/LibraryWidget" -import ChannelsWidget from "@/components/audio/ChannelsWidget" -import ChannelForm from "@/components/audio/ChannelForm" +import LibraryWidget from '@/components/federation/LibraryWidget' +import ChannelsWidget from '@/components/audio/ChannelsWidget' +import ChannelForm from '@/components/audio/ChannelForm' export default { - props: ['object'], - components: {ChannelsWidget, LibraryWidget, ChannelForm, Modal}, + components: { ChannelsWidget, LibraryWidget, ChannelForm, Modal }, + props: { object: { type: Object, required: true } }, data () { return { showCreateModal: false, isLoading: false, submittable: false, step: 1, - category: 'podcast', + category: 'podcast' } } } diff --git a/front/src/views/auth/Signup.vue b/front/src/views/auth/Signup.vue index 0c77f4dafb074f315c18aaaeb5940ab9d45136a7..605658727fd7415d3ef687020263103227fd8279 100644 --- a/front/src/views/auth/Signup.vue +++ b/front/src/views/auth/Signup.vue @@ -1,9 +1,19 @@ <template> - <main class="main pusher" v-title="labels.title"> + <main + v-title="labels.title" + class="main pusher" + > <section class="ui vertical stripe segment"> <div class="ui small text container"> - <h2><translate translate-context="Content/Signup/Title">Create a Funkwhale account</translate></h2> - <signup-form :default-invitation="defaultInvitation" :next="next"></signup-form> + <h2> + <translate translate-context="Content/Signup/Title"> + Create a Funkwhale account + </translate> + </h2> + <signup-form + :default-invitation="defaultInvitation" + :next="next" + /> </div> </section> </main> @@ -11,21 +21,21 @@ <script> -import SignupForm from "@/components/auth/SignupForm" +import SignupForm from '@/components/auth/SignupForm' export default { - props: { - defaultInvitation: { type: String, required: false, default: null }, - next: { type: String, default: "/" } - }, components: { SignupForm }, - data() { + props: { + defaultInvitation: { type: String, required: false, default: null }, + next: { type: String, default: '/' } + }, + data () { return { - username: "", - email: "", - password: "", + username: '', + email: '', + password: '', isLoadingInstanceSetting: true, errors: [], isLoading: false, @@ -33,12 +43,12 @@ export default { } }, computed: { - labels() { - let title = this.$pgettext("*/Signup/Title", "Sign Up") + labels () { + const title = this.$pgettext('*/Signup/Title', 'Sign Up') return { title } } - }, + } } </script> diff --git a/front/src/views/channels/DetailBase.vue b/front/src/views/channels/DetailBase.vue index 76506cacf46c0f3b13cbfd110467e80b0cc662f2..3d878620aebe2676e7caaa8a469e013fda1460a6 100644 --- a/front/src/views/channels/DetailBase.vue +++ b/front/src/views/channels/DetailBase.vue @@ -1,132 +1,240 @@ <template> - <main class="main pusher" v-title="labels.title"> - <div v-if="isLoading" class="ui vertical segment"> - <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> + <main + v-title="labels.title" + class="main pusher" + > + <div + v-if="isLoading" + class="ui vertical segment" + > + <div :class="['ui', 'centered', 'active', 'inline', 'loader']" /> </div> <template v-if="object && !isLoading"> - <section class="ui head vertical stripe segment container" v-title="object.artist.name"> + <section + v-title="object.artist.name" + class="ui head vertical stripe segment container" + > <div class="ui stackable grid"> <div class="seven wide column"> <div class="ui two column grid"> <div class="column"> - <img alt="" class="huge channel-image" v-if="object.artist.cover" :src="$store.getters['instance/absoluteUrl'](object.artist.cover.urls.medium_square_crop)"> - <i v-else class="huge circular inverted users violet icon"></i> + <img + v-if="object.artist.cover" + alt="" + class="huge channel-image" + :src="$store.getters['instance/absoluteUrl'](object.artist.cover.urls.medium_square_crop)" + > + <i + v-else + class="huge circular inverted users violet icon" + /> </div> <div class="ui column right aligned"> - <tags-list v-if="object.artist.tags && object.artist.tags.length > 0" :tags="object.artist.tags"></tags-list> - <actor-link v-if="object.actor" :avatar="false" :actor="object.attributed_to" :display-name="true"></actor-link> + <tags-list + v-if="object.artist.tags && object.artist.tags.length > 0" + :tags="object.artist.tags" + /> + <actor-link + v-if="object.actor" + :avatar="false" + :actor="object.attributed_to" + :display-name="true" + /> <template v-if="totalTracks > 0"> - <div class="ui hidden very small divider"></div> - <translate translate-context="Content/Channel/Paragraph" - key="1" + <div class="ui hidden very small divider" /> + <translate v-if="object.artist.content_category === 'podcast'" + key="1" + translate-context="Content/Channel/Paragraph" translate-plural="%{ count } episodes" :translate-n="totalTracks" - :translate-params="{count: totalTracks}"> + :translate-params="{count: totalTracks}" + > %{ count } episode </translate> - <translate key="2" v-else translate-context="*/*/*" :translate-params="{count: totalTracks}" :translate-n="totalTracks" translate-plural="%{ count } tracks">%{ count } track</translate> + <translate + v-else + key="2" + translate-context="*/*/*" + :translate-params="{count: totalTracks}" + :translate-n="totalTracks" + translate-plural="%{ count } tracks" + > + %{ count } track + </translate> </template> <template v-if="object.attributed_to.full_username === $store.state.auth.fullUsername || $store.getters['channels/isSubscribed'](object.uuid)"> - <br><translate translate-context="Content/Channel/Paragraph" translate-plural="%{ count } subscribers" :translate-n="object.subscriptions_count" :translate-params="{count: object.subscriptions_count}">%{ count } subscriber</translate> - <br><translate translate-context="Content/Channel/Paragraph" translate-plural="%{ count } listenings" :translate-n="object.downloads_count" :translate-params="{count: object.downloads_count}">%{ count } listening</translate> + <br><translate + translate-context="Content/Channel/Paragraph" + translate-plural="%{ count } subscribers" + :translate-n="object.subscriptions_count" + :translate-params="{count: object.subscriptions_count}" + > + %{ count } subscriber + </translate> + <br><translate + translate-context="Content/Channel/Paragraph" + translate-plural="%{ count } listenings" + :translate-n="object.downloads_count" + :translate-params="{count: object.downloads_count}" + > + %{ count } listening + </translate> </template> - <div class="ui hidden small divider"></div> - <a @click.stop.prevent="showSubscribeModal = true" class="ui icon small basic button"> - <i class="feed icon"></i> + <div class="ui hidden small divider" /> + <a + class="ui icon small basic button" + @click.stop.prevent="showSubscribeModal = true" + > + <i class="feed icon" /> </a> - <modal class="tiny" :show.sync="showSubscribeModal"> + <modal + class="tiny" + :show.sync="showSubscribeModal" + > <h4 class="header"> - <translate translate-context="Popup/Channel/Title/Verb">Subscribe to this channel</translate> + <translate translate-context="Popup/Channel/Title/Verb"> + Subscribe to this channel + </translate> </h4> <div class="scrollable content"> <div class="description"> - <template v-if="$store.state.auth.authenticated"> <h3> - <i class="user icon"></i> - <translate translate-context="Content/Channels/Header">Subscribe on Funkwhale</translate> + <i class="user icon" /> + <translate translate-context="Content/Channels/Header"> + Subscribe on Funkwhale + </translate> </h3> - <subscribe-button @subscribed="object.subscriptions_count += 1" @unsubscribed="object.subscriptions_count -= 1" :channel="object"></subscribe-button> + <subscribe-button + :channel="object" + @subscribed="object.subscriptions_count += 1" + @unsubscribed="object.subscriptions_count -= 1" + /> </template> <template v-if="object.rss_url"> <h3> - <i class="feed icon"></i> - <translate translate-context="Content/Channels/Header">Subscribe via RSS</translate> + <i class="feed icon" /> + <translate translate-context="Content/Channels/Header"> + Subscribe via RSS + </translate> </h3> - <p><translate translate-context="Content/Channels/Label">Copy-paste the following URL in your favorite podcatcher:</translate></p> + <p> + <translate translate-context="Content/Channels/Label"> + Copy-paste the following URL in your favorite podcatcher: + </translate> + </p> <copy-input :value="object.rss_url" /> </template> <template v-if="object.actor"> <h3> - <i class="bell icon"></i> - <translate translate-context="Content/Channels/Header">Subscribe on the Fediverse</translate> + <i class="bell icon" /> + <translate translate-context="Content/Channels/Header"> + Subscribe on the Fediverse + </translate> </h3> - <p><translate translate-context="Content/Channels/Label">If you're using Mastodon or other fediverse applications, you can subscribe to this account:</translate></p> + <p> + <translate translate-context="Content/Channels/Label"> + If you're using Mastodon or other fediverse applications, you can subscribe to this account: + </translate> + </p> <copy-input :value="`@${object.actor.full_username}`" /> </template> </div> </div> <div class="actions"> <button class="ui basic deny button"> - <translate translate-context="*/*/Button.Label/Verb">Cancel</translate> + <translate translate-context="*/*/Button.Label/Verb"> + Cancel + </translate> </button> </div> </modal> - <button class="ui right floated pointing dropdown icon small basic button" ref="dropdown" v-dropdown="{direction: 'downward'}"> - <i class="ellipsis vertical icon"></i> + <button + ref="dropdown" + v-dropdown="{direction: 'downward'}" + class="ui right floated pointing dropdown icon small basic button" + > + <i class="ellipsis vertical icon" /> <div class="menu"> <a - href="" v-if="totalTracks > 0" + href="" + class="basic item" @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> </a> <a - :href="object.url" v-if="object.actor && object.actor.domain != $store.getters['instance/domain']" + :href="object.url" target="_blank" - class="basic item"> - <i class="external icon"></i> - <translate :translate-params="{domain: object.actor.domain}" translate-context="Content/*/Button.Label/Verb">View on %{ domain }</translate> + class="basic item" + > + <i class="external icon" /> + <translate + :translate-params="{domain: object.actor.domain}" + translate-context="Content/*/Button.Label/Verb" + >View on %{ domain }</translate> </a> - <div class="divider"></div> + <div class="divider" /> <a - href="" - class="basic item" v-for="obj in getReportableObjs({account: object.attributed_to, channel: object})" :key="obj.target.type + obj.target.id" - @click.stop.prevent="$store.dispatch('moderation/report', obj.target)"> + href="" + class="basic item" + @click.stop.prevent="$store.dispatch('moderation/report', obj.target)" + > <i class="share icon" /> {{ obj.label }} </a> <template v-if="isOwner"> - <div class="divider"></div> + <div class="divider" /> <a class="item" href="" - @click.stop.prevent="showEditModal = true"> + @click.stop.prevent="showEditModal = true" + > <translate translate-context="*/*/*/Verb">Edit…</translate> </a> <dangerous-button - :class="['ui', {loading: isLoading}, 'item']" v-if="object" - @confirm="remove()"> - <translate translate-context="*/*/*/Verb">Delete…</translate> - <p slot="modal-header"><translate translate-context="Popup/Channel/Title">Delete this Channel?</translate></p> + :class="['ui', {loading: isLoading}, 'item']" + @confirm="remove()" + > + <translate translate-context="*/*/*/Verb"> + Delete… + </translate> + <p slot="modal-header"> + <translate translate-context="Popup/Channel/Title"> + Delete this Channel? + </translate> + </p> <div slot="modal-content"> - <p><translate translate-context="Content/Moderation/Paragraph">The channel will be deleted, as well as any related files and data. This action is irreversible.</translate></p> + <p> + <translate translate-context="Content/Moderation/Paragraph"> + The channel will be deleted, as well as any related files and data. This action is irreversible. + </translate> + </p> </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> </template> - <template v-if="$store.state.auth.availablePermissions['library']" > - <div class="divider"></div> - <router-link class="basic item" :to="{name: 'manage.channels.detail', params: {id: object.uuid}}"> - <i class="wrench icon"></i> - <translate translate-context="Content/Moderation/Link">Open in moderation interface</translate> + <template v-if="$store.state.auth.availablePermissions['library']"> + <div class="divider" /> + <router-link + class="basic item" + :to="{name: 'manage.channels.detail', params: {id: object.uuid}}" + > + <i class="wrench icon" /> + <translate translate-context="Content/Moderation/Link"> + Open in moderation interface + </translate> </router-link> </template> </div> @@ -134,56 +242,115 @@ </div> </div> <h1 class="ui header"> - <div class="left aligned" :title="object.artist.name"> + <div + class="left aligned" + :title="object.artist.name" + > {{ object.artist.name }} - <div class="ui hidden very small divider"></div> - <div class="sub header ellipsis" v-if="object.actor" :title="object.actor.full_username"> + <div class="ui hidden very small divider" /> + <div + v-if="object.actor" + class="sub header ellipsis" + :title="object.actor.full_username" + > {{ object.actor.full_username }} </div> - <div v-else class="sub header ellipsis"> - <a :href="object.url || object.rss_url" rel="noopener noreferrer" target="_blank"> - <i class="external link icon"></i> - <translate :translate-params="{domain: externalDomain}" translate-context="Content/Channel/Paragraph">Mirrored from %{ domain }</translate> + <div + v-else + class="sub header ellipsis" + > + <a + :href="object.url || object.rss_url" + rel="noopener noreferrer" + target="_blank" + > + <i class="external link icon" /> + <translate + :translate-params="{domain: externalDomain}" + translate-context="Content/Channel/Paragraph" + >Mirrored from %{ domain }</translate> </a> </div> </div> </h1> <div class="header-buttons"> - <div class="ui buttons" v-if="isOwner"> - <button class="ui basic labeled icon button" @click.prevent.stop="$store.commit('channels/showUploadModal', {show: true, config: {channel: object}})"> - <i class="upload icon"></i> - <translate translate-context="Content/Channels/Button.Label/Verb">Upload</translate> + <div + v-if="isOwner" + class="ui buttons" + > + <button + class="ui basic labeled icon button" + @click.prevent.stop="$store.commit('channels/showUploadModal', {show: true, config: {channel: object}})" + > + <i class="upload icon" /> + <translate translate-context="Content/Channels/Button.Label/Verb"> + Upload + </translate> </button> </div> <div class="ui buttons"> - <play-button :is-playable="isPlayable" class="vibrant" :artist="object.artist"> - <translate translate-context="Content/Channels/Button.Label/Verb">Play</translate> + <play-button + :is-playable="isPlayable" + class="vibrant" + :artist="object.artist" + > + <translate translate-context="Content/Channels/Button.Label/Verb"> + Play + </translate> </play-button> </div> <div class="ui buttons"> - <subscribe-button @subscribed="object.subscriptions_count += 1" @unsubscribed="object.subscriptions_count -= 1" :channel="object"></subscribe-button> + <subscribe-button + :channel="object" + @subscribed="object.subscriptions_count += 1" + @unsubscribed="object.subscriptions_count -= 1" + /> </div> - <modal :show.sync="showEmbedModal" v-if="totalTracks > 0"> + <modal + v-if="totalTracks > 0" + :show.sync="showEmbedModal" + > <h4 class="header"> - <translate translate-context="Popup/Artist/Title/Verb">Embed this artist work on your website</translate> + <translate translate-context="Popup/Artist/Title/Verb"> + Embed this artist work on your website + </translate> </h4> <div class="scrolling content"> <div class="description"> - <embed-wizard type="artist" :id="object.artist.id" /> + <embed-wizard + :id="object.artist.id" + type="artist" + /> </div> </div> <div class="actions"> <button class="ui basic deny button"> - <translate translate-context="*/*/Button.Label/Verb">Cancel</translate> + <translate translate-context="*/*/Button.Label/Verb"> + Cancel + </translate> </button> </div> </modal> - <modal :show.sync="showEditModal" v-if="isOwner"> + <modal + v-if="isOwner" + :show.sync="showEditModal" + > <h4 class="header"> - <translate v-if="object.artist.content_category === 'podcast'" key="1" translate-context="Content/Channel/*">Podcast channel</translate> - <translate v-else key="2" translate-context="Content/Channel/*">Artist channel</translate> - + <translate + v-if="object.artist.content_category === 'podcast'" + key="1" + translate-context="Content/Channel/*" + > + Podcast channel + </translate> + <translate + v-else + key="2" + translate-context="Content/Channel/*" + > + Artist channel + </translate> </h4> <div class="scrolling content"> <channel-form @@ -191,39 +358,75 @@ :object="object" @loading="edit.isLoading = $event" @submittable="edit.submittable = $event" - @updated="fetchData"></channel-form> - <div class="ui hidden divider"></div> + @updated="fetchData" + /> + <div class="ui hidden divider" /> </div> <div class="actions"> <button class="ui left floated basic deny button"> - <translate translate-context="*/*/Button.Label/Verb">Cancel</translate> + <translate translate-context="*/*/Button.Label/Verb"> + Cancel + </translate> </button> - <button @click.stop="$refs.editForm.submit" :class="['ui', 'primary', 'confirm', {loading: edit.isLoading}, 'button']" :disabled="!edit.submittable"> - <translate translate-context="*/Channels/Button.Label">Update channel</translate> + <button + :class="['ui', 'primary', 'confirm', {loading: edit.isLoading}, 'button']" + :disabled="!edit.submittable" + @click.stop="$refs.editForm.submit" + > + <translate translate-context="*/Channels/Button.Label"> + Update channel + </translate> </button> </div> </modal> </div> <div v-if="$store.getters['ui/layoutVersion'] === 'large'"> <rendered-description - @updated="object = $event" :content="object.artist.description" :update-url="`channels/${object.uuid}/`" - :can-update="false"></rendered-description> + :can-update="false" + @updated="object = $event" + /> </div> </div> <div class="nine wide column"> <div class="ui secondary pointing center aligned menu"> - <router-link class="item" :exact="true" :to="{name: 'channels.detail', params: {id: id}}"> - <translate translate-context="Content/Channels/Link">Overview</translate> + <router-link + class="item" + :exact="true" + :to="{name: 'channels.detail', params: {id: id}}" + > + <translate translate-context="Content/Channels/Link"> + Overview + </translate> </router-link> - <router-link class="item" :exact="true" :to="{name: 'channels.detail.episodes', params: {id: id}}"> - <translate key="1" v-if="isPodcast" translate-context="Content/Channels/*">All Episodes</translate> - <translate key="2" v-else translate-context="*/*/*">Tracks</translate> + <router-link + class="item" + :exact="true" + :to="{name: 'channels.detail.episodes', params: {id: id}}" + > + <translate + v-if="isPodcast" + key="1" + translate-context="Content/Channels/*" + > + All Episodes + </translate> + <translate + v-else + key="2" + translate-context="*/*/*" + > + Tracks + </translate> </router-link> </div> - <div class="ui hidden divider"></div> - <router-view v-if="object" :object="object" @tracks-loaded="totalTracks = $event"></router-view> + <div class="ui hidden divider" /> + <router-view + v-if="object" + :object="object" + @tracks-loaded="totalTracks = $event" + /> </div> </div> </section> @@ -232,32 +435,32 @@ </template> <script> -import axios from "axios" -import PlayButton from "@/components/audio/PlayButton" -import ChannelEntries from "@/components/audio/ChannelEntries" -import ChannelSeries from "@/components/audio/ChannelSeries" -import EmbedWizard from "@/components/audio/EmbedWizard" +import axios from 'axios' +import PlayButton from '@/components/audio/PlayButton' +import EmbedWizard from '@/components/audio/EmbedWizard' import Modal from '@/components/semantic/Modal' -import TagsList from "@/components/tags/List" +import TagsList from '@/components/tags/List' import ReportMixin from '@/components/mixins/Report' import SubscribeButton from '@/components/channels/SubscribeButton' -import ChannelForm from "@/components/audio/ChannelForm" +import ChannelForm from '@/components/audio/ChannelForm' export default { - mixins: [ReportMixin], - props: ["id"], components: { PlayButton, EmbedWizard, Modal, TagsList, - ChannelEntries, - ChannelSeries, SubscribeButton, - ChannelForm, + ChannelForm + }, + mixins: [ReportMixin], + beforeRouteUpdate (to, from, next) { + to.meta.preserveScrollPosition = true + next() }, - data() { + props: { id: { type: Number, required: true } }, + data () { return { isLoading: true, object: null, @@ -268,39 +471,67 @@ export default { showSubscribeModal: false, edit: { submittable: false, - loading: false, + loading: false } } }, - beforeRouteUpdate (to, from, next) { - to.meta.preserveScrollPosition = true - next() + computed: { + externalDomain () { + const parser = document.createElement('a') + parser.href = this.object.url || this.object.rss_url + return parser.hostname + }, + + isOwner () { + return this.$store.state.auth.authenticated && this.object.attributed_to.full_username === this.$store.state.auth.fullUsername + }, + isPodcast () { + return this.object.artist.content_category === 'podcast' + }, + labels () { + return { + title: this.$pgettext('*/*/*', 'Channel') + } + }, + contentFilter () { + return this.$store.getters['moderation/artistFilters']().filter((e) => { + return e.target.id === this.object.artist.id + })[0] + }, + isPlayable () { + return this.totalTracks > 0 + } + }, + watch: { + id () { + this.fetchData() + } }, - async created() { + async created () { await this.fetchData() - let authenticated = this.$store.state.auth.authenticated - if (!authenticated && this.$store.getters['instance/domain'] != this.object.actor.domain) { - this.$router.push({name: 'login', query: {next: this.$route.fullPath}}) + const authenticated = this.$store.state.auth.authenticated + if (!authenticated && this.$store.getters['instance/domain'] !== this.object.actor.domain) { + this.$router.push({ name: 'login', query: { next: this.$route.fullPath } }) } }, methods: { - async fetchData() { - var self = this + async fetchData () { + const self = this this.showEditModal = false this.edit.isLoading = false this.isLoading = true - let channelPromise = axios.get(`channels/${this.id}`, {params: {refresh: 'true'}}).then(response => { + const channelPromise = axios.get(`channels/${this.id}`, { params: { refresh: 'true' } }).then(response => { self.object = response.data - if ((self.id == response.data.uuid) && response.data.actor) { + if ((self.id === response.data.uuid) && response.data.actor) { // replace with the pretty channel url if possible - let actor = response.data.actor + const actor = response.data.actor if (actor.is_local) { - self.$router.replace({name: 'channels.detail', params: {id: actor.preferred_username}}) + self.$router.replace({ name: 'channels.detail', params: { id: actor.preferred_username } }) } else { - self.$router.replace({name: 'channels.detail', params: {id: actor.full_username}}) + self.$router.replace({ name: 'channels.detail', params: { id: actor.full_username } }) } } - let tracksPromise = axios.get("tracks", {params: {channel: response.data.uuid, page_size: 1, playable: true, include_channels: true}}).then(response => { + axios.get('tracks', { params: { channel: response.data.uuid, page_size: 1, playable: true, include_channels: true } }).then(response => { self.totalTracks = response.data.count self.isLoading = false }) @@ -308,50 +539,17 @@ export default { await channelPromise }, remove () { - let self = this + const self = this self.isLoading = true axios.delete(`channels/${this.object.uuid}`).then((response) => { self.isLoading = false self.$emit('deleted') - self.$router.push({name: 'profile.overview', params: {username: self.$store.state.auth.username}}) + self.$router.push({ name: 'profile.overview', params: { username: self.$store.state.auth.username } }) }, error => { self.isLoading = false self.errors = error.backendErrors }) } - }, - computed: { - externalDomain () { - let parser = document.createElement('a') - parser.href = this.object.url || this.object.rss_url - return parser.hostname - }, - - isOwner () { - return this.$store.state.auth.authenticated && this.object.attributed_to.full_username === this.$store.state.auth.fullUsername - }, - isPodcast () { - return this.object.artist.content_category === 'podcast' - }, - labels () { - return { - title: this.$pgettext('*/*/*', 'Channel') - } - }, - contentFilter () { - let self = this - return this.$store.getters['moderation/artistFilters']().filter((e) => { - return e.target.id === this.object.artist.id - })[0] - }, - isPlayable () { - return this.totalTracks > 0 - }, - }, - watch: { - id() { - this.fetchData() - } } } </script> diff --git a/front/src/views/channels/DetailEpisodes.vue b/front/src/views/channels/DetailEpisodes.vue index 09de17f9038192c2422a45110c07b96426d1d908..6d01f8736e055de2d8a87eace074f74d52a00004 100644 --- a/front/src/views/channels/DetailEpisodes.vue +++ b/front/src/views/channels/DetailEpisodes.vue @@ -1,18 +1,21 @@ <template> <section> - <channel-entries :default-cover="object.artist.cover" :is-podcast="object.artist.content_category === 'podcast'" :limit="25" :filters="{channel: object.uuid, ordering: 'creation_date'}"> - </channel-entries> + <channel-entries + :default-cover="object.artist.cover" + :is-podcast="object.artist.content_category === 'podcast'" + :limit="25" + :filters="{channel: object.uuid, ordering: 'creation_date'}" + /> </section> </template> <script> -import ChannelEntries from "@/components/audio/ChannelEntries" - +import ChannelEntries from '@/components/audio/ChannelEntries' export default { - props: ['object'], components: { - ChannelEntries, + ChannelEntries }, + props: { object: { type: Object, required: true } } } </script> diff --git a/front/src/views/channels/DetailOverview.vue b/front/src/views/channels/DetailOverview.vue index 54c53a9b4cf03c93175c2dc29e08681c3d7509a4..cca63ded17f3e38d40ef0b939a9dc3e8cfb216f6 100644 --- a/front/src/views/channels/DetailOverview.vue +++ b/front/src/views/channels/DetailOverview.vue @@ -1,79 +1,141 @@ <template> <section> - <div class="ui info message" v-if="pendingUploads.length > 0"> + <div + v-if="pendingUploads.length > 0" + class="ui info message" + > <template v-if="isSuccessfull"> - <i role="button" class="close icon" @click="pendingUploads = []"></i> + <i + role="button" + class="close icon" + @click="pendingUploads = []" + /> <h3 class="ui header"> - <translate translate-context="Content/Channel/Header">Uploads published successfully</translate> + <translate translate-context="Content/Channel/Header"> + Uploads published successfully + </translate> </h3> <p> - <translate translate-context="Content/Channel/Paragraph">Processed uploads:</translate> {{ processedUploads.length }}/{{ pendingUploads.length }} + <translate translate-context="Content/Channel/Paragraph"> + Processed uploads: + </translate> {{ processedUploads.length }}/{{ pendingUploads.length }} </p> </template> <template v-else-if="isOver"> <h3 class="ui header"> - <translate translate-context="Content/Channel/Header">Some uploads couldn't be published</translate> + <translate translate-context="Content/Channel/Header"> + Some uploads couldn't be published + </translate> </h3> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <router-link + v-if="skippedUploads.length > 0" class="ui basic button" :to="{name: 'content.libraries.files', query: {q: 'status:skipped'}}" - v-if="skippedUploads.length > 0"> - <translate translate-context="Content/Channel/Button">View skipped uploads</translate> + > + <translate translate-context="Content/Channel/Button"> + View skipped uploads + </translate> </router-link> <router-link + v-if="erroredUploads.length > 0" class="ui basic button" :to="{name: 'content.libraries.files', query: {q: 'status:errored'}}" - v-if="erroredUploads.length > 0"> - <translate translate-context="Content/Channel/Button">View errored uploads</translate> + > + <translate translate-context="Content/Channel/Button"> + View errored uploads + </translate> </router-link> </template> <template v-else> - <div class="ui inline right floated active loader"></div> + <div class="ui inline right floated active loader" /> <h3 class="ui header"> - <translate translate-context="Content/Channel/Header">Uploads are being processed</translate> + <translate translate-context="Content/Channel/Header"> + Uploads are being processed + </translate> </h3> <p> - <translate translate-context="Content/Channel/Paragraph">Your uploads are being processed by Funkwhale and will be live very soon.</translate> + <translate translate-context="Content/Channel/Paragraph"> + Your uploads are being processed by Funkwhale and will be live very soon. + </translate> </p> <p> - <translate translate-context="Content/Channel/Paragraph">Processed uploads:</translate> {{ processedUploads.length }}/{{ pendingUploads.length }} + <translate translate-context="Content/Channel/Paragraph"> + Processed uploads: + </translate> {{ processedUploads.length }}/{{ pendingUploads.length }} </p> - </template> </div> <div v-if="$store.getters['ui/layoutVersion'] === 'small'"> <rendered-description :content="object.artist.description" :update-url="`channels/${object.uuid}/`" - :can-update="false"></rendered-description> - <div class="ui hidden divider"></div> + :can-update="false" + /> + <div class="ui hidden divider" /> </div> - <channel-entries :is-podcast="isPodcast" :key="String(episodesKey) + 'entries'" :default-cover='object.artist.cover' :limit='25' :filters="{channel: object.uuid, ordering: '-creation_date', page_size: '25'}"> + <channel-entries + :key="String(episodesKey) + 'entries'" + :is-podcast="isPodcast" + :default-cover="object.artist.cover" + :limit="25" + :filters="{channel: object.uuid, ordering: '-creation_date', page_size: '25'}" + > <h2 class="ui header"> - <translate key="1" v-if="isPodcast" translate-context="Content/Channel/Paragraph">Latest episodes</translate> - <translate key="2" v-else translate-context="Content/Channel/Paragraph">Latest tracks</translate> + <translate + v-if="isPodcast" + key="1" + translate-context="Content/Channel/Paragraph" + > + Latest episodes + </translate> + <translate + v-else + key="2" + translate-context="Content/Channel/Paragraph" + > + Latest tracks + </translate> </h2> </channel-entries> - <div class="ui hidden divider"></div> - <channel-series :key="String(seriesKey) + 'series'" :filters="seriesFilters" :is-podcast="isPodcast"> + <div class="ui hidden divider" /> + <channel-series + :key="String(seriesKey) + 'series'" + :filters="seriesFilters" + :is-podcast="isPodcast" + > <h2 class="ui with-actions header"> - - <translate key="1" v-if="isPodcast" translate-context="Content/Channel/Paragraph">Series</translate> - <translate key="2" v-else translate-context="*/*/*">Albums</translate> - <div class="actions" v-if="isOwner"> + <translate + v-if="isPodcast" + key="1" + translate-context="Content/Channel/Paragraph" + > + Series + </translate> + <translate + v-else + key="2" + translate-context="*/*/*" + > + Albums + </translate> + <div + v-if="isOwner" + class="actions" + > <a @click.stop.prevent="$refs.albumModal.show = true"> - <i class="plus icon"></i> + <i class="plus icon" /> <translate translate-context="Content/Profile/Button">Add new</translate> </a> </div> </h2> </channel-series> <album-modal - ref="albumModal" v-if="isOwner" + ref="albumModal" :channel="object" - @created="$refs.albumModal.show = false; seriesKey = new Date()"></album-modal> + @created="$refs.albumModal.show = false; seriesKey = new Date()" + /> </section> </template> @@ -81,41 +143,24 @@ import axios from 'axios' import qs from 'qs' -import ChannelEntries from "@/components/audio/ChannelEntries" -import ChannelSeries from "@/components/audio/ChannelSeries" -import AlbumModal from "@/components/channels/AlbumModal" - +import ChannelEntries from '@/components/audio/ChannelEntries' +import ChannelSeries from '@/components/audio/ChannelSeries' +import AlbumModal from '@/components/channels/AlbumModal' export default { - props: ['object'], components: { ChannelEntries, ChannelSeries, - AlbumModal, + AlbumModal }, + props: { object: { type: Object, required: true } }, data () { return { seriesKey: new Date(), episodesKey: new Date(), - pendingUploads: [], - } - }, - async created () { - if (this.isOwner) { - await this.fetchPendingUploads() - this.$store.commit("ui/addWebsocketEventHandler", { - eventName: "import.status_updated", - id: "fileUploadChannel", - handler: this.handleImportEvent - }); + pendingUploads: [] } }, - destroyed() { - this.$store.commit("ui/removeWebsocketEventHandler", { - eventName: "import.status_updated", - id: "fileUploadChannel" - }); - }, computed: { isPodcast () { return this.object.artist.content_category === 'podcast' @@ -124,7 +169,7 @@ export default { return this.$store.state.auth.authenticated && this.object.attributed_to.full_username === this.$store.state.auth.fullUsername }, seriesFilters () { - let filters = {artist: this.object.artist.id, ordering: '-creation_date'} + const filters = { artist: this.object.artist.id, ordering: '-creation_date' } if (!this.isOwner) { filters.playable = 'true' } @@ -132,26 +177,26 @@ export default { }, processedUploads () { return this.pendingUploads.filter((u) => { - return u.import_status != "pending" + return u.import_status !== 'pending' }) }, erroredUploads () { return this.pendingUploads.filter((u) => { - return u.import_status === "errored" + return u.import_status === 'errored' }) }, skippedUploads () { return this.pendingUploads.filter((u) => { - return u.import_status === "skipped" + return u.import_status === 'skipped' }) }, finishedUploads () { return this.pendingUploads.filter((u) => { - return u.import_status === "finished" + return u.import_status === 'finished' }) }, pendingUploadsById () { - let d = {} + const d = {} this.pendingUploads.forEach((u) => { d[u.uuid] = u }) @@ -164,37 +209,51 @@ export default { return this.pendingUploads && this.finishedUploads.length === this.pendingUploads.length } }, + watch: { + '$store.state.channels.latestPublication' (v) { + if (v && v.uploads && v.channel.uuid === this.object.uuid) { + this.pendingUploads = [...this.pendingUploads, ...v.uploads] + } + }, + 'isOver' (v) { + if (v) { + this.seriesKey = new Date() + this.episodesKey = new Date() + } + } + }, + async created () { + if (this.isOwner) { + await this.fetchPendingUploads() + this.$store.commit('ui/addWebsocketEventHandler', { + eventName: 'import.status_updated', + id: 'fileUploadChannel', + handler: this.handleImportEvent + }) + } + }, + destroyed () { + this.$store.commit('ui/removeWebsocketEventHandler', { + eventName: 'import.status_updated', + id: 'fileUploadChannel' + }) + }, methods: { - handleImportEvent(event) { - let self = this; + handleImportEvent (event) { if (!this.pendingUploadsById[event.upload.uuid]) { - return; + return } Object.assign(this.pendingUploadsById[event.upload.uuid], event.upload) }, async fetchPendingUploads () { - let response = await axios.get('uploads/', { - params: {channel: this.object.uuid, import_status: ['pending', 'skipped', 'errored'], include_channels: 'true'}, - paramsSerializer: function(params) { + const response = await axios.get('uploads/', { + params: { channel: this.object.uuid, import_status: ['pending', 'skipped', 'errored'], include_channels: 'true' }, + paramsSerializer: function (params) { return qs.stringify(params, { indices: false }) } }) this.pendingUploads = response.data.results } - }, - watch: { - "$store.state.channels.latestPublication" (v) { - if (v && v.uploads && v.channel.uuid === this.object.uuid) { - let test - this.pendingUploads = [...this.pendingUploads, ...v.uploads] - } - }, - "isOver" (v) { - if (v) { - this.seriesKey = new Date() - this.episodesKey = new Date() - } - } } } </script> diff --git a/front/src/views/channels/SubscriptionsList.vue b/front/src/views/channels/SubscriptionsList.vue index 6ac12fd9da60dc29d0b4fb616f5d4ebbefbe7ba0..840c252ae8ce8acb60c2d1f34b7035ba87a1d80b 100644 --- a/front/src/views/channels/SubscriptionsList.vue +++ b/front/src/views/channels/SubscriptionsList.vue @@ -1,65 +1,89 @@ <template> - <main class="main pusher" v-title="labels.title"> + <main + v-title="labels.title" + class="main pusher" + > <section class="ui head vertical stripe segment container"> <h1 class="ui with-actions header"> {{ labels.title }} <div class="actions"> <a @click.stop.prevent="showSubscribeModal = true"> - <i class="plus icon"></i> + <i class="plus icon" /> <translate translate-context="Content/Profile/Button">Add new</translate> </a> </div> </h1> - <modal class="tiny" :show.sync="showSubscribeModal" :fullscreen="false"> + <modal + class="tiny" + :show.sync="showSubscribeModal" + :fullscreen="false" + > <h2 class="header"> - <translate translate-context="*/*/*/Noun">Subscription</translate> + <translate translate-context="*/*/*/Noun"> + Subscription + </translate> </h2> - <div class="scrolling content" ref="modalContent"> + <div + ref="modalContent" + class="scrolling content" + > <remote-search-form type="both" :show-submit="false" :standalone="false" + :redirect="true" @subscribed="showSubscribeModal = false; reloadWidget()" - :redirect="true"></remote-search-form> + /> </div> <div class="actions"> <button class="ui basic deny button"> - <translate translate-context="*/*/Button.Label/Verb">Cancel</translate> + <translate translate-context="*/*/Button.Label/Verb"> + Cancel + </translate> </button> - <button form="remote-search" type="submit" class="ui primary button"> - <i class="bookmark icon"></i> - <translate translate-context="*/*/*/Verb">Subscribe</translate> + <button + form="remote-search" + type="submit" + class="ui primary button" + > + <i class="bookmark icon" /> + <translate translate-context="*/*/*/Verb"> + Subscribe + </translate> </button> </div> </modal> - - - <inline-search-bar v-model="query" @search="reloadWidget" :placeholder="labels.searchPlaceholder"></inline-search-bar> + <inline-search-bar + v-model="query" + :placeholder="labels.searchPlaceholder" + @search="reloadWidget" + /> <channels-widget :key="widgetKey" :limit="50" :show-modification-date="true" - :filters="{q: query, subscribed: 'true', ordering: '-modification_date'}"></channels-widget> + :filters="{q: query, subscribed: 'true', ordering: '-modification_date'}" + /> </section> </main> </template> <script> -import axios from "axios" +import axios from 'axios' import Modal from '@/components/semantic/Modal' -import ChannelsWidget from "@/components/audio/ChannelsWidget" -import RemoteSearchForm from "@/components/RemoteSearchForm" +import ChannelsWidget from '@/components/audio/ChannelsWidget' +import RemoteSearchForm from '@/components/RemoteSearchForm' export default { - props: ["defaultQuery"], components: { ChannelsWidget, RemoteSearchForm, - Modal, + Modal }, - data() { + props: { defaultQuery: { type: String, required: false, default: '' } }, + data () { return { query: this.defaultQuery || '', channels: [], @@ -69,25 +93,25 @@ export default { previousPage: null, nextPage: null, widgetKey: String(new Date()), - showSubscribeModal: false, + showSubscribeModal: false } }, - created () { - this.fetchData() - }, computed: { labels () { return { - title: this.$pgettext("Content/Subscriptions/Header", "Subscribed Channels"), - searchPlaceholder: this.$pgettext("Content/Subscriptions/Form.Placeholder", "Filter by name…"), + title: this.$pgettext('Content/Subscriptions/Header', 'Subscribed Channels'), + searchPlaceholder: this.$pgettext('Content/Subscriptions/Form.Placeholder', 'Filter by name…') } - }, + } + }, + created () { + this.fetchData() }, methods: { - fetchData() { - var self = this + fetchData () { + const self = this this.isLoading = true - axios.get('channels/', {params: {subscribed: "true", q: this.query}}).then(response => { + axios.get('channels/', { params: { subscribed: 'true', q: this.query } }).then(response => { self.previousPage = response.data.previous self.nextPage = response.data.next self.isLoading = false @@ -98,6 +122,6 @@ export default { reloadWidget () { this.widgetKey = String(new Date()) } - }, + } } </script> diff --git a/front/src/views/content/Base.vue b/front/src/views/content/Base.vue index bd593be55ec186570cb648b9d40725767b1b7070..e7c700d3b23bf06b6e92458074d8c353e7ea2aaa 100644 --- a/front/src/views/content/Base.vue +++ b/front/src/views/content/Base.vue @@ -1,22 +1,39 @@ <template> - <main class="main pusher" v-title="labels.title"> - <nav class="ui secondary pointing menu" role="navigation" :aria-label="labels.secondaryMenu"> + <main + v-title="labels.title" + class="main pusher" + > + <nav + class="ui secondary pointing menu" + role="navigation" + :aria-label="labels.secondaryMenu" + > <router-link class="ui item" - :to="{name: 'content.libraries.index'}"><translate translate-context="*/*/*/Noun">Libraries</translate></router-link> + :to="{name: 'content.libraries.index'}" + > + <translate translate-context="*/*/*/Noun"> + Libraries + </translate> + </router-link> <router-link class="ui item" - :to="{name: 'content.libraries.files'}"><translate translate-context="*/*/*">Tracks</translate></router-link> + :to="{name: 'content.libraries.files'}" + > + <translate translate-context="*/*/*"> + Tracks + </translate> + </router-link> </nav> - <router-view :key="$route.fullPath"></router-view> + <router-view :key="$route.fullPath" /> </main> </template> <script> export default { computed: { - labels() { - let title = this.$pgettext('*/Library/*/Verb', "Add content") - let secondaryMenu = this.$pgettext('Menu/*/Hidden text', "Secondary menu") + labels () { + const title = this.$pgettext('*/Library/*/Verb', 'Add content') + const secondaryMenu = this.$pgettext('Menu/*/Hidden text', 'Secondary menu') return { title, secondaryMenu diff --git a/front/src/views/content/Home.vue b/front/src/views/content/Home.vue index 533e6d1c86a41be7a6df29bf10458d00ced25641..8532999ee0f0682a9782d610ec2a1f58a1a30ac9 100644 --- a/front/src/views/content/Home.vue +++ b/front/src/views/content/Home.vue @@ -1,60 +1,98 @@ <template> - <section class="ui vertical aligned stripe segment" v-title="labels.title"> + <section + v-title="labels.title" + class="ui vertical aligned stripe segment" + > <div class="ui text container"> <h1>{{ labels.title }}</h1> <p> - <strong><translate translate-context="Content/Library/Paragraph" :translate-params="{quota: defaultQuota}">This instance offers up to %{quota} of storage space for every user.</translate></strong> + <strong><translate + translate-context="Content/Library/Paragraph" + :translate-params="{quota: defaultQuota}" + >This instance offers up to %{quota} of storage space for every user.</translate></strong> </p> <div class="ui segment"> <h2> - <i class="feed icon"></i> - <translate translate-context="Content/Library/Title/Verb">Publish your work in a channel</translate> + <i class="feed icon" /> + <translate translate-context="Content/Library/Title/Verb"> + Publish your work in a channel + </translate> </h2> <p> - <translate translate-context="Content/Library/Paragraph">If you are a musician or a podcaster, channels are designed for you!</translate>  - <translate translate-context="Content/Library/Paragraph">Share your work publicly and get subscribers on Funkwhale, the Fediverse or any podcasting application.</translate> + <translate translate-context="Content/Library/Paragraph"> + If you are a musician or a podcaster, channels are designed for you! + </translate>  + <translate translate-context="Content/Library/Paragraph"> + Share your work publicly and get subscribers on Funkwhale, the Fediverse or any podcasting application. + </translate> </p> - <router-link :to="{name: 'profile.overview', params: {username: $store.state.auth.username}, hash: '#channels'}" class="ui primary button"> - <translate translate-context="Content/Library/Button.Label/Verb">Get started</translate> + <router-link + :to="{name: 'profile.overview', params: {username: $store.state.auth.username}, hash: '#channels'}" + class="ui primary button" + > + <translate translate-context="Content/Library/Button.Label/Verb"> + Get started + </translate> </router-link> </div> <div class="ui segment"> <h2> - <i class="cloud icon"></i> - <translate translate-context="Content/Library/Title/Verb">Upload third-party content in a library</translate> + <i class="cloud icon" /> + <translate translate-context="Content/Library/Title/Verb"> + Upload third-party content in a library + </translate> </h2> - <p><translate translate-context="Content/Library/Paragraph">Upload your personal music library to Funkwhale to enjoy it from anywhere and share it with friends and family.</translate></p> - <router-link :to="{name: 'content.libraries.index'}" class="ui primary button"> - <translate translate-context="Content/Library/Button.Label/Verb">Get started</translate> + <p> + <translate translate-context="Content/Library/Paragraph"> + Upload your personal music library to Funkwhale to enjoy it from anywhere and share it with friends and family. + </translate> + </p> + <router-link + :to="{name: 'content.libraries.index'}" + class="ui primary button" + > + <translate translate-context="Content/Library/Button.Label/Verb"> + Get started + </translate> </router-link> </div> <div class="ui segment"> <h2> - <i class="download icon"></i> - <translate translate-context="Content/Library/Title/Verb">Follow remote libraries</translate> + <i class="download icon" /> + <translate translate-context="Content/Library/Title/Verb"> + Follow remote libraries + </translate> </h2> - <p><translate translate-context="Content/Library/Paragraph">Follow libraries from other users to get access to new music. Public libraries can be followed immediately, while following a private library requires approval from its owner.</translate></p> - <router-link :to="{name: 'content.remote.index'}" class="ui primary button"> - <translate translate-context="Content/Library/Button.Label/Verb">Get started</translate> + <p> + <translate translate-context="Content/Library/Paragraph"> + Follow libraries from other users to get access to new music. Public libraries can be followed immediately, while following a private library requires approval from its owner. + </translate> + </p> + <router-link + :to="{name: 'content.remote.index'}" + class="ui primary button" + > + <translate translate-context="Content/Library/Button.Label/Verb"> + Get started + </translate> </router-link> </div> - </div> </section> </template> <script> -import { humanSize } from "@/filters" +import { humanSize } from '@/filters' export default { computed: { - labels() { + labels () { return { - title: this.$pgettext('Content/Library/Title/Verb', "Add and manage content") + title: this.$pgettext('Content/Library/Title/Verb', 'Add and manage content') } }, - defaultQuota() { - let quota = + defaultQuota () { + const quota = this.$store.state.instance.settings.users.upload_quota.value * 1000 * 1000 diff --git a/front/src/views/content/libraries/Card.vue b/front/src/views/content/libraries/Card.vue index ca8e53e25482392eec67b5f649bffdcf8347a9fe..d7cb065d69672dfc2c12b5a3ccccd3b65169f0d0 100644 --- a/front/src/views/content/libraries/Card.vue +++ b/front/src/views/content/libraries/Card.vue @@ -6,20 +6,23 @@ <span v-if="library.privacy_level === 'me'" class="right floated" - :data-tooltip="privacy_tooltips('me')"> - <i class="small lock icon"></i> + :data-tooltip="privacy_tooltips('me')" + > + <i class="small lock icon" /> </span> <span v-else-if="library.privacy_level === 'instance'" class="right floated" - :data-tooltip="privacy_tooltips('instance')"> - <i class="small circle outline icon"></i> + :data-tooltip="privacy_tooltips('instance')" + > + <i class="small circle outline icon" /> </span> <span v-else-if="library.privacy_level === 'everyone'" class="right floated" - :data-tooltip="privacy_tooltips('everyone')"> - <i class="small globe icon"></i> + :data-tooltip="privacy_tooltips('everyone')" + > + <i class="small globe icon" /> </span> </h4> <div class="meta"> @@ -30,23 +33,45 @@ </div> <div class="description"> {{ library.description }} - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> </div> <div class="content"> - <span v-if="library.size" class="right floated" :data-tooltip="size_label"> - <i class="database icon"></i> + <span + v-if="library.size" + class="right floated" + :data-tooltip="size_label" + > + <i class="database icon" /> {{ library.size | humanSize }} </span> - <i class="music icon"></i> - <translate translate-context="*/*/*" :translate-params="{count: library.uploads_count}" :translate-n="library.uploads_count" translate-plural="%{ count } tracks">%{ count } track</translate> + <i class="music icon" /> + <translate + translate-context="*/*/*" + :translate-params="{count: library.uploads_count}" + :translate-n="library.uploads_count" + translate-plural="%{ count } tracks" + > + %{ count } track + </translate> </div> </div> <div class="ui bottom basic attached buttons"> - <router-link :to="{name: 'library.detail.upload', params: {id: library.uuid}}" class="ui button"> - <translate translate-context="Content/Library/Card.Button.Label/Verb">Upload</translate> + <router-link + :to="{name: 'library.detail.upload', params: {id: library.uuid}}" + class="ui button" + > + <translate translate-context="Content/Library/Card.Button.Label/Verb"> + Upload + </translate> </router-link> - <router-link :to="{name: 'library.detail', params: {id: library.uuid}}" exact class="ui button"> - <translate translate-context="Content/Library/Card.Button.Label/Noun">Library Details</translate> + <router-link + :to="{name: 'library.detail', params: {id: library.uuid}}" + exact + class="ui button" + > + <translate translate-context="Content/Library/Card.Button.Label/Noun"> + Library Details + </translate> </router-link> </div> </div> @@ -57,16 +82,16 @@ import TranslationsMixin from '@/components/mixins/Translations' export default { mixins: [TranslationsMixin], - props: ['library'], - methods: { - privacy_tooltips (level) { - return 'Visibility: ' + this.sharedLabels.fields.privacy_level.choices[level].toLowerCase() - }, - }, + props: { library: { type: Object, required: true } }, computed: { size_label () { return this.$pgettext('Content/Library/Card.Help text', 'Total size of the files in this library') - }, + } + }, + methods: { + privacy_tooltips (level) { + return 'Visibility: ' + this.sharedLabels.fields.privacy_level.choices[level].toLowerCase() + } } } </script> diff --git a/front/src/views/content/libraries/Files.vue b/front/src/views/content/libraries/Files.vue index 0184df3864d0080900e1fa8aeb7f7243acf2c70a..c33824ef41706d233c8c3b661e7259378613111e 100644 --- a/front/src/views/content/libraries/Files.vue +++ b/front/src/views/content/libraries/Files.vue @@ -1,16 +1,16 @@ <template> <section class="ui vertical aligned stripe segment"> - <library-files-table :default-query="query"></library-files-table> + <library-files-table :default-query="query" /> </section> </template> <script> -import LibraryFilesTable from "./FilesTable" +import LibraryFilesTable from './FilesTable' export default { - props: ["query"], components: { LibraryFilesTable - } + }, + props: { query: { type: String, required: true } } } </script> diff --git a/front/src/views/content/libraries/FilesTable.vue b/front/src/views/content/libraries/FilesTable.vue index 5481f368ac6a5b726f2590fe7ffbe048ae51906a..9e794961ac841060cff8537f148e04761e1206ff 100644 --- a/front/src/views/content/libraries/FilesTable.vue +++ b/front/src/views/content/libraries/FilesTable.vue @@ -8,13 +8,13 @@ </label> <form @submit.prevent="search.query = $refs.search.value"> <input - name="search" + id="files-search" ref="search" + name="search" type="text" - id="files-search" :value="search.query" :placeholder="labels.searchPlaceholder" - /> + > </form> </div> <div class="field"> @@ -24,26 +24,38 @@ <select id="import-status" class="ui dropdown" - @change="addSearchToken('status', $event.target.value)" :value="getTokenValue('status', '')" + @change="addSearchToken('status', $event.target.value)" > <option value> - <translate translate-context="Content/*/Dropdown">All</translate> + <translate translate-context="Content/*/Dropdown"> + All + </translate> </option> <option value="draft"> - <translate translate-context="Content/Library/*/Short">Draft</translate> + <translate translate-context="Content/Library/*/Short"> + Draft + </translate> </option> <option value="pending"> - <translate translate-context="Content/Library/*/Short">Pending</translate> + <translate translate-context="Content/Library/*/Short"> + Pending + </translate> </option> <option value="skipped"> - <translate translate-context="Content/Library/*">Skipped</translate> + <translate translate-context="Content/Library/*"> + Skipped + </translate> </option> <option value="errored"> - <translate translate-context="Content/Library/Dropdown">Failed</translate> + <translate translate-context="Content/Library/Dropdown"> + Failed + </translate> </option> <option value="finished"> - <translate translate-context="Content/Library/*">Finished</translate> + <translate translate-context="Content/Library/*"> + Finished + </translate> </option> </select> </div> @@ -51,44 +63,69 @@ <label for="ordering-select"> <translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate> </label> - <select id="ordering-select" class="ui dropdown" v-model="ordering"> + <select + id="ordering-select" + v-model="ordering" + class="ui dropdown" + > <option - v-for="option in orderingOptions" + v-for="(option, key) in orderingOptions" + :key="key" :value="option[0]" - >{{ sharedLabels.filters[option[1]] }}</option> + > + {{ sharedLabels.filters[option[1]] }} + </option> </select> </div> <div class="field"> <label for="ordering-direction"> <translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate> </label> - <select id="ordering-direction" class="ui dropdown" v-model="orderingDirection"> + <select + id="ordering-direction" + v-model="orderingDirection" + class="ui dropdown" + > <option value="+"> - <translate translate-context="Content/Search/Dropdown">Ascending</translate> + <translate translate-context="Content/Search/Dropdown"> + Ascending + </translate> </option> <option value="-"> - <translate translate-context="Content/Search/Dropdown">Descending</translate> + <translate translate-context="Content/Search/Dropdown"> + Descending + </translate> </option> </select> </div> </div> </div> - <import-status-modal :upload="detailedUpload" :show.sync="showUploadDetailModal" /> + <import-status-modal + :upload="detailedUpload" + :show.sync="showUploadDetailModal" + /> <div class="dimmable"> - <div v-if="isLoading" class="ui active inverted dimmer"> - <div class="ui loader"></div> + <div + v-if="isLoading" + class="ui active inverted dimmer" + > + <div class="ui loader" /> </div> - <div v-else-if="!result && result.results.length === 0 && !needsRefresh" class="ui placeholder segment"> + <div + v-else-if="!result && result.results.length === 0 && !needsRefresh" + class="ui placeholder segment" + > <div class="ui icon header"> - <i class="upload icon"></i> + <i class="upload icon" /> <translate - translate-context="Content/Home/Placeholder" - >No tracks have been added to this library yet</translate> + translate-context="Content/Home/Placeholder" + > + No tracks have been added to this library yet + </translate> </div> </div> <action-table v-else - @action-launched="fetchData" :id-field="'uuid'" :objects-data="result" :custom-objects="customObjects" @@ -96,33 +133,51 @@ :refreshable="true" :needs-refresh="needsRefresh" :action-url="'uploads/action/'" - @refresh="fetchData" :filters="actionFilters" + @action-launched="fetchData" + @refresh="fetchData" > <template slot="header-cells"> <th> - <translate translate-context="*/*/*/Noun">Title</translate> + <translate translate-context="*/*/*/Noun"> + Title + </translate> </th> <th> - <translate translate-context="*/*/*/Noun">Artist</translate> + <translate translate-context="*/*/*/Noun"> + Artist + </translate> </th> <th> - <translate translate-context="*/*/*">Album</translate> + <translate translate-context="*/*/*"> + Album + </translate> </th> <th> - <translate translate-context="*/*/*/Noun">Upload date</translate> + <translate translate-context="*/*/*/Noun"> + Upload date + </translate> </th> <th> - <translate translate-context="Content/*/*/Noun">Import status</translate> + <translate translate-context="Content/*/*/Noun"> + Import status + </translate> </th> <th> - <translate translate-context="Content/*/*">Duration</translate> + <translate translate-context="Content/*/*"> + Duration + </translate> </th> <th> - <translate translate-context="Content/*/*/Noun">Size</translate> + <translate translate-context="Content/*/*/Noun"> + Size + </translate> </th> </template> - <template slot="row-cells" slot-scope="scope"> + <template + slot="row-cells" + slot-scope="scope" + > <template v-if="scope.obj.track"> <td> <router-link :to="{name: 'library.tracks.detail', params: {id: scope.obj.track.id }}"> @@ -138,27 +193,29 @@ </td> <td> <a - href="" v-if="scope.obj.track.album" + href="" class="discrete link" @click.prevent="addSearchToken('album', scope.obj.track.album.title)" >{{ scope.obj.track.album.title|truncate(20) }}</a> </td> </template> <template v-else> - <td :title="scope.obj.source">{{ scope.obj.source | truncate(25) }}</td> - <td></td> - <td></td> + <td :title="scope.obj.source"> + {{ scope.obj.source | truncate(25) }} + </td> + <td /> + <td /> </template> <td> - <human-date :date="scope.obj.creation_date"></human-date> + <human-date :date="scope.obj.creation_date" /> </td> <td> - <a + <a href="" class="discrete link" - @click.prevent="addSearchToken('status', scope.obj.import_status)" :title="sharedLabels.fields.import_status.choices[scope.obj.import_status].help" + @click.prevent="addSearchToken('status', scope.obj.import_status)" >{{ sharedLabels.fields.import_status.choices[scope.obj.import_status].label }}</a> <button class="ui tiny basic icon button" @@ -166,16 +223,24 @@ :aria-label="labels.showStatus" @click="detailedUpload = scope.obj; showUploadDetailModal = true" > - <i class="question circle outline icon"></i> + <i class="question circle outline icon" /> </button> </td> - <td v-if="scope.obj.duration">{{ scope.obj.duration | duration }}</td> + <td v-if="scope.obj.duration"> + {{ scope.obj.duration | duration }} + </td> <td v-else> - <translate translate-context="*/*/*">N/A</translate> + <translate translate-context="*/*/*"> + N/A + </translate> + </td> + <td v-if="scope.obj.size"> + {{ scope.obj.size | humanSize }} </td> - <td v-if="scope.obj.size">{{ scope.obj.size | humanSize }}</td> <td v-else> - <translate translate-context="*/*/*">N/A</translate> + <translate translate-context="*/*/*"> + N/A + </translate> </td> </template> </action-table> @@ -183,12 +248,12 @@ <div> <pagination v-if="result && result.count > paginateBy" - @page-changed="page = $event; fetchData()" :compact="true" :current="page" :paginate-by="paginateBy" :total="result.count" - ></pagination> + @page-changed="page = $event; fetchData()" + /> <span v-if="result && result.results.length > 0"> <translate @@ -201,37 +266,37 @@ </template> <script> -import axios from "axios"; -import _ from "@/lodash"; -import time from "@/utils/time"; -import { normalizeQuery, parseTokens } from "@/search"; +import axios from 'axios' +import _ from '@/lodash' +import time from '@/utils/time' +import { normalizeQuery, parseTokens } from '@/search' -import Pagination from "@/components/Pagination"; -import ActionTable from "@/components/common/ActionTable"; -import OrderingMixin from "@/components/mixins/Ordering"; -import TranslationsMixin from "@/components/mixins/Translations"; -import SmartSearchMixin from "@/components/mixins/SmartSearch"; -import ImportStatusModal from "@/components/library/ImportStatusModal"; +import Pagination from '@/components/Pagination' +import ActionTable from '@/components/common/ActionTable' +import OrderingMixin from '@/components/mixins/Ordering' +import TranslationsMixin from '@/components/mixins/Translations' +import SmartSearchMixin from '@/components/mixins/SmartSearch' +import ImportStatusModal from '@/components/library/ImportStatusModal' export default { + components: { + Pagination, + ActionTable, + ImportStatusModal + }, mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin], props: { - filters: { type: Object, required: false }, + filters: { type: Object, required: false, default: function () { return {} } }, needsRefresh: { type: Boolean, required: false, default: false }, customObjects: { type: Array, required: false, default: () => { - return []; + return [] } } }, - components: { - Pagination, - ActionTable, - ImportStatusModal - }, - data() { + data () { return { time, detailedUpload: null, @@ -244,109 +309,109 @@ export default { tokens: parseTokens(normalizeQuery(this.defaultQuery)) }, orderingOptions: [ - ["creation_date", "creation_date"], - ["title", "track_title"], - ["size", "size"], - ["duration", "duration"], - ["bitrate", "bitrate"], - ["album_title", "album_title"], - ["artist_name", "artist_name"] + ['creation_date', 'creation_date'], + ['title', 'track_title'], + ['size', 'size'], + ['duration', 'duration'], + ['bitrate', 'bitrate'], + ['album_title', 'album_title'], + ['artist_name', 'artist_name'] ] - }; - }, - created() { - this.fetchData(); - }, - methods: { - fetchData() { - this.$emit("fetch-start"); - let params = _.merge( - { - page: this.page, - page_size: this.paginateBy, - ordering: this.getOrderingAsString(), - q: this.search.query, - include_channels: 'true', - }, - this.filters || {} - ); - let self = this; - self.isLoading = true; - self.checked = []; - axios.get("/uploads/", { params: params }).then( - response => { - self.result = response.data; - self.isLoading = false; - }, - error => { - self.isLoading = false; - self.errors = error.backendErrors; - } - ); - }, + } }, computed: { - labels() { + labels () { return { searchPlaceholder: this.$pgettext( - "Content/Library/Input.Placeholder", - "Search by title, artist, album…" + 'Content/Library/Input.Placeholder', + 'Search by title, artist, album…' ), showStatus: this.$pgettext('Content/Library/Button.Label/Verb', 'Show information about the upload status for this track') - }; + } }, - actionFilters() { - var currentFilters = { + actionFilters () { + const currentFilters = { q: this.search.query, - include_channels: 'true', - }; + include_channels: 'true' + } if (this.filters) { - return _.merge(currentFilters, this.filters); + return _.merge(currentFilters, this.filters) } else { - return currentFilters; + return currentFilters } }, - actions() { - let deleteMsg = this.$pgettext("*/*/*/Verb", "Delete"); - let relaunchMsg = this.$pgettext( - "Content/Library/Dropdown/Verb", - "Restart import" - ); + actions () { + const deleteMsg = this.$pgettext('*/*/*/Verb', 'Delete') + const relaunchMsg = this.$pgettext( + 'Content/Library/Dropdown/Verb', + 'Restart import' + ) return [ { - name: "delete", + name: 'delete', label: deleteMsg, isDangerous: true, allowAll: true }, { - name: "relaunch_import", + name: 'relaunch_import', label: relaunchMsg, isDangerous: true, allowAll: true, filterCheckable: f => { - return f.import_status != "finished"; + return f.import_status !== 'finished' } } - ]; + ] } }, watch: { - orderingDirection: function() { - this.page = 1; - this.fetchData(); + orderingDirection: function () { + this.page = 1 + this.fetchData() }, - page: function() { - this.fetchData(); + page: function () { + this.fetchData() }, - ordering: function() { - this.page = 1; - this.fetchData(); + ordering: function () { + this.page = 1 + this.fetchData() }, - search(newValue) { - this.page = 1; - this.fetchData(); + search (newValue) { + this.page = 1 + this.fetchData() + } + }, + created () { + this.fetchData() + }, + methods: { + fetchData () { + this.$emit('fetch-start') + const params = _.merge( + { + page: this.page, + page_size: this.paginateBy, + ordering: this.getOrderingAsString(), + q: this.search.query, + include_channels: 'true' + }, + this.filters || {} + ) + const self = this + self.isLoading = true + self.checked = [] + axios.get('/uploads/', { params: params }).then( + response => { + self.result = response.data + self.isLoading = false + }, + error => { + self.isLoading = false + self.errors = error.backendErrors + } + ) } } -}; +} </script> diff --git a/front/src/views/content/libraries/Form.vue b/front/src/views/content/libraries/Form.vue index dd8e6421675fa2383a6bc6fcadb0e4d5f450ab77..3a81d322aa3b09990783ab30fe39ff8590a2817a 100644 --- a/front/src/views/content/libraries/Form.vue +++ b/front/src/views/content/libraries/Form.vue @@ -1,35 +1,103 @@ <template> - <form class="ui form" @submit.prevent="submit"> - <p v-if="!library"><translate translate-context="Content/Library/Paragraph">Libraries help you organize and share your music collections. You can upload your own music collection to Funkwhale and share it with your friends and family.</translate></p> - <div v-if="errors.length > 0" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Content/*/Error message.Title">Error</translate></h4> + <form + class="ui form" + @submit.prevent="submit" + > + <p v-if="!library"> + <translate translate-context="Content/Library/Paragraph"> + Libraries help you organize and share your music collections. You can upload your own music collection to Funkwhale and share it with your friends and family. + </translate> + </p> + <div + v-if="errors.length > 0" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Content/*/Error message.Title"> + Error + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> <div class="required field"> <label for="current-name"><translate translate-context="*/*/*/Noun">Name</translate></label> - <input id="current-name" name="name" v-model="currentName" :placeholder="labels.namePlaceholder" required maxlength="100"> + <input + id="current-name" + v-model="currentName" + name="name" + :placeholder="labels.namePlaceholder" + required + maxlength="100" + > </div> <div class="field"> <label for="current-description"><translate translate-context="*/*/*/Noun">Description</translate></label> - <textarea id="current-description" v-model="currentDescription" :placeholder="labels.descriptionPlaceholder" maxlength="2000"></textarea> + <textarea + id="current-description" + v-model="currentDescription" + :placeholder="labels.descriptionPlaceholder" + maxlength="2000" + /> </div> <div class="field"> <label for="visibility-level"><translate translate-context="*/*/*">Visibility</translate></label> - <p><translate translate-context="Content/Library/Paragraph">You are able to share your library with other people, regardless of its visibility.</translate></p> - <select id="visibility-level" class="ui dropdown" v-model="currentVisibilityLevel"> - <option :value="c" v-for="c in ['me', 'instance', 'everyone']">{{ sharedLabels.fields.privacy_level.choices[c] }}</option> + <p> + <translate translate-context="Content/Library/Paragraph"> + You are able to share your library with other people, regardless of its visibility. + </translate> + </p> + <select + id="visibility-level" + v-model="currentVisibilityLevel" + class="ui dropdown" + > + <option + v-for="(c, key) in ['me', 'instance', 'everyone']" + :key="key" + :value="c" + > + {{ sharedLabels.fields.privacy_level.choices[c] }} + </option> </select> </div> - <button class="ui submit button" type="submit"> - <translate translate-context="Content/Library/Button.Label/Verb" v-if="library">Update library</translate> - <translate translate-context="Content/Library/Button.Label/Verb" v-else>Create library</translate> + <button + class="ui submit button" + type="submit" + > + <translate + v-if="library" + translate-context="Content/Library/Button.Label/Verb" + > + Update library + </translate> + <translate + v-else + translate-context="Content/Library/Button.Label/Verb" + > + Create library + </translate> </button> - <dangerous-button v-if="library" type="button" class="ui right floated basic danger button" @confirm="remove()"> - <translate translate-context="*/*/*/Verb">Delete</translate> + <dangerous-button + v-if="library" + type="button" + class="ui right floated basic danger button" + @confirm="remove()" + > + <translate translate-context="*/*/*/Verb"> + Delete + </translate> <p slot="modal-header"> - <translate translate-context="Popup/Library/Title">Delete this library?</translate> + <translate translate-context="Popup/Library/Title"> + Delete this library? + </translate> </p> <p slot="modal-content"> <translate translate-context="Popup/Library/Paragraph"> @@ -37,7 +105,9 @@ </translate> </p> <div slot="modal-confirm"> - <translate translate-context="Popup/Library/Button.Label/Verb">Delete library</translate> + <translate translate-context="Popup/Library/Button.Label/Verb"> + Delete library + </translate> </div> </dangerous-button> </form> @@ -49,9 +119,9 @@ import MixinsTranslation from '@/components/mixins/Translations.vue' export default { mixins: [MixinsTranslation], - props: ['library'], + props: { library: { type: Object, required: true } }, data () { - let d = { + const d = { isLoading: false, over: false, errors: [] @@ -69,19 +139,19 @@ export default { }, computed: { labels () { - let namePlaceholder = this.$pgettext('Content/Library/Input.Placeholder', 'My awesome library') - let descriptionPlaceholder = this.$pgettext('Content/Library/Input.Placeholder', 'This library contains my personal music, I hope you like it.') + const namePlaceholder = this.$pgettext('Content/Library/Input.Placeholder', 'My awesome library') + const descriptionPlaceholder = this.$pgettext('Content/Library/Input.Placeholder', 'This library contains my personal music, I hope you like it.') return { namePlaceholder, - descriptionPlaceholder, + descriptionPlaceholder } } }, methods: { submit () { - let self = this + const self = this this.isLoading = true - let payload = { + const payload = { name: this.currentName, description: this.currentDescription, privacy_level: this.currentVisibilityLevel @@ -117,10 +187,10 @@ export default { this.currentDescription = '' }, remove () { - let self = this + const self = this axios.delete(`libraries/${this.library.uuid}/`).then((response) => { self.isLoading = false - let msg = this.$pgettext('Content/Library/Message', 'Library deleted') + const msg = this.$pgettext('Content/Library/Message', 'Library deleted') self.$emit('deleted', {}) self.$store.commit('ui/addMessage', { content: msg, diff --git a/front/src/views/content/libraries/Home.vue b/front/src/views/content/libraries/Home.vue index 42fc5c77e514432a1fdbd3d8c2150ab596d3a640..85b88ddd069ccd56741bb6c5d94cf4155df4d5a7 100644 --- a/front/src/views/content/libraries/Home.vue +++ b/front/src/views/content/libraries/Home.vue @@ -1,25 +1,62 @@ <template> <section class="ui vertical aligned stripe segment"> - <div v-if="isLoading" :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']"> - <div class="ui text loader"><translate translate-context="Content/Library/Paragraph">Loading Libraries…</translate></div> + <div + v-if="isLoading" + :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']" + > + <div class="ui text loader"> + <translate translate-context="Content/Library/Paragraph"> + Loading Libraries… + </translate> + </div> </div> - <div v-else class="ui text container"> - <h1 class="ui header"><translate translate-context="Content/Library/Title">My libraries</translate></h1> + <div + v-else + class="ui text container" + > + <h1 class="ui header"> + <translate translate-context="Content/Library/Title"> + My libraries + </translate> + </h1> <p v-if="libraries.length == 0"> - <translate translate-context="Content/Library/Paragraph">Looks like you don't have a library, it's time to create one.</translate> + <translate translate-context="Content/Library/Paragraph"> + Looks like you don't have a library, it's time to create one. + </translate> </p> - <a :aria-expanded="!hiddenForm" @click.prevent="hiddenForm = !hiddenForm" href=""> - <i class="plus icon" v-if="hiddenForm" /> - <i class="minus icon" v-else /> + <a + :aria-expanded="!hiddenForm" + href="" + @click.prevent="hiddenForm = !hiddenForm" + > + <i + v-if="hiddenForm" + class="plus icon" + /> + <i + v-else + class="minus icon" + /> <translate translate-context="Content/Library/Link/Verb">Create a new library</translate> </a> - <library-form :library="null" v-if="!hiddenForm" @created="libraryCreated" /> - <div class="ui hidden divider"></div> + <library-form + v-if="!hiddenForm" + :library="null" + @created="libraryCreated" + /> + <div class="ui hidden divider" /> <quota /> - <div class="ui hidden divider"></div> - <div v-if="libraries.length > 0" class="ui two column grid"> - <div v-for="library in libraries" :key="library.uuid" class="column"> + <div class="ui hidden divider" /> + <div + v-if="libraries.length > 0" + class="ui two column grid" + > + <div + v-for="library in libraries" + :key="library.uuid" + class="column" + > <library-card :library="library" /> </div> </div> @@ -28,32 +65,32 @@ </template> <script> -import axios from "axios" -import LibraryForm from "./Form" -import LibraryCard from "./Card" -import Quota from "./Quota" +import axios from 'axios' +import LibraryForm from './Form' +import LibraryCard from './Card' +import Quota from './Quota' export default { - data() { + components: { + LibraryForm, + LibraryCard, + Quota + }, + data () { return { isLoading: false, hiddenForm: true, libraries: [] } }, - created() { + created () { this.fetch() }, - components: { - LibraryForm, - LibraryCard, - Quota - }, methods: { - fetch() { + fetch () { this.isLoading = true - let self = this - axios.get("libraries/", {params: {scope: 'me'}}).then(response => { + const self = this + axios.get('libraries/', { params: { scope: 'me' } }).then(response => { self.isLoading = false self.libraries = response.data.results if (self.libraries.length === 0) { @@ -61,8 +98,8 @@ export default { } }) }, - libraryCreated(library) { - this.$router.push({name: 'library.detail', params: {id: library.uuid}}) + libraryCreated (library) { + this.$router.push({ name: 'library.detail', params: { id: library.uuid } }) } } } diff --git a/front/src/views/content/libraries/Quota.vue b/front/src/views/content/libraries/Quota.vue index ada8351ea903dff3d4a020103aeebe436a20f261..4ebc5a5a70e169c434fede2a27a8edcc65180126 100644 --- a/front/src/views/content/libraries/Quota.vue +++ b/front/src/views/content/libraries/Quota.vue @@ -1,92 +1,191 @@ <template> <div class="ui segment"> - <h3 class="ui header"><translate translate-context="Content/Library/Title">Current usage</translate></h3> - <div v-if="isLoading" :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']"> - <div class="ui text loader"><translate translate-context="Content/Library/Paragraph">Loading usage data…</translate></div> + <h3 class="ui header"> + <translate translate-context="Content/Library/Title"> + Current usage + </translate> + </h3> + <div + v-if="isLoading" + :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']" + > + <div class="ui text loader"> + <translate translate-context="Content/Library/Paragraph"> + Loading usage data… + </translate> + </div> </div> - <div :class="['ui', {'success': progress < 60}, {'warning': progress >= 60 && progress < 96}, {'error': progress >= 95}, 'progress']" data-percent=progress> - <div class="bar" :style="{width: `${progress}%`}"> - <div class="progress">{{ progress }}%</div> + <div + :class="['ui', {'success': progress < 60}, {'warning': progress >= 60 && progress < 96}, {'error': progress >= 95}, 'progress']" + data-percent="progress" + > + <div + class="bar" + :style="{width: `${progress}%`}" + > + <div class="progress"> + {{ progress }}% + </div> </div> - <div class="label" v-if="quotaStatus"> - <translate translate-context="Content/Library/Paragraph" :translate-params="{max: humanSize(quotaStatus.max * 1000 * 1000), current: humanSize(quotaStatus.current * 1000 * 1000)}">%{ current } used on %{ max } allowed</translate> + <div + v-if="quotaStatus" + class="label" + > + <translate + translate-context="Content/Library/Paragraph" + :translate-params="{max: humanSize(quotaStatus.max * 1000 * 1000), current: humanSize(quotaStatus.current * 1000 * 1000)}" + > + %{ current } used on %{ max } allowed + </translate> </div> </div> - <div class="ui hidden divider"></div> - <div v-if="quotaStatus" class="ui stackable three column grid"> - <div v-if="quotaStatus.pending > 0" class="column"> + <div class="ui hidden divider" /> + <div + v-if="quotaStatus" + class="ui stackable three column grid" + > + <div + v-if="quotaStatus.pending > 0" + class="column" + > <div class="ui tiny warning statistic"> <div class="value"> {{ humanSize(quotaStatus.pending * 1000 * 1000) }} </div> <div class="label"> - <translate translate-context="Content/Library/Label">Pending files</translate> + <translate translate-context="Content/Library/Label"> + Pending files + </translate> </div> </div> <div> <router-link class="ui basic primary tiny button" - :to="{name: 'content.libraries.files', query: {q: compileTokens([{field: 'status', value: 'pending'}])}}"> - <translate translate-context="Content/Library/Link/Verb">View files</translate> + :to="{name: 'content.libraries.files', query: {q: compileTokens([{field: 'status', value: 'pending'}])}}" + > + <translate translate-context="Content/Library/Link/Verb"> + View files + </translate> </router-link> <dangerous-button class="ui basic tiny button" - :action="purgePendingFiles"> - <translate translate-context="*/*/*/Verb">Purge</translate> - <p slot="modal-header"><translate translate-context="Popup/Library/Title">Purge pending files?</translate></p> - <p slot="modal-content"><translate translate-context="Popup/Library/Paragraph">Removes uploaded but yet to be processed tracks completely, adding the corresponding data to your quota.</translate></p> - <div slot="modal-confirm"><translate translate-context="*/*/*/Verb">Purge</translate></div> + :action="purgePendingFiles" + > + <translate translate-context="*/*/*/Verb"> + Purge + </translate> + <p slot="modal-header"> + <translate translate-context="Popup/Library/Title"> + Purge pending files? + </translate> + </p> + <p slot="modal-content"> + <translate translate-context="Popup/Library/Paragraph"> + Removes uploaded but yet to be processed tracks completely, adding the corresponding data to your quota. + </translate> + </p> + <div slot="modal-confirm"> + <translate translate-context="*/*/*/Verb"> + Purge + </translate> + </div> </dangerous-button> </div> </div> - <div v-if="quotaStatus.skipped > 0" class="column"> + <div + v-if="quotaStatus.skipped > 0" + class="column" + > <div class="ui tiny statistic"> <div class="value"> {{ humanSize(quotaStatus.skipped * 1000 * 1000) }} </div> <div class="label"> - <translate translate-context="Content/Library/Label">Skipped files</translate> + <translate translate-context="Content/Library/Label"> + Skipped files + </translate> </div> </div> <div> <router-link class="ui basic primary tiny button" - :to="{name: 'content.libraries.files', query: {q: compileTokens([{field: 'status', value: 'skipped'}])}}"> - <translate translate-context="Content/Library/Link/Verb">View files</translate> + :to="{name: 'content.libraries.files', query: {q: compileTokens([{field: 'status', value: 'skipped'}])}}" + > + <translate translate-context="Content/Library/Link/Verb"> + View files + </translate> </router-link> <dangerous-button class="ui basic tiny button" - :action="purgeSkippedFiles"> - <translate translate-context="*/*/*/Verb">Purge</translate> - <p slot="modal-header"><translate translate-context="Popup/Library/Title">Purge skipped files?</translate></p> - <p slot="modal-content"><translate translate-context="Popup/Library/Paragraph">Removes uploaded tracks skipped during the import processes completely, adding the corresponding data to your quota.</translate></p> - <div slot="modal-confirm"><translate translate-context="*/*/*/Verb">Purge</translate></div> + :action="purgeSkippedFiles" + > + <translate translate-context="*/*/*/Verb"> + Purge + </translate> + <p slot="modal-header"> + <translate translate-context="Popup/Library/Title"> + Purge skipped files? + </translate> + </p> + <p slot="modal-content"> + <translate translate-context="Popup/Library/Paragraph"> + Removes uploaded tracks skipped during the import processes completely, adding the corresponding data to your quota. + </translate> + </p> + <div slot="modal-confirm"> + <translate translate-context="*/*/*/Verb"> + Purge + </translate> + </div> </dangerous-button> </div> </div> - <div v-if="quotaStatus.errored > 0" class="column"> + <div + v-if="quotaStatus.errored > 0" + class="column" + > <div class="ui tiny danger statistic"> <div class="value"> {{ humanSize(quotaStatus.errored * 1000 * 1000) }} </div> <div class="label"> - <translate translate-context="Content/Library/Label">Errored files</translate> + <translate translate-context="Content/Library/Label"> + Errored files + </translate> </div> </div> <div> <router-link class="ui basic primary tiny button" - :to="{name: 'content.libraries.files', query: {q: compileTokens([{field: 'status', value: 'errored'}])}}"> - <translate translate-context="Content/Library/Link/Verb">View files</translate> + :to="{name: 'content.libraries.files', query: {q: compileTokens([{field: 'status', value: 'errored'}])}}" + > + <translate translate-context="Content/Library/Link/Verb"> + View files + </translate> </router-link> <dangerous-button class="ui basic tiny button" - :action="purgeErroredFiles"> - <translate translate-context="*/*/*/Verb">Purge</translate> - <p slot="modal-header"><translate translate-context="Popup/Library/Title">Purge errored files?</translate></p> - <p slot="modal-content"><translate translate-context="Popup/Library/Paragraph">Removes uploaded tracks that could not be processed by the server completely, adding the corresponding data to your quota.</translate></p> - <div slot="modal-confirm"><translate translate-context="*/*/*/Verb">Purge</translate></div> + :action="purgeErroredFiles" + > + <translate translate-context="*/*/*/Verb"> + Purge + </translate> + <p slot="modal-header"> + <translate translate-context="Popup/Library/Title"> + Purge errored files? + </translate> + </p> + <p slot="modal-content"> + <translate translate-context="Popup/Library/Paragraph"> + Removes uploaded tracks that could not be processed by the server completely, adding the corresponding data to your quota. + </translate> + </p> + <div slot="modal-confirm"> + <translate translate-context="*/*/*/Verb"> + Purge + </translate> + </div> </dangerous-button> </div> </div> @@ -95,8 +194,8 @@ </template> <script> import axios from 'axios' -import {humanSize} from '@/filters' -import {compileTokens} from '@/search' +import { humanSize } from '@/filters' +import { compileTokens } from '@/search' export default { data () { @@ -107,12 +206,20 @@ export default { compileTokens } }, + computed: { + progress () { + if (!this.quotaStatus) { + return 0 + } + return Math.min(parseInt(this.quotaStatus.current * 100 / this.quotaStatus.max), 100) + } + }, created () { this.fetch() }, methods: { fetch () { - let self = this + const self = this self.isLoading = true axios.get('users/me/').then((response) => { self.quotaStatus = response.data.quota_status @@ -120,8 +227,8 @@ export default { }) }, purge (status) { - let self = this - let payload = { + const self = this + const payload = { action: 'delete', objects: 'all', filters: { @@ -140,14 +247,6 @@ export default { }, purgeErroredFiles () { this.purge('errored') - }, - }, - computed: { - progress () { - if (!this.quotaStatus) { - return 0 - } - return Math.min(parseInt(this.quotaStatus.current * 100 / this.quotaStatus.max), 100) } } } diff --git a/front/src/views/content/remote/Card.vue b/front/src/views/content/remote/Card.vue index 3e082cee2b8fec8ab91c206cedc3edc4fb63d8d4..c66ed4d94005a8e616d265a0460bbee9cb9bef82 100644 --- a/front/src/views/content/remote/Card.vue +++ b/front/src/views/content/remote/Card.vue @@ -5,14 +5,18 @@ <router-link :to="{name: 'library.detail', params: {id: library.uuid}}"> {{ library.name }} </router-link> - <div class="ui right floated dropdown" v-dropdown> - <i class="ellipsis vertical large icon nomargin"></i> + <div + v-dropdown + class="ui right floated dropdown" + > + <i class="ellipsis vertical large icon nomargin" /> <div class="menu"> <button v-for="obj in getReportableObjs({library, account: library.actor})" :key="obj.target.type + obj.target.id" class="item basic" - @click.stop.prevent="$store.dispatch('moderation/report', obj.target)"> + @click.stop.prevent="$store.dispatch('moderation/report', obj.target)" + > <i class="share icon" /> {{ obj.label }} </button> </div> @@ -20,14 +24,16 @@ <span v-if="library.privacy_level === 'me'" class="right floated" - :data-tooltip="labels.tooltips.me"> - <i class="small lock icon"></i> + :data-tooltip="labels.tooltips.me" + > + <i class="small lock icon" /> </span> <span v-else-if="library.privacy_level === 'everyone'" class="right floated" - :data-tooltip="labels.tooltips.everyone"> - <i class="small globe icon"></i> + :data-tooltip="labels.tooltips.everyone" + > + <i class="small globe icon" /> </span> </h4> <div class="meta"> @@ -38,92 +44,178 @@ </div> <div class="description"> {{ library.description }} - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> </div> <div class="meta"> - <i class="music icon"></i> - <translate translate-context="*/*/*" :translate-params="{count: library.uploads_count}" :translate-n="library.uploads_count" translate-plural="%{ count } tracks">%{ count } track</translate> + <i class="music icon" /> + <translate + translate-context="*/*/*" + :translate-params="{count: library.uploads_count}" + :translate-n="library.uploads_count" + translate-plural="%{ count } tracks" + > + %{ count } track + </translate> </div> - <div v-if="displayScan && latestScan" class="meta"> + <div + v-if="displayScan && latestScan" + class="meta" + > <template v-if="latestScan.status === 'pending'"> - <i class="hourglass icon"></i> - <translate translate-context="Content/Library/Card.List item">Scan pending</translate> + <i class="hourglass icon" /> + <translate translate-context="Content/Library/Card.List item"> + Scan pending + </translate> </template> <template v-if="latestScan.status === 'scanning'"> - <i class="loading spinner icon"></i> - <translate translate-context="Content/Library/Card.List item" :translate-params="{progress: scanProgress}">Scanning… (%{ progress }%)</translate> + <i class="loading spinner icon" /> + <translate + translate-context="Content/Library/Card.List item" + :translate-params="{progress: scanProgress}" + > + Scanning… (%{ progress }%) + </translate> </template> <template v-else-if="latestScan.status === 'errored'"> - <i class="dangerdownload icon"></i> - <translate translate-context="Content/Library/Card.List item">Problem during scanning</translate> + <i class="dangerdownload icon" /> + <translate translate-context="Content/Library/Card.List item"> + Problem during scanning + </translate> </template> <template v-else-if="latestScan.status === 'finished' && latestScan.errored_files === 0"> - <i class="success download icon"></i> - <translate translate-context="Content/Library/Card.List item">Scanned</translate> + <i class="success download icon" /> + <translate translate-context="Content/Library/Card.List item"> + Scanned + </translate> </template> <template v-else-if="latestScan.status === 'finished' && latestScan.errored_files > 0"> - <i class="warning download icon"></i> - <translate translate-context="Content/Library/Card.List item">Scanned with errors</translate> + <i class="warning download icon" /> + <translate translate-context="Content/Library/Card.List item"> + Scanned with errors + </translate> </template> - <a href="" class="link right floated" @click.prevent="showScan = !showScan"> + <a + href="" + class="link right floated" + @click.prevent="showScan = !showScan" + > <translate translate-context="Content/Library/Card.Button.Label/Noun">Details</translate> - <i v-if="showScan" class="angle down icon" /> - <i v-else class="angle right icon" /> + <i + v-if="showScan" + class="angle down icon" + /> + <i + v-else + class="angle right icon" + /> </a> <div v-if="showScan"> <template v-if="latestScan.modification_date"> - <translate translate-context="Content/Library/Card.List item/Noun">Last update:</translate><human-date :date="latestScan.modification_date" /><br /> + <translate translate-context="Content/Library/Card.List item/Noun"> + Last update: + </translate><human-date :date="latestScan.modification_date" /><br> </template> - <translate translate-context="Content/Library/Card.List item/Noun">Failed tracks:</translate> {{ latestScan.errored_files }} + <translate translate-context="Content/Library/Card.List item/Noun"> + Failed tracks: + </translate> {{ latestScan.errored_files }} </div> </div> - <div v-if="displayScan && canLaunchScan" class="clearfix"> - <a href="" class="right floated link" @click.prevent="launchScan"> + <div + v-if="displayScan && canLaunchScan" + class="clearfix" + > + <a + href="" + class="right floated link" + @click.prevent="launchScan" + > <translate translate-context="Content/Library/Card.Button.Label/Verb">Scan now</translate> <i class="paper plane icon" /> </a> </div> </div> <div class="extra content"> - <actor-link style="color: var(--link-color)" :actor="library.actor" /> + <actor-link + style="color: var(--link-color)" + :actor="library.actor" + /> </div> - <div v-if="displayCopyFid" class="extra content"> + <div + v-if="displayCopyFid" + class="extra content" + > <div class="ui form"> <div class="field"> <label :for="library.fid"><translate translate-context="Content/Library/Title">Sharing link</translate></label> - <copy-input :id="library.fid" :button-classes="'basic'" :value="library.fid" /> + <copy-input + :id="library.fid" + :button-classes="'basic'" + :value="library.fid" + /> </div> </div> </div> - <div v-if="displayFollow || radioPlayable" :class="['ui', {two: displayFollow && radioPlayable}, 'bottom', 'attached', 'buttons']"> - <radio-button v-if="radioPlayable" :type="'library'" :object-id="library.uuid"></radio-button> + <div + v-if="displayFollow || radioPlayable" + :class="['ui', {two: displayFollow && radioPlayable}, 'bottom', 'attached', 'buttons']" + > + <radio-button + v-if="radioPlayable" + :type="'library'" + :object-id="library.uuid" + /> <template v-if="displayFollow"> <button v-if="!library.follow" + :class="['ui', 'success', {'loading': isLoadingFollow}, 'button']" @click="follow()" - :class="['ui', 'success', {'loading': isLoadingFollow}, 'button']"> - <translate translate-context="Content/Library/Card.Button.Label/Verb">Follow</translate> + > + <translate translate-context="Content/Library/Card.Button.Label/Verb"> + Follow + </translate> </button> <template v-else-if="!library.follow.approved"> <button - class="ui disabled button"><i class="hourglass icon"></i> - <translate translate-context="Content/Library/Card.Paragraph">Follow request pending approval</translate> + class="ui disabled button" + > + <i class="hourglass icon" /> + <translate translate-context="Content/Library/Card.Paragraph"> + Follow request pending approval + </translate> </button> <button + class="ui button" @click="unfollow" - class="ui button"> - <translate translate-context="Content/Library/Card.Paragraph">Cancel follow request</translate> + > + <translate translate-context="Content/Library/Card.Paragraph"> + Cancel follow request + </translate> </button> </template> <template v-else-if="library.follow.approved"> <dangerous-button :class="['ui', 'button']" - :action="unfollow"> - <translate translate-context="*/Library/Button.Label/Verb">Unfollow</translate> - <p slot="modal-header"><translate translate-context="Popup/Library/Title">Unfollow this library?</translate></p> + :action="unfollow" + > + <translate translate-context="*/Library/Button.Label/Verb"> + Unfollow + </translate> + <p slot="modal-header"> + <translate translate-context="Popup/Library/Title"> + Unfollow this library? + </translate> + </p> <div slot="modal-content"> - <p><translate translate-context="Popup/Library/Paragraph">By unfollowing this library, you loose access to its content.</translate></p> + <p> + <translate translate-context="Popup/Library/Paragraph"> + By unfollowing this library, you loose access to its content. + </translate> + </p> + </div> + <div slot="modal-confirm"> + <translate translate-context="*/Library/Button.Label/Verb"> + Unfollow + </translate> </div> - <div slot="modal-confirm"><translate translate-context="*/Library/Button.Label/Verb">Unfollow</translate></div> </dangerous-button> </template> </template> @@ -134,31 +226,31 @@ import axios from 'axios' import ReportMixin from '@/components/mixins/Report' import RadioButton from '@/components/radios/Button' -import jQuery from 'jquery' export default { - mixins: [ReportMixin], - props: { - library: {type: Object, required: true}, - displayFollow: {type: Boolean, default: true}, - displayScan: {type: Boolean, default: true}, - displayCopyFid: {type: Boolean, default: true}, - }, components: { RadioButton }, + mixins: [ReportMixin], + props: { + initialLibrary: { type: Object, required: true }, + displayFollow: { type: Boolean, default: true }, + displayScan: { type: Boolean, default: true }, + displayCopyFid: { type: Boolean, default: true } + }, data () { return { + library: this.initialLibrary, isLoadingFollow: false, showScan: false, scanTimeout: null, - latestScan: this.library.latest_scan, + latestScan: this.library.latest_scan } }, computed: { labels () { - let me = this.$pgettext('Content/Library/Card.Help text', 'This library is private and your approval from its owner is needed to access its content') - let everyone = this.$pgettext('Content/Library/Card.Help text', 'This library is public and you can access its content freely') + const me = this.$pgettext('Content/Library/Card.Help text', 'This library is private and your approval from its owner is needed to access its content') + const everyone = this.$pgettext('Content/Library/Card.Help text', 'This library is public and you can access its content freely') return { tooltips: { @@ -168,8 +260,8 @@ export default { } }, scanProgress () { - let scan = this.latestScan - let progress = scan.processed_files * 100 / scan.total_files + const scan = this.latestScan + const progress = scan.processed_files * 100 / scan.total_files return Math.min(parseInt(progress), 100) }, scanStatus () { @@ -192,16 +284,29 @@ export default { (this.library.actor.is_local || this.scanStatus === 'finished') && (this.library.privacy_level === 'everyone' || (this.library.follow && this.library.follow.is_approved)) ) - }, + } + }, + watch: { + showScan (newValue, oldValue) { + if (newValue) { + if (this.scanStatus === 'pending' || this.scanStatus === 'scanning') { + this.fetchScanStatus() + } + } else { + if (this.scanTimeout) { + clearTimeout(this.scanTimeout) + } + } + } }, methods: { launchScan () { - let self = this - let successMsg = this.$pgettext('Content/Library/Message', 'Scan launched') - let skippedMsg = this.$pgettext('Content/Library/Message', 'Scan skipped (previous scan is too recent)') + const self = this + const successMsg = this.$pgettext('Content/Library/Message', 'Scan launched') + const skippedMsg = this.$pgettext('Content/Library/Message', 'Scan skipped (previous scan is too recent)') axios.post(`federation/libraries/${this.library.uuid}/scan/`).then((response) => { let msg - if (response.data.status == 'skipped') { + if (response.data.status === 'skipped') { msg = skippedMsg } else { self.latestScan = response.data.scan @@ -214,19 +319,22 @@ export default { }) }, follow () { - let self = this + const self = this this.isLoadingFollow = true - axios.post('federation/follows/library/', {target: this.library.uuid}).then((response) => { + axios.post('federation/follows/library/', { target: this.library.uuid }).then((response) => { self.library.follow = response.data self.isLoadingFollow = false self.$emit('followed') - }, error => { self.isLoadingFollow = false + self.$store.commit('ui/addMessage', { + content: 'Cannot follow remote library: ' + error, + date: new Date() + }) }) }, unfollow () { - let self = this + const self = this this.isLoadingFollow = true axios.delete(`federation/follows/library/${this.library.follow.uuid}/`).then((response) => { self.$emit('deleted') @@ -234,10 +342,14 @@ export default { self.isLoadingFollow = false }, error => { self.isLoadingFollow = false + self.$store.commit('ui/addMessage', { + content: 'Cannot unfollow remote library: ' + error, + date: new Date() + }) }) }, fetchScanStatus () { - let self = this + const self = this axios.get(`federation/follows/library/${this.library.follow.uuid}/`).then((response) => { self.latestScan = response.data.target.latest_scan if (self.scanStatus === 'pending' || self.scanStatus === 'scanning') { @@ -247,19 +359,6 @@ export default { } }) } - }, - watch: { - showScan (newValue, oldValue) { - if (newValue) { - if (this.scanStatus === 'pending' || this.scanStatus === 'scanning') { - this.fetchScanStatus() - } - } else { - if (this.scanTimeout) { - clearTimeout(this.scanTimeout) - } - } - } } } </script> diff --git a/front/src/views/content/remote/Home.vue b/front/src/views/content/remote/Home.vue index 0a896c9def90e5ef635e6f03c8e56e0a8e2065f0..eb24bb1b67d5ac2ec834dc95ebde617ed9ffb3d1 100644 --- a/front/src/views/content/remote/Home.vue +++ b/front/src/views/content/remote/Home.vue @@ -1,29 +1,63 @@ <template> <div class="ui vertical aligned stripe segment"> - <div v-if="isLoading" :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']"> - <div class="ui text loader"><translate translate-context="Content/Library/Paragraph">Loading remote libraries…</translate></div> + <div + v-if="isLoading" + :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']" + > + <div class="ui text loader"> + <translate translate-context="Content/Library/Paragraph"> + Loading remote libraries… + </translate> + </div> </div> - <div v-else class="ui text container"> - <h1 class="ui header"><translate translate-context="Content/Library/Title/Noun">Remote libraries</translate></h1> - <p><translate translate-context="Content/Library/Paragraph">Remote libraries are owned by other users on the network. You can access them as long as they are public or you are granted access.</translate></p> - <scan-form @scanned="scanResult = $event"></scan-form> - <div class="ui hidden divider"></div> - <div v-if="scanResult && scanResult.results.length > 0" class="ui two cards"> - <library-card :library="library" v-for="library in scanResult.results" :key="library.fid" /> + <div + v-else + class="ui text container" + > + <h1 class="ui header"> + <translate translate-context="Content/Library/Title/Noun"> + Remote libraries + </translate> + </h1> + <p> + <translate translate-context="Content/Library/Paragraph"> + Remote libraries are owned by other users on the network. You can access them as long as they are public or you are granted access. + </translate> + </p> + <scan-form @scanned="scanResult = $event" /> + <div class="ui hidden divider" /> + <div + v-if="scanResult && scanResult.results.length > 0" + class="ui two cards" + > + <library-card + v-for="library in scanResult.results" + :key="library.fid" + :library="library" + /> </div> <template v-if="existingFollows && existingFollows.count > 0"> - <h2><translate translate-context="Content/Library/Title">Known libraries</translate></h2> - <a href="" class="discrete link" @click.prevent="fetch()" > + <h2> + <translate translate-context="Content/Library/Title"> + Known libraries + </translate> + </h2> + <a + href="" + class="discrete link" + @click.prevent="fetch()" + > <i :class="['ui', 'circular', 'refresh', 'icon']" /> <translate translate-context="Content/*/Button.Label/Short, Verb">Refresh</translate> </a> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <div class="ui two cards"> <library-card + v-for="follow in existingFollows.results" + :key="follow.fid" + :library="getLibraryFromFollow(follow)" @deleted="fetch()" @followed="fetch()" - :library="getLibraryFromFollow(follow)" - v-for="follow in existingFollows.results" - :key="follow.fid" /> + /> </div> </template> </div> @@ -36,25 +70,26 @@ import ScanForm from './ScanForm' import LibraryCard from './Card' export default { + components: { + ScanForm, + LibraryCard + }, data () { return { isLoading: false, scanResult: null, - existingFollows: null + existingFollows: null, + errors: [] } }, created () { this.fetch() }, - components: { - ScanForm, - LibraryCard - }, methods: { fetch () { this.isLoading = true - let self = this - axios.get('federation/follows/library/', {params: {'page_size': 100, 'ordering': '-creation_date'}}).then((response) => { + const self = this + axios.get('federation/follows/library/', { params: { page_size: 100, ordering: '-creation_date' } }).then((response) => { self.existingFollows = response.data self.existingFollows.results.forEach(f => { f.target.follow = f @@ -62,10 +97,11 @@ export default { self.isLoading = false }, error => { self.isLoading = false + self.errors.push(error) }) }, getLibraryFromFollow (follow) { - let d = follow.target + const d = follow.target d.follow = follow return d } diff --git a/front/src/views/content/remote/ScanForm.vue b/front/src/views/content/remote/ScanForm.vue index af5fa6e40b829b2c07b4d61af65d21f1e8ee6eb2..3d6533c5bc00ebc03dadc258409de27b6957a6c4 100644 --- a/front/src/views/content/remote/ScanForm.vue +++ b/front/src/views/content/remote/ScanForm.vue @@ -1,17 +1,43 @@ <template> - <form class="ui form" @submit.prevent="scan"> - <div v-if="errors.length > 0" role="alert" class="ui negative message"> - <h4 class="header"><translate translate-context="Content/Library/Error message.Title">Could not fetch remote library</translate></h4> + <form + class="ui form" + @submit.prevent="scan" + > + <div + v-if="errors.length > 0" + role="alert" + class="ui negative message" + > + <h4 class="header"> + <translate translate-context="Content/Library/Error message.Title"> + Could not fetch remote library + </translate> + </h4> <ul class="list"> - <li v-for="error in errors">{{ error }}</li> + <li + v-for="(error, key) in errors" + :key="key" + > + {{ error }} + </li> </ul> </div> <div class="ui field"> <label for="library-search"><translate translate-context="Content/Library/Input.Label/Verb">Search a remote library</translate></label> <div :class="['ui', 'action', {loading: isLoading}, 'input']"> - <input id="library-search" name="url" v-model="query" :placeholder="labels.placeholder" type="url"> - <button :aria-label="labels.submitLibrarySearch" type="submit" :class="['ui', 'icon', {loading: isLoading}, 'button']"> - <i class="search icon"></i> + <input + id="library-search" + v-model="query" + name="url" + :placeholder="labels.placeholder" + type="url" + > + <button + :aria-label="labels.submitLibrarySearch" + type="submit" + :class="['ui', 'icon', {loading: isLoading}, 'button']" + > + <i class="search icon" /> </button> </div> </div> @@ -28,15 +54,23 @@ export default { errors: [] } }, + computed: { + labels () { + return { + placeholder: this.$pgettext('Content/Library/Input.Placeholder', 'Enter a library URL'), + submitLibrarySearch: this.$pgettext('Content/Library/Input.Label', 'Submit search') + } + } + }, methods: { scan () { if (!this.query) { return } - let self = this + const self = this self.errors = [] self.isLoading = true - axios.post('federation/libraries/fetch/', {fid: this.query}).then((response) => { + axios.post('federation/libraries/fetch/', { fid: this.query }).then((response) => { self.$emit('scanned', response.data) self.isLoading = false }, error => { @@ -44,14 +78,6 @@ export default { self.errors = error.backendErrors }) } - }, - computed: { - labels () { - return { - placeholder: this.$pgettext('Content/Library/Input.Placeholder', 'Enter a library URL'), - submitLibrarySearch: this.$pgettext('Content/Library/Input.Label', 'Submit search') - } - } } } </script> diff --git a/front/src/views/library/DetailAlbums.vue b/front/src/views/library/DetailAlbums.vue index 35dec511be654b3f2d0459d34e13c45849fd1500..713eacceabdcd4216d9a84755ab3a559c2cbfa36 100644 --- a/front/src/views/library/DetailAlbums.vue +++ b/front/src/views/library/DetailAlbums.vue @@ -5,11 +5,24 @@ :header="false" :search="true" :controls="false" - :filters="{playable: true, ordering: '-creation_date', library: object.uuid}"> + :filters="{playable: true, ordering: '-creation_date', library: object.uuid}" + > <empty-state slot="empty-state"> <p> - <translate key="1" v-if="isOwner" translate-context="*/*/*">This library is empty, you should upload something in it!</translate> - <translate key="2" v-else translate-context="*/*/*">You may need to follow this library to see its content.</translate> + <translate + v-if="isOwner" + key="1" + translate-context="*/*/*" + > + This library is empty, you should upload something in it! + </translate> + <translate + v-else + key="2" + translate-context="*/*/*" + > + You may need to follow this library to see its content. + </translate> </p> </empty-state> </album-widget> @@ -17,12 +30,15 @@ </template> <script> -import AlbumWidget from "@/components/audio/album/Widget" +import AlbumWidget from '@/components/audio/album/Widget' export default { - props: ['object', 'isOwner'], components: { - AlbumWidget, + AlbumWidget }, + props: { + object: { type: String, required: true }, + isOwner: { type: Boolean, required: true } + } } </script> diff --git a/front/src/views/library/DetailBase.vue b/front/src/views/library/DetailBase.vue index c570be1e7506862098d64ae63203c4e2fb957a66..88b5cbc3bed8abfcb03c3681ef7d507323f43285 100644 --- a/front/src/views/library/DetailBase.vue +++ b/front/src/views/library/DetailBase.vue @@ -1,77 +1,136 @@ <template> <main v-title="labels.title"> <div class="ui vertical stripe segment container"> - <div v-if="isLoading" class="ui centered active inline loader"></div> - <div class="ui stackable grid" v-else-if="object"> + <div + v-if="isLoading" + class="ui centered active inline loader" + /> + <div + v-else-if="object" + class="ui stackable grid" + > <div class="ui five wide column"> - <button class="ui pointing dropdown icon small basic right floated button" ref="dropdown" v-dropdown="{direction: 'downward'}" style="position: absolute; right: 1em; top: 1em;"> - <i class="ellipsis vertical icon"></i> + <button + ref="dropdown" + v-dropdown="{direction: 'downward'}" + class="ui pointing dropdown icon small basic right floated button" + style="position: absolute; right: 1em; top: 1em;" + > + <i class="ellipsis vertical icon" /> <div class="menu"> <a - :href="object.fid" v-if="object.actor.domain != $store.getters['instance/domain']" + :href="object.fid" target="_blank" - class="basic item"> - <i class="external icon"></i> - <translate :translate-params="{domain: object.actor.domain}" translate-context="Content/*/Button.Label/Verb">View on %{ domain }</translate> + class="basic item" + > + <i class="external icon" /> + <translate + :translate-params="{domain: object.actor.domain}" + translate-context="Content/*/Button.Label/Verb" + >View on %{ domain }</translate> </a> <div - role="button" - class="basic item" v-for="obj in getReportableObjs({library: object})" :key="obj.target.type + obj.target.id" - @click.stop.prevent="$store.dispatch('moderation/report', obj.target)"> + role="button" + class="basic item" + @click.stop.prevent="$store.dispatch('moderation/report', obj.target)" + > <i class="share icon" /> {{ obj.label }} </div> - <div class="divider"></div> - <router-link class="basic item" v-if="$store.state.auth.availablePermissions['moderation']" :to="{name: 'manage.library.libraries.detail', params: {id: object.uuid}}"> - <i class="wrench icon"></i> - <translate translate-context="Content/Moderation/Link">Open in moderation interface</translate> + <div class="divider" /> + <router-link + v-if="$store.state.auth.availablePermissions['moderation']" + class="basic item" + :to="{name: 'manage.library.libraries.detail', params: {id: object.uuid}}" + > + <i class="wrench icon" /> + <translate translate-context="Content/Moderation/Link"> + Open in moderation interface + </translate> </router-link> </div> </button> <h1 class="ui header"> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <div class="ellipsis content"> - <i class="layer group small icon"></i> + <i class="layer group small icon" /> <span>{{ object.name }}</span> - <div class="ui very small hidden divider"></div> - <div class="sub header ellipsis" :title="object.full_username"> - <actor-link :avatar="false" :actor="object.actor" :truncate-length="0"> - <translate translate-context="*/*/*" :translate-params="{username: object.actor.full_username}">Owned by %{ username }</translate> + <div class="ui very small hidden divider" /> + <div + class="sub header ellipsis" + :title="object.full_username" + > + <actor-link + :avatar="false" + :actor="object.actor" + :truncate-length="0" + > + <translate + translate-context="*/*/*" + :translate-params="{username: object.actor.full_username}" + > + Owned by %{ username } + </translate> </actor-link> </div> </div> </h1> <p> - <span v-if="object.privacy_level === 'me'" :title="labels.tooltips.me"> - <i class="lock icon"></i> + <span + v-if="object.privacy_level === 'me'" + :title="labels.tooltips.me" + > + <i class="lock icon" /> {{ labels.visibility.me }} </span> <span - v-else-if="object.privacy_level === 'instance'" :title="labels.tooltips.instance"> - <i class="lock open icon"></i> + v-else-if="object.privacy_level === 'instance'" + :title="labels.tooltips.instance" + > + <i class="lock open icon" /> {{ labels.visibility.instance }} </span> - <span v-else-if="object.privacy_level === 'everyone'" :title="labels.tooltips.everyone"> - <i class="globe icon"></i> + <span + v-else-if="object.privacy_level === 'everyone'" + :title="labels.tooltips.everyone" + > + <i class="globe icon" /> {{ labels.visibility.everyone }} </span> · - <i class="music icon"></i> - <translate translate-context="*/*/*" :translate-params="{count: object.uploads_count}" :translate-n="object.uploads_count" translate-plural="%{ count } tracks">%{ count } track</translate> + <i class="music icon" /> + <translate + translate-context="*/*/*" + :translate-params="{count: object.uploads_count}" + :translate-n="object.uploads_count" + translate-plural="%{ count } tracks" + > + %{ count } track + </translate> <span v-if="object.size"> - · <i class="database icon"></i> + · <i class="database icon" /> {{ object.size | humanSize }} </span> </p> <div class="header-buttons"> <div class="ui small buttons"> - <radio-button :disabled="!isPlayable" type="library" :object-id="object.uuid"></radio-button> + <radio-button + :disabled="!isPlayable" + type="library" + :object-id="object.uuid" + /> </div> - <div class="ui small buttons" v-if="!isOwner"> - <library-follow-button v-if="$store.state.auth.authenticated" :library="object"></library-follow-button> + <div + v-if="!isOwner" + class="ui small buttons" + > + <library-follow-button + v-if="$store.state.auth.authenticated" + :library="object" + /> </div> </div> @@ -79,15 +138,20 @@ <rendered-description :content="object.description ? {html: object.description} : null" :update-url="`channels/${object.uuid}/`" - :can-update="false"></rendered-description> - <div class="ui hidden divider"></div> + :can-update="false" + /> + <div class="ui hidden divider" /> </template> <div class="ui form"> <div class="field"> <label for="copy-input"> <translate translate-context="Content/Library/Title">Sharing link</translate> </label> - <p><translate translate-context="Content/Library/Paragraph">Share this link with other users so they can request access to this library by copy-pasting it in their pod search bar.</translate></p> + <p> + <translate translate-context="Content/Library/Paragraph"> + Share this link with other users so they can request access to this library by copy-pasting it in their pod search bar. + </translate> + </p> <copy-input :value="object.fid" /> </div> </div> @@ -96,30 +160,63 @@ <div class="ui head vertical stripe segment"> <div class="ui container"> <div class="ui secondary pointing center aligned menu"> - <router-link class="item" :exact="true" :to="{name: 'library.detail'}"> - <translate translate-context="*/*/*">Artists</translate> + <router-link + class="item" + :exact="true" + :to="{name: 'library.detail'}" + > + <translate translate-context="*/*/*"> + Artists + </translate> </router-link> - <router-link class="item" :exact="true" :to="{name: 'library.detail.albums'}"> - <translate translate-context="*/*/*">Albums</translate> + <router-link + class="item" + :exact="true" + :to="{name: 'library.detail.albums'}" + > + <translate translate-context="*/*/*"> + Albums + </translate> </router-link> - <router-link class="item" :exact="true" :to="{name: 'library.detail.tracks'}"> - <translate translate-context="*/*/*">Tracks</translate> + <router-link + class="item" + :exact="true" + :to="{name: 'library.detail.tracks'}" + > + <translate translate-context="*/*/*"> + Tracks + </translate> </router-link> - <router-link v-if="isOwner" class="item" :exact="true" :to="{name: 'library.detail.upload'}"> - <i class="upload icon"></i> - <translate translate-context="Content/Library/Card.Button.Label/Verb">Upload</translate> + <router-link + v-if="isOwner" + class="item" + :exact="true" + :to="{name: 'library.detail.upload'}" + > + <i class="upload icon" /> + <translate translate-context="Content/Library/Card.Button.Label/Verb"> + Upload + </translate> </router-link> - <router-link v-if="isOwner" class="item" :exact="true" :to="{name: 'library.detail.edit'}"> - <i class="pencil icon"></i> - <translate translate-context="Content/*/Button.Label/Verb">Edit</translate> + <router-link + v-if="isOwner" + class="item" + :exact="true" + :to="{name: 'library.detail.edit'}" + > + <i class="pencil icon" /> + <translate translate-context="Content/*/Button.Label/Verb"> + Edit + </translate> </router-link> </div> - <div class="ui hidden divider"></div> - <router-view - @updated="fetchData" - @uploads-finished="object.uploads_count += $event" - :is-owner="isOwner" - :object="object"></router-view> + <div class="ui hidden divider" /> + <router-view + :is-owner="isOwner" + :object="object" + @updated="fetchData" + @uploads-finished="object.uploads_count += $event" + /> </div> </div> </div> @@ -129,49 +226,29 @@ </template> <script> -import axios from "axios" -import PlayButton from "@/components/audio/PlayButton" -import LibraryFollowButton from "@/components/audio/LibraryFollowButton" +import axios from 'axios' +import LibraryFollowButton from '@/components/audio/LibraryFollowButton' import ReportMixin from '@/components/mixins/Report' import RadioButton from '@/components/radios/Button' export default { - mixins: [ReportMixin], - props: ["id"], components: { - PlayButton, RadioButton, LibraryFollowButton }, - data() { - return { - isLoading: true, - object: null, - latestTracks: null, - } - }, + mixins: [ReportMixin], beforeRouteUpdate (to, from, next) { to.meta.preserveScrollPosition = true next() }, - async created() { - await this.fetchData() - let authenticated = this.$store.state.auth.authenticated - if (!authenticated && this.$store.getters['instance/domain'] != this.object.actor.domain) { - this.$router.push({name: 'login', query: {next: this.$route.fullPath}}) + props: { id: { type: String, required: true } }, + data () { + return { + isLoading: true, + object: null, + latestTracks: null } }, - methods: { - async fetchData() { - var self = this - this.isLoading = true - let libraryPromise = axios.get(`libraries/${this.id}`).then(response => { - self.object = response.data - }) - await libraryPromise - self.isLoading = false - }, - }, computed: { isOwner () { return this.$store.state.auth.authenticated && this.object.actor.full_username === this.$store.state.auth.fullUsername @@ -182,12 +259,12 @@ export default { visibility: { me: this.$pgettext('Content/Library/Card.Help text', 'Private'), instance: this.$pgettext('Content/Library/Card.Help text', 'Restricted'), - everyone: this.$pgettext('Content/Library/Card.Help text', 'Public'), + everyone: this.$pgettext('Content/Library/Card.Help text', 'Public') }, tooltips: { me: this.$pgettext('Content/Library/Card.Help text', 'This library is private and your approval from its owner is needed to access its content'), instance: this.$pgettext('Content/Library/Card.Help text', 'This library is restricted to users on this pod only'), - everyone: this.$pgettext('Content/Library/Card.Help text', 'This library is public and you can access its content freely'), + everyone: this.$pgettext('Content/Library/Card.Help text', 'This library is public and you can access its content freely') } } }, @@ -198,12 +275,30 @@ export default { (this.object.privacy_level === 'instance' && this.$store.state.auth.authenticated && this.object.actor.domain === this.$store.getters['instance/domain']) || (this.$store.getters['libraries/follow'](this.object.uuid) || {}).approved === true ) - }, + } }, watch: { - id() { + id () { this.fetchData() } + }, + async created () { + await this.fetchData() + const authenticated = this.$store.state.auth.authenticated + if (!authenticated && this.$store.getters['instance/domain'] !== this.object.actor.domain) { + this.$router.push({ name: 'login', query: { next: this.$route.fullPath } }) + } + }, + methods: { + async fetchData () { + const self = this + this.isLoading = true + const libraryPromise = axios.get(`libraries/${this.id}`).then(response => { + self.object = response.data + }) + await libraryPromise + self.isLoading = false + } } } </script> diff --git a/front/src/views/library/DetailOverview.vue b/front/src/views/library/DetailOverview.vue index facbfb708d9fb7d168e7536d5fbd497863ad7098..14db9f12ada7fa5af0bc2a8bea5a4616bdc07700 100644 --- a/front/src/views/library/DetailOverview.vue +++ b/front/src/views/library/DetailOverview.vue @@ -4,8 +4,9 @@ <rendered-description :content="object.description ? {html: object.description} : null" :update-url="`channels/${object.uuid}/`" - :can-update="false"></rendered-description> - <div class="ui hidden divider"></div> + :can-update="false" + /> + <div class="ui hidden divider" /> </template> <artist-widget :key="object.uploads_count" @@ -13,11 +14,24 @@ :header="false" :search="true" :controls="false" - :filters="{playable: true, ordering: '-creation_date', library: object.uuid}"> + :filters="{playable: true, ordering: '-creation_date', library: object.uuid}" + > <empty-state slot="empty-state"> <p> - <translate key="1" v-if="isOwner" translate-context="*/*/*">This library is empty, you should upload something in it!</translate> - <translate key="2" v-else translate-context="*/*/*">You may need to follow this library to see its content.</translate> + <translate + v-if="isOwner" + key="1" + translate-context="*/*/*" + > + This library is empty, you should upload something in it! + </translate> + <translate + v-else + key="2" + translate-context="*/*/*" + > + You may need to follow this library to see its content. + </translate> </p> </empty-state> </artist-widget> @@ -25,15 +39,18 @@ </template> <script> -import ArtistWidget from "@/components/audio/artist/Widget" +import ArtistWidget from '@/components/audio/artist/Widget' export default { - props: ['object', 'isOwner'], components: { - ArtistWidget, + ArtistWidget + }, + props: { + object: { type: String, required: true }, + isOwner: { type: Boolean, required: true } }, data () { - return { + return { query: '' } } diff --git a/front/src/views/library/DetailTracks.vue b/front/src/views/library/DetailTracks.vue index 0fa1869a9ebcb7a1d6f00f4af0ed0a7df7b693c6..c9dd17bb7f2083305e3e303088b2017859e4455b 100644 --- a/front/src/views/library/DetailTracks.vue +++ b/front/src/views/library/DetailTracks.vue @@ -4,11 +4,24 @@ :key="object.uploads_count" :display-actions="false" :search="true" - :filters="{playable: true, library: object.uuid, ordering: '-creation_date'}"> + :filters="{playable: true, library: object.uuid, ordering: '-creation_date'}" + > <empty-state slot="empty-state"> <p> - <translate key="1" v-if="isOwner" translate-context="*/*/*">This library is empty, you should upload something in it!</translate> - <translate key="2" v-else translate-context="*/*/*">You may need to follow this library to see its content.</translate> + <translate + v-if="isOwner" + key="1" + translate-context="*/*/*" + > + This library is empty, you should upload something in it! + </translate> + <translate + v-else + key="2" + translate-context="*/*/*" + > + You may need to follow this library to see its content. + </translate> </p> </empty-state> </track-table> @@ -19,9 +32,12 @@ import TrackTable from '@/components/audio/track/Table' export default { - props: ['object', 'isOwner'], components: { - TrackTable, + TrackTable }, + props: { + object: { type: String, required: true }, + isOwner: { type: Boolean, required: true } + } } </script> diff --git a/front/src/views/library/Edit.vue b/front/src/views/library/Edit.vue index 998e364c19835284bb4a1e3e6e744c1de5c0e255..00ba50c71e7cefddc1bf1bd6991a8ba27c6cc1b0 100644 --- a/front/src/views/library/Edit.vue +++ b/front/src/views/library/Edit.vue @@ -1,93 +1,153 @@ <template> <section> - <library-form :library="object" @updated="$emit('updated')" @deleted="$router.push({name: 'profile.overview', params: {username: $store.state.auth.username}})" /> - <div class="ui hidden divider"></div> + <library-form + :library="object" + @updated="$emit('updated')" + @deleted="$router.push({name: 'profile.overview', params: {username: $store.state.auth.username}})" + /> + <div class="ui hidden divider" /> <h2 class="ui header"> - <translate translate-context="*/*/*">Library contents</translate> + <translate translate-context="*/*/*"> + Library contents + </translate> </h2> - <library-files-table :filters="{library: object.uuid}"></library-files-table> + <library-files-table :filters="{library: object.uuid}" /> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <h2 class="ui header"> - <translate translate-context="Content/Federation/*/Noun">Followers</translate> + <translate translate-context="Content/Federation/*/Noun"> + Followers + </translate> </h2> - <div v-if="isLoadingFollows" :class="['ui', {'active': isLoadingFollows}, 'inverted', 'dimmer']"> - <div class="ui text loader"><translate translate-context="Content/Library/Paragraph">Loading followers…</translate></div> + <div + v-if="isLoadingFollows" + :class="['ui', {'active': isLoadingFollows}, 'inverted', 'dimmer']" + > + <div class="ui text loader"> + <translate translate-context="Content/Library/Paragraph"> + Loading followers… + </translate> + </div> </div> - <table v-else-if="follows && follows.count > 0" class="ui table"> + <table + v-else-if="follows && follows.count > 0" + class="ui table" + > <thead> <tr> - <th><translate translate-context="Content/Library/Table.Label">User</translate></th> - <th><translate translate-context="Content/Library/Table.Label">Date</translate></th> - <th><translate translate-context="*/*/*">Status</translate></th> - <th><translate translate-context="Content/Library/Table.Label">Action</translate></th> + <th> + <translate translate-context="Content/Library/Table.Label"> + User + </translate> + </th> + <th> + <translate translate-context="Content/Library/Table.Label"> + Date + </translate> + </th> + <th> + <translate translate-context="*/*/*"> + Status + </translate> + </th> + <th> + <translate translate-context="Content/Library/Table.Label"> + Action + </translate> + </th> </tr> </thead> - <tr v-for="follow in follows.results" :key="follow.fid"> + <tr + v-for="follow in follows.results" + :key="follow.fid" + > <td><actor-link :actor="follow.actor" /></td> <td><human-date :date="follow.creation_date" /></td> <td> - <span :class="['ui', 'warning', 'basic', 'label']" v-if="follow.approved === null"> + <span + v-if="follow.approved === null" + :class="['ui', 'warning', 'basic', 'label']" + > <translate translate-context="Content/Library/Table/Short">Pending approval</translate> </span> - <span :class="['ui', 'success', 'basic', 'label']" v-else-if="follow.approved === true"> + <span + v-else-if="follow.approved === true" + :class="['ui', 'success', 'basic', 'label']" + > <translate translate-context="Content/Library/Table/Short">Accepted</translate> </span> - <span :class="['ui', 'danger', 'basic', 'label']" v-else-if="follow.approved === false"> + <span + v-else-if="follow.approved === false" + :class="['ui', 'danger', 'basic', 'label']" + > <translate translate-context="Content/Library/*/Short">Rejected</translate> </span> </td> <td> - <button @click="updateApproved(follow, true)" :class="['ui', 'mini', 'icon', 'labeled', 'success', 'button']" v-if="follow.approved === null || follow.approved === false"> - <i class="ui check icon"></i> <translate translate-context="Content/Library/Button.Label">Accept</translate> + <button + v-if="follow.approved === null || follow.approved === false" + :class="['ui', 'mini', 'icon', 'labeled', 'success', 'button']" + @click="updateApproved(follow, true)" + > + <i class="ui check icon" /> <translate translate-context="Content/Library/Button.Label"> + Accept + </translate> </button> - <button @click="updateApproved(follow, false)" :class="['ui', 'mini', 'icon', 'labeled', 'danger', 'button']" v-if="follow.approved === null || follow.approved === true"> - <i class="ui x icon"></i> <translate translate-context="Content/Library/Button.Label">Reject</translate> + <button + v-if="follow.approved === null || follow.approved === true" + :class="['ui', 'mini', 'icon', 'labeled', 'danger', 'button']" + @click="updateApproved(follow, false)" + > + <i class="ui x icon" /> <translate translate-context="Content/Library/Button.Label"> + Reject + </translate> </button> </td> </tr> - </table> - <p v-else><translate translate-context="Content/Library/Paragraph">Nobody is following this library</translate></p> + <p v-else> + <translate translate-context="Content/Library/Paragraph"> + Nobody is following this library + </translate> + </p> </section> </template> <script> -import LibraryFilesTable from "@/views/content/libraries/FilesTable" -import LibraryForm from "@/views/content/libraries/Form" -import axios from "axios" +import LibraryFilesTable from '@/views/content/libraries/FilesTable' +import LibraryForm from '@/views/content/libraries/Form' +import axios from 'axios' export default { - props: ['object'], components: { LibraryForm, LibraryFilesTable }, + props: { object: { type: String, required: true } }, data () { return { isLoadingFollows: false, follows: null } }, - created() { + created () { this.fetchFollows() }, methods: { - fetchFollows() { - let self = this + fetchFollows () { + const self = this self.isLoadingLibrary = true axios.get(`libraries/${this.object.uuid}/follows/`).then(response => { self.follows = response.data self.isLoadingFollows = false }) }, - updateApproved(follow, value) { - let self = this + updateApproved (follow, value) { let action if (value) { - action = "accept" + action = 'accept' } else { - action = "reject" + action = 'reject' } axios .post(`federation/follows/library/${follow.uuid}/${action}/`) diff --git a/front/src/views/library/Upload.vue b/front/src/views/library/Upload.vue index 66442c43a5e0e66c50a5bdf9766398af0bdadd0b..48dda7e29132abc3c9fb5772e47be6c34d8559a0 100644 --- a/front/src/views/library/Upload.vue +++ b/front/src/views/library/Upload.vue @@ -1,10 +1,11 @@ <template> <section> - <file-upload ref="fileupload" + <file-upload + ref="fileupload" :default-import-reference="defaultImportReference" :library="object" - @uploads-finished="$emit('uploads-finished', $event)" /> - + @uploads-finished="$emit('uploads-finished', $event)" + /> </section> </template> @@ -13,23 +14,25 @@ import FileUpload from '@/components/library/FileUpload' export default { - props: ['object', 'defaultImportReference'], components: { - FileUpload, + FileUpload }, - beforeRouteLeave (to, from, next){ - if (this.$refs.fileupload.hasActiveUploads){ + beforeRouteLeave (to, from, next) { + if (this.$refs.fileupload.hasActiveUploads) { const answer = window.confirm('This page is asking you to confirm that you want to leave - data you have entered may not be saved.') if (answer) { next() } else { next(false) } - } - else{ + } else { next() } + }, + props: { + object: { type: String, required: true }, + defaultImportReference: { type: String, required: true } } } </script> diff --git a/front/src/views/playlists/Detail.vue b/front/src/views/playlists/Detail.vue index de2a7b3e4151a6b8d37789f1225d8bc7b6cd36fa..272a0d535a57816be5e6d8d23d56ccb626d9420d 100644 --- a/front/src/views/playlists/Detail.vue +++ b/front/src/views/playlists/Detail.vue @@ -1,12 +1,20 @@ <template> <main> - <div v-if="isLoading" class="ui vertical segment" v-title="labels.playlist"> - <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> + <div + v-if="isLoading" + v-title="labels.playlist" + class="ui vertical segment" + > + <div :class="['ui', 'centered', 'active', 'inline', 'loader']" /> </div> - <section v-if="!isLoading && playlist" class="ui head vertical center aligned stripe segment" v-title="playlist.name"> + <section + v-if="!isLoading && playlist" + v-title="playlist.name" + class="ui head vertical center aligned stripe segment" + > <div class="segment-content"> <h2 class="ui center aligned icon header"> - <i class="circular inverted list warning icon"></i> + <i class="circular inverted list warning icon" /> <div class="content"> {{ playlist.name }} <div class="sub header"> @@ -14,146 +22,213 @@ translate-plural="Playlist containing %{ count } tracks, by %{ username }" :translate-n="playlist.tracks_count" :translate-params="{count: playlist.tracks_count, username: playlist.user.username}" - translate-context="Content/Playlist/Header.Subtitle"> - Playlist containing %{ count } track, by %{ username } + translate-context="Content/Playlist/Header.Subtitle" + > + Playlist containing %{ count } track, by %{ username } </translate><br> <duration :seconds="playlist.duration" /> </div> </div> </h2> - <div class="ui hidden divider"></div> + <div class="ui hidden divider" /> <div class="header-buttons"> <div class="ui buttons"> - <play-button class="vibrant" :is-playable="playlist.is_playable" :tracks="tracks"><translate translate-context="Content/Queue/Button.Label/Short, Verb">Play all</translate></play-button> + <play-button + class="vibrant" + :is-playable="playlist.is_playable" + :tracks="tracks" + > + <translate translate-context="Content/Queue/Button.Label/Short, Verb"> + Play all + </translate> + </play-button> </div> <div class="ui buttons"> <button - class="ui icon labeled button" v-if="$store.state.auth.profile && playlist.user.id === $store.state.auth.profile.id" - @click="edit = !edit"> - <i class="pencil icon"></i> - <template v-if="edit"><translate translate-context="Content/Playlist/Button.Label/Verb">Stop Editing</translate></template> - <template v-else><translate translate-context="Content/*/Button.Label/Verb">Edit</translate></template> + class="ui icon labeled button" + @click="edit = !edit" + > + <i class="pencil icon" /> + <template v-if="edit"> + <translate translate-context="Content/Playlist/Button.Label/Verb"> + Stop Editing + </translate> + </template> + <template v-else> + <translate translate-context="Content/*/Button.Label/Verb"> + Edit + </translate> + </template> </button> </div> - <div class="ui buttons"> + <div class="ui buttons"> <button - class="ui icon labeled button" v-if="playlist.privacy_level === 'everyone' && playlist.is_playable" - @click="showEmbedModal = !showEmbedModal"> - <i class="code icon"></i> - <translate translate-context="Content/*/Button.Label/Verb">Embed</translate> + class="ui icon labeled button" + @click="showEmbedModal = !showEmbedModal" + > + <i class="code icon" /> + <translate translate-context="Content/*/Button.Label/Verb"> + Embed + </translate> </button> - <dangerous-button v-if="$store.state.auth.profile && playlist.user.id === $store.state.auth.profile.id" class="ui labeled danger icon button" :action="deletePlaylist"> - <i class="trash icon"></i> <translate translate-context="*/*/*/Verb">Delete</translate> - <p slot="modal-header" v-translate="{playlist: playlist.name}" translate-context="Popup/Playlist/Title/Call to action" :translate-params="{playlist: playlist.name}"> - Do you want to delete the playlist "%{ playlist }"? + <dangerous-button + v-if="$store.state.auth.profile && playlist.user.id === $store.state.auth.profile.id" + class="ui labeled danger icon button" + :action="deletePlaylist" + > + <i class="trash icon" /> <translate translate-context="*/*/*/Verb"> + Delete + </translate> + <p + slot="modal-header" + v-translate="{playlist: playlist.name}" + translate-context="Popup/Playlist/Title/Call to action" + :translate-params="{playlist: playlist.name}" + > + Do you want to delete the playlist "%{ playlist }"? </p> - <p slot="modal-content"><translate translate-context="Popup/Playlist/Paragraph">This will completely delete this playlist and cannot be undone.</translate></p> - <div slot="modal-confirm"><translate translate-context="Popup/Playlist/Button.Label/Verb">Delete playlist</translate></div> + <p slot="modal-content"> + <translate translate-context="Popup/Playlist/Paragraph"> + This will completely delete this playlist and cannot be undone. + </translate> + </p> + <div slot="modal-confirm"> + <translate translate-context="Popup/Playlist/Button.Label/Verb"> + Delete playlist + </translate> + </div> </dangerous-button> </div> </div> - <modal v-if="playlist.privacy_level === 'everyone' && playlist.is_playable" :show.sync="showEmbedModal"> - <h4 class="header"> - <translate translate-context="Popup/Album/Title/Verb">Embed this playlist on your website</translate> - </h4> - <div class="scrolling content"> - <div class="description"> - <embed-wizard type="playlist" :id="playlist.id" /> + <modal + v-if="playlist.privacy_level === 'everyone' && playlist.is_playable" + :show.sync="showEmbedModal" + > + <h4 class="header"> + <translate translate-context="Popup/Album/Title/Verb"> + Embed this playlist on your website + </translate> + </h4> + <div class="scrolling content"> + <div class="description"> + <embed-wizard + :id="playlist.id" + type="playlist" + /> + </div> + </div> + <div class="actions"> + <button class="ui basic deny button"> + <translate translate-context="*/*/Button.Label/Verb"> + Cancel + </translate> + </button> </div> - </div> - <div class="actions"> - <button class="ui basic deny button"> - <translate translate-context="*/*/Button.Label/Verb">Cancel</translate> - </button> - </div> </modal> </div> </section> <section class="ui vertical stripe segment"> <template v-if="edit"> <playlist-editor + :playlist="playlist" + :playlist-tracks="playlistTracks" @playlist-updated="playlist = $event" @tracks-updated="updatePlts" - :playlist="playlist" :playlist-tracks="playlistTracks"></playlist-editor> + /> </template> <template v-else-if="tracks.length > 0"> - <h2><translate translate-context="*/*/*">Tracks</translate></h2> - <track-table :display-position="true" :tracks="tracks"></track-table> + <h2> + <translate translate-context="*/*/*"> + Tracks + </translate> + </h2> + <track-table + :display-position="true" + :tracks="tracks" + /> </template> - <div v-else class="ui placeholder segment"> + <div + v-else + class="ui placeholder segment" + > <div class="ui icon header"> - <i class="list icon"></i> - <translate translate-context="Content/Home/Placeholder">There are no tracks in this playlist yet</translate> + <i class="list icon" /> + <translate translate-context="Content/Home/Placeholder"> + There are no tracks in this playlist yet + </translate> </div> - <button @click="edit = !edit" class="ui success icon labeled button"> - <i class="pencil icon"></i> - <translate translate-context="Content/Home/CreatePlaylist">Edit</translate> + <button + class="ui success icon labeled button" + @click="edit = !edit" + > + <i class="pencil icon" /> + <translate translate-context="Content/Home/CreatePlaylist"> + Edit + </translate> </button> </div> </section> </main> </template> <script> -import axios from "axios" -import TrackTable from "@/components/audio/track/Table" -import RadioButton from "@/components/radios/Button" -import PlayButton from "@/components/audio/PlayButton" -import PlaylistEditor from "@/components/playlists/Editor" -import EmbedWizard from "@/components/audio/EmbedWizard" +import axios from 'axios' +import TrackTable from '@/components/audio/track/Table' +import PlayButton from '@/components/audio/PlayButton' +import PlaylistEditor from '@/components/playlists/Editor' +import EmbedWizard from '@/components/audio/EmbedWizard' import Modal from '@/components/semantic/Modal' export default { - props: { - id: { required: true }, - defaultEdit: { type: Boolean, default: false } - }, components: { PlaylistEditor, TrackTable, PlayButton, - RadioButton, Modal, - EmbedWizard, + EmbedWizard }, - data: function() { + props: { + id: { type: Number, required: true }, + defaultEdit: { type: Boolean, default: false } + }, + data: function () { return { edit: this.defaultEdit, isLoading: false, playlist: null, tracks: [], playlistTracks: [], - showEmbedModal: false, + showEmbedModal: false } }, - created: function() { - this.fetch() - }, computed: { - labels() { + labels () { return { playlist: this.$pgettext('*/*/*', 'Playlist') } } }, + created: function () { + this.fetch() + }, methods: { - updatePlts(v) { + updatePlts (v) { this.playlistTracks = v this.tracks = v.map((e, i) => { - let track = e.track + const track = e.track track.position = i + 1 return track }) }, - fetch: function() { - let self = this + fetch: function () { + const self = this self.isLoading = true - let url = "playlists/" + this.id + "/" + const url = 'playlists/' + this.id + '/' axios.get(url).then(response => { self.playlist = response.data axios - .get(url + "tracks/") + .get(url + 'tracks/') .then(response => { self.updatePlts(response.data.results) }) @@ -162,13 +237,13 @@ export default { }) }) }, - deletePlaylist() { - let self = this - let url = "playlists/" + this.id + "/" + deletePlaylist () { + const self = this + const url = 'playlists/' + this.id + '/' axios.delete(url).then(response => { - self.$store.dispatch("playlists/fetchOwn") + self.$store.dispatch('playlists/fetchOwn') self.$router.push({ - path: "/library" + path: '/library' }) }) } diff --git a/front/src/views/playlists/List.vue b/front/src/views/playlists/List.vue index ddfabd6fb4bcacfd08aa7335fabaf95f2a16e4c1..4ad0cdb1260c885887c498adad67e864b7b33f42 100644 --- a/front/src/views/playlists/List.vue +++ b/front/src/views/playlists/List.vue @@ -1,152 +1,217 @@ <template> <main v-title="labels.playlists"> <section class="ui vertical stripe segment"> - <h2 class="ui header"><translate translate-context="Content/Playlist/Title">Browsing playlists</translate></h2> + <h2 class="ui header"> + <translate translate-context="Content/Playlist/Title"> + Browsing playlists + </translate> + </h2> <template v-if="$store.state.auth.authenticated"> <button + class="ui success button" @click="$store.commit('playlists/chooseTrack', null)" - class="ui success button"><translate translate-context="Content/Playlist/Button.Label/Verb">Manage your playlists</translate></button> - <div class="ui hidden divider"></div> + > + <translate translate-context="Content/Playlist/Button.Label/Verb"> + Manage your playlists + </translate> + </button> + <div class="ui hidden divider" /> </template> - <form :class="['ui', {'loading': isLoading}, 'form']" @submit.prevent="updateQueryString();fetchData()"> + <form + :class="['ui', {'loading': isLoading}, 'form']" + @submit.prevent="updateQueryString();fetchData()" + > <div class="fields"> <div class="field"> <label for="playlists-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label> <div class="ui action input"> - <input id="playlists-search" stype="text" name="search" v-model="query" :placeholder="labels.searchPlaceholder"/> - <button class="ui icon button" type="submit" :aria-label="$pgettext('Content/Search/Input.Label/Noun', 'Search')"> - <i class="search icon"></i> + <input + id="playlists-search" + v-model="query" + stype="text" + name="search" + :placeholder="labels.searchPlaceholder" + > + <button + class="ui icon button" + type="submit" + :aria-label="$pgettext('Content/Search/Input.Label/Noun', 'Search')" + > + <i class="search icon" /> </button> </div> </div> <div class="field"> <label for="playlists-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> - <select id="playlists-ordering" class="ui dropdown" v-model="ordering"> - <option v-for="option in orderingOptions" :value="option[0]"> + <select + id="playlists-ordering" + v-model="ordering" + class="ui dropdown" + > + <option + v-for="option in orderingOptions" + :key="option[0]" + :value="option[0]" + > {{ sharedLabels.filters[option[1]] }} </option> </select> </div> <div class="field"> <label for="playlists-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Order</translate></label> - <select id="playlists-ordering-direction" class="ui dropdown" v-model="orderingDirection"> - <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option> - <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option> + <select + id="playlists-ordering-direction" + v-model="orderingDirection" + class="ui dropdown" + > + <option value="+"> + <translate translate-context="Content/Search/Dropdown"> + Ascending + </translate> + </option> + <option value="-"> + <translate translate-context="Content/Search/Dropdown"> + Descending + </translate> + </option> </select> </div> <div class="field"> <label for="playlists-results"><translate translate-context="Content/Search/Dropdown.Label/Noun">Results per page</translate></label> - <select id="playlists-results" class="ui dropdown" v-model="paginateBy"> - <option :value="parseInt(12)">12</option> - <option :value="parseInt(25)">25</option> - <option :value="parseInt(50)">50</option> + <select + id="playlists-results" + v-model="paginateBy" + class="ui dropdown" + > + <option :value="parseInt(12)"> + 12 + </option> + <option :value="parseInt(25)"> + 25 + </option> + <option :value="parseInt(50)"> + 50 + </option> </select> </div> </div> </form> - <div class="ui hidden divider"></div> - <playlist-card-list v-if="result && result.results.length > 0" :playlists="result.results"></playlist-card-list> - <div v-else-if="result && !result.results.length > 0" class="ui placeholder segment sixteen wide column" style="text-align: center; display: flex; align-items: center"> + <div class="ui hidden divider" /> + <playlist-card-list + v-if="result && result.results.length > 0" + :playlists="result.results" + /> + <div + v-else-if="result && !result.results.length > 0" + class="ui placeholder segment sixteen wide column" + style="text-align: center; display: flex; align-items: center" + > <div class="ui icon header"> - <i class="list icon"></i> + <i class="list icon" /> <translate translate-context="Content/Playlists/Placeholder"> No results matching your query </translate> </div> <button - v-if="$store.state.auth.authenticated" - @click="$store.commit('playlists/chooseTrack', null)" - class="ui success button labeled icon"> - <i class="list icon"></i> - <translate translate-context="Content/*/Verb"> - Create a playlist + v-if="$store.state.auth.authenticated" + class="ui success button labeled icon" + @click="$store.commit('playlists/chooseTrack', null)" + > + <i class="list icon" /> + <translate translate-context="Content/*/Verb"> + Create a playlist </translate> </button> </div> <div class="ui center aligned basic segment"> <pagination v-if="result && result.results.length > 0" - @page-changed="selectPage" :current="page" :paginate-by="paginateBy" :total="result.count" - ></pagination> + @page-changed="selectPage" + /> </div> </section> </main> </template> <script> -import axios from "axios" -import _ from "@/lodash" -import $ from "jquery" +import axios from 'axios' +import $ from 'jquery' -import OrderingMixin from "@/components/mixins/Ordering" -import PaginationMixin from "@/components/mixins/Pagination" -import TranslationsMixin from "@/components/mixins/Translations" -import PlaylistCardList from "@/components/playlists/CardList" -import Pagination from "@/components/Pagination" +import OrderingMixin from '@/components/mixins/Ordering' +import PaginationMixin from '@/components/mixins/Pagination' +import TranslationsMixin from '@/components/mixins/Translations' +import PlaylistCardList from '@/components/playlists/CardList' +import Pagination from '@/components/Pagination' -const FETCH_URL = "playlists/" +const FETCH_URL = 'playlists/' export default { - mixins: [OrderingMixin, PaginationMixin, TranslationsMixin], - props: { - defaultQuery: { type: String, required: false, default: "" }, - scope: { type: String, required: false, default: "all" }, - }, components: { PlaylistCardList, Pagination }, - data() { + mixins: [OrderingMixin, PaginationMixin, TranslationsMixin], + props: { + defaultQuery: { type: String, required: false, default: '' }, + scope: { type: String, required: false, default: 'all' } + }, + data () { return { isLoading: true, result: null, page: parseInt(this.defaultPage), query: this.defaultQuery, orderingOptions: [ - ["creation_date", "creation_date"], - ["modification_date", "modification_date"], - ["name", "name"] + ['creation_date', 'creation_date'], + ['modification_date', 'modification_date'], + ['name', 'name'] ] } }, - created() { - this.fetchData() - }, - mounted() { - $(".ui.dropdown").dropdown() - }, computed: { - labels() { - let playlists = this.$pgettext('*/*/*', 'Playlists') - let searchPlaceholder = this.$pgettext('Content/Playlist/Placeholder/Call to action', 'Enter playlist name…') + labels () { + const playlists = this.$pgettext('*/*/*', 'Playlists') + const searchPlaceholder = this.$pgettext('Content/Playlist/Placeholder/Call to action', 'Enter playlist name…') return { playlists, searchPlaceholder } } }, + watch: { + page () { + this.updateQueryString() + this.fetchData() + } + }, + created () { + this.fetchData() + }, + mounted () { + $('.ui.dropdown').dropdown() + }, methods: { - updateQueryString: function() { + updateQueryString: function () { history.pushState( {}, null, this.$route.path + '?' + new URLSearchParams( { - query: this.query, - page: this.page, - paginateBy: this.paginateBy, - ordering: this.getOrderingAsString() - }).toString() + query: this.query, + page: this.page, + paginateBy: this.paginateBy, + ordering: this.getOrderingAsString() + }).toString() ) }, - fetchData: function() { - var self = this + fetchData: function () { + const self = this this.isLoading = true - let url = FETCH_URL - let params = { + const url = FETCH_URL + const params = { scope: this.scope, page: this.page, page_size: this.paginateBy, @@ -159,15 +224,9 @@ export default { self.isLoading = false }) }, - selectPage: function(page) { + selectPage: function (page) { this.page = page } - }, - watch: { - page() { - this.updateQueryString() - this.fetchData() - }, } } </script> diff --git a/front/src/views/radios/Detail.vue b/front/src/views/radios/Detail.vue index 37c2b9acc79e060f02c48e10f367cad1904c4a50..7c72ff87b233765a4c859e7f58245be88adb7c91 100644 --- a/front/src/views/radios/Detail.vue +++ b/front/src/views/radios/Detail.vue @@ -1,61 +1,108 @@ <template> <main> - <div v-if="isLoading" class="ui vertical segment" v-title="labels.title"> - <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> + <div + v-if="isLoading" + v-title="labels.title" + class="ui vertical segment" + > + <div :class="['ui', 'centered', 'active', 'inline', 'loader']" /> </div> - <section v-if="!isLoading && radio" class="ui head vertical center aligned stripe segment" v-title="radio.name"> + <section + v-if="!isLoading && radio" + v-title="radio.name" + class="ui head vertical center aligned stripe segment" + > <div class="segment-content"> <h2 class="ui center aligned icon header"> - <i class="circular inverted feed primary icon"></i> + <i class="circular inverted feed primary icon" /> <div class="content"> {{ radio.name }} <div class="sub header"> Radio containing {{ totalTracks }} tracks, - by <username :username="radio.user.username"></username> + by <username :username="radio.user.username" /> </div> </div> </h2> - <div class="ui hidden divider"></div> - <radio-button type="custom" :custom-radio-id="radio.id"></radio-button> + <div class="ui hidden divider" /> + <radio-button + type="custom" + :custom-radio-id="radio.id" + /> <template v-if="$store.state.auth.username === radio.user.username"> - <router-link class="ui icon labeled button" :to="{name: 'library.radios.edit', params: {id: radio.id}}" exact> - <i class="pencil icon"></i> + <router-link + class="ui icon labeled button" + :to="{name: 'library.radios.edit', params: {id: radio.id}}" + exact + > + <i class="pencil icon" /> Edit… </router-link> - <dangerous-button class="ui labeled danger icon button" :action="deleteRadio"> - <i class="trash icon"></i> Delete - <p slot="modal-header" v-translate="{radio: radio.name}" translate-context="Popup/Radio/Title" :translate-params="{radio: radio.name}">Do you want to delete the radio "%{ radio }"?</p> - <p slot="modal-content"><translate translate-context="Popup/Radio/Paragraph">This will completely delete this radio and cannot be undone.</translate></p> - <p slot="modal-confirm"><translate translate-context="Popup/Radio/Button.Label/Verb">Delete radio</translate></p> + <dangerous-button + class="ui labeled danger icon button" + :action="deleteRadio" + > + <i class="trash icon" /> Delete + <p + slot="modal-header" + v-translate="{radio: radio.name}" + translate-context="Popup/Radio/Title" + :translate-params="{radio: radio.name}" + > + Do you want to delete the radio "%{ radio }"? + </p> + <p slot="modal-content"> + <translate translate-context="Popup/Radio/Paragraph"> + This will completely delete this radio and cannot be undone. + </translate> + </p> + <p slot="modal-confirm"> + <translate translate-context="Popup/Radio/Button.Label/Verb"> + Delete radio + </translate> + </p> </dangerous-button> </template> </div> </section> - <section v-if="totalTracks > 0" class="ui vertical stripe segment"> - <h2><translate translate-context="*/*/*">Tracks</translate></h2> - <track-table :tracks="tracks"></track-table> + <section + v-if="totalTracks > 0" + class="ui vertical stripe segment" + > + <h2> + <translate translate-context="*/*/*"> + Tracks + </translate> + </h2> + <track-table :tracks="tracks" /> <div class="ui center aligned basic segment"> <pagination v-if="totalTracks > 25" - @page-changed="selectPage" :current="page" :paginate-by="25" :total="totalTracks" - ></pagination> + @page-changed="selectPage" + /> </div> </section> - <div v-else-if="!isLoading && !totalTracks > 0" class="ui placeholder segment"> + <div + v-else-if="!isLoading && !totalTracks > 0" + class="ui placeholder segment" + > <div class="ui icon header"> - <i class="rss icon"></i> + <i class="rss icon" /> <translate - translate-context="Content/Radios/Placeholder" - >No tracks have been added to this radio yet</translate> + translate-context="Content/Radios/Placeholder" + > + No tracks have been added to this radio yet + </translate> </div> <router-link - v-if="$store.state.auth.username === radio.user.username" - class="ui success icon labeled button" - :to="{name: 'library.radios.edit', params: {id: radio.id}}" exact> - <i class="pencil icon"></i> + v-if="$store.state.auth.username === radio.user.username" + class="ui success icon labeled button" + :to="{name: 'library.radios.edit', params: {id: radio.id}}" + exact + > + <i class="pencil icon" /> Edit… </router-link> </div> @@ -63,21 +110,21 @@ </template> <script> -import axios from "axios" -import TrackTable from "@/components/audio/track/Table" -import RadioButton from "@/components/radios/Button" -import Pagination from "@/components/Pagination" +import axios from 'axios' +import TrackTable from '@/components/audio/track/Table' +import RadioButton from '@/components/radios/Button' +import Pagination from '@/components/Pagination' export default { - props: { - id: { required: true } - }, components: { TrackTable, RadioButton, Pagination }, - data: function() { + props: { + id: { type: Number, required: true } + }, + data: function () { return { isLoading: false, radio: null, @@ -86,28 +133,33 @@ export default { page: 1 } }, - created: function() { - this.fetch() - }, computed: { - labels() { + labels () { return { - title: this.$pgettext('Head/Radio/Title', "Radio") + title: this.$pgettext('Head/Radio/Title', 'Radio') } } }, + watch: { + page: function () { + this.fetch() + } + }, + created: function () { + this.fetch() + }, methods: { - selectPage: function(page) { + selectPage: function (page) { this.page = page }, - fetch: function() { - let self = this + fetch: function () { + const self = this self.isLoading = true - let url = "radios/radios/" + this.id + "/" + const url = 'radios/radios/' + this.id + '/' axios.get(url).then(response => { self.radio = response.data axios - .get(url + "tracks/", { params: { page: this.page } }) + .get(url + 'tracks/', { params: { page: this.page } }) .then(response => { this.totalTracks = response.data.count this.tracks = response.data.results @@ -117,20 +169,15 @@ export default { }) }) }, - deleteRadio() { - let self = this - let url = "radios/radios/" + this.id + "/" + deleteRadio () { + const self = this + const url = 'radios/radios/' + this.id + '/' axios.delete(url).then(response => { self.$router.push({ - path: "/library" + path: '/library' }) }) } - }, - watch: { - page: function() { - this.fetch() - } } } </script>