Cleanup a lot of stuff
I've replaced `lodash` with `lodash-es`, so it can be tree-shaken `~/modules` is a directory with application modules that run before app is mounted. Useful for configuration, web socket connection, and other stuff `~/composables` is a directory with our custom composables. Much like `~/utils` but each util is in its own file
This commit is contained in:
parent
a129f7f882
commit
2b40707f4f
|
@ -28,7 +28,7 @@ Submitting a new language
|
||||||
|
|
||||||
1. Pull the latest version of ``develop``
|
1. Pull the latest version of ``develop``
|
||||||
2. Create a new branch, e.g ``git checkout -b translations-new-fr-ca``
|
2. Create a new branch, e.g ``git checkout -b translations-new-fr-ca``
|
||||||
3. Add your new language code and name in ``front/src/locales.js``. Use the native language name, as it is what appears in the UI selector.
|
3. Add your new language code and name in ``front/src/locales.ts``. Use the native language name, as it is what appears in the UI selector.
|
||||||
4. Create the ``po`` file from template:
|
4. Create the ``po`` file from template:
|
||||||
|
|
||||||
.. code-block:: shell
|
.. code-block:: shell
|
||||||
|
|
|
@ -24,6 +24,8 @@ module.exports = {
|
||||||
'vue/no-v-html': 'off', // TODO: tackle this properly
|
'vue/no-v-html': 'off', // TODO: tackle this properly
|
||||||
'vue/no-use-v-if-with-v-for': 'off',
|
'vue/no-use-v-if-with-v-for': 'off',
|
||||||
|
|
||||||
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
|
'no-undef': 'off',
|
||||||
// TODO: Enable typescript rules later
|
// TODO: Enable typescript rules later
|
||||||
'@typescript-eslint/no-this-alias': 'off',
|
'@typescript-eslint/no-this-alias': 'off',
|
||||||
'@typescript-eslint/no-empty-function': 'off'
|
'@typescript-eslint/no-empty-function': 'off'
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
<meta name="generator" content="Funkwhale">
|
<meta name="generator" content="Funkwhale">
|
||||||
<link rel="icon" href="/favicon.png">
|
<link rel="icon" href="/favicon.png">
|
||||||
<title>Funkwhale</title>
|
<title>Funkwhale</title>
|
||||||
<script type="module" src="/src/main.js"></script>
|
|
||||||
<style>
|
<style>
|
||||||
#fake-app {
|
#fake-app {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
|
@ -86,9 +85,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="app">
|
<div id="app"></div>
|
||||||
</div>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
<!-- built files will be auto injected -->
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/composition-api": "1.4.9",
|
"@vue/composition-api": "1.4.9",
|
||||||
"@vueuse/core": "8.2.6",
|
"@vueuse/core": "8.2.5",
|
||||||
"axios": "0.26.1",
|
"axios": "0.26.1",
|
||||||
"axios-auth-refresh": "3.2.2",
|
"axios-auth-refresh": "3.2.2",
|
||||||
"diff": "5.0.0",
|
"diff": "5.0.0",
|
||||||
|
@ -26,10 +26,10 @@
|
||||||
"fomantic-ui-css": "2.8.8",
|
"fomantic-ui-css": "2.8.8",
|
||||||
"howler": "2.2.3",
|
"howler": "2.2.3",
|
||||||
"js-logger": "1.6.1",
|
"js-logger": "1.6.1",
|
||||||
"lodash": "4.17.21",
|
|
||||||
"moment": "2.29.3",
|
"moment": "2.29.3",
|
||||||
"qs": "6.10.5",
|
"qs": "6.10.5",
|
||||||
"pinia": "^2.0.13",
|
"pinia": "^2.0.13",
|
||||||
|
"lodash-es": "4.17.21",
|
||||||
"register-service-worker": "1.7.2",
|
"register-service-worker": "1.7.2",
|
||||||
"sanitize-html": "2.7.0",
|
"sanitize-html": "2.7.0",
|
||||||
"sass": "1.49.11",
|
"sass": "1.49.11",
|
||||||
|
@ -42,6 +42,7 @@
|
||||||
"vue-router": "3.5.4",
|
"vue-router": "3.5.4",
|
||||||
"vue-upload-component": "2.8.22",
|
"vue-upload-component": "2.8.22",
|
||||||
"vuedraggable": "2.24.3",
|
"vuedraggable": "2.24.3",
|
||||||
|
"vuex": "3.6.2",
|
||||||
"vuex-persistedstate": "4.1.0",
|
"vuex-persistedstate": "4.1.0",
|
||||||
"vuex-router-sync": "5.0.0"
|
"vuex-router-sync": "5.0.0"
|
||||||
},
|
},
|
||||||
|
@ -49,6 +50,8 @@
|
||||||
"@babel/core": "7.17.12",
|
"@babel/core": "7.17.12",
|
||||||
"@babel/plugin-transform-runtime": "7.17.12",
|
"@babel/plugin-transform-runtime": "7.17.12",
|
||||||
"@babel/preset-env": "7.16.11",
|
"@babel/preset-env": "7.16.11",
|
||||||
|
"@types/jquery": "^3.5.14",
|
||||||
|
"@types/lodash-es": "^4.17.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.19.0",
|
"@typescript-eslint/eslint-plugin": "^5.19.0",
|
||||||
"@vue/eslint-config-standard": "^6.1.0",
|
"@vue/eslint-config-standard": "^6.1.0",
|
||||||
"@vue/eslint-config-typescript": "^10.0.0",
|
"@vue/eslint-config-typescript": "^10.0.0",
|
||||||
|
@ -60,7 +63,6 @@
|
||||||
"easygettext": "2.17.0",
|
"easygettext": "2.17.0",
|
||||||
"eslint": "8.11.0",
|
"eslint": "8.11.0",
|
||||||
"eslint-config-standard": "16.0.3",
|
"eslint-config-standard": "16.0.3",
|
||||||
"eslint-config-standard-with-typescript": "^21.0.1",
|
|
||||||
"eslint-plugin-html": "6.2.0",
|
"eslint-plugin-html": "6.2.0",
|
||||||
"eslint-plugin-import": "2.25.4",
|
"eslint-plugin-import": "2.25.4",
|
||||||
"eslint-plugin-node": "11.1.0",
|
"eslint-plugin-node": "11.1.0",
|
||||||
|
@ -71,9 +73,8 @@
|
||||||
"moxios": "0.4.0",
|
"moxios": "0.4.0",
|
||||||
"sinon": "13.0.2",
|
"sinon": "13.0.2",
|
||||||
"typescript": "^4.6.3",
|
"typescript": "^4.6.3",
|
||||||
"unplugin-vue-components": "^0.19.3",
|
|
||||||
"unplugin-vue2-script-setup": "^0.10.2",
|
"unplugin-vue2-script-setup": "^0.10.2",
|
||||||
"vite": "2.9.5",
|
"vite": "2.8.6",
|
||||||
"vite-plugin-vue2": "1.9.3",
|
"vite-plugin-vue2": "1.9.3",
|
||||||
"vue-jest": "3.0.7",
|
"vue-jest": "3.0.7",
|
||||||
"vue-template-compiler": "2.6.14"
|
"vue-template-compiler": "2.6.14"
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
cd "$(dirname $0)/.." # change into base directory
|
cd "$(dirname $0)/.." # change into base directory
|
||||||
source scripts/utils.sh
|
source scripts/utils.sh
|
||||||
|
|
||||||
locales=$(tail -n +2 src/locales.js | sed -e 's/export default //' | jq '.locales[].code' | grep -v 'en_US' | xargs echo)
|
locales=$(tail -n +3 src/locales.ts | sed -E 's/^[^[]+\[] =//' | jq -r '.[].code' | grep -v 'en_US')
|
||||||
mkdir -p src/translations
|
mkdir -p src/translations
|
||||||
|
|
||||||
for locale in $locales; do
|
for locale in $locales; do
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
cd "$(dirname $0)/.." # change into base directory
|
cd "$(dirname $0)/.." # change into base directory
|
||||||
source scripts/utils.sh
|
source scripts/utils.sh
|
||||||
|
|
||||||
locales=$(tail -n +2 src/locales.js | sed -e 's/export default //' | jq '.locales[].code' | xargs echo)
|
locales=$(tail -n +3 src/locales.ts | sed -E 's/^[^[]+\[] =//' | jq -r '.[].code')
|
||||||
locales_dir="locales"
|
locales_dir="locales"
|
||||||
sources=$(find src -name '*.vue' -o -name '*.html' 2> /dev/null)
|
sources=$(find src -name '*.vue' -o -name '*.html' 2> /dev/null)
|
||||||
js_sources=$(find src -name '*.vue' -o -name '*.js')
|
js_sources=$(find src -name '*.vue' -o -name '*.js')
|
||||||
|
|
|
@ -11,7 +11,7 @@ cd "$(dirname $0)/.." # change into base directory
|
||||||
old_locales_dir=$1
|
old_locales_dir=$1
|
||||||
new_locales_dir=$2
|
new_locales_dir=$2
|
||||||
|
|
||||||
locales=$(tail -n +2 src/locales.js | sed -e 's/export default //' | jq '.locales[].code' | xargs echo)
|
locales=$(tail -n +3 src/locales.ts | sed -E 's/^[^[]+\[] =//' | jq -r '.[].code')
|
||||||
|
|
||||||
# Generate .po files for each available language.
|
# Generate .po files for each available language.
|
||||||
echo $locales
|
echo $locales
|
||||||
|
|
|
@ -1,9 +1,164 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import AudioPlayer from '@/components/audio/Player.vue'
|
||||||
|
import Queue from '@/components/Queue.vue'
|
||||||
|
import PlaylistModal from '@/components/playlists/PlaylistModal.vue'
|
||||||
|
import ChannelUploadModal from '@/components/channels/UploadModal.vue'
|
||||||
|
import Sidebar from '@/components/Sidebar.vue'
|
||||||
|
import ServiceMessages from '@/components/ServiceMessages.vue'
|
||||||
|
import SetInstanceModal from '@/components/SetInstanceModal.vue'
|
||||||
|
import ShortcutsModal from '@/components/ShortcutsModal.vue'
|
||||||
|
import FilterModal from '@/components/moderation/FilterModal.vue'
|
||||||
|
import ReportModal from '@/components/moderation/ReportModal.vue'
|
||||||
|
import {useIntervalFn, useWindowSize} from '@vueuse/core'
|
||||||
|
import GlobalEvents from '@/components/utils/global-events.vue'
|
||||||
|
|
||||||
|
import { computed, nextTick, onMounted, ref, watchEffect } from '@vue/composition-api'
|
||||||
|
import store from '@/store'
|
||||||
|
import { PendingReviewReports, Track } from '@/types'
|
||||||
|
import useWebSocketHandler from '~/composables/useWebSocketHandler'
|
||||||
|
import { getClientOnlyRadio } from '@/radios'
|
||||||
|
|
||||||
|
// Tracks
|
||||||
|
const currentTrack = computed(() => store.getters['queue/currentTrack'])
|
||||||
|
const getTrackInformationText = (track: Track | undefined) => {
|
||||||
|
if (!track) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const artist = track.artist ?? track.album?.artist
|
||||||
|
return `♫ ${track.title} – ${artist?.name} ♫`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update title
|
||||||
|
const initialTitle = document.title
|
||||||
|
watchEffect(() => {
|
||||||
|
const parts = [
|
||||||
|
getTrackInformationText(currentTrack.value),
|
||||||
|
store.state.ui.pageTitle,
|
||||||
|
initialTitle || 'Funkwhale'
|
||||||
|
]
|
||||||
|
|
||||||
|
document.title = parts.filter(i => i).join(' – ')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
const customStylesheets = computed(() => {
|
||||||
|
return store.state.instance?.frontSettings?.additionalStylesheets ?? []
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fake content
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick()
|
||||||
|
document.getElementById('fake-content')?.classList.add('loaded')
|
||||||
|
})
|
||||||
|
|
||||||
|
// WebSocket handlers
|
||||||
|
useWebSocketHandler('inbox.item_added', () => {
|
||||||
|
store.commit('ui/incrementNotifications', { type: 'inbox', count: 1 })
|
||||||
|
})
|
||||||
|
|
||||||
|
useWebSocketHandler('mutation.created', (event) => {
|
||||||
|
store.commit('ui/incrementNotifications', { type: 'pendingReviewEdits', value: event.pending_review_count })
|
||||||
|
})
|
||||||
|
|
||||||
|
useWebSocketHandler('mutation.updated', (event) => {
|
||||||
|
store.commit('ui/incrementNotifications', { type: 'pendingReviewEdits', value: event.pending_review_count })
|
||||||
|
})
|
||||||
|
|
||||||
|
useWebSocketHandler('report.created', (event) => {
|
||||||
|
store.commit('ui/incrementNotifications', { type: 'pendingReviewReports', value: event.unresolved_count })
|
||||||
|
})
|
||||||
|
|
||||||
|
useWebSocketHandler('user_request.created', (event) => {
|
||||||
|
store.commit('ui/incrementNotifications', { type: 'pendingReviewRequests', value: event.pending_count })
|
||||||
|
})
|
||||||
|
|
||||||
|
useWebSocketHandler('Listen', (event) => {
|
||||||
|
if (store.state.radios.current && store.state.radios.running) {
|
||||||
|
const { current } = store.state.radios
|
||||||
|
|
||||||
|
if (current.clientOnly && current.type === 'account') {
|
||||||
|
getClientOnlyRadio(current).handleListen(current, event, store)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Time ago
|
||||||
|
useIntervalFn(() => {
|
||||||
|
// used to redraw ago dates every minute
|
||||||
|
store.commit('ui/computeLastDate')
|
||||||
|
}, 1000 * 60)
|
||||||
|
|
||||||
|
const { width } = useWindowSize()
|
||||||
|
const player = ref()
|
||||||
|
const showShortcutsModal = ref(false)
|
||||||
|
const showSetInstanceModal = ref(false)
|
||||||
|
// export default {
|
||||||
|
// computed: {
|
||||||
|
// ...mapState({
|
||||||
|
// serviceWorker: state => state.ui.serviceWorker
|
||||||
|
// }),
|
||||||
|
// },
|
||||||
|
// watch: {
|
||||||
|
// 'serviceWorker.updateAvailable': {
|
||||||
|
// handler (v) {
|
||||||
|
// if (!v) {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// const self = this
|
||||||
|
// this.$store.commit('ui/addMessage', {
|
||||||
|
// content: this.$pgettext('App/Message/Paragraph', 'A new version of the app is available.'),
|
||||||
|
// date: new Date(),
|
||||||
|
// key: 'refreshApp',
|
||||||
|
// displayTime: 0,
|
||||||
|
// classActions: 'bottom attached opaque',
|
||||||
|
// actions: [
|
||||||
|
// {
|
||||||
|
// text: this.$pgettext('App/Message/Paragraph', 'Update'),
|
||||||
|
// class: 'primary',
|
||||||
|
// click: function () {
|
||||||
|
// self.updateApp()
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// text: this.$pgettext('App/Message/Paragraph', 'Later'),
|
||||||
|
// class: 'basic'
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// })
|
||||||
|
// },
|
||||||
|
// immediate: true
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// async created () {
|
||||||
|
// if (navigator.serviceWorker) {
|
||||||
|
// navigator.serviceWorker.addEventListener(
|
||||||
|
// 'controllerchange', () => {
|
||||||
|
// if (this.serviceWorker.refreshing) return
|
||||||
|
// this.$store.commit('ui/serviceWorker', {
|
||||||
|
// refreshing: true
|
||||||
|
// })
|
||||||
|
// window.location.reload()
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// methods: {
|
||||||
|
// updateApp () {
|
||||||
|
// this.$store.commit('ui/serviceWorker', { updateAvailable: false })
|
||||||
|
// if (!this.serviceWorker.registration || !this.serviceWorker.registration.waiting) { return }
|
||||||
|
// this.serviceWorker.registration.waiting.postMessage({ command: 'skipWaiting' })
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
id="app"
|
id="app"
|
||||||
:key="String($store.state.instance.instanceUrl)"
|
:key="String(store.state.instance.instanceUrl)"
|
||||||
:class="[$store.state.ui.queueFocused ? 'queue-focused' : '',
|
:class="[store.state.ui.queueFocused ? 'queue-focused' : '',
|
||||||
{'has-bottom-player': $store.state.queue.tracks.length > 0}]"
|
{'has-bottom-player': store.state.queue.tracks.length > 0}]"
|
||||||
>
|
>
|
||||||
<!-- here, we display custom stylesheets, if any -->
|
<!-- here, we display custom stylesheets, if any -->
|
||||||
<link
|
<link
|
||||||
|
@ -25,18 +180,18 @@
|
||||||
<service-messages />
|
<service-messages />
|
||||||
<transition name="queue">
|
<transition name="queue">
|
||||||
<queue
|
<queue
|
||||||
v-if="$store.state.ui.queueFocused"
|
v-if="store.state.ui.queueFocused"
|
||||||
@touch-progress="$refs.player.setCurrentTime($event)"
|
@touch-progress="player.setCurrentTime($event)"
|
||||||
/>
|
/>
|
||||||
</transition>
|
</transition>
|
||||||
<router-view
|
<router-view
|
||||||
role="main"
|
role="main"
|
||||||
:class="{hidden: $store.state.ui.queueFocused}"
|
:class="{hidden: store.state.ui.queueFocused}"
|
||||||
/>
|
/>
|
||||||
<player ref="player" />
|
<audio-player ref="player" />
|
||||||
<playlist-modal v-if="$store.state.auth.authenticated" />
|
<playlist-modal v-if="store.state.auth.authenticated" />
|
||||||
<channel-upload-modal v-if="$store.state.auth.authenticated" />
|
<channel-upload-modal v-if="store.state.auth.authenticated" />
|
||||||
<filter-modal v-if="$store.state.auth.authenticated" />
|
<filter-modal v-if="store.state.auth.authenticated" />
|
||||||
<report-modal />
|
<report-modal />
|
||||||
<shortcuts-modal
|
<shortcuts-modal
|
||||||
:show="showShortcutsModal"
|
:show="showShortcutsModal"
|
||||||
|
@ -46,414 +201,6 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from 'axios'
|
|
||||||
import _ from 'lodash'
|
|
||||||
import { mapState, mapGetters } from 'vuex'
|
|
||||||
import { useWebSocket, whenever } from '@vueuse/core'
|
|
||||||
import GlobalEvents from '@/components/utils/global-events.vue'
|
|
||||||
import locales from './locales'
|
|
||||||
import { getClientOnlyRadio } from '@/radios'
|
|
||||||
|
|
||||||
import Player from '@/components/audio/Player.vue'
|
|
||||||
import Queue from '@/components/Queue.vue'
|
|
||||||
import PlaylistModal from '@/components/playlists/PlaylistModal.vue'
|
|
||||||
import ChannelUploadModal from '@/components/channels/UploadModal.vue'
|
|
||||||
import Sidebar from '@/components/Sidebar.vue'
|
|
||||||
import ServiceMessages from '@/components/ServiceMessages.vue'
|
|
||||||
import SetInstanceModal from '@/components/SetInstanceModal.vue'
|
|
||||||
import ShortcutsModal from '@/components/ShortcutsModal.vue'
|
|
||||||
import FilterModal from '@/components/moderation/FilterModal.vue'
|
|
||||||
import ReportModal from '@/components/moderation/ReportModal.vue'
|
|
||||||
import { watch, watchEffect } from '@vue/composition-api'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'App',
|
|
||||||
components: {
|
|
||||||
Player,
|
|
||||||
Queue,
|
|
||||||
PlaylistModal,
|
|
||||||
ChannelUploadModal,
|
|
||||||
Sidebar,
|
|
||||||
ServiceMessages,
|
|
||||||
SetInstanceModal,
|
|
||||||
ShortcutsModal,
|
|
||||||
FilterModal,
|
|
||||||
ReportModal,
|
|
||||||
GlobalEvents
|
|
||||||
},
|
|
||||||
setup (props, { root }) {
|
|
||||||
const store = root.$store
|
|
||||||
|
|
||||||
const url = store.getters['instance/absoluteUrl']('api/v1/activity')
|
|
||||||
.replace(/^http/, 'ws')
|
|
||||||
|
|
||||||
const { data, status, open, close } = useWebSocket(url, {
|
|
||||||
autoReconnect: true,
|
|
||||||
immediate: false
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(() => store.state.auth.authenticated, (authenticated) => {
|
|
||||||
if (authenticated) return open()
|
|
||||||
close()
|
|
||||||
})
|
|
||||||
|
|
||||||
whenever(data, () => {
|
|
||||||
store.dispatch('ui/websocketEvent', JSON.parse(data.value))
|
|
||||||
})
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
console.log('Websocket status:', status.value)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
instanceUrl: null,
|
|
||||||
showShortcutsModal: false,
|
|
||||||
showSetInstanceModal: false,
|
|
||||||
initialTitle: document.title,
|
|
||||||
width: window.innerWidth
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState({
|
|
||||||
messages: state => state.ui.messages,
|
|
||||||
nodeinfo: state => state.instance.nodeinfo,
|
|
||||||
playing: state => state.player.playing,
|
|
||||||
bufferProgress: state => state.player.bufferProgress,
|
|
||||||
isLoadingAudio: state => state.player.isLoadingAudio,
|
|
||||||
serviceWorker: state => state.ui.serviceWorker
|
|
||||||
}),
|
|
||||||
...mapGetters({
|
|
||||||
hasNext: 'queue/hasNext',
|
|
||||||
currentTrack: 'queue/currentTrack',
|
|
||||||
progress: 'player/progress'
|
|
||||||
}),
|
|
||||||
labels () {
|
|
||||||
const play = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Play track')
|
|
||||||
const pause = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Pause track')
|
|
||||||
const next = this.$pgettext('Sidebar/Player/Icon.Tooltip', 'Next track')
|
|
||||||
const expandQueue = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Expand queue')
|
|
||||||
return {
|
|
||||||
play,
|
|
||||||
pause,
|
|
||||||
next,
|
|
||||||
expandQueue
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
instances.push(this.$store.getters['instance/defaultUrl'](), 'https://demo.funkwhale.audio/')
|
|
||||||
return _.uniq(instances.filter((e) => { return e }))
|
|
||||||
},
|
|
||||||
version () {
|
|
||||||
if (!this.nodeinfo) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return _.get(this.nodeinfo, 'software.version')
|
|
||||||
},
|
|
||||||
customStylesheets () {
|
|
||||||
if (this.$store.state.instance.frontSettings) {
|
|
||||||
return this.$store.state.instance.frontSettings.additionalStylesheets || []
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
matchDarkColorScheme () {
|
|
||||||
if (window.matchMedia) {
|
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)')
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
'$store.state.instance.instanceUrl' (v) {
|
|
||||||
this.$store.dispatch('instance/fetchSettings')
|
|
||||||
this.fetchNodeInfo()
|
|
||||||
},
|
|
||||||
'$store.state.ui.theme': {
|
|
||||||
immediate: true,
|
|
||||||
handler (newValue) {
|
|
||||||
const matchesDark = this.matchDarkColorScheme
|
|
||||||
if (matchesDark) {
|
|
||||||
if (newValue === 'system') {
|
|
||||||
newValue = matchesDark.matches ? 'dark' : 'light'
|
|
||||||
matchesDark.addEventListener('change', this.handleThemeChange)
|
|
||||||
} else {
|
|
||||||
matchesDark.removeEventListener('change', this.handleThemeChange)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (newValue === 'system') {
|
|
||||||
newValue = 'light'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.setTheme(newValue)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'$store.state.ui.currentLanguage': {
|
|
||||||
immediate: true,
|
|
||||||
handler (newValue) {
|
|
||||||
const self = this
|
|
||||||
const htmlLocale = newValue.toLowerCase().replace('_', '-')
|
|
||||||
document.documentElement.setAttribute('lang', htmlLocale)
|
|
||||||
if (newValue === 'en_US') {
|
|
||||||
self.$language.current = 'noop'
|
|
||||||
self.$language.current = newValue
|
|
||||||
return self.$store.commit('ui/momentLocale', 'en')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
currentTrack: {
|
|
||||||
immediate: true,
|
|
||||||
handler (newValue) {
|
|
||||||
this.updateDocumentTitle()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'$store.state.ui.pageTitle': {
|
|
||||||
immediate: true,
|
|
||||||
handler (newValue) {
|
|
||||||
this.updateDocumentTitle()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'serviceWorker.updateAvailable': {
|
|
||||||
handler (v) {
|
|
||||||
if (!v) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const self = this
|
|
||||||
this.$store.commit('ui/addMessage', {
|
|
||||||
content: this.$pgettext('App/Message/Paragraph', 'A new version of the app is available.'),
|
|
||||||
date: new Date(),
|
|
||||||
key: 'refreshApp',
|
|
||||||
displayTime: 0,
|
|
||||||
classActions: 'bottom attached opaque',
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
text: this.$pgettext('App/Message/Paragraph', 'Update'),
|
|
||||||
class: 'primary',
|
|
||||||
click: function () {
|
|
||||||
self.updateApp()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: this.$pgettext('App/Message/Paragraph', 'Later'),
|
|
||||||
class: 'basic'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
},
|
|
||||||
immediate: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async created () {
|
|
||||||
if (navigator.serviceWorker) {
|
|
||||||
navigator.serviceWorker.addEventListener(
|
|
||||||
'controllerchange', () => {
|
|
||||||
if (this.serviceWorker.refreshing) return
|
|
||||||
this.$store.commit('ui/serviceWorker', {
|
|
||||||
refreshing: true
|
|
||||||
})
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
window.addEventListener('resize', this.handleResize)
|
|
||||||
this.handleResize()
|
|
||||||
const self = this
|
|
||||||
if (!this.$store.state.ui.selectedLanguage) {
|
|
||||||
this.autodetectLanguage()
|
|
||||||
}
|
|
||||||
setInterval(() => {
|
|
||||||
// used to redraw ago dates every minute
|
|
||||||
self.$store.commit('ui/computeLastDate')
|
|
||||||
}, 1000 * 60)
|
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
|
||||||
const serverUrl = urlParams.get('_server')
|
|
||||||
if (serverUrl) {
|
|
||||||
this.$store.commit('instance/instanceUrl', serverUrl)
|
|
||||||
}
|
|
||||||
const url = urlParams.get('_url')
|
|
||||||
if (url) {
|
|
||||||
await this.$router.replace(url)
|
|
||||||
} else if (!this.$store.state.instance.instanceUrl) {
|
|
||||||
// we have several way to guess the API server url. By order of precedence:
|
|
||||||
// 1. use the url provided in settings.json, if any
|
|
||||||
// 2. use the url specified when building via VUE_APP_INSTANCE_URL
|
|
||||||
// 3. use the current url
|
|
||||||
const defaultInstanceUrl =
|
|
||||||
this.$store.state.instance.frontSettings.defaultServerUrl ||
|
|
||||||
import.meta.env.VUE_APP_INSTANCE_URL || this.$store.getters['instance/defaultUrl']()
|
|
||||||
this.$store.commit('instance/instanceUrl', defaultInstanceUrl)
|
|
||||||
} else {
|
|
||||||
// needed to trigger initialization of axios / service worker
|
|
||||||
this.$store.commit('instance/instanceUrl', this.$store.state.instance.instanceUrl)
|
|
||||||
}
|
|
||||||
await this.fetchNodeInfo()
|
|
||||||
this.$store.dispatch('instance/fetchSettings')
|
|
||||||
this.$store.commit('ui/addWebsocketEventHandler', {
|
|
||||||
eventName: 'inbox.item_added',
|
|
||||||
id: 'sidebarCount',
|
|
||||||
handler: this.incrementNotificationCountInSidebar
|
|
||||||
})
|
|
||||||
this.$store.commit('ui/addWebsocketEventHandler', {
|
|
||||||
eventName: 'mutation.created',
|
|
||||||
id: 'sidebarReviewEditCount',
|
|
||||||
handler: this.incrementReviewEditCountInSidebar
|
|
||||||
})
|
|
||||||
this.$store.commit('ui/addWebsocketEventHandler', {
|
|
||||||
eventName: 'mutation.updated',
|
|
||||||
id: 'sidebarReviewEditCount',
|
|
||||||
handler: this.incrementReviewEditCountInSidebar
|
|
||||||
})
|
|
||||||
this.$store.commit('ui/addWebsocketEventHandler', {
|
|
||||||
eventName: 'report.created',
|
|
||||||
id: 'sidebarPendingReviewReportCount',
|
|
||||||
handler: this.incrementPendingReviewReportsCountInSidebar
|
|
||||||
})
|
|
||||||
this.$store.commit('ui/addWebsocketEventHandler', {
|
|
||||||
eventName: 'user_request.created',
|
|
||||||
id: 'sidebarPendingReviewRequestCount',
|
|
||||||
handler: this.incrementPendingReviewRequestsCountInSidebar
|
|
||||||
})
|
|
||||||
this.$store.commit('ui/addWebsocketEventHandler', {
|
|
||||||
eventName: 'Listen',
|
|
||||||
id: 'handleListen',
|
|
||||||
handler: this.handleListen
|
|
||||||
})
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
const self = this
|
|
||||||
// slight hack to allow use to have internal links in <translate> tags
|
|
||||||
// while preserving router behaviour
|
|
||||||
document.documentElement.addEventListener('click', function (event) {
|
|
||||||
if (!event.target.matches('a.internal')) return
|
|
||||||
self.$router.push(event.target.getAttribute('href'))
|
|
||||||
event.preventDefault()
|
|
||||||
}, false)
|
|
||||||
this.$nextTick(() => {
|
|
||||||
document.getElementById('fake-content').classList.add('loaded')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
destroyed () {
|
|
||||||
this.$store.commit('ui/removeWebsocketEventHandler', {
|
|
||||||
eventName: 'inbox.item_added',
|
|
||||||
id: 'sidebarCount'
|
|
||||||
})
|
|
||||||
this.$store.commit('ui/removeWebsocketEventHandler', {
|
|
||||||
eventName: 'mutation.created',
|
|
||||||
id: 'sidebarReviewEditCount'
|
|
||||||
})
|
|
||||||
this.$store.commit('ui/removeWebsocketEventHandler', {
|
|
||||||
eventName: 'mutation.updated',
|
|
||||||
id: 'sidebarReviewEditCount'
|
|
||||||
})
|
|
||||||
this.$store.commit('ui/removeWebsocketEventHandler', {
|
|
||||||
eventName: 'mutation.updated',
|
|
||||||
id: 'sidebarPendingReviewReportCount'
|
|
||||||
})
|
|
||||||
this.$store.commit('ui/removeWebsocketEventHandler', {
|
|
||||||
eventName: 'user_request.created',
|
|
||||||
id: 'sidebarPendingReviewRequestCount'
|
|
||||||
})
|
|
||||||
this.$store.commit('ui/removeWebsocketEventHandler', {
|
|
||||||
eventName: 'Listen',
|
|
||||||
id: 'handleListen'
|
|
||||||
})
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
incrementNotificationCountInSidebar (event) {
|
|
||||||
this.$store.commit('ui/incrementNotifications', { type: 'inbox', count: 1 })
|
|
||||||
},
|
|
||||||
incrementReviewEditCountInSidebar (event) {
|
|
||||||
this.$store.commit('ui/incrementNotifications', { type: 'pendingReviewEdits', value: event.pending_review_count })
|
|
||||||
},
|
|
||||||
incrementPendingReviewReportsCountInSidebar (event) {
|
|
||||||
this.$store.commit('ui/incrementNotifications', { type: 'pendingReviewReports', value: event.unresolved_count })
|
|
||||||
},
|
|
||||||
incrementPendingReviewRequestsCountInSidebar (event) {
|
|
||||||
this.$store.commit('ui/incrementNotifications', { type: 'pendingReviewRequests', value: event.pending_count })
|
|
||||||
},
|
|
||||||
handleListen (event) {
|
|
||||||
if (this.$store.state.radios.current && this.$store.state.radios.running) {
|
|
||||||
const current = this.$store.state.radios.current
|
|
||||||
if (current.clientOnly && current.type === 'account') {
|
|
||||||
getClientOnlyRadio(current).handleListen(current, event, this.$store)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async fetchNodeInfo () {
|
|
||||||
const response = await axios.get('instance/nodeinfo/2.0/')
|
|
||||||
this.$store.commit('instance/nodeinfo', response.data)
|
|
||||||
},
|
|
||||||
autodetectLanguage () {
|
|
||||||
const userLanguage = navigator.language || navigator.userLanguage
|
|
||||||
const available = locales.locales.map(e => { return e.code })
|
|
||||||
let candidate
|
|
||||||
const matching = available.filter((a) => {
|
|
||||||
return userLanguage.replace('-', '_') === a
|
|
||||||
})
|
|
||||||
const almostMatching = available.filter((a) => {
|
|
||||||
return userLanguage.replace('-', '_').split('_')[0] === a.split('_')[0]
|
|
||||||
})
|
|
||||||
if (matching.length > 0) {
|
|
||||||
candidate = matching[0]
|
|
||||||
} else if (almostMatching.length > 0) {
|
|
||||||
candidate = almostMatching[0]
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.$store.commit('ui/currentLanguage', candidate)
|
|
||||||
},
|
|
||||||
getTrackInformationText (track) {
|
|
||||||
const trackTitle = track.title
|
|
||||||
const albumArtist = (track.album) ? track.album.artist.name : null
|
|
||||||
const artistName = (
|
|
||||||
(track.artist) ? track.artist.name : albumArtist)
|
|
||||||
const text = `♫ ${trackTitle} – ${artistName} ♫`
|
|
||||||
return text
|
|
||||||
},
|
|
||||||
updateDocumentTitle () {
|
|
||||||
const parts = []
|
|
||||||
const currentTrackPart = (
|
|
||||||
(this.currentTrack)
|
|
||||||
? this.getTrackInformationText(this.currentTrack)
|
|
||||||
: null)
|
|
||||||
if (currentTrackPart) {
|
|
||||||
parts.push(currentTrackPart)
|
|
||||||
}
|
|
||||||
if (this.$store.state.ui.pageTitle) {
|
|
||||||
parts.push(this.$store.state.ui.pageTitle)
|
|
||||||
}
|
|
||||||
parts.push(this.initialTitle || 'Funkwhale')
|
|
||||||
document.title = parts.join(' – ')
|
|
||||||
},
|
|
||||||
|
|
||||||
updateApp () {
|
|
||||||
this.$store.commit('ui/serviceWorker', { updateAvailable: false })
|
|
||||||
if (!this.serviceWorker.registration || !this.serviceWorker.registration.waiting) { return }
|
|
||||||
this.serviceWorker.registration.waiting.postMessage({ command: 'skipWaiting' })
|
|
||||||
},
|
|
||||||
handleResize () {
|
|
||||||
this.width = window.innerWidth
|
|
||||||
},
|
|
||||||
handleThemeChange (event) {
|
|
||||||
this.setTheme(event.matches ? 'dark' : 'light')
|
|
||||||
},
|
|
||||||
setTheme (theme) {
|
|
||||||
const oldTheme = (theme === 'light') ? 'dark' : 'light'
|
|
||||||
document.body.classList.remove(`theme-${oldTheme}`)
|
|
||||||
document.body.classList.add(`theme-${theme}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import "style/_main";
|
@import "style/_main";
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,460 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
id="app"
|
||||||
|
:key="String($store.state.instance.instanceUrl)"
|
||||||
|
:class="[$store.state.ui.queueFocused ? 'queue-focused' : '',
|
||||||
|
{'has-bottom-player': $store.state.queue.tracks.length > 0}]"
|
||||||
|
>
|
||||||
|
<!-- here, we display custom stylesheets, if any -->
|
||||||
|
<link
|
||||||
|
v-for="url in customStylesheets"
|
||||||
|
:key="url"
|
||||||
|
rel="stylesheet"
|
||||||
|
property="stylesheet"
|
||||||
|
:href="url"
|
||||||
|
>
|
||||||
|
<sidebar
|
||||||
|
:width="width"
|
||||||
|
@show:set-instance-modal="showSetInstanceModal = !showSetInstanceModal"
|
||||||
|
@show:shortcuts-modal="showShortcutsModal = !showShortcutsModal"
|
||||||
|
/>
|
||||||
|
<set-instance-modal
|
||||||
|
:show="showSetInstanceModal"
|
||||||
|
@update:show="showSetInstanceModal = $event"
|
||||||
|
/>
|
||||||
|
<service-messages />
|
||||||
|
<transition name="queue">
|
||||||
|
<queue
|
||||||
|
v-if="$store.state.ui.queueFocused"
|
||||||
|
@touch-progress="$refs.player.setCurrentTime($event)"
|
||||||
|
/>
|
||||||
|
</transition>
|
||||||
|
<router-view
|
||||||
|
role="main"
|
||||||
|
:class="{hidden: $store.state.ui.queueFocused}"
|
||||||
|
/>
|
||||||
|
<player ref="player" />
|
||||||
|
<playlist-modal v-if="$store.state.auth.authenticated" />
|
||||||
|
<channel-upload-modal v-if="$store.state.auth.authenticated" />
|
||||||
|
<filter-modal v-if="$store.state.auth.authenticated" />
|
||||||
|
<report-modal />
|
||||||
|
<shortcuts-modal
|
||||||
|
:show="showShortcutsModal"
|
||||||
|
@update:show="showShortcutsModal = $event"
|
||||||
|
/>
|
||||||
|
<GlobalEvents @keydown.h.exact="showShortcutsModal = !showShortcutsModal" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
|
import { uniq, get } from 'lodash-es'
|
||||||
|
import { mapState, mapGetters } from 'vuex'
|
||||||
|
import { useWebSocket, whenever } from '@vueuse/core'
|
||||||
|
import GlobalEvents from '@/components/utils/global-events.vue'
|
||||||
|
import locales from './locales'
|
||||||
|
import { getClientOnlyRadio } from '@/radios'
|
||||||
|
|
||||||
|
import Player from '@/components/audio/Player.vue'
|
||||||
|
import Queue from '@/components/Queue.vue'
|
||||||
|
import PlaylistModal from '@/components/playlists/PlaylistModal.vue'
|
||||||
|
import ChannelUploadModal from '@/components/channels/UploadModal.vue'
|
||||||
|
import Sidebar from '@/components/Sidebar.vue'
|
||||||
|
import ServiceMessages from '@/components/ServiceMessages.vue'
|
||||||
|
import SetInstanceModal from '@/components/SetInstanceModal.vue'
|
||||||
|
import ShortcutsModal from '@/components/ShortcutsModal.vue'
|
||||||
|
import FilterModal from '@/components/moderation/FilterModal.vue'
|
||||||
|
import ReportModal from '@/components/moderation/ReportModal.vue'
|
||||||
|
import { watch, watchEffect } from '@vue/composition-api'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
components: {
|
||||||
|
Player,
|
||||||
|
Queue,
|
||||||
|
PlaylistModal,
|
||||||
|
ChannelUploadModal,
|
||||||
|
Sidebar,
|
||||||
|
ServiceMessages,
|
||||||
|
SetInstanceModal,
|
||||||
|
ShortcutsModal,
|
||||||
|
FilterModal,
|
||||||
|
ReportModal,
|
||||||
|
GlobalEvents
|
||||||
|
},
|
||||||
|
setup (props, { root }) {
|
||||||
|
const store = root.$store
|
||||||
|
|
||||||
|
const url = store.getters['instance/absoluteUrl']('api/v1/activity')
|
||||||
|
.replace(/^http/, 'ws')
|
||||||
|
|
||||||
|
const { data, status, open, close } = useWebSocket(url, {
|
||||||
|
autoReconnect: true,
|
||||||
|
immediate: false
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => store.state.auth.authenticated, (authenticated) => {
|
||||||
|
if (authenticated) return open()
|
||||||
|
close()
|
||||||
|
})
|
||||||
|
|
||||||
|
whenever(data, () => {
|
||||||
|
store.dispatch('ui/websocketEvent', JSON.parse(data.value))
|
||||||
|
})
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
console.log('Websocket status:', status.value)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
instanceUrl: null,
|
||||||
|
showShortcutsModal: false,
|
||||||
|
showSetInstanceModal: false,
|
||||||
|
initialTitle: document.title,
|
||||||
|
width: window.innerWidth
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
messages: state => state.ui.messages,
|
||||||
|
nodeinfo: state => state.instance.nodeinfo,
|
||||||
|
playing: state => state.player.playing,
|
||||||
|
bufferProgress: state => state.player.bufferProgress,
|
||||||
|
isLoadingAudio: state => state.player.isLoadingAudio,
|
||||||
|
serviceWorker: state => state.ui.serviceWorker
|
||||||
|
}),
|
||||||
|
...mapGetters({
|
||||||
|
hasNext: 'queue/hasNext',
|
||||||
|
currentTrack: 'queue/currentTrack',
|
||||||
|
progress: 'player/progress'
|
||||||
|
}),
|
||||||
|
labels () {
|
||||||
|
const play = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Play track')
|
||||||
|
const pause = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Pause track')
|
||||||
|
const next = this.$pgettext('Sidebar/Player/Icon.Tooltip', 'Next track')
|
||||||
|
const expandQueue = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Expand queue')
|
||||||
|
return {
|
||||||
|
play,
|
||||||
|
pause,
|
||||||
|
next,
|
||||||
|
expandQueue
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
instances.push(this.$store.getters['instance/defaultUrl'](), 'https://demo.funkwhale.audio/')
|
||||||
|
return uniq(instances.filter((e) => { return e }))
|
||||||
|
},
|
||||||
|
version () {
|
||||||
|
if (!this.nodeinfo) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return get(this.nodeinfo, 'software.version')
|
||||||
|
},
|
||||||
|
customStylesheets () {
|
||||||
|
if (this.$store.state.instance.frontSettings) {
|
||||||
|
return this.$store.state.instance.frontSettings.additionalStylesheets || []
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
matchDarkColorScheme () {
|
||||||
|
if (window.matchMedia) {
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'$store.state.instance.instanceUrl' (v) {
|
||||||
|
this.$store.dispatch('instance/fetchSettings')
|
||||||
|
this.fetchNodeInfo()
|
||||||
|
},
|
||||||
|
'$store.state.ui.theme': {
|
||||||
|
immediate: true,
|
||||||
|
handler (newValue) {
|
||||||
|
const matchesDark = this.matchDarkColorScheme
|
||||||
|
if (matchesDark) {
|
||||||
|
if (newValue === 'system') {
|
||||||
|
newValue = matchesDark.matches ? 'dark' : 'light'
|
||||||
|
matchesDark.addEventListener('change', this.handleThemeChange)
|
||||||
|
} else {
|
||||||
|
matchesDark.removeEventListener('change', this.handleThemeChange)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (newValue === 'system') {
|
||||||
|
newValue = 'light'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setTheme(newValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'$store.state.ui.currentLanguage': {
|
||||||
|
immediate: true,
|
||||||
|
handler (newValue) {
|
||||||
|
const self = this
|
||||||
|
const htmlLocale = newValue.toLowerCase().replace('_', '-')
|
||||||
|
document.documentElement.setAttribute('lang', htmlLocale)
|
||||||
|
if (newValue === 'en_US') {
|
||||||
|
self.$language.current = 'noop'
|
||||||
|
self.$language.current = newValue
|
||||||
|
return self.$store.commit('ui/momentLocale', 'en')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
currentTrack: {
|
||||||
|
immediate: true,
|
||||||
|
handler (newValue) {
|
||||||
|
this.updateDocumentTitle()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'$store.state.ui.pageTitle': {
|
||||||
|
immediate: true,
|
||||||
|
handler (newValue) {
|
||||||
|
this.updateDocumentTitle()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'serviceWorker.updateAvailable': {
|
||||||
|
handler (v) {
|
||||||
|
if (!v) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const self = this
|
||||||
|
this.$store.commit('ui/addMessage', {
|
||||||
|
content: this.$pgettext('App/Message/Paragraph', 'A new version of the app is available.'),
|
||||||
|
date: new Date(),
|
||||||
|
key: 'refreshApp',
|
||||||
|
displayTime: 0,
|
||||||
|
classActions: 'bottom attached opaque',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
text: this.$pgettext('App/Message/Paragraph', 'Update'),
|
||||||
|
class: 'primary',
|
||||||
|
click: function () {
|
||||||
|
self.updateApp()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$pgettext('App/Message/Paragraph', 'Later'),
|
||||||
|
class: 'basic'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created () {
|
||||||
|
if (navigator.serviceWorker) {
|
||||||
|
navigator.serviceWorker.addEventListener(
|
||||||
|
'controllerchange', () => {
|
||||||
|
if (this.serviceWorker.refreshing) return
|
||||||
|
this.$store.commit('ui/serviceWorker', {
|
||||||
|
refreshing: true
|
||||||
|
})
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', this.handleResize)
|
||||||
|
this.handleResize()
|
||||||
|
const self = this
|
||||||
|
if (!this.$store.state.ui.selectedLanguage) {
|
||||||
|
this.autodetectLanguage()
|
||||||
|
}
|
||||||
|
setInterval(() => {
|
||||||
|
// used to redraw ago dates every minute
|
||||||
|
self.$store.commit('ui/computeLastDate')
|
||||||
|
}, 1000 * 60)
|
||||||
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
|
const serverUrl = urlParams.get('_server')
|
||||||
|
if (serverUrl) {
|
||||||
|
this.$store.commit('instance/instanceUrl', serverUrl)
|
||||||
|
}
|
||||||
|
const url = urlParams.get('_url')
|
||||||
|
if (url) {
|
||||||
|
await this.$router.replace(url)
|
||||||
|
} else if (!this.$store.state.instance.instanceUrl) {
|
||||||
|
// we have several way to guess the API server url. By order of precedence:
|
||||||
|
// 1. use the url provided in settings.json, if any
|
||||||
|
// 2. use the url specified when building via VUE_APP_INSTANCE_URL
|
||||||
|
// 3. use the current url
|
||||||
|
const defaultInstanceUrl =
|
||||||
|
this.$store.state.instance.frontSettings.defaultServerUrl ||
|
||||||
|
import.meta.env.VUE_APP_INSTANCE_URL || this.$store.getters['instance/defaultUrl']()
|
||||||
|
this.$store.commit('instance/instanceUrl', defaultInstanceUrl)
|
||||||
|
} else {
|
||||||
|
// needed to trigger initialization of axios / service worker
|
||||||
|
this.$store.commit('instance/instanceUrl', this.$store.state.instance.instanceUrl)
|
||||||
|
}
|
||||||
|
await this.fetchNodeInfo()
|
||||||
|
this.$store.dispatch('instance/fetchSettings')
|
||||||
|
this.$store.commit('ui/addWebsocketEventHandler', {
|
||||||
|
eventName: 'inbox.item_added',
|
||||||
|
id: 'sidebarCount',
|
||||||
|
handler: this.incrementNotificationCountInSidebar
|
||||||
|
})
|
||||||
|
this.$store.commit('ui/addWebsocketEventHandler', {
|
||||||
|
eventName: 'mutation.created',
|
||||||
|
id: 'sidebarReviewEditCount',
|
||||||
|
handler: this.incrementReviewEditCountInSidebar
|
||||||
|
})
|
||||||
|
this.$store.commit('ui/addWebsocketEventHandler', {
|
||||||
|
eventName: 'mutation.updated',
|
||||||
|
id: 'sidebarReviewEditCount',
|
||||||
|
handler: this.incrementReviewEditCountInSidebar
|
||||||
|
})
|
||||||
|
this.$store.commit('ui/addWebsocketEventHandler', {
|
||||||
|
eventName: 'report.created',
|
||||||
|
id: 'sidebarPendingReviewReportCount',
|
||||||
|
handler: this.incrementPendingReviewReportsCountInSidebar
|
||||||
|
})
|
||||||
|
this.$store.commit('ui/addWebsocketEventHandler', {
|
||||||
|
eventName: 'user_request.created',
|
||||||
|
id: 'sidebarPendingReviewRequestCount',
|
||||||
|
handler: this.incrementPendingReviewRequestsCountInSidebar
|
||||||
|
})
|
||||||
|
this.$store.commit('ui/addWebsocketEventHandler', {
|
||||||
|
eventName: 'Listen',
|
||||||
|
id: 'handleListen',
|
||||||
|
handler: this.handleListen
|
||||||
|
})
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
const self = this
|
||||||
|
// slight hack to allow use to have internal links in <translate> tags
|
||||||
|
// while preserving router behaviour
|
||||||
|
document.documentElement.addEventListener('click', function (event) {
|
||||||
|
if (!event.target.matches('a.internal')) return
|
||||||
|
self.$router.push(event.target.getAttribute('href'))
|
||||||
|
event.preventDefault()
|
||||||
|
}, false)
|
||||||
|
this.$nextTick(() => {
|
||||||
|
document.getElementById('fake-content').classList.add('loaded')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
this.$store.commit('ui/removeWebsocketEventHandler', {
|
||||||
|
eventName: 'inbox.item_added',
|
||||||
|
id: 'sidebarCount'
|
||||||
|
})
|
||||||
|
this.$store.commit('ui/removeWebsocketEventHandler', {
|
||||||
|
eventName: 'mutation.created',
|
||||||
|
id: 'sidebarReviewEditCount'
|
||||||
|
})
|
||||||
|
this.$store.commit('ui/removeWebsocketEventHandler', {
|
||||||
|
eventName: 'mutation.updated',
|
||||||
|
id: 'sidebarReviewEditCount'
|
||||||
|
})
|
||||||
|
this.$store.commit('ui/removeWebsocketEventHandler', {
|
||||||
|
eventName: 'mutation.updated',
|
||||||
|
id: 'sidebarPendingReviewReportCount'
|
||||||
|
})
|
||||||
|
this.$store.commit('ui/removeWebsocketEventHandler', {
|
||||||
|
eventName: 'user_request.created',
|
||||||
|
id: 'sidebarPendingReviewRequestCount'
|
||||||
|
})
|
||||||
|
this.$store.commit('ui/removeWebsocketEventHandler', {
|
||||||
|
eventName: 'Listen',
|
||||||
|
id: 'handleListen'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
incrementNotificationCountInSidebar (event) {
|
||||||
|
this.$store.commit('ui/incrementNotifications', { type: 'inbox', count: 1 })
|
||||||
|
},
|
||||||
|
incrementReviewEditCountInSidebar (event) {
|
||||||
|
this.$store.commit('ui/incrementNotifications', { type: 'pendingReviewEdits', value: event.pending_review_count })
|
||||||
|
},
|
||||||
|
incrementPendingReviewReportsCountInSidebar (event) {
|
||||||
|
this.$store.commit('ui/incrementNotifications', { type: 'pendingReviewReports', value: event.unresolved_count })
|
||||||
|
},
|
||||||
|
incrementPendingReviewRequestsCountInSidebar (event) {
|
||||||
|
this.$store.commit('ui/incrementNotifications', { type: 'pendingReviewRequests', value: event.pending_count })
|
||||||
|
},
|
||||||
|
handleListen (event) {
|
||||||
|
if (this.$store.state.radios.current && this.$store.state.radios.running) {
|
||||||
|
const current = this.$store.state.radios.current
|
||||||
|
if (current.clientOnly && current.type === 'account') {
|
||||||
|
getClientOnlyRadio(current).handleListen(current, event, this.$store)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async fetchNodeInfo () {
|
||||||
|
const response = await axios.get('instance/nodeinfo/2.0/')
|
||||||
|
this.$store.commit('instance/nodeinfo', response.data)
|
||||||
|
},
|
||||||
|
autodetectLanguage () {
|
||||||
|
const userLanguage = navigator.language || navigator.userLanguage
|
||||||
|
const available = locales.locales.map(e => { return e.code })
|
||||||
|
let candidate
|
||||||
|
const matching = available.filter((a) => {
|
||||||
|
return userLanguage.replace('-', '_') === a
|
||||||
|
})
|
||||||
|
const almostMatching = available.filter((a) => {
|
||||||
|
return userLanguage.replace('-', '_').split('_')[0] === a.split('_')[0]
|
||||||
|
})
|
||||||
|
if (matching.length > 0) {
|
||||||
|
candidate = matching[0]
|
||||||
|
} else if (almostMatching.length > 0) {
|
||||||
|
candidate = almostMatching[0]
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$store.commit('ui/currentLanguage', candidate)
|
||||||
|
},
|
||||||
|
getTrackInformationText (track) {
|
||||||
|
const trackTitle = track.title
|
||||||
|
const albumArtist = (track.album) ? track.album.artist.name : null
|
||||||
|
const artistName = (
|
||||||
|
(track.artist) ? track.artist.name : albumArtist)
|
||||||
|
const text = `♫ ${trackTitle} – ${artistName} ♫`
|
||||||
|
return text
|
||||||
|
},
|
||||||
|
updateDocumentTitle () {
|
||||||
|
const parts = []
|
||||||
|
const currentTrackPart = (
|
||||||
|
(this.currentTrack)
|
||||||
|
? this.getTrackInformationText(this.currentTrack)
|
||||||
|
: null)
|
||||||
|
if (currentTrackPart) {
|
||||||
|
parts.push(currentTrackPart)
|
||||||
|
}
|
||||||
|
if (this.$store.state.ui.pageTitle) {
|
||||||
|
parts.push(this.$store.state.ui.pageTitle)
|
||||||
|
}
|
||||||
|
parts.push(this.initialTitle || 'Funkwhale')
|
||||||
|
document.title = parts.join(' – ')
|
||||||
|
},
|
||||||
|
|
||||||
|
updateApp () {
|
||||||
|
this.$store.commit('ui/serviceWorker', { updateAvailable: false })
|
||||||
|
if (!this.serviceWorker.registration || !this.serviceWorker.registration.waiting) { return }
|
||||||
|
this.serviceWorker.registration.waiting.postMessage({ command: 'skipWaiting' })
|
||||||
|
},
|
||||||
|
handleResize () {
|
||||||
|
this.width = window.innerWidth
|
||||||
|
},
|
||||||
|
handleThemeChange (event) {
|
||||||
|
this.setTheme(event.matches ? 'dark' : 'light')
|
||||||
|
},
|
||||||
|
setTheme (theme) {
|
||||||
|
const oldTheme = (theme === 'light') ? 'dark' : 'light'
|
||||||
|
document.body.classList.remove(`theme-${oldTheme}`)
|
||||||
|
document.body.classList.add(`theme-${theme}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "style/_main";
|
||||||
|
|
||||||
|
</style>
|
|
@ -1,7 +1,6 @@
|
||||||
// generated by unplugin-vue-components
|
// generated by unplugin-vue-components
|
||||||
// We suggest you to commit this file into source control
|
// We suggest you to commit this file into source control
|
||||||
// Read more: https://github.com/vuejs/vue-next/pull/3399
|
// Read more: https://github.com/vuejs/vue-next/pull/3399
|
||||||
import '@vue/runtime-core'
|
|
||||||
|
|
||||||
declare module '@vue/runtime-core' {
|
declare module '@vue/runtime-core' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
@ -161,4 +160,4 @@ declare module '@vue/runtime-core' {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {}
|
export { }
|
||||||
|
|
|
@ -250,9 +250,9 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import _ from 'lodash'
|
import { get } from 'lodash-es'
|
||||||
import showdown from 'showdown'
|
import showdown from 'showdown'
|
||||||
import { humanSize } from '@/filters'
|
import { humanSize } from '@/modules/filters'
|
||||||
|
|
||||||
import SignupForm from '@/components/auth/SignupForm.vue'
|
import SignupForm from '@/components/auth/SignupForm.vue'
|
||||||
import LogoText from '@/components/LogoText.vue'
|
import LogoText from '@/components/LogoText.vue'
|
||||||
|
@ -279,31 +279,31 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
podName () {
|
podName () {
|
||||||
return _.get(this.nodeinfo, 'metadata.nodeName') || 'Funkwhale'
|
return get(this.nodeinfo, 'metadata.nodeName') || 'Funkwhale'
|
||||||
},
|
},
|
||||||
banner () {
|
banner () {
|
||||||
return _.get(this.nodeinfo, 'metadata.banner')
|
return get(this.nodeinfo, 'metadata.banner')
|
||||||
},
|
},
|
||||||
shortDescription () {
|
shortDescription () {
|
||||||
return _.get(this.nodeinfo, 'metadata.shortDescription')
|
return get(this.nodeinfo, 'metadata.shortDescription')
|
||||||
},
|
},
|
||||||
longDescription () {
|
longDescription () {
|
||||||
return _.get(this.nodeinfo, 'metadata.longDescription')
|
return get(this.nodeinfo, 'metadata.longDescription')
|
||||||
},
|
},
|
||||||
rules () {
|
rules () {
|
||||||
return _.get(this.nodeinfo, 'metadata.rules')
|
return get(this.nodeinfo, 'metadata.rules')
|
||||||
},
|
},
|
||||||
terms () {
|
terms () {
|
||||||
return _.get(this.nodeinfo, 'metadata.terms')
|
return get(this.nodeinfo, 'metadata.terms')
|
||||||
},
|
},
|
||||||
stats () {
|
stats () {
|
||||||
const data = {
|
const data = {
|
||||||
users: _.get(this.nodeinfo, 'usage.users.activeMonth', null),
|
users: get(this.nodeinfo, 'usage.users.activeMonth', null),
|
||||||
hours: _.get(this.nodeinfo, 'metadata.library.music.hours', null),
|
hours: get(this.nodeinfo, 'metadata.library.music.hours', null),
|
||||||
artists: _.get(this.nodeinfo, 'metadata.library.artists.total', null),
|
artists: get(this.nodeinfo, 'metadata.library.artists.total', null),
|
||||||
albums: _.get(this.nodeinfo, 'metadata.library.albums.total', null),
|
albums: get(this.nodeinfo, 'metadata.library.albums.total', null),
|
||||||
tracks: _.get(this.nodeinfo, 'metadata.library.tracks.total', null),
|
tracks: get(this.nodeinfo, 'metadata.library.tracks.total', null),
|
||||||
listenings: _.get(this.nodeinfo, 'metadata.usage.listenings.total', null)
|
listenings: get(this.nodeinfo, 'metadata.usage.listenings.total', null)
|
||||||
}
|
}
|
||||||
if (data.users === null || data.artists === null) {
|
if (data.users === null || data.artists === null) {
|
||||||
return
|
return
|
||||||
|
@ -311,28 +311,28 @@ export default {
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
contactEmail () {
|
contactEmail () {
|
||||||
return _.get(this.nodeinfo, 'metadata.contactEmail')
|
return get(this.nodeinfo, 'metadata.contactEmail')
|
||||||
},
|
},
|
||||||
anonymousCanListen () {
|
anonymousCanListen () {
|
||||||
return _.get(this.nodeinfo, 'metadata.library.anonymousCanListen')
|
return get(this.nodeinfo, 'metadata.library.anonymousCanListen')
|
||||||
},
|
},
|
||||||
allowListEnabled () {
|
allowListEnabled () {
|
||||||
return _.get(this.nodeinfo, 'metadata.allowList.enabled')
|
return get(this.nodeinfo, 'metadata.allowList.enabled')
|
||||||
},
|
},
|
||||||
allowListDomains () {
|
allowListDomains () {
|
||||||
return _.get(this.nodeinfo, 'metadata.allowList.domains')
|
return get(this.nodeinfo, 'metadata.allowList.domains')
|
||||||
},
|
},
|
||||||
version () {
|
version () {
|
||||||
return _.get(this.nodeinfo, 'software.version')
|
return get(this.nodeinfo, 'software.version')
|
||||||
},
|
},
|
||||||
openRegistrations () {
|
openRegistrations () {
|
||||||
return _.get(this.nodeinfo, 'openRegistrations')
|
return get(this.nodeinfo, 'openRegistrations')
|
||||||
},
|
},
|
||||||
defaultUploadQuota () {
|
defaultUploadQuota () {
|
||||||
return humanSize(_.get(this.nodeinfo, 'metadata.defaultUploadQuota') * 1000 * 1000)
|
return humanSize(get(this.nodeinfo, 'metadata.defaultUploadQuota') * 1000 * 1000)
|
||||||
},
|
},
|
||||||
federationEnabled () {
|
federationEnabled () {
|
||||||
return _.get(this.nodeinfo, 'metadata.library.federationEnabled')
|
return get(this.nodeinfo, 'metadata.library.federationEnabled')
|
||||||
},
|
},
|
||||||
headerStyle () {
|
headerStyle () {
|
||||||
if (!this.banner) {
|
if (!this.banner) {
|
||||||
|
|
|
@ -434,7 +434,7 @@ We render some markdown to html here, the content is set by the admin so we shou
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import _ from 'lodash'
|
import { get } from 'lodash-es'
|
||||||
import showdown from 'showdown'
|
import showdown from 'showdown'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -455,31 +455,31 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
podName () {
|
podName () {
|
||||||
return _.get(this.nodeinfo, 'metadata.nodeName') || 'Funkwhale'
|
return get(this.nodeinfo, 'metadata.nodeName') || 'Funkwhale'
|
||||||
},
|
},
|
||||||
banner () {
|
banner () {
|
||||||
return _.get(this.nodeinfo, 'metadata.banner')
|
return get(this.nodeinfo, 'metadata.banner')
|
||||||
},
|
},
|
||||||
shortDescription () {
|
shortDescription () {
|
||||||
return _.get(this.nodeinfo, 'metadata.shortDescription')
|
return get(this.nodeinfo, 'metadata.shortDescription')
|
||||||
},
|
},
|
||||||
longDescription () {
|
longDescription () {
|
||||||
return _.get(this.nodeinfo, 'metadata.longDescription')
|
return get(this.nodeinfo, 'metadata.longDescription')
|
||||||
},
|
},
|
||||||
rules () {
|
rules () {
|
||||||
return _.get(this.nodeinfo, 'metadata.rules')
|
return get(this.nodeinfo, 'metadata.rules')
|
||||||
},
|
},
|
||||||
terms () {
|
terms () {
|
||||||
return _.get(this.nodeinfo, 'metadata.terms')
|
return get(this.nodeinfo, 'metadata.terms')
|
||||||
},
|
},
|
||||||
stats () {
|
stats () {
|
||||||
const data = {
|
const data = {
|
||||||
users: _.get(this.nodeinfo, 'usage.users.activeMonth', null),
|
users: get(this.nodeinfo, 'usage.users.activeMonth', null),
|
||||||
hours: _.get(this.nodeinfo, 'metadata.library.music.hours', null),
|
hours: get(this.nodeinfo, 'metadata.library.music.hours', null),
|
||||||
artists: _.get(this.nodeinfo, 'metadata.library.artists.total', null),
|
artists: get(this.nodeinfo, 'metadata.library.artists.total', null),
|
||||||
albums: _.get(this.nodeinfo, 'metadata.library.albums.total', null),
|
albums: get(this.nodeinfo, 'metadata.library.albums.total', null),
|
||||||
tracks: _.get(this.nodeinfo, 'metadata.library.tracks.total', null),
|
tracks: get(this.nodeinfo, 'metadata.library.tracks.total', null),
|
||||||
listenings: _.get(this.nodeinfo, 'metadata.usage.listenings.total', null)
|
listenings: get(this.nodeinfo, 'metadata.usage.listenings.total', null)
|
||||||
}
|
}
|
||||||
if (data.users === null || data.artists === null) {
|
if (data.users === null || data.artists === null) {
|
||||||
return
|
return
|
||||||
|
@ -487,28 +487,28 @@ export default {
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
contactEmail () {
|
contactEmail () {
|
||||||
return _.get(this.nodeinfo, 'metadata.contactEmail')
|
return get(this.nodeinfo, 'metadata.contactEmail')
|
||||||
},
|
},
|
||||||
anonymousCanListen () {
|
anonymousCanListen () {
|
||||||
return _.get(this.nodeinfo, 'metadata.library.anonymousCanListen')
|
return get(this.nodeinfo, 'metadata.library.anonymousCanListen')
|
||||||
},
|
},
|
||||||
allowListEnabled () {
|
allowListEnabled () {
|
||||||
return _.get(this.nodeinfo, 'metadata.allowList.enabled')
|
return get(this.nodeinfo, 'metadata.allowList.enabled')
|
||||||
},
|
},
|
||||||
allowListDomains () {
|
allowListDomains () {
|
||||||
return _.get(this.nodeinfo, 'metadata.allowList.domains')
|
return get(this.nodeinfo, 'metadata.allowList.domains')
|
||||||
},
|
},
|
||||||
version () {
|
version () {
|
||||||
return _.get(this.nodeinfo, 'software.version')
|
return get(this.nodeinfo, 'software.version')
|
||||||
},
|
},
|
||||||
openRegistrations () {
|
openRegistrations () {
|
||||||
return _.get(this.nodeinfo, 'openRegistrations')
|
return get(this.nodeinfo, 'openRegistrations')
|
||||||
},
|
},
|
||||||
defaultUploadQuota () {
|
defaultUploadQuota () {
|
||||||
return _.get(this.nodeinfo, 'metadata.defaultUploadQuota')
|
return get(this.nodeinfo, 'metadata.defaultUploadQuota')
|
||||||
},
|
},
|
||||||
federationEnabled () {
|
federationEnabled () {
|
||||||
return _.get(this.nodeinfo, 'metadata.library.federationEnabled')
|
return get(this.nodeinfo, 'metadata.library.federationEnabled')
|
||||||
},
|
},
|
||||||
headerStyle () {
|
headerStyle () {
|
||||||
if (!this.banner) {
|
if (!this.banner) {
|
||||||
|
|
|
@ -325,14 +325,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _ from 'lodash'
|
import { get } from 'lodash-es'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import showdown from 'showdown'
|
import showdown from 'showdown'
|
||||||
import AlbumWidget from '@/components/audio/album/Widget.vue'
|
import AlbumWidget from '@/components/audio/album/Widget.vue'
|
||||||
import ChannelsWidget from '@/components/audio/ChannelsWidget.vue'
|
import ChannelsWidget from '@/components/audio/ChannelsWidget.vue'
|
||||||
import LoginForm from '@/components/auth/LoginForm.vue'
|
import LoginForm from '@/components/auth/LoginForm.vue'
|
||||||
import SignupForm from '@/components/auth/SignupForm.vue'
|
import SignupForm from '@/components/auth/SignupForm.vue'
|
||||||
import { humanSize } from '@/filters'
|
import { humanSize } from '@/modules/filters'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -358,19 +358,19 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
podName () {
|
podName () {
|
||||||
return _.get(this.nodeinfo, 'metadata.nodeName') || 'Funkwhale'
|
return get(this.nodeinfo, 'metadata.nodeName') || 'Funkwhale'
|
||||||
},
|
},
|
||||||
banner () {
|
banner () {
|
||||||
return _.get(this.nodeinfo, 'metadata.banner')
|
return get(this.nodeinfo, 'metadata.banner')
|
||||||
},
|
},
|
||||||
shortDescription () {
|
shortDescription () {
|
||||||
return _.get(this.nodeinfo, 'metadata.shortDescription')
|
return get(this.nodeinfo, 'metadata.shortDescription')
|
||||||
},
|
},
|
||||||
longDescription () {
|
longDescription () {
|
||||||
return _.get(this.nodeinfo, 'metadata.longDescription')
|
return get(this.nodeinfo, 'metadata.longDescription')
|
||||||
},
|
},
|
||||||
rules () {
|
rules () {
|
||||||
return _.get(this.nodeinfo, 'metadata.rules')
|
return get(this.nodeinfo, 'metadata.rules')
|
||||||
},
|
},
|
||||||
renderedDescription () {
|
renderedDescription () {
|
||||||
if (!this.longDescription) {
|
if (!this.longDescription) {
|
||||||
|
@ -381,8 +381,8 @@ export default {
|
||||||
},
|
},
|
||||||
stats () {
|
stats () {
|
||||||
const data = {
|
const data = {
|
||||||
users: _.get(this.nodeinfo, 'usage.users.activeMonth', null),
|
users: get(this.nodeinfo, 'usage.users.activeMonth', null),
|
||||||
hours: _.get(this.nodeinfo, 'metadata.library.music.hours', null)
|
hours: get(this.nodeinfo, 'metadata.library.music.hours', null)
|
||||||
}
|
}
|
||||||
if (data.users === null || data.artists === null) {
|
if (data.users === null || data.artists === null) {
|
||||||
return
|
return
|
||||||
|
@ -390,16 +390,16 @@ export default {
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
contactEmail () {
|
contactEmail () {
|
||||||
return _.get(this.nodeinfo, 'metadata.contactEmail')
|
return get(this.nodeinfo, 'metadata.contactEmail')
|
||||||
},
|
},
|
||||||
defaultUploadQuota () {
|
defaultUploadQuota () {
|
||||||
return _.get(this.nodeinfo, 'metadata.defaultUploadQuota')
|
return get(this.nodeinfo, 'metadata.defaultUploadQuota')
|
||||||
},
|
},
|
||||||
anonymousCanListen () {
|
anonymousCanListen () {
|
||||||
return _.get(this.nodeinfo, 'metadata.library.anonymousCanListen')
|
return get(this.nodeinfo, 'metadata.library.anonymousCanListen')
|
||||||
},
|
},
|
||||||
openRegistrations () {
|
openRegistrations () {
|
||||||
return _.get(this.nodeinfo, 'openRegistrations')
|
return get(this.nodeinfo, 'openRegistrations')
|
||||||
},
|
},
|
||||||
headerStyle () {
|
headerStyle () {
|
||||||
if (!this.banner) {
|
if (!this.banner) {
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _ from 'lodash'
|
import { range as lodashRange, sortBy, uniq } from 'lodash-es'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
@ -57,15 +57,15 @@ export default {
|
||||||
pages: function () {
|
pages: function () {
|
||||||
const range = 2
|
const range = 2
|
||||||
const current = this.current
|
const current = this.current
|
||||||
const beginning = _.range(1, Math.min(this.maxPage, 1 + range))
|
const beginning = lodashRange(1, Math.min(this.maxPage, 1 + range))
|
||||||
const middle = _.range(
|
const middle = lodashRange(
|
||||||
Math.max(1, current - range + 1),
|
Math.max(1, current - range + 1),
|
||||||
Math.min(this.maxPage, current + range)
|
Math.min(this.maxPage, current + range)
|
||||||
)
|
)
|
||||||
const end = _.range(this.maxPage, Math.max(1, this.maxPage - range))
|
const end = lodashRange(this.maxPage, Math.max(1, this.maxPage - range))
|
||||||
let allowed = beginning.concat(middle, end)
|
let allowed = beginning.concat(middle, end)
|
||||||
allowed = _.uniq(allowed)
|
allowed = uniq(allowed)
|
||||||
allowed = _.sortBy(allowed, [
|
allowed = sortBy(allowed, [
|
||||||
e => {
|
e => {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
|
@ -345,8 +345,8 @@
|
||||||
import { mapState, mapGetters, mapActions } from 'vuex'
|
import { mapState, mapGetters, mapActions } from 'vuex'
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import lodash from 'lodash'
|
import { sum } from 'lodash-es'
|
||||||
import time from '@/utils/time.js'
|
import time from '@/utils/time'
|
||||||
import { createFocusTrap } from 'focus-trap'
|
import { createFocusTrap } from 'focus-trap'
|
||||||
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon.vue'
|
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon.vue'
|
||||||
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon.vue'
|
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon.vue'
|
||||||
|
@ -405,7 +405,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
timeLeft () {
|
timeLeft () {
|
||||||
const seconds = lodash.sum(
|
const seconds = sum(
|
||||||
this.queue.tracks.slice(this.queue.currentIndex).map((t) => {
|
this.queue.tracks.slice(this.queue.currentIndex).map((t) => {
|
||||||
return (t.uploads || []).map((u) => {
|
return (t.uploads || []).map((u) => {
|
||||||
return u.duration || 0
|
return u.duration || 0
|
||||||
|
|
|
@ -107,7 +107,7 @@
|
||||||
<script>
|
<script>
|
||||||
import Modal from '@/components/semantic/Modal.vue'
|
import Modal from '@/components/semantic/Modal.vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import _ from 'lodash'
|
import { uniq } from 'lodash-es'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -135,7 +135,7 @@ export default {
|
||||||
}
|
}
|
||||||
const self = this
|
const self = this
|
||||||
instances.push(this.$store.getters['instance/defaultUrl'](), 'https://demo.funkwhale.audio/')
|
instances.push(this.$store.getters['instance/defaultUrl'](), 'https://demo.funkwhale.audio/')
|
||||||
return _.uniq(instances.filter((e) => { return e !== self.$store.state.instance.instanceUrl }))
|
return uniq(instances.filter((e) => { return e !== self.$store.state.instance.instanceUrl }))
|
||||||
},
|
},
|
||||||
instanceHostname () {
|
instanceHostname () {
|
||||||
const url = this.$store.state.instance.instanceUrl
|
const url = this.$store.state.instance.instanceUrl
|
||||||
|
|
|
@ -208,17 +208,17 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<fieldset
|
<fieldset
|
||||||
v-for="theme in themes"
|
v-for="t in themes"
|
||||||
:key="theme.key"
|
:key="t.key"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
:id="theme.key"
|
:id="t.key"
|
||||||
v-model="themeSelection"
|
v-model="theme"
|
||||||
type="radio"
|
type="radio"
|
||||||
name="theme"
|
name="theme"
|
||||||
:value="theme.key"
|
:value="t.key"
|
||||||
>
|
>
|
||||||
<label :for="theme.key">{{ theme.name }}</label>
|
<label :for="t.key">{{ t.name }}</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
|
@ -475,11 +475,11 @@ import { mapState, mapActions, mapGetters } from 'vuex'
|
||||||
import UserModal from '@/components/common/UserModal.vue'
|
import UserModal from '@/components/common/UserModal.vue'
|
||||||
import Logo from '@/components/Logo.vue'
|
import Logo from '@/components/Logo.vue'
|
||||||
import SearchBar from '@/components/audio/SearchBar.vue'
|
import SearchBar from '@/components/audio/SearchBar.vue'
|
||||||
import ThemesMixin from '@/components/mixins/Themes.vue'
|
|
||||||
import UserMenu from '@/components/common/UserMenu.vue'
|
import UserMenu from '@/components/common/UserMenu.vue'
|
||||||
import Modal from '@/components/semantic/Modal.vue'
|
import Modal from '@/components/semantic/Modal.vue'
|
||||||
|
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
|
import useThemeList from '@/composables/useThemeList'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Sidebar',
|
name: 'Sidebar',
|
||||||
|
@ -490,10 +490,15 @@ export default {
|
||||||
UserModal,
|
UserModal,
|
||||||
Modal
|
Modal
|
||||||
},
|
},
|
||||||
mixins: [ThemesMixin],
|
|
||||||
props: {
|
props: {
|
||||||
width: { type: Number, required: true }
|
width: { type: Number, required: true }
|
||||||
},
|
},
|
||||||
|
setup () {
|
||||||
|
const theme = useTheme()
|
||||||
|
const themes = useThemeList()
|
||||||
|
|
||||||
|
return { theme, themes }
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
selectedTab: 'library',
|
selectedTab: 'library',
|
||||||
|
@ -504,8 +509,7 @@ export default {
|
||||||
showUserModal: false,
|
showUserModal: false,
|
||||||
showLanguageModal: false,
|
showLanguageModal: false,
|
||||||
showThemeModal: false,
|
showThemeModal: false,
|
||||||
languageSelection: this.$language.current,
|
languageSelection: this.$language.current
|
||||||
themeSelection: this.$store.state.ui.theme
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroy () {
|
destroy () {
|
||||||
|
@ -645,10 +649,6 @@ export default {
|
||||||
languageSelection: function (v) {
|
languageSelection: function (v) {
|
||||||
this.$store.dispatch('ui/currentLanguage', v)
|
this.$store.dispatch('ui/currentLanguage', v)
|
||||||
this.$refs.languageModal.closeModal()
|
this.$refs.languageModal.closeModal()
|
||||||
},
|
|
||||||
themeSelection: function (v) {
|
|
||||||
this.$store.dispatch('ui/theme', v)
|
|
||||||
this.$refs.themeModal.closeModal()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
@ -691,11 +691,14 @@ export default {
|
||||||
// works as expected
|
// works as expected
|
||||||
const link = $($el).closest('a')
|
const link = $($el).closest('a')
|
||||||
const url = link.attr('href')
|
const url = link.attr('href')
|
||||||
if (url.startsWith('http')) {
|
if (url) {
|
||||||
window.open(url, '_blank').focus()
|
if (url.startsWith('http')) {
|
||||||
} else {
|
window.open(url, '_blank').focus()
|
||||||
self.$router.push(url)
|
} else {
|
||||||
|
self.$router.push(url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$(self.$el).find(selector).dropdown('hide')
|
$(self.$el).find(selector).dropdown('hide')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -157,7 +157,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import lodash from 'lodash'
|
import { cloneDeep } from 'lodash-es'
|
||||||
import SignupFormBuilder from '@/components/admin/SignupFormBuilder.vue'
|
import SignupFormBuilder from '@/components/admin/SignupFormBuilder.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -241,7 +241,7 @@ export default {
|
||||||
},
|
},
|
||||||
set (key, value) {
|
set (key, value) {
|
||||||
// otherwise reactivity doesn't trigger :/
|
// otherwise reactivity doesn't trigger :/
|
||||||
this.values = lodash.cloneDeep(this.values)
|
this.values = cloneDeep(this.values)
|
||||||
this.$set(this.values, key, value)
|
this.$set(this.values, key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -161,7 +161,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import lodash from 'lodash'
|
import { cloneDeep, tap, set } from 'lodash-es'
|
||||||
|
|
||||||
import SignupForm from '@/components/auth/SignupForm.vue'
|
import SignupForm from '@/components/auth/SignupForm.vue'
|
||||||
|
|
||||||
|
@ -209,7 +209,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addField () {
|
addField () {
|
||||||
const newValue = lodash.tap(lodash.cloneDeep(this.local), v => v.fields.push({
|
const newValue = tap(cloneDeep(this.local), v => v.fields.push({
|
||||||
label: this.$pgettext('*/*/Form-builder', 'Additional field') + ' ' + (this.local.fields.length + 1),
|
label: this.$pgettext('*/*/Form-builder', 'Additional field') + ' ' + (this.local.fields.length + 1),
|
||||||
required: true,
|
required: true,
|
||||||
input_type: 'short_text'
|
input_type: 'short_text'
|
||||||
|
@ -217,7 +217,7 @@ export default {
|
||||||
this.$emit('input', newValue)
|
this.$emit('input', newValue)
|
||||||
},
|
},
|
||||||
remove (idx) {
|
remove (idx) {
|
||||||
this.$emit('input', lodash.tap(lodash.cloneDeep(this.local), v => v.fields.splice(idx, 1)))
|
this.$emit('input', tap(cloneDeep(this.local), v => v.fields.splice(idx, 1)))
|
||||||
},
|
},
|
||||||
move (idx, incr) {
|
move (idx, incr) {
|
||||||
if (idx === 0 && incr < 0) {
|
if (idx === 0 && incr < 0) {
|
||||||
|
@ -226,7 +226,7 @@ export default {
|
||||||
if (idx + incr >= this.local.fields.length) {
|
if (idx + incr >= this.local.fields.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const newFields = arrayMove(lodash.cloneDeep(this.local).fields, idx, idx + incr)
|
const newFields = arrayMove(cloneDeep(this.local).fields, idx, idx + incr)
|
||||||
this.update('fields', newFields)
|
this.update('fields', newFields)
|
||||||
},
|
},
|
||||||
update (key, value) {
|
update (key, value) {
|
||||||
|
@ -241,7 +241,7 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.$emit('input', lodash.tap(lodash.cloneDeep(this.local), v => lodash.set(v, key, value)))
|
this.$emit('input', tap(cloneDeep(this.local), v => set(v, key, value)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@
|
||||||
import PlayButton from '@/components/audio/PlayButton.vue'
|
import PlayButton from '@/components/audio/PlayButton.vue'
|
||||||
import TagsList from '@/components/tags/List.vue'
|
import TagsList from '@/components/tags/List.vue'
|
||||||
|
|
||||||
import { momentFormat } from '@/filters'
|
import { momentFormat } from '@/modules/filters'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _ from 'lodash'
|
import { clone } from 'lodash-es'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import PodcastTable from '@/components/audio/podcast/Table.vue'
|
import PodcastTable from '@/components/audio/podcast/Table.vue'
|
||||||
import TrackTable from '@/components/audio/track/Table.vue'
|
import TrackTable from '@/components/audio/track/Table.vue'
|
||||||
|
@ -96,7 +96,7 @@ export default {
|
||||||
}
|
}
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
const self = this
|
const self = this
|
||||||
const params = _.clone(this.filters)
|
const params = clone(this.filters)
|
||||||
params.page_size = this.limit
|
params.page_size = this.limit
|
||||||
params.page = this.page
|
params.page = this.page
|
||||||
params.include_channels = true
|
params.include_channels = true
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _ from 'lodash'
|
import { clone } from 'lodash-es'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import ChannelSerieCard from '@/components/audio/ChannelSerieCard.vue'
|
import ChannelSerieCard from '@/components/audio/ChannelSerieCard.vue'
|
||||||
import AlbumCard from '@/components/audio/album/Card.vue'
|
import AlbumCard from '@/components/audio/album/Card.vue'
|
||||||
|
@ -87,7 +87,7 @@ export default {
|
||||||
}
|
}
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
const self = this
|
const self = this
|
||||||
const params = _.clone(this.filters)
|
const params = clone(this.filters)
|
||||||
params.page_size = this.limit
|
params.page_size = this.limit
|
||||||
params.include_channels = true
|
params.include_channels = true
|
||||||
axios.get(url, { params: params }).then((response) => {
|
axios.get(url, { params: params }).then((response) => {
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _ from 'lodash'
|
import { clone } from 'lodash-es'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import ChannelCard from '@/components/audio/ChannelCard.vue'
|
import ChannelCard from '@/components/audio/ChannelCard.vue'
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ export default {
|
||||||
}
|
}
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
const self = this
|
const self = this
|
||||||
const params = _.clone(this.filters)
|
const params = clone(this.filters)
|
||||||
params.page_size = this.limit
|
params.page_size = this.limit
|
||||||
params.include_channels = true
|
params.include_channels = true
|
||||||
axios.get(url, { params: params }).then((response) => {
|
axios.get(url, { params: params }).then((response) => {
|
||||||
|
|
|
@ -106,7 +106,7 @@
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import _ from 'lodash'
|
import { get } from 'lodash-es'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
@ -131,7 +131,7 @@ export default {
|
||||||
nodeinfo: state => state.instance.nodeinfo
|
nodeinfo: state => state.instance.nodeinfo
|
||||||
}),
|
}),
|
||||||
anonymousCanListen () {
|
anonymousCanListen () {
|
||||||
return _.get(this.nodeinfo, 'metadata.library.anonymousCanListen', false)
|
return get(this.nodeinfo, 'metadata.library.anonymousCanListen', false)
|
||||||
},
|
},
|
||||||
iframeSrc () {
|
iframeSrc () {
|
||||||
let base = import.meta.env.BASE_URL
|
let base = import.meta.env.BASE_URL
|
||||||
|
|
|
@ -342,12 +342,12 @@ import { mapState, mapGetters, mapActions } from 'vuex'
|
||||||
import GlobalEvents from '@/components/utils/global-events.vue'
|
import GlobalEvents from '@/components/utils/global-events.vue'
|
||||||
import { toLinearVolumeScale } from '@/audio/volume.js'
|
import { toLinearVolumeScale } from '@/audio/volume.js'
|
||||||
import { Howl, Howler } from 'howler'
|
import { Howl, Howler } from 'howler'
|
||||||
import _ from 'lodash'
|
import { throttle, reverse } from 'lodash-es'
|
||||||
import url from '@/utils/url'
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import VolumeControl from './VolumeControl.vue'
|
import VolumeControl from './VolumeControl.vue'
|
||||||
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon.vue'
|
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon.vue'
|
||||||
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon.vue'
|
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon.vue'
|
||||||
|
import updateQueryString from '@/composables/updateQueryString'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -399,7 +399,7 @@ export default {
|
||||||
progress: 'player/progress'
|
progress: 'player/progress'
|
||||||
}),
|
}),
|
||||||
updateProgressThrottled () {
|
updateProgressThrottled () {
|
||||||
return _.throttle(this.updateProgress, 50)
|
return throttle(this.updateProgress, 50)
|
||||||
},
|
},
|
||||||
labels () {
|
labels () {
|
||||||
const audioPlayer = this.$pgettext('Sidebar/Player/Hidden text', 'Media player')
|
const audioPlayer = this.$pgettext('Sidebar/Player/Hidden text', 'Media player')
|
||||||
|
@ -660,7 +660,7 @@ export default {
|
||||||
// not support other codecs to be able to play it :)
|
// not support other codecs to be able to play it :)
|
||||||
sources.push({
|
sources.push({
|
||||||
type: 'mp3',
|
type: 'mp3',
|
||||||
url: url.updateQueryString(
|
url: updateQueryString(
|
||||||
this.$store.getters['instance/absoluteUrl'](trackData.listen_url),
|
this.$store.getters['instance/absoluteUrl'](trackData.listen_url),
|
||||||
'to',
|
'to',
|
||||||
'mp3'
|
'mp3'
|
||||||
|
@ -680,7 +680,7 @@ export default {
|
||||||
value = this.$store.state.auth.scopedTokens.listen
|
value = this.$store.state.auth.scopedTokens.listen
|
||||||
}
|
}
|
||||||
sources.forEach(e => {
|
sources.forEach(e => {
|
||||||
e.url = url.updateQueryString(e.url, param, value)
|
e.url = updateQueryString(e.url, param, value)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return sources
|
return sources
|
||||||
|
@ -821,14 +821,14 @@ export default {
|
||||||
checkCache () {
|
checkCache () {
|
||||||
const self = this
|
const self = this
|
||||||
const toKeep = []
|
const toKeep = []
|
||||||
_.reverse(this.soundsCache).forEach((e) => {
|
reverse(this.soundsCache).forEach((e) => {
|
||||||
if (toKeep.length < self.maxPreloaded) {
|
if (toKeep.length < self.maxPreloaded) {
|
||||||
toKeep.push(e)
|
toKeep.push(e)
|
||||||
} else {
|
} else {
|
||||||
e.sound.unload()
|
e.sound.unload()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.soundsCache = _.reverse(toKeep)
|
this.soundsCache = reverse(toKeep)
|
||||||
},
|
},
|
||||||
removeFromCache (sound) {
|
removeFromCache (sound) {
|
||||||
const toKeep = []
|
const toKeep = []
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _ from 'lodash'
|
import { debounce } from 'lodash-es'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import AlbumCard from '@/components/audio/album/Card.vue'
|
import AlbumCard from '@/components/audio/album/Card.vue'
|
||||||
|
@ -112,7 +112,7 @@ export default {
|
||||||
this.search()
|
this.search()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
search: _.debounce(function () {
|
search: debounce(function () {
|
||||||
if (this.query.length < 1) {
|
if (this.query.length < 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
<script>
|
<script>
|
||||||
import jQuery from 'jquery'
|
import jQuery from 'jquery'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import lodash from 'lodash'
|
import { trim } from 'lodash-es'
|
||||||
import GlobalEvents from '@/components/utils/global-events.vue'
|
import GlobalEvents from '@/components/utils/global-events.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -248,8 +248,8 @@ export default {
|
||||||
this.$refs.search.focus()
|
this.$refs.search.focus()
|
||||||
},
|
},
|
||||||
extractObjId (query) {
|
extractObjId (query) {
|
||||||
query = lodash.trim(query)
|
query = trim(query)
|
||||||
query = lodash.trim(query, '@')
|
query = trim(query, '@')
|
||||||
if (query.indexOf(' ') > -1) {
|
if (query.indexOf(' ') > -1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,12 +158,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _ from 'lodash'
|
import { clone } from 'lodash-es'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import TrackRow from '@/components/audio/track/Row.vue'
|
import TrackRow from '@/components/audio/track/Row.vue'
|
||||||
import TrackMobileRow from '@/components/audio/track/MobileRow.vue'
|
import TrackMobileRow from '@/components/audio/track/MobileRow.vue'
|
||||||
import Pagination from '@/components/Pagination.vue'
|
import Pagination from '@/components/Pagination.vue'
|
||||||
import { unique } from '@/filters'
|
import { unique } from '@/modules/filters'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -228,7 +228,7 @@ export default {
|
||||||
}
|
}
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
const self = this
|
const self = this
|
||||||
const params = _.clone(this.filters)
|
const params = clone(this.filters)
|
||||||
params.page_size = this.paginateBy
|
params.page_size = this.paginateBy
|
||||||
params.page = this.currentPage
|
params.page = this.currentPage
|
||||||
params.include_channels = true
|
params.include_channels = true
|
||||||
|
|
|
@ -135,7 +135,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _ from 'lodash'
|
import { clone } from 'lodash-es'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import PlayButton from '@/components/audio/PlayButton.vue'
|
import PlayButton from '@/components/audio/PlayButton.vue'
|
||||||
import TagsList from '@/components/tags/List.vue'
|
import TagsList from '@/components/tags/List.vue'
|
||||||
|
@ -184,7 +184,7 @@ export default {
|
||||||
}
|
}
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
const self = this
|
const self = this
|
||||||
const params = _.clone(this.filters)
|
const params = clone(this.filters)
|
||||||
params.page_size = this.limit
|
params.page_size = this.limit
|
||||||
params.offset = this.offset
|
params.offset = this.offset
|
||||||
axios.get(url, { params: params }).then((response) => {
|
axios.get(url, { params: params }).then((response) => {
|
||||||
|
|
|
@ -119,7 +119,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _ from 'lodash'
|
import { uniq } from 'lodash-es'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import TranslationsMixin from '@/components/mixins/Translations.vue'
|
import TranslationsMixin from '@/components/mixins/Translations.vue'
|
||||||
|
|
||||||
|
@ -163,7 +163,7 @@ export default {
|
||||||
return this.fields.scopes.split(' ')
|
return this.fields.scopes.split(' ')
|
||||||
},
|
},
|
||||||
set (v) {
|
set (v) {
|
||||||
this.fields.scopes = _.uniq(v).join(' ')
|
this.fields.scopes = uniq(v).join(' ')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
allScopes () {
|
allScopes () {
|
||||||
|
|
|
@ -229,8 +229,8 @@ export default {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
async created () {
|
||||||
checkRedirectToLogin(this.$store, this.$router)
|
await checkRedirectToLogin(this.$store, this.$router)
|
||||||
if (this.clientId) {
|
if (this.clientId) {
|
||||||
this.fetchApplication()
|
this.fetchApplication()
|
||||||
}
|
}
|
||||||
|
|
|
@ -166,7 +166,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import lodash from 'lodash'
|
import { clone } from 'lodash-es'
|
||||||
import showdown from 'showdown'
|
import showdown from 'showdown'
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
@ -178,7 +178,7 @@ export default {
|
||||||
markdown: new showdown.Converter(),
|
markdown: new showdown.Converter(),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
enabled: this.plugin.enabled,
|
enabled: this.plugin.enabled,
|
||||||
values: lodash.clone(this.plugin.values || {}),
|
values: clone(this.plugin.values || {}),
|
||||||
errors: []
|
errors: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -144,7 +144,7 @@
|
||||||
<script>
|
<script>
|
||||||
import Modal from '@/components/semantic/Modal.vue'
|
import Modal from '@/components/semantic/Modal.vue'
|
||||||
import ChannelUploadForm from '@/components/channels/UploadForm.vue'
|
import ChannelUploadForm from '@/components/channels/UploadForm.vue'
|
||||||
import { humanSize } from '@/filters'
|
import { humanSize } from '@/modules/filters'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { secondsToObject } from '@/filters'
|
import { secondsToObject } from '@/modules/filters'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: { seconds: { type: Number, default: null } },
|
props: { seconds: { type: Number, default: null } },
|
||||||
|
|
|
@ -26,14 +26,14 @@
|
||||||
class="menu"
|
class="menu"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
v-for="theme in themes"
|
v-for="t in themes"
|
||||||
:key="theme.key"
|
:key="t.key"
|
||||||
:class="[{'active': $store.state.ui.theme === theme.key}, 'item']"
|
:class="[{'active': theme === t.key}, 'item']"
|
||||||
:value="theme.key"
|
:value="t.key"
|
||||||
@click="$store.dispatch('ui/theme', theme.key)"
|
@click="theme = t.key"
|
||||||
>
|
>
|
||||||
<i :class="theme.icon" />
|
<i :class="t.icon" />
|
||||||
{{ theme.name }}
|
{{ t.name }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -154,11 +154,16 @@
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
|
import useThemeList from '@/composables/useThemeList'
|
||||||
import ThemesMixin from '@/components/mixins/Themes.vue'
|
import useTheme from '@/composables/useTheme'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [ThemesMixin],
|
setup () {
|
||||||
|
return {
|
||||||
|
theme: useTheme(),
|
||||||
|
themes: useThemeList()
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
labels () {
|
labels () {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
<i class="palette icon user-modal list-icon" />
|
<i class="palette icon user-modal list-icon" />
|
||||||
<span class="user-modal list-item">{{ labels.theme }}:</span>
|
<span class="user-modal list-item">{{ labels.theme }}:</span>
|
||||||
<div class="right floated">
|
<div class="right floated">
|
||||||
<span class="user-modal list-item"> {{ themes.find(x => x.key ===$store.state.ui.theme).name }}</span>
|
<span class="user-modal list-item"> {{ themes.find(x => x.key === theme).name }}</span>
|
||||||
<i class="action-hint chevron right icon user-modal" />
|
<i class="action-hint chevron right icon user-modal" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -175,17 +175,23 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Modal from '@/components/semantic/Modal.vue'
|
import Modal from '@/components/semantic/Modal.vue'
|
||||||
import ThemesMixin from '@/components/mixins/Themes.vue'
|
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
|
import useThemeList from '@/composables/useThemeList'
|
||||||
|
import useTheme from '@/composables/useTheme'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Modal
|
Modal
|
||||||
},
|
},
|
||||||
mixins: [ThemesMixin],
|
|
||||||
props: {
|
props: {
|
||||||
show: { type: Boolean, required: true }
|
show: { type: Boolean, required: true }
|
||||||
},
|
},
|
||||||
|
setup () {
|
||||||
|
return {
|
||||||
|
theme: useTheme(),
|
||||||
|
themes: useThemeList()
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
labels () {
|
labels () {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -191,8 +191,8 @@ export default {
|
||||||
this.updateQueryString()
|
this.updateQueryString()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
async created () {
|
||||||
checkRedirectToLogin(this.$store, this.$router)
|
await checkRedirectToLogin(this.$store, this.$router)
|
||||||
this.fetchFavorites(FAVORITES_URL)
|
this.fetchFavorites(FAVORITES_URL)
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _ from 'lodash'
|
import { clone } from 'lodash-es'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import LibraryCard from '@/views/content/remote/Card.vue'
|
import LibraryCard from '@/views/content/remote/Card.vue'
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ export default {
|
||||||
fetchData (url) {
|
fetchData (url) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
const self = this
|
const self = this
|
||||||
const params = _.clone({})
|
const params = clone({})
|
||||||
params.page_size = this.limit
|
params.page_size = this.limit
|
||||||
params.offset = this.offset
|
params.offset = this.offset
|
||||||
axios.get(url, { params: params }).then((response) => {
|
axios.get(url, { params: params }).then((response) => {
|
||||||
|
|
|
@ -249,7 +249,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import lodash from 'lodash'
|
import { sum } from 'lodash-es'
|
||||||
import PlayButton from '@/components/audio/PlayButton.vue'
|
import PlayButton from '@/components/audio/PlayButton.vue'
|
||||||
import TagsList from '@/components/tags/List.vue'
|
import TagsList from '@/components/tags/List.vue'
|
||||||
import ArtistLabel from '@/components/audio/ArtistLabel.vue'
|
import ArtistLabel from '@/components/audio/ArtistLabel.vue'
|
||||||
|
@ -307,7 +307,7 @@ export default {
|
||||||
durations.push(t.uploads[0].duration)
|
durations.push(t.uploads[0].duration)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return lodash.sum(durations)
|
return sum(durations)
|
||||||
},
|
},
|
||||||
labels () {
|
labels () {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -92,7 +92,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import time from '@/utils/time.js'
|
import time from '@/utils/time'
|
||||||
import LibraryWidget from '@/components/federation/LibraryWidget.vue'
|
import LibraryWidget from '@/components/federation/LibraryWidget.vue'
|
||||||
import ChannelEntries from '@/components/audio/ChannelEntries.vue'
|
import ChannelEntries from '@/components/audio/ChannelEntries.vue'
|
||||||
import TrackTable from '@/components/audio/track/Table.vue'
|
import TrackTable from '@/components/audio/track/Table.vue'
|
||||||
|
|
|
@ -215,7 +215,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import logger from '@/logging.js'
|
import logger from '@/logging'
|
||||||
import PlayButton from '@/components/audio/PlayButton.vue'
|
import PlayButton from '@/components/audio/PlayButton.vue'
|
||||||
import EmbedWizard from '@/components/audio/EmbedWizard.vue'
|
import EmbedWizard from '@/components/audio/EmbedWizard.vue'
|
||||||
import Modal from '@/components/semantic/Modal.vue'
|
import Modal from '@/components/semantic/Modal.vue'
|
||||||
|
@ -223,7 +223,7 @@ import RadioButton from '@/components/radios/Button.vue'
|
||||||
import TagsList from '@/components/tags/List.vue'
|
import TagsList from '@/components/tags/List.vue'
|
||||||
import ReportMixin from '@/components/mixins/Report.vue'
|
import ReportMixin from '@/components/mixins/Report.vue'
|
||||||
|
|
||||||
import { getDomain } from '@/utils.js'
|
import { getDomain } from '@/utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -167,7 +167,7 @@ import qs from 'qs'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
|
|
||||||
import logger from '@/logging.js'
|
import logger from '@/logging'
|
||||||
|
|
||||||
import OrderingMixin from '@/components/mixins/Ordering.vue'
|
import OrderingMixin from '@/components/mixins/Ordering.vue'
|
||||||
import PaginationMixin from '@/components/mixins/Pagination.vue'
|
import PaginationMixin from '@/components/mixins/Pagination.vue'
|
||||||
|
|
|
@ -240,7 +240,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
import _ from 'lodash'
|
import { isEqual, clone } from 'lodash-es'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import AttachmentInput from '@/components/common/AttachmentInput.vue'
|
import AttachmentInput from '@/components/common/AttachmentInput.vue'
|
||||||
import EditList from '@/components/library/EditList.vue'
|
import EditList from '@/components/library/EditList.vue'
|
||||||
|
@ -296,7 +296,7 @@ export default {
|
||||||
mutationPayload () {
|
mutationPayload () {
|
||||||
const self = this
|
const self = this
|
||||||
const changedFields = this.config.fields.filter(f => {
|
const changedFields = this.config.fields.filter(f => {
|
||||||
return !_.isEqual(self.values[f.id], self.initialValues[f.id])
|
return !isEqual(self.values[f.id], self.initialValues[f.id])
|
||||||
})
|
})
|
||||||
if (changedFields.length === 0) {
|
if (changedFields.length === 0) {
|
||||||
return null
|
return null
|
||||||
|
@ -339,15 +339,15 @@ export default {
|
||||||
setValues () {
|
setValues () {
|
||||||
const self = this
|
const self = this
|
||||||
this.config.fields.forEach(f => {
|
this.config.fields.forEach(f => {
|
||||||
self.$set(self.values, f.id, _.clone(f.getValue(self.object)))
|
self.$set(self.values, f.id, clone(f.getValue(self.object)))
|
||||||
self.$set(self.initialValues, f.id, _.clone(self.values[f.id]))
|
self.$set(self.initialValues, f.id, clone(self.values[f.id]))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
submit () {
|
submit () {
|
||||||
const self = this
|
const self = this
|
||||||
self.isLoading = true
|
self.isLoading = true
|
||||||
self.errors = []
|
self.errors = []
|
||||||
const payload = _.clone(this.mutationPayload || {})
|
const payload = clone(this.mutationPayload || {})
|
||||||
if (this.canEdit) {
|
if (this.canEdit) {
|
||||||
payload.is_approved = true
|
payload.is_approved = true
|
||||||
}
|
}
|
||||||
|
@ -363,10 +363,10 @@ export default {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
fieldValuesChanged (fieldId) {
|
fieldValuesChanged (fieldId) {
|
||||||
return !_.isEqual(this.values[fieldId], this.initialValues[fieldId])
|
return !isEqual(this.values[fieldId], this.initialValues[fieldId])
|
||||||
},
|
},
|
||||||
resetField (fieldId) {
|
resetField (fieldId) {
|
||||||
this.values[fieldId] = _.clone(this.initialValues[fieldId])
|
this.values[fieldId] = clone(this.initialValues[fieldId])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _ from 'lodash'
|
import { clone } from 'lodash-es'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import EditCard from '@/components/library/EditCard.vue'
|
import EditCard from '@/components/library/EditCard.vue'
|
||||||
|
@ -84,7 +84,7 @@ export default {
|
||||||
}
|
}
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
const self = this
|
const self = this
|
||||||
const params = _.clone(this.filters)
|
const params = clone(this.filters)
|
||||||
params.page_size = this.limit
|
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.previousPage = response.data.previous
|
||||||
|
|
|
@ -311,7 +311,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _ from 'lodash'
|
import { sortBy, debounce } from 'lodash-es'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import FileUploadWidget from './FileUploadWidget.vue'
|
import FileUploadWidget from './FileUploadWidget.vue'
|
||||||
import FsBrowser from './FsBrowser.vue'
|
import FsBrowser from './FsBrowser.vue'
|
||||||
|
@ -435,7 +435,7 @@ export default {
|
||||||
sortedFiles () {
|
sortedFiles () {
|
||||||
// return errored files on top
|
// return errored files on top
|
||||||
|
|
||||||
return _.sortBy(this.files.map(f => {
|
return sortBy(this.files.map(f => {
|
||||||
let statusIndex = 0
|
let statusIndex = 0
|
||||||
if (f.errored) {
|
if (f.errored) {
|
||||||
statusIndex = -1
|
statusIndex = -1
|
||||||
|
@ -467,7 +467,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
importReference: _.debounce(function () {
|
importReference: debounce(function () {
|
||||||
this.$router.replace({ query: { import: this.importReference } })
|
this.$router.replace({ query: { import: this.importReference } })
|
||||||
}, 500),
|
}, 500),
|
||||||
remainingSpace (newValue) {
|
remainingSpace (newValue) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import FileUpload from 'vue-upload-component'
|
import FileUpload from 'vue-upload-component'
|
||||||
import { setCsrf } from '@/utils.js'
|
import { setCsrf } from '@/utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
extends: FileUpload,
|
extends: FileUpload,
|
||||||
|
|
|
@ -72,7 +72,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import logger from '@/logging.js'
|
import logger from '@/logging'
|
||||||
import ChannelsWidget from '@/components/audio/ChannelsWidget.vue'
|
import ChannelsWidget from '@/components/audio/ChannelsWidget.vue'
|
||||||
import TrackWidget from '@/components/audio/track/Widget.vue'
|
import TrackWidget from '@/components/audio/track/Widget.vue'
|
||||||
import AlbumWidget from '@/components/audio/album/Widget.vue'
|
import AlbumWidget from '@/components/audio/album/Widget.vue'
|
||||||
|
|
|
@ -199,7 +199,7 @@ import qs from 'qs'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
|
|
||||||
import logger from '@/logging.js'
|
import logger from '@/logging'
|
||||||
|
|
||||||
import OrderingMixin from '@/components/mixins/Ordering.vue'
|
import OrderingMixin from '@/components/mixins/Ordering.vue'
|
||||||
import PaginationMixin from '@/components/mixins/Pagination.vue'
|
import PaginationMixin from '@/components/mixins/Pagination.vue'
|
||||||
|
|
|
@ -178,7 +178,7 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
|
|
||||||
import logger from '@/logging.js'
|
import logger from '@/logging'
|
||||||
|
|
||||||
import OrderingMixin from '@/components/mixins/Ordering.vue'
|
import OrderingMixin from '@/components/mixins/Ordering.vue'
|
||||||
import PaginationMixin from '@/components/mixins/Pagination.vue'
|
import PaginationMixin from '@/components/mixins/Pagination.vue'
|
||||||
|
|
|
@ -20,14 +20,14 @@
|
||||||
<script>
|
<script>
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
|
|
||||||
import lodash from 'lodash'
|
import { isEqual } from 'lodash-es'
|
||||||
export default {
|
export default {
|
||||||
props: { value: { type: Array, required: true } },
|
props: { value: { type: Array, required: true } },
|
||||||
watch: {
|
watch: {
|
||||||
value: {
|
value: {
|
||||||
handler (v) {
|
handler (v) {
|
||||||
const current = $(this.$refs.dropdown).dropdown('get value').split(',').sort()
|
const current = $(this.$refs.dropdown).dropdown('get value').split(',').sort()
|
||||||
if (!lodash.isEqual([...v].sort(), current)) {
|
if (!isEqual([...v].sort(), current)) {
|
||||||
$(this.$refs.dropdown).dropdown('set exactly', v)
|
$(this.$refs.dropdown).dropdown('set exactly', v)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -219,18 +219,18 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import time from '@/utils/time.js'
|
import time from '@/utils/time'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import url from '@/utils/url.js'
|
import { getDomain } from '@/utils'
|
||||||
import { getDomain } from '@/utils.js'
|
import logger from '@/logging'
|
||||||
import logger from '@/logging.js'
|
|
||||||
import PlayButton from '@/components/audio/PlayButton.vue'
|
import PlayButton from '@/components/audio/PlayButton.vue'
|
||||||
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon.vue'
|
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon.vue'
|
||||||
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon.vue'
|
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon.vue'
|
||||||
import Modal from '@/components/semantic/Modal.vue'
|
import Modal from '@/components/semantic/Modal.vue'
|
||||||
import EmbedWizard from '@/components/audio/EmbedWizard.vue'
|
import EmbedWizard from '@/components/audio/EmbedWizard.vue'
|
||||||
import ReportMixin from '@/components/mixins/Report.vue'
|
import ReportMixin from '@/components/mixins/Report.vue'
|
||||||
import { momentFormat } from '@/filters'
|
import { momentFormat } from '@/modules/filters'
|
||||||
|
import updateQueryString from '@/composables/updateQueryString'
|
||||||
|
|
||||||
const FETCH_URL = 'tracks/'
|
const FETCH_URL = 'tracks/'
|
||||||
|
|
||||||
|
@ -322,7 +322,7 @@ export default {
|
||||||
param = 'token'
|
param = 'token'
|
||||||
value = this.$store.state.auth.scopedTokens.listen
|
value = this.$store.state.auth.scopedTokens.listen
|
||||||
}
|
}
|
||||||
u = url.updateQueryString(
|
u = updateQueryString(
|
||||||
u,
|
u,
|
||||||
param,
|
param,
|
||||||
encodeURI(value)
|
encodeURI(value)
|
||||||
|
|
|
@ -186,7 +186,7 @@
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
import _ from 'lodash'
|
import { clone } from 'lodash-es'
|
||||||
import BuilderFilter from './Filter.vue'
|
import BuilderFilter from './Filter.vue'
|
||||||
import TrackTable from '@/components/audio/track/Table.vue'
|
import TrackTable from '@/components/audio/track/Table.vue'
|
||||||
import RadioButton from '@/components/radios/Button.vue'
|
import RadioButton from '@/components/radios/Button.vue'
|
||||||
|
@ -309,7 +309,7 @@ export default {
|
||||||
const self = this
|
const self = this
|
||||||
const url = 'radios/radios/validate/'
|
const url = 'radios/radios/validate/'
|
||||||
let final = this.filters.map(f => {
|
let final = this.filters.map(f => {
|
||||||
const c = _.clone(f.config)
|
const c = clone(f.config)
|
||||||
c.type = f.filter.type
|
c.type = f.filter.type
|
||||||
return c
|
return c
|
||||||
})
|
})
|
||||||
|
@ -326,7 +326,7 @@ export default {
|
||||||
self.isLoading = true
|
self.isLoading = true
|
||||||
|
|
||||||
let final = this.filters.map(f => {
|
let final = this.filters.map(f => {
|
||||||
const c = _.clone(f.config)
|
const c = clone(f.config)
|
||||||
c.type = f.filter.type
|
c.type = f.filter.type
|
||||||
return c
|
return c
|
||||||
})
|
})
|
||||||
|
|
|
@ -107,7 +107,7 @@
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
import _ from 'lodash'
|
import { clone } from 'lodash-es'
|
||||||
|
|
||||||
import Modal from '@/components/semantic/Modal.vue'
|
import Modal from '@/components/semantic/Modal.vue'
|
||||||
import TrackTable from '@/components/audio/track/Table.vue'
|
import TrackTable from '@/components/audio/track/Table.vue'
|
||||||
|
@ -181,7 +181,7 @@ export default {
|
||||||
fetchCandidates: function () {
|
fetchCandidates: function () {
|
||||||
const self = this
|
const self = this
|
||||||
const url = 'radios/radios/validate/'
|
const url = 'radios/radios/validate/'
|
||||||
let final = _.clone(this.config)
|
let final = clone(this.config)
|
||||||
final.type = this.filter.type
|
final.type = this.filter.type
|
||||||
final = { filters: [final] }
|
final = { filters: [final] }
|
||||||
axios.post(url, final).then((response) => {
|
axios.post(url, final).then((response) => {
|
||||||
|
|
|
@ -201,8 +201,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import _ from 'lodash'
|
import { merge } from 'lodash-es'
|
||||||
import time from '@/utils/time.js'
|
import time from '@/utils/time'
|
||||||
import { normalizeQuery, parseTokens } from '@/search'
|
import { normalizeQuery, parseTokens } from '@/search'
|
||||||
import Pagination from '@/components/Pagination.vue'
|
import Pagination from '@/components/Pagination.vue'
|
||||||
import ActionTable from '@/components/common/ActionTable.vue'
|
import ActionTable from '@/components/common/ActionTable.vue'
|
||||||
|
@ -247,7 +247,7 @@ export default {
|
||||||
q: this.search.query
|
q: this.search.query
|
||||||
}
|
}
|
||||||
if (this.filters) {
|
if (this.filters) {
|
||||||
return _.merge(currentFilters, this.filters)
|
return merge(currentFilters, this.filters)
|
||||||
} else {
|
} else {
|
||||||
return currentFilters
|
return currentFilters
|
||||||
}
|
}
|
||||||
|
@ -287,7 +287,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchData () {
|
fetchData () {
|
||||||
const params = _.merge({
|
const params = merge({
|
||||||
page: this.page,
|
page: this.page,
|
||||||
page_size: this.paginateBy,
|
page_size: this.paginateBy,
|
||||||
q: this.search.query,
|
q: this.search.query,
|
||||||
|
|
|
@ -186,8 +186,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import _ from 'lodash'
|
import { merge } from 'lodash-es'
|
||||||
import time from '@/utils/time.js'
|
import time from '@/utils/time'
|
||||||
import { normalizeQuery, parseTokens } from '@/search'
|
import { normalizeQuery, parseTokens } from '@/search'
|
||||||
import Pagination from '@/components/Pagination.vue'
|
import Pagination from '@/components/Pagination.vue'
|
||||||
import ActionTable from '@/components/common/ActionTable.vue'
|
import ActionTable from '@/components/common/ActionTable.vue'
|
||||||
|
@ -233,7 +233,7 @@ export default {
|
||||||
q: this.search.query
|
q: this.search.query
|
||||||
}
|
}
|
||||||
if (this.filters) {
|
if (this.filters) {
|
||||||
return _.merge(currentFilters, this.filters)
|
return merge(currentFilters, this.filters)
|
||||||
} else {
|
} else {
|
||||||
return currentFilters
|
return currentFilters
|
||||||
}
|
}
|
||||||
|
@ -273,7 +273,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchData () {
|
fetchData () {
|
||||||
const params = _.merge({
|
const params = merge({
|
||||||
page: this.page,
|
page: this.page,
|
||||||
page_size: this.paginateBy,
|
page_size: this.paginateBy,
|
||||||
q: this.search.query,
|
q: this.search.query,
|
||||||
|
|
|
@ -185,8 +185,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import _ from 'lodash'
|
import { merge } from 'lodash-es'
|
||||||
import time from '@/utils/time.js'
|
import time from '@/utils/time'
|
||||||
import { normalizeQuery, parseTokens } from '@/search'
|
import { normalizeQuery, parseTokens } from '@/search'
|
||||||
import Pagination from '@/components/Pagination.vue'
|
import Pagination from '@/components/Pagination.vue'
|
||||||
import ActionTable from '@/components/common/ActionTable.vue'
|
import ActionTable from '@/components/common/ActionTable.vue'
|
||||||
|
@ -230,7 +230,7 @@ export default {
|
||||||
q: this.search.query
|
q: this.search.query
|
||||||
}
|
}
|
||||||
if (this.filters) {
|
if (this.filters) {
|
||||||
return _.merge(currentFilters, this.filters)
|
return merge(currentFilters, this.filters)
|
||||||
} else {
|
} else {
|
||||||
return currentFilters
|
return currentFilters
|
||||||
}
|
}
|
||||||
|
@ -276,7 +276,7 @@ export default {
|
||||||
return { name: 'manage.library.artists.detail', params: { id: artist.id } }
|
return { name: 'manage.library.artists.detail', params: { id: artist.id } }
|
||||||
},
|
},
|
||||||
fetchData () {
|
fetchData () {
|
||||||
const params = _.merge({
|
const params = merge({
|
||||||
page: this.page,
|
page: this.page,
|
||||||
page_size: this.paginateBy,
|
page_size: this.paginateBy,
|
||||||
q: this.search.query,
|
q: this.search.query,
|
||||||
|
|
|
@ -131,8 +131,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import _ from 'lodash'
|
import { uniq, merge } from 'lodash-es'
|
||||||
import time from '@/utils/time.js'
|
import time from '@/utils/time'
|
||||||
import Pagination from '@/components/Pagination.vue'
|
import Pagination from '@/components/Pagination.vue'
|
||||||
import OrderingMixin from '@/components/mixins/Ordering.vue'
|
import OrderingMixin from '@/components/mixins/Ordering.vue'
|
||||||
import TranslationsMixin from '@/components/mixins/Translations.vue'
|
import TranslationsMixin from '@/components/mixins/Translations.vue'
|
||||||
|
@ -197,7 +197,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchData () {
|
fetchData () {
|
||||||
const params = _.merge({
|
const params = merge({
|
||||||
page: this.page,
|
page: this.page,
|
||||||
page_size: this.paginateBy,
|
page_size: this.paginateBy,
|
||||||
q: this.search.query,
|
q: this.search.query,
|
||||||
|
@ -236,7 +236,7 @@ export default {
|
||||||
if (config.ids.length === 0) {
|
if (config.ids.length === 0) {
|
||||||
return
|
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) => {
|
response.data.results.forEach((e) => {
|
||||||
self.$set(self.targets[k], e.id, {
|
self.$set(self.targets[k], e.id, {
|
||||||
payload: e,
|
payload: e,
|
||||||
|
|
|
@ -216,8 +216,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import _ from 'lodash'
|
import { merge } from 'lodash-es'
|
||||||
import time from '@/utils/time.js'
|
import time from '@/utils/time'
|
||||||
import { normalizeQuery, parseTokens } from '@/search'
|
import { normalizeQuery, parseTokens } from '@/search'
|
||||||
import Pagination from '@/components/Pagination.vue'
|
import Pagination from '@/components/Pagination.vue'
|
||||||
import ActionTable from '@/components/common/ActionTable.vue'
|
import ActionTable from '@/components/common/ActionTable.vue'
|
||||||
|
@ -262,7 +262,7 @@ export default {
|
||||||
q: this.search.query
|
q: this.search.query
|
||||||
}
|
}
|
||||||
if (this.filters) {
|
if (this.filters) {
|
||||||
return _.merge(currentFilters, this.filters)
|
return merge(currentFilters, this.filters)
|
||||||
} else {
|
} else {
|
||||||
return currentFilters
|
return currentFilters
|
||||||
}
|
}
|
||||||
|
@ -302,7 +302,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchData () {
|
fetchData () {
|
||||||
const params = _.merge({
|
const params = merge({
|
||||||
page: this.page,
|
page: this.page,
|
||||||
page_size: this.paginateBy,
|
page_size: this.paginateBy,
|
||||||
q: this.search.query,
|
q: this.search.query,
|
||||||
|
|
|
@ -147,8 +147,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import _ from 'lodash'
|
import { merge } from 'lodash-es'
|
||||||
import time from '@/utils/time.js'
|
import time from '@/utils/time'
|
||||||
import { normalizeQuery, parseTokens } from '@/search'
|
import { normalizeQuery, parseTokens } from '@/search'
|
||||||
import Pagination from '@/components/Pagination.vue'
|
import Pagination from '@/components/Pagination.vue'
|
||||||
import ActionTable from '@/components/common/ActionTable.vue'
|
import ActionTable from '@/components/common/ActionTable.vue'
|
||||||
|
@ -198,7 +198,7 @@ export default {
|
||||||
q: this.search.query
|
q: this.search.query
|
||||||
}
|
}
|
||||||
if (this.filters) {
|
if (this.filters) {
|
||||||
return _.merge(currentFilters, this.filters)
|
return merge(currentFilters, this.filters)
|
||||||
} else {
|
} else {
|
||||||
return currentFilters
|
return currentFilters
|
||||||
}
|
}
|
||||||
|
@ -238,7 +238,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchData () {
|
fetchData () {
|
||||||
const params = _.merge({
|
const params = merge({
|
||||||
page: this.page,
|
page: this.page,
|
||||||
page_size: this.paginateBy,
|
page_size: this.paginateBy,
|
||||||
q: this.search.query,
|
q: this.search.query,
|
||||||
|
|
|
@ -199,8 +199,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import _ from 'lodash'
|
import { merge } from 'lodash-es'
|
||||||
import time from '@/utils/time.js'
|
import time from '@/utils/time'
|
||||||
import { normalizeQuery, parseTokens } from '@/search'
|
import { normalizeQuery, parseTokens } from '@/search'
|
||||||
import Pagination from '@/components/Pagination.vue'
|
import Pagination from '@/components/Pagination.vue'
|
||||||
import ActionTable from '@/components/common/ActionTable.vue'
|
import ActionTable from '@/components/common/ActionTable.vue'
|
||||||
|
@ -243,7 +243,7 @@ export default {
|
||||||
q: this.search.query
|
q: this.search.query
|
||||||
}
|
}
|
||||||
if (this.filters) {
|
if (this.filters) {
|
||||||
return _.merge(currentFilters, this.filters)
|
return merge(currentFilters, this.filters)
|
||||||
} else {
|
} else {
|
||||||
return currentFilters
|
return currentFilters
|
||||||
}
|
}
|
||||||
|
@ -283,7 +283,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchData () {
|
fetchData () {
|
||||||
const params = _.merge({
|
const params = merge({
|
||||||
page: this.page,
|
page: this.page,
|
||||||
page_size: this.paginateBy,
|
page_size: this.paginateBy,
|
||||||
q: this.search.query,
|
q: this.search.query,
|
||||||
|
|
|
@ -308,8 +308,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import _ from 'lodash'
|
import { merge } from 'lodash-es'
|
||||||
import time from '@/utils/time.js'
|
import time from '@/utils/time'
|
||||||
import { normalizeQuery, parseTokens } from '@/search'
|
import { normalizeQuery, parseTokens } from '@/search'
|
||||||
import Pagination from '@/components/Pagination.vue'
|
import Pagination from '@/components/Pagination.vue'
|
||||||
import ActionTable from '@/components/common/ActionTable.vue'
|
import ActionTable from '@/components/common/ActionTable.vue'
|
||||||
|
@ -361,7 +361,7 @@ export default {
|
||||||
q: this.search.query
|
q: this.search.query
|
||||||
}
|
}
|
||||||
if (this.filters) {
|
if (this.filters) {
|
||||||
return _.merge(currentFilters, this.filters)
|
return merge(currentFilters, this.filters)
|
||||||
} else {
|
} else {
|
||||||
return currentFilters
|
return currentFilters
|
||||||
}
|
}
|
||||||
|
@ -401,7 +401,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchData () {
|
fetchData () {
|
||||||
const params = _.merge({
|
const params = merge({
|
||||||
page: this.page,
|
page: this.page,
|
||||||
page_size: this.paginateBy,
|
page_size: this.paginateBy,
|
||||||
q: this.search.query,
|
q: this.search.query,
|
||||||
|
|
|
@ -172,8 +172,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import _ from 'lodash'
|
import { merge } from 'lodash-es'
|
||||||
import time from '@/utils/time.js'
|
import time from '@/utils/time'
|
||||||
import { normalizeQuery, parseTokens } from '@/search'
|
import { normalizeQuery, parseTokens } from '@/search'
|
||||||
import Pagination from '@/components/Pagination.vue'
|
import Pagination from '@/components/Pagination.vue'
|
||||||
import ActionTable from '@/components/common/ActionTable.vue'
|
import ActionTable from '@/components/common/ActionTable.vue'
|
||||||
|
@ -220,7 +220,7 @@ export default {
|
||||||
q: this.search.query
|
q: this.search.query
|
||||||
}
|
}
|
||||||
if (this.filters) {
|
if (this.filters) {
|
||||||
return _.merge(currentFilters, this.filters)
|
return merge(currentFilters, this.filters)
|
||||||
} else {
|
} else {
|
||||||
return currentFilters
|
return currentFilters
|
||||||
}
|
}
|
||||||
|
@ -255,7 +255,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchData () {
|
fetchData () {
|
||||||
const params = _.merge({
|
const params = merge({
|
||||||
page: this.page,
|
page: this.page,
|
||||||
page_size: this.paginateBy,
|
page_size: this.paginateBy,
|
||||||
q: this.search.query,
|
q: this.search.query,
|
||||||
|
|
|
@ -183,8 +183,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import _ from 'lodash'
|
import { merge } from 'lodash-es'
|
||||||
import time from '@/utils/time.js'
|
import time from '@/utils/time'
|
||||||
import Pagination from '@/components/Pagination.vue'
|
import Pagination from '@/components/Pagination.vue'
|
||||||
import ActionTable from '@/components/common/ActionTable.vue'
|
import ActionTable from '@/components/common/ActionTable.vue'
|
||||||
import OrderingMixin from '@/components/mixins/Ordering.vue'
|
import OrderingMixin from '@/components/mixins/Ordering.vue'
|
||||||
|
@ -229,7 +229,7 @@ export default {
|
||||||
q: this.search
|
q: this.search
|
||||||
}
|
}
|
||||||
if (this.filters) {
|
if (this.filters) {
|
||||||
return _.merge(currentFilters, this.filters)
|
return merge(currentFilters, this.filters)
|
||||||
} else {
|
} else {
|
||||||
return currentFilters
|
return currentFilters
|
||||||
}
|
}
|
||||||
|
@ -290,7 +290,7 @@ export default {
|
||||||
if (this.allowed !== null) {
|
if (this.allowed !== null) {
|
||||||
baseFilters.allowed = this.allowed
|
baseFilters.allowed = this.allowed
|
||||||
}
|
}
|
||||||
const params = _.merge(baseFilters, this.filters)
|
const params = merge(baseFilters, this.filters)
|
||||||
const self = this
|
const self = this
|
||||||
self.isLoading = true
|
self.isLoading = true
|
||||||
self.checked = []
|
self.checked = []
|
||||||
|
|
|
@ -169,7 +169,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import _ from 'lodash'
|
import { get } from 'lodash-es'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
@ -183,12 +183,12 @@ export default {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
errors: [],
|
errors: [],
|
||||||
current: {
|
current: {
|
||||||
summary: _.get(current, 'summary', ''),
|
summary: get(current, 'summary', ''),
|
||||||
isActive: _.get(current, 'is_active', true),
|
isActive: get(current, 'is_active', true),
|
||||||
blockAll: _.get(current, 'block_all', true),
|
blockAll: get(current, 'block_all', true),
|
||||||
silenceActivity: _.get(current, 'silence_activity', false),
|
silenceActivity: get(current, 'silence_activity', false),
|
||||||
silenceNotifications: _.get(current, 'silence_notifications', false),
|
silenceNotifications: get(current, 'silence_notifications', false),
|
||||||
rejectMedia: _.get(current, 'reject_media', false)
|
rejectMedia: get(current, 'reject_media', false)
|
||||||
},
|
},
|
||||||
fieldConfig: [
|
fieldConfig: [
|
||||||
// we hide those until we actually have the related features implemented :)
|
// we hide those until we actually have the related features implemented :)
|
||||||
|
|
|
@ -158,7 +158,7 @@
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import _ from 'lodash'
|
import { merge } from 'lodash-es'
|
||||||
import Pagination from '@/components/Pagination.vue'
|
import Pagination from '@/components/Pagination.vue'
|
||||||
import ActionTable from '@/components/common/ActionTable.vue'
|
import ActionTable from '@/components/common/ActionTable.vue'
|
||||||
import OrderingMixin from '@/components/mixins/Ordering.vue'
|
import OrderingMixin from '@/components/mixins/Ordering.vue'
|
||||||
|
@ -199,7 +199,7 @@ export default {
|
||||||
q: this.search
|
q: this.search
|
||||||
}
|
}
|
||||||
if (this.filters) {
|
if (this.filters) {
|
||||||
return _.merge(currentFilters, this.filters)
|
return merge(currentFilters, this.filters)
|
||||||
} else {
|
} else {
|
||||||
return currentFilters
|
return currentFilters
|
||||||
}
|
}
|
||||||
|
@ -243,7 +243,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchData () {
|
fetchData () {
|
||||||
const params = _.merge({
|
const params = merge({
|
||||||
page: this.page,
|
page: this.page,
|
||||||
page_size: this.paginateBy,
|
page_size: this.paginateBy,
|
||||||
q: this.search,
|
q: this.search,
|
||||||
|
|
|
@ -200,8 +200,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import _ from 'lodash'
|
import { merge } from 'lodash-es'
|
||||||
import time from '@/utils/time.js'
|
import time from '@/utils/time'
|
||||||
import Pagination from '@/components/Pagination.vue'
|
import Pagination from '@/components/Pagination.vue'
|
||||||
import ActionTable from '@/components/common/ActionTable.vue'
|
import ActionTable from '@/components/common/ActionTable.vue'
|
||||||
import OrderingMixin from '@/components/mixins/Ordering.vue'
|
import OrderingMixin from '@/components/mixins/Ordering.vue'
|
||||||
|
@ -261,7 +261,7 @@ export default {
|
||||||
q: this.search
|
q: this.search
|
||||||
}
|
}
|
||||||
if (this.filters) {
|
if (this.filters) {
|
||||||
return _.merge(currentFilters, this.filters)
|
return merge(currentFilters, this.filters)
|
||||||
} else {
|
} else {
|
||||||
return currentFilters
|
return currentFilters
|
||||||
}
|
}
|
||||||
|
@ -296,7 +296,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchData () {
|
fetchData () {
|
||||||
const params = _.merge({
|
const params = merge({
|
||||||
page: this.page,
|
page: this.page,
|
||||||
page_size: this.paginateBy,
|
page_size: this.paginateBy,
|
||||||
q: this.search,
|
q: this.search,
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
computed: {
|
|
||||||
themes () {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
icon: 'palette icon',
|
|
||||||
name: this.$pgettext('*/Settings/Dropdown.Label/Theme name', 'Browser default'),
|
|
||||||
key: 'system'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'sun icon',
|
|
||||||
name: this.$pgettext('*/Settings/Dropdown.Label/Theme name', 'Light'),
|
|
||||||
key: 'light'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'moon icon',
|
|
||||||
name: this.$pgettext('*/Settings/Dropdown.Label/Theme name', 'Dark'),
|
|
||||||
key: 'dark'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -26,7 +26,6 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import TranslationsMixin from '@/components/mixins/Translations.vue'
|
import TranslationsMixin from '@/components/mixins/Translations.vue'
|
||||||
import lodash from 'lodash'
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [TranslationsMixin],
|
mixins: [TranslationsMixin],
|
||||||
props: {
|
props: {
|
||||||
|
@ -52,7 +51,7 @@ export default {
|
||||||
if (this.restrictTo.length > 0) {
|
if (this.restrictTo.length > 0) {
|
||||||
choices = this.restrictTo
|
choices = this.restrictTo
|
||||||
} else {
|
} else {
|
||||||
choices = lodash.keys(this.sharedLabels.fields.report_type.choices)
|
choices = Object.keys(this.sharedLabels.fields.report_type.choices)
|
||||||
}
|
}
|
||||||
return c.concat(
|
return c.concat(
|
||||||
choices.sort().map((v) => {
|
choices.sort().map((v) => {
|
||||||
|
|
|
@ -192,9 +192,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import filter from 'lodash/fp/filter'
|
import { filter, sortBy, flow } from 'lodash-es'
|
||||||
import sortBy from 'lodash/fp/sortBy'
|
|
||||||
import flow from 'lodash/fp/flow'
|
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _ from 'lodash'
|
import { clone } from 'lodash-es'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import PlaylistCard from '@/components/playlists/Card.vue'
|
import PlaylistCard from '@/components/playlists/Card.vue'
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ export default {
|
||||||
}
|
}
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
const self = this
|
const self = this
|
||||||
const params = _.clone(this.filters)
|
const params = clone(this.filters)
|
||||||
params.page_size = this.limit
|
params.page_size = this.limit
|
||||||
params.offset = this.offset
|
params.offset = this.offset
|
||||||
axios.get(url, { params: params }).then((response) => {
|
axios.get(url, { params: params }).then((response) => {
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import lodash from 'lodash'
|
import { isEqual } from 'lodash-es'
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
customRadioId: { type: Number, required: false, default: null },
|
customRadioId: { type: Number, required: false, default: null },
|
||||||
|
@ -29,7 +29,7 @@ export default {
|
||||||
if (!state.running) {
|
if (!state.running) {
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
return current.type === this.type && lodash.isEqual(current.objectId, this.objectId) && current.customRadioId === this.customRadioId
|
return current.type === this.type && isEqual(current.objectId, this.objectId) && current.customRadioId === this.customRadioId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label () {
|
label () {
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
export default (uri: string, key: string, value: string) => {
|
||||||
|
const re = new RegExp(`([?&])${key}=.*?(&|$)`, 'i')
|
||||||
|
if (uri.match(re)) {
|
||||||
|
return uri.replace(re, `$1${key}=${value}$2`)
|
||||||
|
} else {
|
||||||
|
const separator = uri.indexOf('?') !== -1 ? '&' : '?'
|
||||||
|
return `${uri}${separator}${key}=${value}`
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { useColorMode } from '@vueuse/core'
|
||||||
|
import { watch } from '@vue/composition-api'
|
||||||
|
|
||||||
|
const theme = useColorMode()
|
||||||
|
|
||||||
|
document.body.classList.add(`theme-${theme.value}`)
|
||||||
|
watch(theme, (newValue, oldValue) => {
|
||||||
|
document.body.classList.remove(`theme-${oldValue}`)
|
||||||
|
document.body.classList.add(`theme-${newValue}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default () => theme
|
|
@ -0,0 +1,24 @@
|
||||||
|
import type { ThemeEntry } from '@/types'
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
const { $pgettext } = Vue.prototype
|
||||||
|
|
||||||
|
const themeList: ThemeEntry[] = [
|
||||||
|
{
|
||||||
|
icon: 'palette icon',
|
||||||
|
name: $pgettext('*/Settings/Dropdown.Label/Theme name', 'Browser default'),
|
||||||
|
key: 'auto'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'sun icon',
|
||||||
|
name: $pgettext('*/Settings/Dropdown.Label/Theme name', 'Light'),
|
||||||
|
key: 'light'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'moon icon',
|
||||||
|
name: $pgettext('*/Settings/Dropdown.Label/Theme name', 'Dark'),
|
||||||
|
key: 'dark'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export default () => themeList
|
|
@ -0,0 +1,15 @@
|
||||||
|
import store from '~/store'
|
||||||
|
import { tryOnScopeDispose } from '@vueuse/core'
|
||||||
|
import { WebSocketEvent } from '~/types'
|
||||||
|
|
||||||
|
export default (eventName: string, handler: (event: WebSocketEvent) => void) => {
|
||||||
|
const id = `${+new Date() + Math.random()}`
|
||||||
|
store.commit('ui/addWebsocketEventHandler', { eventName, handler, id })
|
||||||
|
|
||||||
|
const stop = () => {
|
||||||
|
store.commit('ui/removeWebsocketEventHandler', { eventName, id })
|
||||||
|
}
|
||||||
|
|
||||||
|
tryOnScopeDispose(stop)
|
||||||
|
return stop
|
||||||
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import EmbedFrame from './EmbedFrame.vue'
|
import EmbedFrame from './EmbedFrame.vue'
|
||||||
import VuePlyr from 'vue-plyr'
|
import VuePlyr from 'vue-plyr'
|
|
@ -247,7 +247,7 @@
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import Logo from '@/components/Logo.vue'
|
import Logo from '@/components/Logo.vue'
|
||||||
import url from '@/utils/url'
|
import updateQueryString from './composables/updateQueryString'
|
||||||
import time from '@/utils/time'
|
import time from '@/utils/time'
|
||||||
|
|
||||||
function getURLParams () {
|
function getURLParams () {
|
||||||
|
@ -516,7 +516,7 @@ export default {
|
||||||
// not support other codecs to be able to play it :)
|
// not support other codecs to be able to play it :)
|
||||||
sources.push({
|
sources.push({
|
||||||
type: 'audio/mpeg',
|
type: 'audio/mpeg',
|
||||||
src: url.updateQueryString(
|
src: updateQueryString(
|
||||||
self.fullUrl(sources[0].src),
|
self.fullUrl(sources[0].src),
|
||||||
'to',
|
'to',
|
||||||
'mp3'
|
'mp3'
|
||||||
|
|
|
@ -1,129 +0,0 @@
|
||||||
/* eslint-disable */
|
|
||||||
export default {
|
|
||||||
"locales": [
|
|
||||||
{
|
|
||||||
"code": "ar",
|
|
||||||
"label": "العربية"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "ca",
|
|
||||||
"label": "Català"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "cs",
|
|
||||||
"label": "Čeština"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "de",
|
|
||||||
"label": "Deutsch"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "en_GB",
|
|
||||||
"label": "English (UK)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "en_US",
|
|
||||||
"label": "English (United-States)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "eo",
|
|
||||||
"label": "Esperanto"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "es",
|
|
||||||
"label": "Español"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "eu",
|
|
||||||
"label": "Euskara"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "fr_FR",
|
|
||||||
"label": "Français"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "gl",
|
|
||||||
"label": "Galego"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "hu",
|
|
||||||
"label": "Magyar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "it",
|
|
||||||
"label": "Italiano"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "ja_JP",
|
|
||||||
"label": "日本語"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "kab_DZ",
|
|
||||||
"label": "Taqbaylit"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "ko_KR",
|
|
||||||
"label": "한국어"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "nb_NO",
|
|
||||||
"label": "Bokmål"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "nn_NO",
|
|
||||||
"label": "Nynorsk"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "nl",
|
|
||||||
"label": "Nederlands"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "oc",
|
|
||||||
"label": "Occitan"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "pl",
|
|
||||||
"label": "Polski"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "pt_BR",
|
|
||||||
"label": "Português (Brasil)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "pt_PT",
|
|
||||||
"label": "Português (Portugal)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "ru",
|
|
||||||
"label": "Русский"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "sq",
|
|
||||||
"label": "Shqip"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "zh_Hans",
|
|
||||||
"label": "中文(简体)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "zh_Hant",
|
|
||||||
"label": "中文(繁體)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "fa_IR",
|
|
||||||
"label": "فارسی"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "ml",
|
|
||||||
"label": "മലയാളം"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "sv",
|
|
||||||
"label": "Svenska"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "el",
|
|
||||||
"label": "Ελληνικά"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
import type { Locale } from '@/types'
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
export const locales: Locale[] = [
|
||||||
|
{
|
||||||
|
"code": "ar",
|
||||||
|
"label": "العربية"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ca",
|
||||||
|
"label": "Català"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "cs",
|
||||||
|
"label": "Čeština"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "de",
|
||||||
|
"label": "Deutsch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "en_GB",
|
||||||
|
"label": "English (UK)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "en_US",
|
||||||
|
"label": "English (United-States)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "eo",
|
||||||
|
"label": "Esperanto"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "es",
|
||||||
|
"label": "Español"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "eu",
|
||||||
|
"label": "Euskara"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "fr_FR",
|
||||||
|
"label": "Français"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "gl",
|
||||||
|
"label": "Galego"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "hu",
|
||||||
|
"label": "Magyar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "it",
|
||||||
|
"label": "Italiano"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ja_JP",
|
||||||
|
"label": "日本語"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "kab_DZ",
|
||||||
|
"label": "Taqbaylit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ko_KR",
|
||||||
|
"label": "한국어"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "nb_NO",
|
||||||
|
"label": "Bokmål"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "nn_NO",
|
||||||
|
"label": "Nynorsk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "nl",
|
||||||
|
"label": "Nederlands"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "oc",
|
||||||
|
"label": "Occitan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "pl",
|
||||||
|
"label": "Polski"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "pt_BR",
|
||||||
|
"label": "Português (Brasil)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "pt_PT",
|
||||||
|
"label": "Português (Portugal)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ru",
|
||||||
|
"label": "Русский"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sq",
|
||||||
|
"label": "Shqip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "zh_Hans",
|
||||||
|
"label": "中文(简体)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "zh_Hant",
|
||||||
|
"label": "中文(繁體)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "fa_IR",
|
||||||
|
"label": "فارسی"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ml",
|
||||||
|
"label": "മലയാളം"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sv",
|
||||||
|
"label": "Svenska"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "el",
|
||||||
|
"label": "Ελληνικά"
|
||||||
|
}
|
||||||
|
]
|
|
@ -1,199 +0,0 @@
|
||||||
// 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'
|
|
||||||
import jQuery from '@/jquery'
|
|
||||||
|
|
||||||
import Vue from 'vue'
|
|
||||||
import moment from 'moment'
|
|
||||||
import App from './App.vue'
|
|
||||||
import router from './router'
|
|
||||||
import axios from 'axios'
|
|
||||||
import VueLazyload from 'vue-lazyload'
|
|
||||||
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 VueCompositionAPI from '@vue/composition-api'
|
|
||||||
|
|
||||||
import filters from '@/filters' // eslint-disable-line
|
|
||||||
import { parseAPIErrors } from '@/utils'
|
|
||||||
import globals from '@/components/globals' // eslint-disable-line
|
|
||||||
import './registerServiceWorker'
|
|
||||||
import '@/semantic'
|
|
||||||
|
|
||||||
logger.default.info('Loading environment:', import.meta.env.MODE)
|
|
||||||
logger.default.debug('Environment variables:', import.meta.env)
|
|
||||||
|
|
||||||
sync(store, router)
|
|
||||||
|
|
||||||
let APP = null
|
|
||||||
|
|
||||||
const availableLanguages = (function () {
|
|
||||||
const l = {}
|
|
||||||
locales.locales.forEach(c => {
|
|
||||||
l[c.code] = c.label
|
|
||||||
})
|
|
||||||
return l
|
|
||||||
})()
|
|
||||||
let defaultLanguage = 'en_US'
|
|
||||||
if (availableLanguages[store.state.ui.currentLanguage]) {
|
|
||||||
defaultLanguage = store.state.ui.currentLanguage
|
|
||||||
}
|
|
||||||
Vue.use(GetTextPlugin, {
|
|
||||||
availableLanguages: availableLanguages,
|
|
||||||
defaultLanguage: defaultLanguage,
|
|
||||||
// cf https://github.com/Polyconseil/vue-gettext#configuration
|
|
||||||
// not recommended but this is fixing weird bugs with translation nodes
|
|
||||||
// not being updated when in v-if/v-else clauses
|
|
||||||
autoAddKeyAttributes: true,
|
|
||||||
languageVmMixin: {
|
|
||||||
computed: {
|
|
||||||
currentKebabCase: function () {
|
|
||||||
return this.current.toLowerCase().replace('_', '-')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
translations: {},
|
|
||||||
silent: true
|
|
||||||
})
|
|
||||||
|
|
||||||
Vue.use(VueCompositionAPI)
|
|
||||||
Vue.use(VueLazyload)
|
|
||||||
Vue.directive('title', function (el, binding) {
|
|
||||||
store.commit('ui/pageTitle', binding.value)
|
|
||||||
})
|
|
||||||
Vue.directive('dropdown', function (el, binding) {
|
|
||||||
jQuery(el).dropdown({
|
|
||||||
selectOnKeydown: false,
|
|
||||||
action: function (text, value, $el) {
|
|
||||||
// used to ensure focusing the dropdown and clicking via keyboard
|
|
||||||
// works as expected
|
|
||||||
const button = $el[0]
|
|
||||||
button.click()
|
|
||||||
jQuery(el).find('.ui.dropdown').dropdown('hide')
|
|
||||||
},
|
|
||||||
...(binding.value || {})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
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']
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
}, function (error) {
|
|
||||||
// Do something with request error
|
|
||||||
return Promise.reject(error)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add a response interceptor
|
|
||||||
axios.interceptors.response.use(function (response) {
|
|
||||||
return response
|
|
||||||
}, function (error) {
|
|
||||||
error.backendErrors = []
|
|
||||||
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 } })
|
|
||||||
}
|
|
||||||
if (error.response.status === 404) {
|
|
||||||
error.backendErrors.push('Resource not found')
|
|
||||||
const message = error.response.data
|
|
||||||
store.commit('ui/addMessage', {
|
|
||||||
content: message,
|
|
||||||
class: 'error'
|
|
||||||
})
|
|
||||||
} else if (error.response.status === 403) {
|
|
||||||
error.backendErrors.push('Permission denied')
|
|
||||||
} else if (error.response.status === 429) {
|
|
||||||
let message
|
|
||||||
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']
|
|
||||||
}
|
|
||||||
if (rateLimitStatus.availableSeconds) {
|
|
||||||
rateLimitStatus.availableSeconds = parseInt(rateLimitStatus.availableSeconds)
|
|
||||||
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 })
|
|
||||||
} else {
|
|
||||||
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', {
|
|
||||||
content: message,
|
|
||||||
date: new Date(),
|
|
||||||
class: 'error'
|
|
||||||
})
|
|
||||||
logger.default.error('This client is rate-limited!', rateLimitStatus)
|
|
||||||
} else if (error.response.status === 500) {
|
|
||||||
error.backendErrors.push('A server error occured')
|
|
||||||
} else if (error.response.data) {
|
|
||||||
if (error.response.data.detail) {
|
|
||||||
error.backendErrors.push(error.response.data.detail)
|
|
||||||
} else {
|
|
||||||
error.rawPayload = error.response.data
|
|
||||||
const parsedErrors = parseAPIErrors(error.response.data)
|
|
||||||
error.backendErrors = [...error.backendErrors, ...parsedErrors]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (error.backendErrors.length === 0) {
|
|
||||||
error.backendErrors.push('An unknown error occured, ensure your are connected to the internet and your funkwhale instance is up and running')
|
|
||||||
}
|
|
||||||
// Do something with response error
|
|
||||||
return Promise.reject(error)
|
|
||||||
})
|
|
||||||
|
|
||||||
const refreshAuth = (failedRequest) => {
|
|
||||||
if (store.state.auth.oauth.accessToken) {
|
|
||||||
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()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createAuthRefreshInterceptor(axios, refreshAuth)
|
|
||||||
|
|
||||||
store.dispatch('instance/fetchFrontSettings').finally(() => {
|
|
||||||
/* eslint-disable no-new */
|
|
||||||
new Vue({
|
|
||||||
el: '#app',
|
|
||||||
router,
|
|
||||||
store,
|
|
||||||
components: { App },
|
|
||||||
created () {
|
|
||||||
APP = this
|
|
||||||
window.addEventListener('resize', this.handleResize)
|
|
||||||
this.handleResize()
|
|
||||||
},
|
|
||||||
destroyed () {
|
|
||||||
window.removeEventListener('resize', this.handleResize)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleResize () {
|
|
||||||
this.$store.commit('ui/window', {
|
|
||||||
width: window.innerWidth,
|
|
||||||
height: window.innerHeight
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render (h) {
|
|
||||||
return h('App')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.default.info('Everything loaded!')
|
|
||||||
})
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import logger from '~/logging'
|
||||||
|
import App from '~/App.vue'
|
||||||
|
import router from '~/router'
|
||||||
|
import VueLazyload from 'vue-lazyload'
|
||||||
|
import store from '~/store'
|
||||||
|
import { sync } from 'vuex-router-sync'
|
||||||
|
import VueCompositionAPI, { createApp } from '@vue/composition-api'
|
||||||
|
import { CreateElement } from 'vue'
|
||||||
|
|
||||||
|
logger.default.info('Loading environment:', import.meta.env.MODE)
|
||||||
|
logger.default.debug('Environment variables:', import.meta.env)
|
||||||
|
|
||||||
|
sync(store, router)
|
||||||
|
|
||||||
|
const app = createApp({
|
||||||
|
store,
|
||||||
|
router,
|
||||||
|
render: (h: CreateElement) => h(App)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use(VueCompositionAPI)
|
||||||
|
app.use(VueLazyload)
|
||||||
|
|
||||||
|
for (const module of Object.values(import.meta.globEager('./modules/*.ts'))) {
|
||||||
|
module.install?.({
|
||||||
|
app,
|
||||||
|
router,
|
||||||
|
store
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
store.dispatch('instance/fetchFrontSettings').finally(() => {
|
||||||
|
app.mount('#app')
|
||||||
|
logger.default.info('Everything loaded!')
|
||||||
|
})
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { AppModule } from '@/types'
|
||||||
|
|
||||||
|
import createAuthRefreshInterceptor from 'axios-auth-refresh'
|
||||||
|
import axios, { AxiosError } from 'axios'
|
||||||
|
import moment from 'moment'
|
||||||
|
import logger from '@/logging'
|
||||||
|
import { parseAPIErrors } from '@/utils'
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export const install: AppModule = ({ app, store, router }) => {
|
||||||
|
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 ??= {}
|
||||||
|
config.headers.Authorization = store.getters['auth/header']
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}, function (error) {
|
||||||
|
// Do something with request error
|
||||||
|
return Promise.reject(error)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add a response interceptor
|
||||||
|
axios.interceptors.response.use(function (response) {
|
||||||
|
return response
|
||||||
|
}, async (error) => {
|
||||||
|
error.backendErrors = []
|
||||||
|
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)
|
||||||
|
await router.push({ name: 'login', query: { next: router.currentRoute.fullPath } })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.response.status === 404) {
|
||||||
|
error.backendErrors.push('Resource not found')
|
||||||
|
const message = error.response.data
|
||||||
|
store.commit('ui/addMessage', {
|
||||||
|
content: message,
|
||||||
|
class: 'error'
|
||||||
|
})
|
||||||
|
} else if (error.response.status === 403) {
|
||||||
|
error.backendErrors.push('Permission denied')
|
||||||
|
} else if (error.response.status === 429) {
|
||||||
|
let message
|
||||||
|
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']
|
||||||
|
}
|
||||||
|
if (rateLimitStatus.availableSeconds) {
|
||||||
|
rateLimitStatus.availableSeconds = parseInt(rateLimitStatus.availableSeconds)
|
||||||
|
const tryAgain = moment().add(rateLimitStatus.availableSeconds, 's').toNow(true)
|
||||||
|
message = Vue.prototype.$pgettext('*/Error/Paragraph', 'You sent too many requests and have been rate limited, please try again in %{ delay }')
|
||||||
|
message = Vue.prototype.$gettextInterpolate(message, { delay: tryAgain })
|
||||||
|
} else {
|
||||||
|
message = Vue.prototype.$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', {
|
||||||
|
content: message,
|
||||||
|
date: new Date(),
|
||||||
|
class: 'error'
|
||||||
|
})
|
||||||
|
logger.default.error('This client is rate-limited!', rateLimitStatus)
|
||||||
|
} else if (error.response.status === 500) {
|
||||||
|
error.backendErrors.push('A server error occured')
|
||||||
|
} else if (error.response.data) {
|
||||||
|
if (error.response.data.detail) {
|
||||||
|
error.backendErrors.push(error.response.data.detail)
|
||||||
|
} else {
|
||||||
|
error.rawPayload = error.response.data
|
||||||
|
const parsedErrors = parseAPIErrors(error.response.data)
|
||||||
|
error.backendErrors = [...error.backendErrors, ...parsedErrors]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.backendErrors.length === 0) {
|
||||||
|
error.backendErrors.push('An unknown error occured, ensure your are connected to the internet and your funkwhale instance is up and running')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do something with response error
|
||||||
|
return Promise.reject(error)
|
||||||
|
})
|
||||||
|
|
||||||
|
const refreshAuth = (failedRequest: AxiosError) => {
|
||||||
|
if (store.state.auth.oauth.accessToken) {
|
||||||
|
console.log('Failed request, refreshing auth…')
|
||||||
|
// maybe the token was expired, let's try to refresh it
|
||||||
|
return store.dispatch('auth/refreshOauthToken').then(() => {
|
||||||
|
if (failedRequest.response) {
|
||||||
|
failedRequest.response.config.headers ??= {}
|
||||||
|
failedRequest.response.config.headers.Authorization = store.getters['auth/header']
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
createAuthRefreshInterceptor(axios, refreshAuth)
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { AppModule } from '@/types'
|
||||||
|
import jQuery from '@/jquery'
|
||||||
|
|
||||||
|
export const install: AppModule = ({ app, store }) => {
|
||||||
|
app.directive('title', function (el, binding) {
|
||||||
|
store.commit('ui/pageTitle', binding.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.directive('dropdown', function (el, binding) {
|
||||||
|
// @ts-ignore
|
||||||
|
jQuery(el).dropdown({
|
||||||
|
selectOnKeydown: false,
|
||||||
|
action (text: string, value: string, $el: JQuery<HTMLElement>) {
|
||||||
|
// used to ensure focusing the dropdown and clicking via keyboard
|
||||||
|
// works as expected
|
||||||
|
const button = $el[0]
|
||||||
|
button.click()
|
||||||
|
// @ts-ignore
|
||||||
|
jQuery(el).find('.ui.dropdown').dropdown('hide')
|
||||||
|
},
|
||||||
|
...(binding.value || {})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,18 +1,18 @@
|
||||||
|
import { AppModule } from '~/types'
|
||||||
|
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
import time from '~/utils/time'
|
||||||
import time from '@/utils/time'
|
|
||||||
|
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
|
||||||
export function truncate (str, max, ellipsis, middle) {
|
export function truncate (str: string, max = 100, ellipsis = '…', middle = false) {
|
||||||
if (max === 0) {
|
if (max === 0) {
|
||||||
return
|
return ''
|
||||||
}
|
}
|
||||||
max = max || 100
|
|
||||||
ellipsis = ellipsis || '…'
|
|
||||||
if (str.length <= max) {
|
if (str.length <= max) {
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
if (middle) {
|
if (middle) {
|
||||||
const sepLen = 1
|
const sepLen = 1
|
||||||
const charsToShow = max - sepLen
|
const charsToShow = max - sepLen
|
||||||
|
@ -27,9 +27,7 @@ export function truncate (str, max, ellipsis, middle) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.filter('truncate', truncate)
|
export function ago (date: Date, locale: string) {
|
||||||
|
|
||||||
export function ago (date, locale) {
|
|
||||||
locale = locale || 'en'
|
locale = locale || 'en'
|
||||||
const m = moment(date)
|
const m = moment(date)
|
||||||
m.locale(locale)
|
m.locale(locale)
|
||||||
|
@ -43,9 +41,7 @@ export function ago (date, locale) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.filter('ago', ago)
|
export function fromNow (date: Date, locale: string) {
|
||||||
|
|
||||||
export function fromNow (date, locale) {
|
|
||||||
locale = 'en'
|
locale = 'en'
|
||||||
moment.locale('en', {
|
moment.locale('en', {
|
||||||
relativeTime: {
|
relativeTime: {
|
||||||
|
@ -70,9 +66,7 @@ export function fromNow (date, locale) {
|
||||||
return m.fromNow(true)
|
return m.fromNow(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.filter('fromNow', fromNow)
|
export function secondsToObject (seconds: number) {
|
||||||
|
|
||||||
export function secondsToObject (seconds) {
|
|
||||||
const m = moment.duration(seconds, 'seconds')
|
const m = moment.duration(seconds, 'seconds')
|
||||||
return {
|
return {
|
||||||
seconds: m.seconds(),
|
seconds: m.seconds(),
|
||||||
|
@ -81,42 +75,30 @@ export function secondsToObject (seconds) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.filter('secondsToObject', secondsToObject)
|
export function padDuration (duration: string) {
|
||||||
|
|
||||||
export function padDuration (duration) {
|
|
||||||
let s = String(duration)
|
let s = String(duration)
|
||||||
while (s.length < 2) { s = '0' + s }
|
while (s.length < 2) { s = '0' + s }
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.filter('padDuration', padDuration)
|
export function duration (seconds: string) {
|
||||||
|
return time.parse(+seconds)
|
||||||
export function duration (seconds) {
|
|
||||||
return time.parse(seconds)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.filter('duration', duration)
|
export function momentFormat (date: Date, format: string) {
|
||||||
|
|
||||||
export function momentFormat (date, format) {
|
|
||||||
format = format || 'lll'
|
format = format || 'lll'
|
||||||
return moment(date).format(format)
|
return moment(date).format(format)
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.filter('moment', momentFormat)
|
export function year (date: Date) {
|
||||||
|
|
||||||
export function year (date) {
|
|
||||||
return moment(date).year()
|
return moment(date).year()
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.filter('year', year)
|
export function capitalize (str: string) {
|
||||||
|
|
||||||
export function capitalize (str) {
|
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.filter('capitalize', capitalize)
|
export function humanSize (bytes: number) {
|
||||||
|
|
||||||
export function humanSize (bytes) {
|
|
||||||
const si = true
|
const si = true
|
||||||
const thresh = si ? 1000 : 1024
|
const thresh = si ? 1000 : 1024
|
||||||
if (Math.abs(bytes) < thresh) {
|
if (Math.abs(bytes) < thresh) {
|
||||||
|
@ -133,15 +115,24 @@ export function humanSize (bytes) {
|
||||||
return bytes.toFixed(1) + ' ' + units[u]
|
return bytes.toFixed(1) + ' ' + units[u]
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.filter('humanSize', humanSize)
|
|
||||||
|
|
||||||
// Removes duplicates from a list
|
// Removes duplicates from a list
|
||||||
export function unique (list, property) {
|
export function unique (list: { [key: string]: unknown }[], property: string) {
|
||||||
property = property || 'id'
|
property = property || 'id'
|
||||||
const unique = []
|
const unique: { [key: string]: unknown }[] = []
|
||||||
list.map(x => unique.filter(a => a[property] === x[property]).length > 0 ? null : unique.push(x))
|
list.map(x => unique.filter(a => a[property] === x[property]).length > 0 ? null : unique.push(x))
|
||||||
return unique
|
return unique
|
||||||
}
|
}
|
||||||
Vue.filter('unique', unique)
|
|
||||||
|
|
||||||
export default {}
|
export const install: AppModule = () => {
|
||||||
|
Vue.filter('humanSize', humanSize)
|
||||||
|
Vue.filter('unique', unique)
|
||||||
|
Vue.filter('capitalize', capitalize)
|
||||||
|
Vue.filter('moment', momentFormat)
|
||||||
|
Vue.filter('year', year)
|
||||||
|
Vue.filter('duration', duration)
|
||||||
|
Vue.filter('padDuration', padDuration)
|
||||||
|
Vue.filter('secondsToObject', secondsToObject)
|
||||||
|
Vue.filter('fromNow', fromNow)
|
||||||
|
Vue.filter('ago', ago)
|
||||||
|
Vue.filter('truncate', truncate)
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import Vue from 'vue'
|
import { AppModule } from '@/types'
|
||||||
|
|
||||||
import HumanDate from '@/components/common/HumanDate.vue'
|
import HumanDate from '@/components/common/HumanDate.vue'
|
||||||
import HumanDuration from '@/components/common/HumanDuration.vue'
|
import HumanDuration from '@/components/common/HumanDuration.vue'
|
||||||
import Username from '@/components/common/Username.vue'
|
import Username from '@/components/common/Username.vue'
|
||||||
|
@ -19,24 +20,24 @@ import RenderedDescription from '@/components/common/RenderedDescription.vue'
|
||||||
import ContentForm from '@/components/common/ContentForm.vue'
|
import ContentForm from '@/components/common/ContentForm.vue'
|
||||||
import InlineSearchBar from '@/components/common/InlineSearchBar.vue'
|
import InlineSearchBar from '@/components/common/InlineSearchBar.vue'
|
||||||
|
|
||||||
Vue.component('HumanDate', HumanDate)
|
export const install: AppModule = ({ app }) => {
|
||||||
Vue.component('HumanDuration', HumanDuration)
|
app.component('HumanDate', HumanDate)
|
||||||
Vue.component('Username', Username)
|
app.component('HumanDuration', HumanDuration)
|
||||||
Vue.component('UserLink', UserLink)
|
app.component('Username', Username)
|
||||||
Vue.component('ActorLink', ActorLink)
|
app.component('UserLink', UserLink)
|
||||||
Vue.component('ActorAvatar', ActorAvatar)
|
app.component('ActorLink', ActorLink)
|
||||||
Vue.component('Duration', Duration)
|
app.component('ActorAvatar', ActorAvatar)
|
||||||
Vue.component('DangerousButton', DangerousButton)
|
app.component('Duration', Duration)
|
||||||
Vue.component('Message', Message)
|
app.component('DangerousButton', DangerousButton)
|
||||||
Vue.component('CopyInput', CopyInput)
|
app.component('Message', Message)
|
||||||
Vue.component('AjaxButton', AjaxButton)
|
app.component('CopyInput', CopyInput)
|
||||||
Vue.component('Tooltip', Tooltip)
|
app.component('AjaxButton', AjaxButton)
|
||||||
Vue.component('EmptyState', EmptyState)
|
app.component('Tooltip', Tooltip)
|
||||||
Vue.component('ExpandableDiv', ExpandableDiv)
|
app.component('EmptyState', EmptyState)
|
||||||
Vue.component('CollapseLink', CollapseLink)
|
app.component('ExpandableDiv', ExpandableDiv)
|
||||||
Vue.component('ActionFeedback', ActionFeedback)
|
app.component('CollapseLink', CollapseLink)
|
||||||
Vue.component('RenderedDescription', RenderedDescription)
|
app.component('ActionFeedback', ActionFeedback)
|
||||||
Vue.component('ContentForm', ContentForm)
|
app.component('RenderedDescription', RenderedDescription)
|
||||||
Vue.component('InlineSearchBar', InlineSearchBar)
|
app.component('ContentForm', ContentForm)
|
||||||
|
app.component('InlineSearchBar', InlineSearchBar)
|
||||||
export default {}
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { AppModule } from '@/types'
|
||||||
|
import { watch } from '@vue/composition-api'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export const install: AppModule = async ({ store, router }) => {
|
||||||
|
watch(() => store.state.instance.instanceUrl, async () => {
|
||||||
|
const [{ data }] = await Promise.all([
|
||||||
|
axios.get('instance/nodeinfo/2.0/'),
|
||||||
|
store.dispatch('instance/fetchSettings')
|
||||||
|
])
|
||||||
|
|
||||||
|
store.commit('instance/nodeinfo', data)
|
||||||
|
})
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
|
const serverUrl = urlParams.get('_server')
|
||||||
|
if (serverUrl) {
|
||||||
|
store.commit('instance/instanceUrl', serverUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = urlParams.get('_url')
|
||||||
|
if (url) {
|
||||||
|
return router.replace(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!store.state.instance.instanceUrl) {
|
||||||
|
// We have several way to guess the API server url. By order of precedence:
|
||||||
|
// 1. use the url provided in settings.json, if any
|
||||||
|
// 2. use the url specified when building via VUE_APP_INSTANCE_URL
|
||||||
|
// 3. use the current url
|
||||||
|
const defaultInstanceUrl = store.state.instance.frontSettings.defaultServerUrl ||
|
||||||
|
import.meta.env.VUE_APP_INSTANCE_URL ||
|
||||||
|
store.getters['instance/defaultUrl']()
|
||||||
|
|
||||||
|
store.commit('instance/instanceUrl', defaultInstanceUrl)
|
||||||
|
} else {
|
||||||
|
// needed to trigger initialization of axios / service worker / web socket
|
||||||
|
store.commit('instance/instanceUrl', store.state.instance.instanceUrl)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { AppModule } from '@/types'
|
||||||
|
|
||||||
|
// slight hack to allow use to have internal links in <translate> tags
|
||||||
|
// while preserving router behaviour
|
||||||
|
export const install: AppModule = ({ router }) => {
|
||||||
|
document.documentElement.addEventListener('click', async (event) => {
|
||||||
|
const target = <HTMLAnchorElement> event.target
|
||||||
|
if (!target.matches('a.internal')) return
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
return router.push(target.href)
|
||||||
|
}, false)
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import GetText from 'vue-gettext'
|
||||||
|
import { locales } from '@/locales'
|
||||||
|
import { usePreferredLanguages } from '@vueuse/core'
|
||||||
|
import { watch } from '@vue/composition-api'
|
||||||
|
import { AppModule } from '@/types'
|
||||||
|
|
||||||
|
export const install: AppModule = ({ store, app }) => {
|
||||||
|
const defaultLanguage = store.state.ui.currentLanguage ?? 'en_US'
|
||||||
|
const availableLanguages = locales.reduce((map: { [key: string]: string }, locale) => {
|
||||||
|
map[locale.code] = locale.label
|
||||||
|
return map
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
app.use(GetText, {
|
||||||
|
availableLanguages,
|
||||||
|
defaultLanguage,
|
||||||
|
|
||||||
|
// cf https://github.com/Polyconseil/vue-gettext#configuration
|
||||||
|
// not recommended but this is fixing weird bugs with translation nodes
|
||||||
|
// not being updated when in v-if/v-else clauses
|
||||||
|
autoAddKeyAttributes: true,
|
||||||
|
languageVmMixin: {
|
||||||
|
computed: {
|
||||||
|
currentKebabCase (): string {
|
||||||
|
// @ts-ignore
|
||||||
|
return this.current.toLowerCase().replace('_', '-')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
translations: {},
|
||||||
|
silent: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set default language
|
||||||
|
if (!store.state.ui.selectedLanguage) {
|
||||||
|
// NOTE: We're selecting the language only once, hence we don't need to make it reactive
|
||||||
|
const languages = usePreferredLanguages().value.map((code) => {
|
||||||
|
return code.replace(/-/g, '_')
|
||||||
|
})
|
||||||
|
|
||||||
|
let language = Object.keys(availableLanguages).find(code => {
|
||||||
|
return languages.includes(code)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!language) {
|
||||||
|
language = Object.keys(availableLanguages).find(code => {
|
||||||
|
return languages.map(lang => lang.split('_')[0]).includes(code.split('_')[0])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
store.commit('ui/currentLanguage', language ?? defaultLanguage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle language change
|
||||||
|
watch(() => store.state.ui.currentLanguage, (locale) => {
|
||||||
|
const htmlLocale = locale.toLowerCase().replace('_', '-')
|
||||||
|
document.documentElement.setAttribute('lang', htmlLocale)
|
||||||
|
|
||||||
|
if (locale === 'en_US') {
|
||||||
|
Vue.prototype.$language.current = locale
|
||||||
|
store.commit('ui/momentLocale', 'en')
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { AppModule } from '@/types'
|
||||||
|
import { register } from 'register-service-worker'
|
||||||
|
|
||||||
|
export const install: AppModule = ({ store }) => {
|
||||||
|
if (import.meta.env.PROD) {
|
||||||
|
register(`${import.meta.env.BASE_URL}service-worker.js`, {
|
||||||
|
registrationOptions: { scope: '/' },
|
||||||
|
ready () {
|
||||||
|
console.log(
|
||||||
|
'App is being served from cache by a service worker.'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
registered (registration) {
|
||||||
|
console.log('Service worker has been registered.')
|
||||||
|
// check for updates every 2 hours
|
||||||
|
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 })
|
||||||
|
if (registration.active) {
|
||||||
|
registration.active.postMessage({ command: 'serverChosen', serverUrl: store.state.instance.instanceUrl })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cached () {
|
||||||
|
console.log('Content has been cached for offline use.')
|
||||||
|
},
|
||||||
|
updatefound () {
|
||||||
|
console.log('New content is downloading.')
|
||||||
|
},
|
||||||
|
updated (registration) {
|
||||||
|
console.log('New content is available; please refresh!')
|
||||||
|
store.commit('ui/serviceWorker', { updateAvailable: true, registration: registration })
|
||||||
|
},
|
||||||
|
offline () {
|
||||||
|
console.log('No internet connection found. App is running in offline mode.')
|
||||||
|
},
|
||||||
|
error (error) {
|
||||||
|
console.error('Error during service worker registration:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { AppModule } from '@/types'
|
||||||
|
import { watchEffect, watch } from '@vue/composition-api'
|
||||||
|
import { useWebSocket, whenever } from '@vueuse/core'
|
||||||
|
|
||||||
|
export const install: AppModule = ({ store }) => {
|
||||||
|
watch(() => store.state.instance.instanceUrl, () => {
|
||||||
|
const url = store.getters['instance/absoluteUrl']('api/v1/activity')
|
||||||
|
.replace(/^http/, 'ws')
|
||||||
|
|
||||||
|
const { data, status, open, close } = useWebSocket(url, {
|
||||||
|
autoReconnect: true,
|
||||||
|
immediate: false
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => store.state.auth.authenticated, (authenticated) => {
|
||||||
|
if (authenticated) return open()
|
||||||
|
close()
|
||||||
|
})
|
||||||
|
|
||||||
|
whenever(data, () => {
|
||||||
|
return store.dispatch('ui/websocketEvent', JSON.parse(data.value))
|
||||||
|
})
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
console.log('Websocket status:', status.value)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { AppModule } from '@/types'
|
||||||
|
import { useWindowSize } from '@vueuse/core'
|
||||||
|
import { watchEffect } from '@vue/composition-api'
|
||||||
|
|
||||||
|
export const install: AppModule = ({ store }) => {
|
||||||
|
// NOTE: Due to Vuex 3, when using store in watchEffect, it results in an infinite loop after committing
|
||||||
|
const { commit } = store
|
||||||
|
|
||||||
|
const { width, height } = useWindowSize()
|
||||||
|
watchEffect(() => {
|
||||||
|
commit('ui/window', {
|
||||||
|
width: width.value,
|
||||||
|
height: height.value
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,46 +0,0 @@
|
||||||
/* eslint-disable no-console */
|
|
||||||
|
|
||||||
import { register } from 'register-service-worker'
|
|
||||||
|
|
||||||
import store from './store'
|
|
||||||
|
|
||||||
if (import.meta.env.PROD) {
|
|
||||||
register(`${import.meta.env.BASE_URL}service-worker.js`, {
|
|
||||||
registrationOptions: { scope: '/' },
|
|
||||||
ready () {
|
|
||||||
console.log(
|
|
||||||
'App is being served from cache by a service worker.'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
registered (registration) {
|
|
||||||
console.log('Service worker has been registered.')
|
|
||||||
// check for updates every 2 hours
|
|
||||||
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 })
|
|
||||||
if (registration.active) {
|
|
||||||
registration.active.postMessage({ command: 'serverChosen', serverUrl: store.state.instance.instanceUrl })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cached () {
|
|
||||||
console.log('Content has been cached for offline use.')
|
|
||||||
},
|
|
||||||
updatefound () {
|
|
||||||
console.log('New content is downloading.')
|
|
||||||
},
|
|
||||||
updated (registration) {
|
|
||||||
console.log('New content is available; please refresh!')
|
|
||||||
store.commit('ui/serviceWorker', { updateAvailable: true, registration: registration })
|
|
||||||
},
|
|
||||||
offline () {
|
|
||||||
console.log('No internet connection found. App is running in offline mode.')
|
|
||||||
},
|
|
||||||
error (error) {
|
|
||||||
console.error('Error during service worker registration:', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,7 +1,6 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import lodash from 'lodash'
|
|
||||||
|
|
||||||
function getDefaultScopedTokens () {
|
function getDefaultScopedTokens () {
|
||||||
return {
|
return {
|
||||||
|
@ -110,7 +109,7 @@ export default {
|
||||||
state.availablePermissions[key] = status
|
state.availablePermissions[key] = status
|
||||||
},
|
},
|
||||||
profilePartialUpdate: (state, payload) => {
|
profilePartialUpdate: (state, payload) => {
|
||||||
lodash.keys(payload).forEach((k) => {
|
Object.keys(payload).forEach((k) => {
|
||||||
Vue.set(state.profile, k, payload[k])
|
Vue.set(state.profile, k, payload[k])
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Vuex from 'vuex'
|
import Vuex, { Store } from 'vuex'
|
||||||
import createPersistedState from 'vuex-persistedstate'
|
import createPersistedState from 'vuex-persistedstate'
|
||||||
|
|
||||||
import favorites from './favorites'
|
import favorites from './favorites'
|
||||||
|
@ -16,7 +16,7 @@ import ui from './ui'
|
||||||
|
|
||||||
Vue.use(Vuex)
|
Vue.use(Vuex)
|
||||||
|
|
||||||
export default new Vuex.Store({
|
export default <Store<any>> new Vuex.Store({
|
||||||
modules: {
|
modules: {
|
||||||
ui,
|
ui,
|
||||||
auth,
|
auth,
|
||||||
|
@ -44,7 +44,7 @@ export default new Vuex.Store({
|
||||||
}),
|
}),
|
||||||
createPersistedState({
|
createPersistedState({
|
||||||
key: 'ui',
|
key: 'ui',
|
||||||
paths: ['ui.currentLanguage', 'ui.selectedLanguage', 'ui.momentLocale', 'ui.theme', 'ui.routePreferences']
|
paths: ['ui.currentLanguage', 'ui.selectedLanguage', 'ui.momentLocale', 'ui.routePreferences']
|
||||||
}),
|
}),
|
||||||
createPersistedState({
|
createPersistedState({
|
||||||
key: 'radios',
|
key: 'radios',
|
|
@ -1,6 +1,6 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import _ from 'lodash'
|
import { merge } from 'lodash-es'
|
||||||
|
|
||||||
function getDefaultUrl () {
|
function getDefaultUrl () {
|
||||||
return (
|
return (
|
||||||
|
@ -65,7 +65,7 @@ export default {
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
settings: (state, value) => {
|
settings: (state, value) => {
|
||||||
_.merge(state.settings, value)
|
merge(state.settings, value)
|
||||||
},
|
},
|
||||||
event: (state, value) => {
|
event: (state, value) => {
|
||||||
state.events.unshift(value)
|
state.events.unshift(value)
|
||||||
|
@ -112,11 +112,11 @@ export default {
|
||||||
if (relativeUrl.startsWith('http')) {
|
if (relativeUrl.startsWith('http')) {
|
||||||
return relativeUrl
|
return relativeUrl
|
||||||
}
|
}
|
||||||
if (state.instanceUrl.endsWith('/') && relativeUrl.startsWith('/')) {
|
if (state.instanceUrl?.endsWith('/') && relativeUrl.startsWith('/')) {
|
||||||
relativeUrl = relativeUrl.slice(1)
|
relativeUrl = relativeUrl.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const instanceUrl = state.instanceUrl || getDefaultUrl()
|
const instanceUrl = state.instanceUrl ?? getDefaultUrl()
|
||||||
return instanceUrl + relativeUrl
|
return instanceUrl + relativeUrl
|
||||||
},
|
},
|
||||||
domain: (state) => {
|
domain: (state) => {
|
||||||
|
@ -149,17 +149,15 @@ export default {
|
||||||
fetchSettings ({ commit }, payload) {
|
fetchSettings ({ commit }, payload) {
|
||||||
return axios.get('instance/settings/').then(response => {
|
return axios.get('instance/settings/').then(response => {
|
||||||
logger.default.info('Successfully fetched instance settings')
|
logger.default.info('Successfully fetched instance settings')
|
||||||
const sections = {}
|
|
||||||
response.data.forEach(e => {
|
const sections = response.data.reduce((map, entry) => {
|
||||||
sections[e.section] = {}
|
map[entry.section] ??= {}
|
||||||
})
|
map[entry.section][entry.name] = entry
|
||||||
response.data.forEach(e => {
|
return map
|
||||||
sections[e.section][e.name] = e
|
}, {})
|
||||||
})
|
|
||||||
commit('settings', sections)
|
commit('settings', sections)
|
||||||
if (payload && payload.callback) {
|
payload?.callback?.()
|
||||||
payload.callback()
|
|
||||||
}
|
|
||||||
}, response => {
|
}, response => {
|
||||||
logger.default.error('Error while fetching settings', response.data)
|
logger.default.error('Error while fetching settings', response.data)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import _ from 'lodash'
|
import {sortBy} from "lodash-es";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
|
@ -70,7 +70,7 @@ export default {
|
||||||
const f = state.filters.filter((f) => {
|
const f = state.filters.filter((f) => {
|
||||||
return f.target.type === 'artist'
|
return f.target.type === 'artist'
|
||||||
})
|
})
|
||||||
const p = _.sortBy(f, [(e) => { return e.creation_date }])
|
const p = sortBy(f, [(e) => { return e.creation_date }])
|
||||||
p.reverse()
|
p.reverse()
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue