Merge branch 'webpack-tweaking' into 'develop'
Improve front-end performance See merge request funkwhale/funkwhale!499
This commit is contained in:
commit
e1450d28b8
|
@ -93,5 +93,6 @@ po/*.po
|
|||
docs/swagger
|
||||
_build
|
||||
front/src/translations.json
|
||||
front/src/translations/*.json
|
||||
front/locales/en_US/LC_MESSAGES/app.po
|
||||
*.prof
|
||||
|
|
|
@ -58,15 +58,6 @@ class RavenDSN(types.StringPreference):
|
|||
field_kwargs = {"required": False}
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class RavenEnabled(types.BooleanPreference):
|
||||
show_in_api = True
|
||||
section = raven
|
||||
name = "front_enabled"
|
||||
default = False
|
||||
verbose_name = "Report front-end errors with Raven"
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class InstanceNodeinfoEnabled(types.BooleanPreference):
|
||||
show_in_api = False
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
Improved front-end performance by stripping unused dependencies, reducing bundle size
|
||||
and enabling gzip compression
|
||||
|
||||
Enable gzip compression [manual action suggested]
|
||||
-------------------------------------------------
|
||||
|
||||
Gzip compression will be enabled on new instances by default
|
||||
and will reduce the amount of bandwidth consumed by your instance.
|
||||
|
||||
If you with to benefit from gzip compression on your instance,
|
||||
edit your reverse proxy virtualhost file (located at ``/etc/nginx/sites-available/funkwhale.conf``) and add the following snippet
|
||||
in the server block, then reload your nginx server::
|
||||
|
||||
server {
|
||||
# ... exiting configuration
|
||||
|
||||
# compression settings
|
||||
gzip on;
|
||||
gzip_comp_level 5;
|
||||
gzip_min_length 256;
|
||||
gzip_proxied any;
|
||||
gzip_vary on;
|
||||
|
||||
gzip_types
|
||||
application/atom+xml
|
||||
application/javascript
|
||||
application/json
|
||||
application/ld+json
|
||||
application/activity+json
|
||||
application/manifest+json
|
||||
application/rss+xml
|
||||
application/vnd.geo+json
|
||||
application/vnd.ms-fontobject
|
||||
application/x-font-ttf
|
||||
application/x-web-app-manifest+json
|
||||
application/xhtml+xml
|
||||
application/xml
|
||||
font/opentype
|
||||
image/bmp
|
||||
image/svg+xml
|
||||
image/x-icon
|
||||
text/cache-manifest
|
||||
text/css
|
||||
text/plain
|
||||
text/vcard
|
||||
text/vnd.rim.location.xloc
|
||||
text/vtt
|
||||
text/x-component
|
||||
text/x-cross-domain-policy;
|
||||
# end of compression settings
|
||||
}
|
|
@ -29,6 +29,40 @@ server {
|
|||
# HSTS
|
||||
add_header Strict-Transport-Security "max-age=31536000";
|
||||
|
||||
# compression settings
|
||||
gzip on;
|
||||
gzip_comp_level 5;
|
||||
gzip_min_length 256;
|
||||
gzip_proxied any;
|
||||
gzip_vary on;
|
||||
|
||||
gzip_types
|
||||
application/atom+xml
|
||||
application/javascript
|
||||
application/json
|
||||
application/ld+json
|
||||
application/activity+json
|
||||
application/manifest+json
|
||||
application/rss+xml
|
||||
application/vnd.geo+json
|
||||
application/vnd.ms-fontobject
|
||||
application/x-font-ttf
|
||||
application/x-web-app-manifest+json
|
||||
application/xhtml+xml
|
||||
application/xml
|
||||
font/opentype
|
||||
image/bmp
|
||||
image/svg+xml
|
||||
image/x-icon
|
||||
text/cache-manifest
|
||||
text/css
|
||||
text/plain
|
||||
text/vcard
|
||||
text/vnd.rim.location.xloc
|
||||
text/vtt
|
||||
text/x-component
|
||||
text/x-cross-domain-policy;
|
||||
|
||||
location / {
|
||||
include /etc/nginx/funkwhale_proxy.conf;
|
||||
proxy_pass http://fw/;
|
||||
|
|
|
@ -43,6 +43,41 @@ server {
|
|||
|
||||
root ${FUNKWHALE_FRONTEND_PATH};
|
||||
|
||||
# compression settings
|
||||
gzip on;
|
||||
gzip_comp_level 5;
|
||||
gzip_min_length 256;
|
||||
gzip_proxied any;
|
||||
gzip_vary on;
|
||||
|
||||
gzip_types
|
||||
application/atom+xml
|
||||
application/javascript
|
||||
application/json
|
||||
application/ld+json
|
||||
application/activity+json
|
||||
application/manifest+json
|
||||
application/rss+xml
|
||||
application/vnd.geo+json
|
||||
application/vnd.ms-fontobject
|
||||
application/x-font-ttf
|
||||
application/x-web-app-manifest+json
|
||||
application/xhtml+xml
|
||||
application/xml
|
||||
font/opentype
|
||||
image/bmp
|
||||
image/svg+xml
|
||||
image/x-icon
|
||||
text/cache-manifest
|
||||
text/css
|
||||
text/plain
|
||||
text/vcard
|
||||
text/vnd.rim.location.xloc
|
||||
text/vtt
|
||||
text/x-component
|
||||
text/x-cross-domain-policy;
|
||||
|
||||
# end of compression settings
|
||||
location / {
|
||||
include /etc/nginx/funkwhale_proxy.conf;
|
||||
# this is needed if you have file import via upload enabled
|
||||
|
|
|
@ -43,6 +43,39 @@ http {
|
|||
charset utf-8;
|
||||
client_max_body_size 30M;
|
||||
include /etc/nginx/funkwhale_proxy.conf;
|
||||
# compression settings
|
||||
gzip on;
|
||||
gzip_comp_level 5;
|
||||
gzip_min_length 256;
|
||||
gzip_proxied any;
|
||||
gzip_vary on;
|
||||
|
||||
gzip_types
|
||||
application/atom+xml
|
||||
application/javascript
|
||||
application/json
|
||||
application/ld+json
|
||||
application/activity+json
|
||||
application/manifest+json
|
||||
application/rss+xml
|
||||
application/vnd.geo+json
|
||||
application/vnd.ms-fontobject
|
||||
application/x-font-ttf
|
||||
application/x-web-app-manifest+json
|
||||
application/xhtml+xml
|
||||
application/xml
|
||||
font/opentype
|
||||
image/bmp
|
||||
image/svg+xml
|
||||
image/x-icon
|
||||
text/cache-manifest
|
||||
text/css
|
||||
text/plain
|
||||
text/vcard
|
||||
text/vnd.rim.location.xloc
|
||||
text/vtt
|
||||
text/x-component
|
||||
text/x-cross-domain-policy;
|
||||
|
||||
location /front/ {
|
||||
proxy_pass http://funkwhale-front/front/;
|
||||
|
|
|
@ -182,7 +182,7 @@ work. We will store the necessary files in the ``/srv/funkwhale/custom`` directo
|
|||
mkdir custom
|
||||
cat <<EOF > custom/settings.json
|
||||
{
|
||||
"additionalStylesheets": ["/custom/custom.css"]
|
||||
"additionalStylesheets": ["/front/custom/custom.css"]
|
||||
}
|
||||
EOF
|
||||
cat <<EOF > custom/custom.css
|
||||
|
@ -194,7 +194,7 @@ work. We will store the necessary files in the ``/srv/funkwhale/custom`` directo
|
|||
By executing the previous commands, you will end up with two files in your ``/srv/funkwhale/custom``
|
||||
directory:
|
||||
|
||||
- ``settings.json`` will tell the front-end what stylesheets you want to load (``/custom/custom.css`` in this example)
|
||||
- ``settings.json`` will tell the front-end what stylesheets you want to load (``/front/custom/custom.css`` in this example)
|
||||
- ``custom.css`` will hold your custom CSS
|
||||
|
||||
The last step to make this work is to ensure both files are served by the reverse proxy.
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "scripts/i18n-compile.sh && vue-cli-service serve --port ${VUE_PORT:-8000} --host ${VUE_HOST:-0.0.0.0}",
|
||||
"serve": "vue-cli-service serve --port ${VUE_PORT:-8000} --host ${VUE_HOST:-0.0.0.0}",
|
||||
"build": "scripts/i18n-compile.sh && vue-cli-service build",
|
||||
"lint": "vue-cli-service lint",
|
||||
"i18n-extract": "scripts/i18n-extract.sh",
|
||||
|
@ -20,8 +20,7 @@
|
|||
"lodash": "^4.17.10",
|
||||
"masonry-layout": "^4.2.2",
|
||||
"moment": "^2.22.2",
|
||||
"raven-js": "^3.26.4",
|
||||
"semantic-ui-css": "^2.3.3",
|
||||
"semantic-ui-css": "^2.4.1",
|
||||
"showdown": "^1.8.6",
|
||||
"vue": "^2.5.17",
|
||||
"vue-gettext": "^2.1.0",
|
||||
|
@ -49,7 +48,8 @@
|
|||
"node-sass": "^4.9.3",
|
||||
"sass-loader": "^7.1.0",
|
||||
"sinon": "^6.1.5",
|
||||
"vue-template-compiler": "^2.5.17"
|
||||
"vue-template-compiler": "^2.5.17",
|
||||
"webpack-bundle-size-analyzer": "^3.0.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
{
|
||||
"additionalStylesheets": ["/custom.css"]
|
||||
"additionalStylesheets": [
|
||||
"/front/custom.css"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
#!/bin/bash -eux
|
||||
locales=$(tail -n +2 src/locales.js | sed -e 's/export default //' | jq '.locales[].code' | xargs echo)
|
||||
find locales -name '*.po' | xargs $(yarn bin)/gettext-compile --output src/translations.json
|
||||
locales=$(tail -n +2 src/locales.js | sed -e 's/export default //' | jq '.locales[].code' | grep -v 'en_US' | xargs echo)
|
||||
for locale in $locales; do
|
||||
find "locales/$locale" -name '*.po' | $(yarn bin)/gettext-compile locales/$locale/LC_MESSAGES/app.po --output src/translations/$locale.json
|
||||
done
|
||||
|
||||
# find locales -name '*.po' | xargs $(yarn bin)/gettext-compile --output src/translations.json
|
||||
|
|
|
@ -43,10 +43,6 @@
|
|||
:version="version"
|
||||
@show:shortcuts-modal="showShortcutsModal = !showShortcutsModal"
|
||||
></app-footer>
|
||||
<raven
|
||||
v-if="$store.state.instance.settings.raven.front_enabled.value"
|
||||
:dsn="$store.state.instance.settings.raven.front_dsn.value"
|
||||
></raven>
|
||||
<playlist-modal v-if="$store.state.auth.authenticated"></playlist-modal>
|
||||
<shortcuts-modal @update:show="showShortcutsModal = $event" :show="showShortcutsModal"></shortcuts-modal>
|
||||
<GlobalEvents @keydown.h.exact="showShortcutsModal = !showShortcutsModal"/>
|
||||
|
@ -56,18 +52,16 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import _ from '@/lodash'
|
||||
import {mapState} from 'vuex'
|
||||
import { WebSocketBridge } from 'django-channels'
|
||||
import GlobalEvents from '@/components/utils/global-events'
|
||||
|
||||
import translations from '@/translations'
|
||||
|
||||
import Sidebar from '@/components/Sidebar'
|
||||
import AppFooter from '@/components/Footer'
|
||||
import Raven from '@/components/Raven'
|
||||
import ServiceMessages from '@/components/ServiceMessages'
|
||||
|
||||
import locales from './locales'
|
||||
import PlaylistModal from '@/components/playlists/PlaylistModal'
|
||||
import ShortcutsModal from '@/components/ShortcutsModal'
|
||||
|
||||
|
@ -76,7 +70,6 @@ export default {
|
|||
components: {
|
||||
Sidebar,
|
||||
AppFooter,
|
||||
Raven,
|
||||
PlaylistModal,
|
||||
ShortcutsModal,
|
||||
GlobalEvents,
|
||||
|
@ -139,7 +132,7 @@ export default {
|
|||
},
|
||||
autodetectLanguage () {
|
||||
let userLanguage = navigator.language || navigator.userLanguage
|
||||
let available = _.keys(translations)
|
||||
let available = locales.locales.map(e => { return e.code })
|
||||
let matching = available.filter((a) => {
|
||||
return userLanguage.replace('-', '_') === a
|
||||
})
|
||||
|
@ -223,181 +216,5 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// we do the import here instead in main.js
|
||||
// as resolve order is not deterministric in webpack
|
||||
// and we end up with CSS rules not applied,
|
||||
// see https://github.com/webpack/webpack/issues/215
|
||||
@import "semantic/semantic.css";
|
||||
@import "style/vendor/media";
|
||||
|
||||
html,
|
||||
body {
|
||||
@include media("<desktop") {
|
||||
font-size: 90%;
|
||||
}
|
||||
}
|
||||
#app {
|
||||
font-family: "Avenir", Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.instance-chooser {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
.main.pusher,
|
||||
.footer {
|
||||
@include media(">desktop") {
|
||||
margin-left: 350px !important;
|
||||
margin-top: 50px;
|
||||
}
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.main.pusher > .ui.secondary.menu {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
border: none;
|
||||
box-shadow: inset 0px -2px 0px 0px rgba(34, 36, 38, 0.15);
|
||||
.ui.item {
|
||||
border: none;
|
||||
border-bottom-style: none;
|
||||
margin-bottom: 0px;
|
||||
&.active {
|
||||
box-shadow: inset 0px -2px 0px 0px #000;
|
||||
}
|
||||
}
|
||||
@include media(">tablet") {
|
||||
padding: 0 2.5rem;
|
||||
}
|
||||
@include media(">desktop") {
|
||||
position: fixed;
|
||||
left: 350px;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
z-index: 99;
|
||||
}
|
||||
background-color: white;
|
||||
.item {
|
||||
padding-top: 1.5em;
|
||||
padding-bottom: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.service-messages {
|
||||
position: fixed;
|
||||
bottom: 1em;
|
||||
left: 1em;
|
||||
@include media(">desktop") {
|
||||
left: 350px;
|
||||
}
|
||||
}
|
||||
.main-pusher {
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
.ui.stripe.segment,
|
||||
#footer {
|
||||
padding: 2em;
|
||||
@include media(">tablet") {
|
||||
padding: 4em;
|
||||
}
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ui.small.text.container {
|
||||
max-width: 500px !important;
|
||||
}
|
||||
|
||||
.button.icon.tiny {
|
||||
padding: 0.5em !important;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
.logo {
|
||||
path {
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.discrete {
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
.link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ui.really.basic.button {
|
||||
&:not(:focus) {
|
||||
box-shadow: none !important;
|
||||
background-color: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.floated.buttons .button ~ .dropdown {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.ui.icon.header .circular.icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.segment-content .button {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
.segment.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button.reset {
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: auto;
|
||||
overflow: visible;
|
||||
|
||||
background: transparent;
|
||||
|
||||
/* inherit font & color from ancestor */
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
|
||||
/* Normalize `line-height`. Cannot be changed from `normal` in Firefox 4+. */
|
||||
line-height: normal;
|
||||
|
||||
/* Corrects font smoothing for webkit */
|
||||
-webkit-font-smoothing: inherit;
|
||||
-moz-osx-font-smoothing: inherit;
|
||||
/* Corrects inability to style clickable `input` types in iOS */
|
||||
-webkit-appearance: none;
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
.ui.table > caption {
|
||||
font-weight: bold;
|
||||
padding: 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.left.floated {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.right.floated {
|
||||
float: right;
|
||||
}
|
||||
@import "style/_main";
|
||||
</style>
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 21 KiB |
|
@ -27,9 +27,9 @@
|
|||
<p>{{ instance.short_description.value }}</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="instance.long_description.value"
|
||||
v-if="markdown && instance.long_description.value"
|
||||
class="ui middle aligned stackable text container"
|
||||
v-html="$options.filters.markdown(instance.long_description.value)">
|
||||
v-html="markdown.makeHtml(instance.long_description.value)">
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
@ -43,8 +43,17 @@ export default {
|
|||
components: {
|
||||
Stats
|
||||
},
|
||||
created() {
|
||||
data () {
|
||||
return {
|
||||
markdown: null
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch("instance/fetchSettings")
|
||||
let self = this
|
||||
import('showdown').then(module => {
|
||||
self.markdown = new module.default.Converter()
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<div class="ui form">
|
||||
<div class="ui field">
|
||||
<label><translate>Change language</translate></label>
|
||||
<select class="ui dropdown" v-model="$language.current">
|
||||
<select class="ui dropdown" :value="$language.current" @change="updateLanguage($event.target.value)">
|
||||
<option v-for="(language, key) in $language.available" :key="key" :value="key">{{ language }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
@ -60,7 +60,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import { mapState } from "vuex"
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
props: ["version"],
|
||||
|
@ -74,6 +76,13 @@ export default {
|
|||
if (confirm) {
|
||||
this.$store.commit("instance/instanceUrl", null)
|
||||
}
|
||||
},
|
||||
updateLanguage(value) {
|
||||
let self = this
|
||||
import(`../translations/${value}.json`).then((response) =>{
|
||||
Vue.$translations[value] = response.default[value]
|
||||
self.$language.current = value
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from "lodash"
|
||||
import _ from "@/lodash"
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
<template>
|
||||
<div class="raven"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Raven from 'raven-js'
|
||||
import RavenVue from 'raven-js/plugins/vue'
|
||||
import Vue from 'vue'
|
||||
import logger from '@/logging'
|
||||
|
||||
export default {
|
||||
props: ['dsn'],
|
||||
created () {
|
||||
Raven.uninstall()
|
||||
this.setUp()
|
||||
},
|
||||
destroyed () {
|
||||
Raven.uninstall()
|
||||
},
|
||||
methods: {
|
||||
setUp () {
|
||||
Raven.uninstall()
|
||||
logger.default.info('Installing raven...')
|
||||
Raven.config(this.dsn).addPlugin(RavenVue, Vue).install()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
dsn: function () {
|
||||
this.setUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped >
|
||||
.raven {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
|
@ -16,8 +16,8 @@
|
|||
|
||||
<div class="menu-area">
|
||||
<div class="ui compact fluid two item inverted menu">
|
||||
<a class="active item" role="button" @click.prevent.stop="selectedTab = 'library'" data-tab="library"><translate>Browse</translate></a>
|
||||
<a class="item" role="button" @click.prevent.stop="selectedTab = 'queue'" data-tab="queue">
|
||||
<a :class="[{active: selectedTab === 'library'}, 'item']" role="button" @click.prevent.stop="selectedTab = 'library'" data-tab="library"><translate>Browse</translate></a>
|
||||
<a :class="[{active: selectedTab === 'queue'}, 'item']" role="button" @click.prevent.stop="selectedTab = 'queue'" data-tab="queue">
|
||||
<translate>Queue</translate>
|
||||
<template v-if="queue.tracks.length === 0">
|
||||
<translate>(empty)</translate>
|
||||
|
@ -29,7 +29,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="tabs">
|
||||
<section class="ui bottom attached active tab" data-tab="library" :aria-label="labels.mainMenu">
|
||||
<section :class="['ui', 'bottom', 'attached', {active: selectedTab === 'library'}, 'tab']" :aria-label="labels.mainMenu">
|
||||
<nav class="ui inverted vertical large fluid menu" role="navigation" :aria-label="labels.mainMenu">
|
||||
<div class="item">
|
||||
<header class="header"><translate>My account</translate></header>
|
||||
|
@ -39,7 +39,7 @@
|
|||
<translate :translate-params="{username: $store.state.auth.username}">
|
||||
Logged in as %{ username }
|
||||
</translate>
|
||||
<img class="ui right floated circular tiny avatar image" v-if="$store.state.auth.profile.avatar.square_crop" :src="$store.getters['instance/absoluteUrl']($store.state.auth.profile.avatar.square_crop)" />
|
||||
<img class="ui right floated circular tiny avatar image" v-if="$store.state.auth.profile.avatar.square_crop" v-lazy="$store.getters['instance/absoluteUrl']($store.state.auth.profile.avatar.square_crop)" />
|
||||
</router-link>
|
||||
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{path: '/settings'}"><i class="setting icon"></i><translate>Settings</translate></router-link>
|
||||
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'notifications'}">
|
||||
|
@ -113,7 +113,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section class="ui bottom attached tab" data-tab="queue">
|
||||
<section :class="['ui', 'bottom', 'attached', {active: selectedTab === 'queue'}, 'tab']">
|
||||
<table class="ui compact inverted very basic fixed single line unstackable table">
|
||||
<draggable v-model="tracks" element="tbody" @update="reorder">
|
||||
<tr
|
||||
|
@ -188,11 +188,6 @@ export default {
|
|||
fetchInterval: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
$(this.$el)
|
||||
.find(".menu .item")
|
||||
.tab()
|
||||
},
|
||||
destroy() {
|
||||
if (this.fetchInterval) {
|
||||
clearInterval(this.fetchInterval)
|
||||
|
@ -206,10 +201,8 @@ export default {
|
|||
labels() {
|
||||
let mainMenu = this.$gettext("Main menu")
|
||||
let selectTrack = this.$gettext("Play this track")
|
||||
let pendingRequests = this.$gettext("Pending import requests")
|
||||
let pendingFollows = this.$gettext("Pending follow requests")
|
||||
return {
|
||||
pendingRequests,
|
||||
pendingFollows,
|
||||
mainMenu,
|
||||
selectTrack
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import _ from '@/lodash'
|
||||
import axios from 'axios'
|
||||
import logger from '@/logging'
|
||||
import AlbumCard from '@/components/audio/album/Card'
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
import _ from 'lodash'
|
||||
import _ from '@/lodash'
|
||||
import url from '@/utils/url'
|
||||
import {Howl} from 'howler'
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<div class="ui loader"></div>
|
||||
</div>
|
||||
<div class="card" v-for="album in albums" :key="album.id">
|
||||
<div :class="['ui', 'image', 'with-overlay', {'default-cover': !album.cover.original}]" :style="getImageStyle(album)">
|
||||
<div :class="['ui', 'image', 'with-overlay', {'default-cover': !album.cover.original}]" v-lazy:background-image="getImageUrl(album)">
|
||||
<play-button class="play-overlay" :icon-only="true" :is-playable="album.is_playable" :button-classes="['ui', 'circular', 'large', 'orange', 'icon', 'button']" :album="album.id"></play-button>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
@ -36,7 +36,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import _ from '@/lodash'
|
||||
import axios from 'axios'
|
||||
import PlayButton from '@/components/audio/PlayButton'
|
||||
|
||||
|
@ -87,17 +87,15 @@ export default {
|
|||
this.offset = Math.max(this.offset - this.limit, 0)
|
||||
}
|
||||
},
|
||||
getImageStyle (album) {
|
||||
getImageUrl (album) {
|
||||
let url = '../../../assets/audio/default-cover.png'
|
||||
|
||||
if (album.cover.original) {
|
||||
url = this.$store.getters['instance/absoluteUrl'](album.cover.medium_square_crop)
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
return {
|
||||
'background-image': `url("${url}")`
|
||||
return null
|
||||
}
|
||||
return url
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -108,10 +106,10 @@ export default {
|
|||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import '../../../style/vendor/media';
|
||||
@import "../../../style/vendor/media";
|
||||
|
||||
.default-cover {
|
||||
background-image: url('../../../assets/audio/default-cover.png') !important;
|
||||
background-image: url("../../../assets/audio/default-cover.png") !important;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<tbody>
|
||||
<tr v-for="album in albums">
|
||||
<td>
|
||||
<img class="ui mini image" v-if="album.cover.original" :src="$store.getters['instance/absoluteUrl'](album.cover.small_square_crop)">
|
||||
<img class="ui mini image" v-if="album.cover.original" v-lazy="$store.getters['instance/absoluteUrl'](album.cover.small_square_crop)">
|
||||
<img class="ui mini image" v-else src="../../../assets/audio/default-cover.png">
|
||||
</td>
|
||||
<td colspan="4">
|
||||
|
@ -82,5 +82,4 @@ export default {
|
|||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import _ from '@/lodash'
|
||||
import axios from 'axios'
|
||||
import PlayButton from '@/components/audio/PlayButton'
|
||||
|
||||
|
@ -109,7 +109,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../../../style/vendor/media';
|
||||
@import "../../../style/vendor/media";
|
||||
|
||||
.play-overlay {
|
||||
position: absolute;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<div :class="['ui', 'head', 'vertical', 'center', 'aligned', 'stripe', 'segment']">
|
||||
<h2 class="ui center aligned icon header">
|
||||
<i v-if="!profile.avatar.square_crop" class="circular inverted user green icon"></i>
|
||||
<img class="ui big circular image" v-else :src="$store.getters['instance/absoluteUrl'](profile.avatar.square_crop)" />
|
||||
<img class="ui big circular image" v-else v-lazy="$store.getters['instance/absoluteUrl'](profile.avatar.square_crop)" />
|
||||
<div class="content">
|
||||
{{ profile.username }}
|
||||
<div class="sub header" v-translate="{date: signupDate}">Registered since %{ date }</div>
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
</div>
|
||||
<div class="ui six wide column">
|
||||
<h3 class="ui header"><translate>Current avatar</translate></h3>
|
||||
<img class="ui circular image" v-if="currentAvatar && currentAvatar.square_crop" :src="$store.getters['instance/absoluteUrl'](currentAvatar.medium_square_crop)" />
|
||||
<img class="ui circular image" v-if="currentAvatar && currentAvatar.square_crop" v-lazy="$store.getters['instance/absoluteUrl'](currentAvatar.medium_square_crop)" />
|
||||
<div class="ui hidden divider"></div>
|
||||
<button @click="removeAvatar" v-if="currentAvatar && currentAvatar.square_crop" :class="['ui', {'loading': isLoadingAvatar}, ,'yellow', 'button']">
|
||||
<translate>Remove avatar</translate>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<img
|
||||
class="ui tiny circular avatar"
|
||||
v-if="user.avatar && user.avatar.small_square_crop"
|
||||
:src="$store.getters['instance/absoluteUrl'](user.avatar.small_square_crop)" />
|
||||
v-lazy="$store.getters['instance/absoluteUrl'](user.avatar.small_square_crop)" />
|
||||
<span v-else :style="defaultAvatarStyle" class="ui circular label">{{ user.username[0]}}</span>
|
||||
@{{ user.username }}
|
||||
</span>
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
<template>
|
||||
<div class="comment">
|
||||
<div class="content">
|
||||
<a class="author">{{ user.username }}</a>
|
||||
<div class="metadata">
|
||||
<div class="date"><human-date :date="date"></human-date></div>
|
||||
</div>
|
||||
<div class="text" v-html="comment"></div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<span
|
||||
@click="collapsed = false"
|
||||
v-if="truncated && collapsed"
|
||||
class="expand">
|
||||
<translate>Expand</translate>
|
||||
</span>
|
||||
<span
|
||||
@click="collapsed = true"
|
||||
v-if="truncated && !collapsed"
|
||||
class="collapse">
|
||||
<translate>Collapse</translate>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
user: {type: Object, required: true},
|
||||
date: {required: true},
|
||||
content: {type: String, required: true}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
collapsed: true,
|
||||
length: 50
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
comment () {
|
||||
let text = this.content
|
||||
if (this.collapsed) {
|
||||
text = this.$options.filters.truncate(text, this.length)
|
||||
}
|
||||
return this.$options.filters.markdown(text)
|
||||
},
|
||||
truncated () {
|
||||
return this.content.length > this.length
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -26,7 +26,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import _ from '@/lodash'
|
||||
import axios from 'axios'
|
||||
import LibraryCard from '@/views/content/remote/Card'
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import _ from '@/lodash'
|
||||
import axios from 'axios'
|
||||
import logger from '@/logging'
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from "lodash"
|
||||
import _ from "@/lodash"
|
||||
import axios from "axios"
|
||||
import logger from "@/logging"
|
||||
import backend from "@/audio/backend"
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
|
||||
<script>
|
||||
import axios from "axios"
|
||||
import _ from "lodash"
|
||||
import _ from "@/lodash"
|
||||
import $ from "jquery"
|
||||
|
||||
import logger from "@/logging"
|
||||
|
|
|
@ -123,6 +123,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from "@/lodash"
|
||||
import $ from "jquery";
|
||||
import axios from "axios";
|
||||
import logger from "@/logging";
|
||||
|
|
|
@ -92,7 +92,7 @@
|
|||
|
||||
<script>
|
||||
import axios from "axios"
|
||||
import _ from "lodash"
|
||||
import _ from "@/lodash"
|
||||
import $ from "jquery"
|
||||
|
||||
import logger from "@/logging"
|
||||
|
|
|
@ -94,7 +94,7 @@
|
|||
<script>
|
||||
import axios from "axios"
|
||||
import $ from "jquery"
|
||||
import _ from "lodash"
|
||||
import _ from "@/lodash"
|
||||
import BuilderFilter from "./Filter"
|
||||
import TrackTable from "@/components/audio/track/Table"
|
||||
import RadioButton from "@/components/radios/Button"
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
<script>
|
||||
import axios from 'axios'
|
||||
import $ from 'jquery'
|
||||
import _ from 'lodash'
|
||||
import _ from '@/lodash'
|
||||
|
||||
import Modal from '@/components/semantic/Modal'
|
||||
import TrackTable from '@/components/audio/track/Table'
|
||||
|
|
|
@ -106,7 +106,7 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import _ from '@/lodash'
|
||||
import time from '@/utils/time'
|
||||
import Pagination from '@/components/Pagination'
|
||||
import ActionTable from '@/components/common/ActionTable'
|
||||
|
|
|
@ -86,7 +86,7 @@
|
|||
<script>
|
||||
import axios from 'axios'
|
||||
import moment from 'moment'
|
||||
import _ from 'lodash'
|
||||
import _ from '@/lodash'
|
||||
import Pagination from '@/components/Pagination'
|
||||
import ActionTable from '@/components/common/ActionTable'
|
||||
import OrderingMixin from '@/components/mixins/Ordering'
|
||||
|
|
|
@ -96,7 +96,7 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import _ from '@/lodash'
|
||||
import time from '@/utils/time'
|
||||
import Pagination from '@/components/Pagination'
|
||||
import ActionTable from '@/components/common/ActionTable'
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import _ from '@/lodash'
|
||||
import axios from 'axios'
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import _ from '@/lodash'
|
||||
import axios from 'axios'
|
||||
import PlaylistCard from '@/components/playlists/Card'
|
||||
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
<template>
|
||||
<div :class="['ui', {collapsed: collapsed}, 'card']">
|
||||
<div class="content">
|
||||
<div class="header">{{ request.artist_name }}</div>
|
||||
<div class="description">
|
||||
<div
|
||||
v-if="request.albums" v-html="$options.filters.markdown(request.albums)"></div>
|
||||
<div class="ui comments">
|
||||
<comment
|
||||
:user="request.user"
|
||||
:content="request.comment || ''"
|
||||
:date="request.creation_date"></comment>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<span >
|
||||
<i v-if="request.status === 'pending'" class="hourglass start icon"></i>
|
||||
<i v-if="request.status === 'accepted'" class="hourglass half icon"></i>
|
||||
<i v-if="request.status === 'imported'" class="check icon"></i>
|
||||
{{ request.status | capitalize }}
|
||||
</span>
|
||||
<button
|
||||
@click="createImport"
|
||||
v-if="request.status === 'pending' && importAction && $store.state.auth.availablePermissions['library']"
|
||||
class="ui mini basic green right floated button"><translate>Create import</translate></button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Comment from '@/components/discussion/Comment'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
request: {type: Object, required: true},
|
||||
importAction: {type: Boolean, default: true}
|
||||
},
|
||||
components: {
|
||||
Comment
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
collapsed: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createImport () {
|
||||
this.$router.push({
|
||||
name: 'library.import.launch',
|
||||
query: {request: this.request.id}})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,127 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<form v-if="!over" class="ui form" @submit.prevent="submit">
|
||||
<p><translate>Something's missing in the library? Let us know what you would like to listen!</translate></p>
|
||||
<div class="required field">
|
||||
<label><translate>Artist name</translate></label>
|
||||
<input v-model="currentArtistName" :placeholder="labels.artistNamePlaceholder" required maxlength="200">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label><translate>Albums</translate></label>
|
||||
<p><translate>Leave this field empty if you're requesting the whole discography.</translate></p>
|
||||
<input v-model="currentAlbums" :placeholder="labels.albumTitlePlaceholder" maxlength="2000">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label><translate>Comment</translate></label>
|
||||
<textarea v-model="currentComment" rows="3" :placeholder="labels.commentPlaceholder" maxlength="2000"></textarea>
|
||||
</div>
|
||||
<button class="ui submit button" type="submit"><translate>Submit</translate></button>
|
||||
</form>
|
||||
<div v-else class="ui success message">
|
||||
<div class="header"><translate>Request submitted!</translate></div>
|
||||
<p><translate>We've received your request, you'll get some groove soon ;)</translate></p>
|
||||
<button @click="reset" class="ui button"><translate>Submit another request</translate></button>
|
||||
</div>
|
||||
<div v-if="requests.length > 0">
|
||||
<div class="ui divider"></div>
|
||||
<h3 class="ui header"><translate>Pending requests</translate></h3>
|
||||
<div class="ui list">
|
||||
<div v-for="request in requests" class="item">
|
||||
<div class="content">
|
||||
<div class="header">{{ request.artist_name }}</div>
|
||||
<div v-if="request.albums" class="description">
|
||||
{{ request.albums|truncate }}</div>
|
||||
<div v-if="request.comment" class="description">
|
||||
{{ request.comment|truncate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import $ from 'jquery'
|
||||
import axios from 'axios'
|
||||
|
||||
import logger from '@/logging'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
defaultArtistName: {type: String, default: ''},
|
||||
defaultAlbums: {type: String, default: ''},
|
||||
defaultComment: {type: String, default: ''}
|
||||
},
|
||||
created () {
|
||||
this.fetchRequests()
|
||||
},
|
||||
mounted () {
|
||||
$('.ui.radio.checkbox').checkbox()
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
currentArtistName: this.defaultArtistName,
|
||||
currentAlbums: this.defaultAlbums,
|
||||
currentComment: this.defaultComment,
|
||||
isLoading: false,
|
||||
over: false,
|
||||
requests: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
let artistNamePlaceholder = this.$gettext('The Beatles, Mickael Jackson…')
|
||||
let albumTitlePlaceholder = this.$gettext('The White Album, Thriller…')
|
||||
let commentPlaceholder = this.$gettext('Use this comment box to add details to your request if needed')
|
||||
return {
|
||||
artistNamePlaceholder,
|
||||
albumTitlePlaceholder,
|
||||
commentPlaceholder
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchRequests () {
|
||||
let self = this
|
||||
let url = 'requests/import-requests/'
|
||||
axios.get(url, {}).then((response) => {
|
||||
self.requests = response.data.results
|
||||
})
|
||||
},
|
||||
submit () {
|
||||
let self = this
|
||||
this.isLoading = true
|
||||
let url = 'requests/import-requests/'
|
||||
let payload = {
|
||||
artist_name: this.currentArtistName,
|
||||
albums: this.currentAlbums,
|
||||
comment: this.currentComment
|
||||
}
|
||||
axios.post(url, payload).then((response) => {
|
||||
logger.default.info('Submitted request!')
|
||||
self.isLoading = false
|
||||
self.over = true
|
||||
self.requests.unshift(response.data)
|
||||
}, (response) => {
|
||||
logger.default.error('error while submitting request')
|
||||
self.isLoading = false
|
||||
})
|
||||
},
|
||||
reset () {
|
||||
this.over = false
|
||||
this.currentArtistName = ''
|
||||
this.currentAlbums = ''
|
||||
this.currentComment = ''
|
||||
},
|
||||
truncate (string, length) {
|
||||
if (string.length > length) {
|
||||
return string.substring(0, length) + '…'
|
||||
}
|
||||
return string
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
|
@ -1,7 +1,6 @@
|
|||
|
||||
import Vue from 'vue'
|
||||
import Embed from './Embed'
|
||||
import axios from 'axios'
|
||||
import VuePlyr from 'vue-plyr'
|
||||
|
||||
Vue.use(VuePlyr)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import Vue from 'vue'
|
||||
|
||||
import moment from 'moment'
|
||||
import showdown from 'showdown'
|
||||
|
||||
export function truncate (str, max, ellipsis) {
|
||||
max = max || 100
|
||||
|
@ -14,13 +13,6 @@ export function truncate (str, max, ellipsis) {
|
|||
|
||||
Vue.filter('truncate', truncate)
|
||||
|
||||
export function markdown (str) {
|
||||
const converter = new showdown.Converter()
|
||||
return converter.makeHtml(str)
|
||||
}
|
||||
|
||||
Vue.filter('markdown', markdown)
|
||||
|
||||
export function ago (date) {
|
||||
const m = moment(date)
|
||||
return m.fromNow()
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
// cherry-pick specific lodash methods here to reduce bundle size
|
||||
|
||||
export default {
|
||||
clone: require('lodash/clone'),
|
||||
debounce: require('lodash/debounce'),
|
||||
get: require('lodash/get'),
|
||||
merge: require('lodash/merge'),
|
||||
range: require('lodash/range'),
|
||||
shuffle: require('lodash/shuffle'),
|
||||
sortBy: require('lodash/sortBy'),
|
||||
throttle: require('lodash/throttle'),
|
||||
uniq: require('lodash/uniq'),
|
||||
}
|
|
@ -14,7 +14,6 @@ import VueLazyload from 'vue-lazyload'
|
|||
import store from './store'
|
||||
import GetTextPlugin from 'vue-gettext'
|
||||
import { sync } from 'vuex-router-sync'
|
||||
import translations from './translations.json'
|
||||
import locales from '@/locales'
|
||||
|
||||
import filters from '@/filters' // eslint-disable-line
|
||||
|
@ -23,12 +22,9 @@ import globals from '@/components/globals' // eslint-disable-line
|
|||
sync(store, router)
|
||||
|
||||
window.$ = window.jQuery = require('jquery')
|
||||
|
||||
// this is absolutely dirty but at the moment, semantic UI does not
|
||||
// play really nice with webpack and I want to get rid of Google Fonts
|
||||
// require('./semantic/semantic.css')
|
||||
require('semantic-ui-css/semantic.js')
|
||||
require('./semantic.js')
|
||||
require('masonry-layout')
|
||||
|
||||
let availableLanguages = (function () {
|
||||
let l = {}
|
||||
locales.locales.forEach(c => {
|
||||
|
@ -54,7 +50,7 @@ Vue.use(GetTextPlugin, {
|
|||
}
|
||||
}
|
||||
},
|
||||
translations: translations,
|
||||
translations: {},
|
||||
silent: true
|
||||
})
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
// require('semantic-ui-css/components/accordion.min.js')
|
||||
require('semantic-ui-css/components/api.min.js')
|
||||
require('semantic-ui-css/components/checkbox.min.js')
|
||||
// require('semantic-ui-css/components/colorize.min.js')
|
||||
require('semantic-ui-css/components/dimmer.min.js')
|
||||
require('semantic-ui-css/components/dropdown.min.js')
|
||||
// require('semantic-ui-css/components/embed.min.js')
|
||||
// require('semantic-ui-css/components/form.min.js')
|
||||
require('semantic-ui-css/components/modal.min.js')
|
||||
// require('semantic-ui-css/components/nag.min.js')
|
||||
// require('semantic-ui-css/components/popup.min.js')
|
||||
require('semantic-ui-css/components/progress.min.js')
|
||||
// require('semantic-ui-css/components/rating.min.js')
|
||||
require('semantic-ui-css/components/search.min.js')
|
||||
// require('semantic-ui-css/components/shape.min.js')
|
||||
// require('semantic-ui-css/components/sidebar.min.js')
|
||||
require('semantic-ui-css/components/site.min.js')
|
||||
require('semantic-ui-css/components/state.min.js')
|
||||
require('semantic-ui-css/components/sticky.min.js')
|
||||
// require('semantic-ui-css/components/tab.min.js')
|
||||
require('semantic-ui-css/components/transition.min.js')
|
||||
// require('semantic-ui-css/components/video.min.js')
|
||||
require('semantic-ui-css/components/visibility.min.js')
|
||||
// require('semantic-ui-css/components/visit.min.js')
|
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 957 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 28 KiB |
|
@ -1,6 +1,6 @@
|
|||
import axios from 'axios'
|
||||
import logger from '@/logging'
|
||||
import _ from 'lodash'
|
||||
import _ from '@/lodash'
|
||||
|
||||
function getDefaultUrl () {
|
||||
return (
|
||||
|
@ -40,14 +40,6 @@ export default {
|
|||
enabled: {
|
||||
value: true
|
||||
}
|
||||
},
|
||||
raven: {
|
||||
front_enabled: {
|
||||
value: false
|
||||
},
|
||||
front_dsn: {
|
||||
value: null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -131,7 +123,7 @@ export default {
|
|||
})
|
||||
},
|
||||
fetchFrontSettings ({commit}) {
|
||||
return axios.get('/settings.json').then(response => {
|
||||
return axios.get('/front/settings.json').then(response => {
|
||||
commit('frontSettings', response.data)
|
||||
}, response => {
|
||||
logger.default.error('Error when fetching front-end configuration (or no customization available)')
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import logger from '@/logging'
|
||||
import _ from 'lodash'
|
||||
import _ from '@/lodash'
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
|
@ -86,7 +86,7 @@ export default {
|
|||
if (callback && i + 1 === total) {
|
||||
p.then(callback)
|
||||
}
|
||||
if (shouldPlay && p) {
|
||||
if (shouldPlay && p && i + 1 === total) {
|
||||
p.then(() => {
|
||||
dispatch('next')
|
||||
})
|
||||
|
|
|
@ -0,0 +1,250 @@
|
|||
/*
|
||||
|
||||
███████╗███████╗███╗ ███╗ █████╗ ███╗ ██╗████████╗██╗ ██████╗ ██╗ ██╗██╗
|
||||
██╔════╝██╔════╝████╗ ████║██╔══██╗████╗ ██║╚══██╔══╝██║██╔════╝ ██║ ██║██║
|
||||
███████╗█████╗ ██╔████╔██║███████║██╔██╗ ██║ ██║ ██║██║ ██║ ██║██║
|
||||
╚════██║██╔══╝ ██║╚██╔╝██║██╔══██║██║╚██╗██║ ██║ ██║██║ ██║ ██║██║
|
||||
███████║███████╗██║ ╚═╝ ██║██║ ██║██║ ╚████║ ██║ ██║╚██████╗ ╚██████╔╝██║
|
||||
╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝
|
||||
|
||||
Import this file into your LESS project to use Semantic UI without build tools
|
||||
*/
|
||||
|
||||
/* Global */
|
||||
@import "~semantic-ui-css/components/reset.css";
|
||||
// we use our custom site css here to avoid loading google font
|
||||
@import "./site";
|
||||
|
||||
/* Elements */
|
||||
@import "~semantic-ui-css/components/button.css";
|
||||
@import "~semantic-ui-css/components/container.css";
|
||||
@import "~semantic-ui-css/components/divider.css";
|
||||
// @import "~semantic-ui-css/components/flag.css";
|
||||
@import "~semantic-ui-css/components/header.css";
|
||||
@import "~semantic-ui-css/components/icon.css";
|
||||
@import "~semantic-ui-css/components/image.css";
|
||||
@import "~semantic-ui-css/components/input.css";
|
||||
@import "~semantic-ui-css/components/label.css";
|
||||
@import "~semantic-ui-css/components/list.css";
|
||||
@import "~semantic-ui-css/components/loader.css";
|
||||
// @import "~semantic-ui-css/components/placeholder.css";
|
||||
// @import "~semantic-ui-css/components/rail.css";
|
||||
// @import "~semantic-ui-css/components/reveal.css";
|
||||
@import "~semantic-ui-css/components/segment.css";
|
||||
@import "~semantic-ui-css/components/step.css";
|
||||
|
||||
/* Collections */
|
||||
// @import "~semantic-ui-css/components/breadcrumb.css";
|
||||
@import "~semantic-ui-css/components/form.css";
|
||||
@import "~semantic-ui-css/components/grid.css";
|
||||
@import "~semantic-ui-css/components/menu.css";
|
||||
@import "~semantic-ui-css/components/message.css";
|
||||
@import "~semantic-ui-css/components/table.css";
|
||||
|
||||
/* Views */
|
||||
// @import "~semantic-ui-css/components/ad.css";
|
||||
@import "~semantic-ui-css/components/card.css";
|
||||
// @import "~semantic-ui-css/components/comment.css";
|
||||
// @import "~semantic-ui-css/components/feed.css";
|
||||
@import "~semantic-ui-css/components/item.css";
|
||||
@import "~semantic-ui-css/components/statistic.css";
|
||||
|
||||
/* Modules */
|
||||
// @import "~semantic-ui-css/components/accordion.css";
|
||||
@import "~semantic-ui-css/components/checkbox.css";
|
||||
@import "~semantic-ui-css/components/dimmer.css";
|
||||
@import "~semantic-ui-css/components/dropdown.css";
|
||||
// @import "~semantic-ui-css/components/embed.css";
|
||||
@import "~semantic-ui-css/components/modal.css";
|
||||
// @import "~semantic-ui-css/components/nag.css";
|
||||
@import "~semantic-ui-css/components/popup.css";
|
||||
@import "~semantic-ui-css/components/progress.css";
|
||||
// @import "~semantic-ui-css/components/rating.css";
|
||||
@import "~semantic-ui-css/components/search.css";
|
||||
// @import "~semantic-ui-css/components/shape.css";
|
||||
@import "~semantic-ui-css/components/sidebar.css";
|
||||
@import "~semantic-ui-css/components/sticky.css";
|
||||
@import "~semantic-ui-css/components/tab.css";
|
||||
@import "~semantic-ui-css/components/transition.css";
|
||||
|
||||
|
||||
|
||||
// we do the import here instead in main.js
|
||||
// as resolve order is not deterministric in webpack
|
||||
// and we end up with CSS rules not applied,
|
||||
// see https://github.com/webpack/webpack/issues/215
|
||||
@import "./vendor/media";
|
||||
|
||||
html,
|
||||
body {
|
||||
@include media("<desktop") {
|
||||
font-size: 90%;
|
||||
}
|
||||
}
|
||||
#app {
|
||||
font-family: "Avenir", Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.instance-chooser {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
.main.pusher,
|
||||
.footer {
|
||||
@include media(">desktop") {
|
||||
margin-left: 350px !important;
|
||||
margin-top: 50px;
|
||||
}
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.main.pusher > .ui.secondary.menu {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
border: none;
|
||||
box-shadow: inset 0px -2px 0px 0px rgba(34, 36, 38, 0.15);
|
||||
.ui.item {
|
||||
border: none;
|
||||
border-bottom-style: none;
|
||||
margin-bottom: 0px;
|
||||
&.active {
|
||||
box-shadow: inset 0px -2px 0px 0px #000;
|
||||
}
|
||||
}
|
||||
@include media(">tablet") {
|
||||
padding: 0 2.5rem;
|
||||
}
|
||||
@include media(">desktop") {
|
||||
position: fixed;
|
||||
left: 350px;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
z-index: 99;
|
||||
}
|
||||
background-color: white;
|
||||
.item {
|
||||
padding-top: 1.5em;
|
||||
padding-bottom: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.service-messages {
|
||||
position: fixed;
|
||||
bottom: 1em;
|
||||
left: 1em;
|
||||
@include media(">desktop") {
|
||||
left: 350px;
|
||||
}
|
||||
}
|
||||
.main-pusher {
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
.ui.stripe.segment,
|
||||
#footer {
|
||||
padding: 2em;
|
||||
@include media(">tablet") {
|
||||
padding: 4em;
|
||||
}
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ui.small.text.container {
|
||||
max-width: 500px !important;
|
||||
}
|
||||
|
||||
.button.icon.tiny {
|
||||
padding: 0.5em !important;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
.logo {
|
||||
&.bordered.icon {
|
||||
padding: .5em .41em !important;
|
||||
}
|
||||
path {
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.discrete {
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
.link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ui.really.basic.button {
|
||||
&:not(:focus) {
|
||||
box-shadow: none !important;
|
||||
background-color: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.floated.buttons .button ~ .dropdown {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.ui.icon.header .circular.icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.segment-content .button {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
.segment.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button.reset {
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: auto;
|
||||
overflow: visible;
|
||||
|
||||
background: transparent;
|
||||
|
||||
/* inherit font & color from ancestor */
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
|
||||
/* Normalize `line-height`. Cannot be changed from `normal` in Firefox 4+. */
|
||||
line-height: normal;
|
||||
|
||||
/* Corrects font smoothing for webkit */
|
||||
-webkit-font-smoothing: inherit;
|
||||
-moz-osx-font-smoothing: inherit;
|
||||
/* Corrects inability to style clickable `input` types in iOS */
|
||||
-webkit-appearance: none;
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
.ui.table > caption {
|
||||
font-weight: bold;
|
||||
padding: 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.left.floated {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.right.floated {
|
||||
float: right;
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
/*!
|
||||
* # Semantic UI 2.4.1 - Site
|
||||
* http://github.com/semantic-org/semantic-ui/
|
||||
*
|
||||
*
|
||||
* Released under the MIT license
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
/*******************************
|
||||
Page
|
||||
*******************************/
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
body {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
overflow-x: hidden;
|
||||
min-width: 320px;
|
||||
background: #FFFFFF;
|
||||
font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.4285em;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
|
||||
/*******************************
|
||||
Headers
|
||||
*******************************/
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||
line-height: 1.28571429em;
|
||||
margin: calc(2rem - 0.14285714em ) 0em 1rem;
|
||||
font-weight: bold;
|
||||
padding: 0em;
|
||||
}
|
||||
h1 {
|
||||
min-height: 1rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.71428571rem;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.28571429rem;
|
||||
}
|
||||
h4 {
|
||||
font-size: 1.07142857rem;
|
||||
}
|
||||
h5 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
h1:first-child,
|
||||
h2:first-child,
|
||||
h3:first-child,
|
||||
h4:first-child,
|
||||
h5:first-child {
|
||||
margin-top: 0em;
|
||||
}
|
||||
h1:last-child,
|
||||
h2:last-child,
|
||||
h3:last-child,
|
||||
h4:last-child,
|
||||
h5:last-child {
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
|
||||
|
||||
/*******************************
|
||||
Text
|
||||
*******************************/
|
||||
|
||||
p {
|
||||
margin: 0em 0em 1em;
|
||||
line-height: 1.4285em;
|
||||
}
|
||||
p:first-child {
|
||||
margin-top: 0em;
|
||||
}
|
||||
p:last-child {
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
|
||||
/*-------------------
|
||||
Links
|
||||
--------------------*/
|
||||
|
||||
a {
|
||||
color: #4183C4;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
color: #1e70bf;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
/*******************************
|
||||
Scrollbars
|
||||
*******************************/
|
||||
|
||||
|
||||
|
||||
/*******************************
|
||||
Highlighting
|
||||
*******************************/
|
||||
|
||||
|
||||
/* Site */
|
||||
::-webkit-selection {
|
||||
background-color: #CCE2FF;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
::-moz-selection {
|
||||
background-color: #CCE2FF;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
::selection {
|
||||
background-color: #CCE2FF;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
|
||||
/* Form */
|
||||
textarea::-webkit-selection,
|
||||
input::-webkit-selection {
|
||||
background-color: rgba(100, 100, 100, 0.4);
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
textarea::-moz-selection,
|
||||
input::-moz-selection {
|
||||
background-color: rgba(100, 100, 100, 0.4);
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
textarea::selection,
|
||||
input::selection {
|
||||
background-color: rgba(100, 100, 100, 0.4);
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
|
||||
/* Force Simple Scrollbars */
|
||||
body ::-webkit-scrollbar {
|
||||
-webkit-appearance: none;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
body ::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0px;
|
||||
}
|
||||
body ::-webkit-scrollbar-thumb {
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
-webkit-transition: color 0.2s ease;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
body ::-webkit-scrollbar-thumb:window-inactive {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
body ::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(128, 135, 139, 0.8);
|
||||
}
|
||||
|
||||
/* Inverted UI */
|
||||
body .ui.inverted::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
body .ui.inverted::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
body .ui.inverted::-webkit-scrollbar-thumb:window-inactive {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
body .ui.inverted::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
|
||||
/*******************************
|
||||
Global Overrides
|
||||
*******************************/
|
||||
|
||||
|
||||
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
|
@ -16,7 +16,7 @@ export default {
|
|||
durationFormatted (v) {
|
||||
let duration = parseInt(v)
|
||||
if (duration % 1 !== 0) {
|
||||
return time.parse(0)
|
||||
return this.parse(0)
|
||||
}
|
||||
duration = Math.round(duration)
|
||||
return this.parse(duration)
|
||||
|
|
|
@ -142,11 +142,6 @@ export default {
|
|||
"instance__nodeinfo_stats_enabled",
|
||||
"instance__nodeinfo_private"
|
||||
]
|
||||
},
|
||||
{
|
||||
label: errorLabel,
|
||||
id: "reporting",
|
||||
settings: ["raven__front_enabled", "raven__front_dsn"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -120,7 +120,7 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import _ from '@/lodash'
|
||||
import time from '@/utils/time'
|
||||
import {normalizeQuery, parseTokens, compileTokens} from '@/search'
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
|
||||
<script>
|
||||
import axios from "axios"
|
||||
import _ from "lodash"
|
||||
import _ from "@/lodash"
|
||||
import $ from "jquery"
|
||||
|
||||
import OrderingMixin from "@/components/mixins/Ordering"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {expect} from 'chai'
|
||||
|
||||
import {truncate, markdown, ago, capitalize, year} from '@/filters'
|
||||
import {truncate, ago, capitalize, year} from '@/filters'
|
||||
|
||||
describe('filters', () => {
|
||||
describe('truncate', () => {
|
||||
|
@ -20,13 +20,6 @@ describe('filters', () => {
|
|||
expect(output).to.equal('Hello pouet')
|
||||
})
|
||||
})
|
||||
describe('markdown', () => {
|
||||
it('renders markdown', () => {
|
||||
const input = 'Hello world'
|
||||
let output = markdown(input)
|
||||
expect(output).to.equal('<p>Hello world</p>')
|
||||
})
|
||||
})
|
||||
describe('ago', () => {
|
||||
it('works', () => {
|
||||
const input = new Date()
|
||||
|
|
|
@ -18,11 +18,11 @@ describe('store/instance', () => {
|
|||
|
||||
describe('mutations', () => {
|
||||
it('settings', () => {
|
||||
const state = {settings: {raven: {front_dsn: {value: 'test'}}}}
|
||||
let settings = {raven: {front_enabled: {value: true}}}
|
||||
const state = {settings: {users: {upload_quota: {value: 1}}}}
|
||||
let settings = {users: {registration_enabled: {value: true}}}
|
||||
store.mutations.settings(state, settings)
|
||||
expect(state.settings).to.deep.equal({
|
||||
raven: {front_dsn: {value: 'test'}, front_enabled: {value: true}}
|
||||
users: {upload_quota: {value: 1}, registration_enabled: {value: true}}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -32,13 +32,13 @@ describe('store/instance', () => {
|
|||
status: 200,
|
||||
response: [
|
||||
{
|
||||
section: 'raven',
|
||||
name: 'front_dsn',
|
||||
value: 'test'
|
||||
section: 'users',
|
||||
name: 'upload_quota',
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
section: 'raven',
|
||||
name: 'front_enabled',
|
||||
section: 'users',
|
||||
name: 'registration_enabled',
|
||||
value: false
|
||||
}
|
||||
]
|
||||
|
@ -50,15 +50,15 @@ describe('store/instance', () => {
|
|||
{
|
||||
type: 'settings',
|
||||
payload: {
|
||||
raven: {
|
||||
front_dsn: {
|
||||
section: 'raven',
|
||||
name: 'front_dsn',
|
||||
value: 'test'
|
||||
users: {
|
||||
upload_quota: {
|
||||
section: 'users',
|
||||
name: 'upload_quota',
|
||||
value: 1
|
||||
},
|
||||
front_enabled: {
|
||||
section: 'raven',
|
||||
name: 'front_enabled',
|
||||
registration_enabled: {
|
||||
section: 'users',
|
||||
name: 'registration_enabled',
|
||||
value: false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
var sinon = require('sinon')
|
||||
import {expect} from 'chai'
|
||||
|
||||
import _ from 'lodash'
|
||||
import _ from '@/lodash'
|
||||
|
||||
import store from '@/store/queue'
|
||||
import { testAction } from '../../utils'
|
||||
|
|
|
@ -1,4 +1,14 @@
|
|||
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
const webpack = require('webpack');
|
||||
|
||||
let plugins = [
|
||||
// do not include moment.js locales since it's quite heavy
|
||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
||||
]
|
||||
if (process.env.BUNDLE_ANALYZE === '1') {
|
||||
plugins.push(new BundleAnalyzerPlugin())
|
||||
}
|
||||
module.exports = {
|
||||
baseUrl: '/front/',
|
||||
pages: {
|
||||
|
@ -17,6 +27,7 @@ module.exports = {
|
|||
config.optimization.delete('splitChunks')
|
||||
},
|
||||
configureWebpack: {
|
||||
plugins: plugins,
|
||||
resolve: {
|
||||
alias: {
|
||||
'vue$': 'vue/dist/vue.esm.js'
|
||||
|
|
5060
front/yarn.lock
5060
front/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue