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
e8d767f79d
commit
d9ef5a9b6b
|
@ -28,7 +28,7 @@ Submitting a new language
|
|||
|
||||
1. Pull the latest version of ``develop``
|
||||
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:
|
||||
|
||||
.. code-block:: shell
|
||||
|
|
|
@ -24,6 +24,8 @@ module.exports = {
|
|||
'vue/no-v-html': 'off', // TODO: tackle this properly
|
||||
'vue/no-use-v-if-with-v-for': 'off',
|
||||
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'no-undef': 'off',
|
||||
// TODO: Enable typescript rules later
|
||||
'@typescript-eslint/no-this-alias': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off'
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
<meta name="generator" content="Funkwhale">
|
||||
<link rel="icon" href="/favicon.png">
|
||||
<title>Funkwhale</title>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<style>
|
||||
#fake-app {
|
||||
width: 100vw;
|
||||
|
@ -86,9 +85,8 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="app">
|
||||
</div>
|
||||
<!-- built files will be auto injected -->
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@vue/composition-api": "1.4.9",
|
||||
"@vueuse/core": "8.2.6",
|
||||
"@vueuse/core": "8.2.5",
|
||||
"axios": "0.26.1",
|
||||
"axios-auth-refresh": "3.2.2",
|
||||
"diff": "5.0.0",
|
||||
|
@ -26,10 +26,10 @@
|
|||
"fomantic-ui-css": "2.8.8",
|
||||
"howler": "2.2.3",
|
||||
"js-logger": "1.6.1",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.3",
|
||||
"qs": "6.10.5",
|
||||
"pinia": "^2.0.13",
|
||||
"lodash-es": "4.17.21",
|
||||
"register-service-worker": "1.7.2",
|
||||
"sanitize-html": "2.7.0",
|
||||
"sass": "1.49.11",
|
||||
|
@ -42,6 +42,7 @@
|
|||
"vue-router": "3.5.4",
|
||||
"vue-upload-component": "2.8.22",
|
||||
"vuedraggable": "2.24.3",
|
||||
"vuex": "3.6.2",
|
||||
"vuex-persistedstate": "4.1.0",
|
||||
"vuex-router-sync": "5.0.0"
|
||||
},
|
||||
|
@ -49,6 +50,8 @@
|
|||
"@babel/core": "7.17.12",
|
||||
"@babel/plugin-transform-runtime": "7.17.12",
|
||||
"@babel/preset-env": "7.16.11",
|
||||
"@types/jquery": "^3.5.14",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.19.0",
|
||||
"@vue/eslint-config-standard": "^6.1.0",
|
||||
"@vue/eslint-config-typescript": "^10.0.0",
|
||||
|
@ -60,7 +63,6 @@
|
|||
"easygettext": "2.17.0",
|
||||
"eslint": "8.11.0",
|
||||
"eslint-config-standard": "16.0.3",
|
||||
"eslint-config-standard-with-typescript": "^21.0.1",
|
||||
"eslint-plugin-html": "6.2.0",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
|
@ -71,9 +73,8 @@
|
|||
"moxios": "0.4.0",
|
||||
"sinon": "13.0.2",
|
||||
"typescript": "^4.6.3",
|
||||
"unplugin-vue-components": "^0.19.3",
|
||||
"unplugin-vue2-script-setup": "^0.10.2",
|
||||
"vite": "2.9.5",
|
||||
"vite": "2.8.6",
|
||||
"vite-plugin-vue2": "1.9.3",
|
||||
"vue-jest": "3.0.7",
|
||||
"vue-template-compiler": "2.6.14"
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
cd "$(dirname $0)/.." # change into base directory
|
||||
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
|
||||
|
||||
for locale in $locales; do
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
cd "$(dirname $0)/.." # change into base directory
|
||||
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"
|
||||
sources=$(find src -name '*.vue' -o -name '*.html' 2> /dev/null)
|
||||
js_sources=$(find src -name '*.vue' -o -name '*.js')
|
||||
|
|
|
@ -11,7 +11,7 @@ cd "$(dirname $0)/.." # change into base directory
|
|||
old_locales_dir=$1
|
||||
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.
|
||||
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>
|
||||
<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}]"
|
||||
: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
|
||||
|
@ -25,18 +180,18 @@
|
|||
<service-messages />
|
||||
<transition name="queue">
|
||||
<queue
|
||||
v-if="$store.state.ui.queueFocused"
|
||||
@touch-progress="$refs.player.setCurrentTime($event)"
|
||||
v-if="store.state.ui.queueFocused"
|
||||
@touch-progress="player.setCurrentTime($event)"
|
||||
/>
|
||||
</transition>
|
||||
<router-view
|
||||
role="main"
|
||||
:class="{hidden: $store.state.ui.queueFocused}"
|
||||
: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" />
|
||||
<audio-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"
|
||||
|
@ -46,414 +201,6 @@
|
|||
</div>
|
||||
</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">
|
||||
@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
|
||||
// We suggest you to commit this file into source control
|
||||
// Read more: https://github.com/vuejs/vue-next/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
|
@ -161,4 +160,4 @@ declare module '@vue/runtime-core' {
|
|||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
export { }
|
||||
|
|
|
@ -250,9 +250,9 @@
|
|||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import _ from 'lodash'
|
||||
import { get } from 'lodash-es'
|
||||
import showdown from 'showdown'
|
||||
import { humanSize } from '@/filters'
|
||||
import { humanSize } from '@/modules/filters'
|
||||
|
||||
import SignupForm from '@/components/auth/SignupForm.vue'
|
||||
import LogoText from '@/components/LogoText.vue'
|
||||
|
@ -279,31 +279,31 @@ export default {
|
|||
}
|
||||
},
|
||||
podName () {
|
||||
return _.get(this.nodeinfo, 'metadata.nodeName') || 'Funkwhale'
|
||||
return get(this.nodeinfo, 'metadata.nodeName') || 'Funkwhale'
|
||||
},
|
||||
banner () {
|
||||
return _.get(this.nodeinfo, 'metadata.banner')
|
||||
return get(this.nodeinfo, 'metadata.banner')
|
||||
},
|
||||
shortDescription () {
|
||||
return _.get(this.nodeinfo, 'metadata.shortDescription')
|
||||
return get(this.nodeinfo, 'metadata.shortDescription')
|
||||
},
|
||||
longDescription () {
|
||||
return _.get(this.nodeinfo, 'metadata.longDescription')
|
||||
return get(this.nodeinfo, 'metadata.longDescription')
|
||||
},
|
||||
rules () {
|
||||
return _.get(this.nodeinfo, 'metadata.rules')
|
||||
return get(this.nodeinfo, 'metadata.rules')
|
||||
},
|
||||
terms () {
|
||||
return _.get(this.nodeinfo, 'metadata.terms')
|
||||
return get(this.nodeinfo, 'metadata.terms')
|
||||
},
|
||||
stats () {
|
||||
const data = {
|
||||
users: _.get(this.nodeinfo, 'usage.users.activeMonth', null),
|
||||
hours: _.get(this.nodeinfo, 'metadata.library.music.hours', null),
|
||||
artists: _.get(this.nodeinfo, 'metadata.library.artists.total', null),
|
||||
albums: _.get(this.nodeinfo, 'metadata.library.albums.total', null),
|
||||
tracks: _.get(this.nodeinfo, 'metadata.library.tracks.total', null),
|
||||
listenings: _.get(this.nodeinfo, 'metadata.usage.listenings.total', null)
|
||||
users: get(this.nodeinfo, 'usage.users.activeMonth', null),
|
||||
hours: get(this.nodeinfo, 'metadata.library.music.hours', null),
|
||||
artists: get(this.nodeinfo, 'metadata.library.artists.total', null),
|
||||
albums: get(this.nodeinfo, 'metadata.library.albums.total', null),
|
||||
tracks: get(this.nodeinfo, 'metadata.library.tracks.total', null),
|
||||
listenings: get(this.nodeinfo, 'metadata.usage.listenings.total', null)
|
||||
}
|
||||
if (data.users === null || data.artists === null) {
|
||||
return
|
||||
|
@ -311,28 +311,28 @@ export default {
|
|||
return data
|
||||
},
|
||||
contactEmail () {
|
||||
return _.get(this.nodeinfo, 'metadata.contactEmail')
|
||||
return get(this.nodeinfo, 'metadata.contactEmail')
|
||||
},
|
||||
anonymousCanListen () {
|
||||
return _.get(this.nodeinfo, 'metadata.library.anonymousCanListen')
|
||||
return get(this.nodeinfo, 'metadata.library.anonymousCanListen')
|
||||
},
|
||||
allowListEnabled () {
|
||||
return _.get(this.nodeinfo, 'metadata.allowList.enabled')
|
||||
return get(this.nodeinfo, 'metadata.allowList.enabled')
|
||||
},
|
||||
allowListDomains () {
|
||||
return _.get(this.nodeinfo, 'metadata.allowList.domains')
|
||||
return get(this.nodeinfo, 'metadata.allowList.domains')
|
||||
},
|
||||
version () {
|
||||
return _.get(this.nodeinfo, 'software.version')
|
||||
return get(this.nodeinfo, 'software.version')
|
||||
},
|
||||
openRegistrations () {
|
||||
return _.get(this.nodeinfo, 'openRegistrations')
|
||||
return get(this.nodeinfo, 'openRegistrations')
|
||||
},
|
||||
defaultUploadQuota () {
|
||||
return humanSize(_.get(this.nodeinfo, 'metadata.defaultUploadQuota') * 1000 * 1000)
|
||||
return humanSize(get(this.nodeinfo, 'metadata.defaultUploadQuota') * 1000 * 1000)
|
||||
},
|
||||
federationEnabled () {
|
||||
return _.get(this.nodeinfo, 'metadata.library.federationEnabled')
|
||||
return get(this.nodeinfo, 'metadata.library.federationEnabled')
|
||||
},
|
||||
headerStyle () {
|
||||
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>
|
||||
import { mapState } from 'vuex'
|
||||
import _ from 'lodash'
|
||||
import { get } from 'lodash-es'
|
||||
import showdown from 'showdown'
|
||||
|
||||
export default {
|
||||
|
@ -455,31 +455,31 @@ export default {
|
|||
}
|
||||
},
|
||||
podName () {
|
||||
return _.get(this.nodeinfo, 'metadata.nodeName') || 'Funkwhale'
|
||||
return get(this.nodeinfo, 'metadata.nodeName') || 'Funkwhale'
|
||||
},
|
||||
banner () {
|
||||
return _.get(this.nodeinfo, 'metadata.banner')
|
||||
return get(this.nodeinfo, 'metadata.banner')
|
||||
},
|
||||
shortDescription () {
|
||||
return _.get(this.nodeinfo, 'metadata.shortDescription')
|
||||
return get(this.nodeinfo, 'metadata.shortDescription')
|
||||
},
|
||||
longDescription () {
|
||||
return _.get(this.nodeinfo, 'metadata.longDescription')
|
||||
return get(this.nodeinfo, 'metadata.longDescription')
|
||||
},
|
||||
rules () {
|
||||
return _.get(this.nodeinfo, 'metadata.rules')
|
||||
return get(this.nodeinfo, 'metadata.rules')
|
||||
},
|
||||
terms () {
|
||||
return _.get(this.nodeinfo, 'metadata.terms')
|
||||
return get(this.nodeinfo, 'metadata.terms')
|
||||
},
|
||||
stats () {
|
||||
const data = {
|
||||
users: _.get(this.nodeinfo, 'usage.users.activeMonth', null),
|
||||
hours: _.get(this.nodeinfo, 'metadata.library.music.hours', null),
|
||||
artists: _.get(this.nodeinfo, 'metadata.library.artists.total', null),
|
||||
albums: _.get(this.nodeinfo, 'metadata.library.albums.total', null),
|
||||
tracks: _.get(this.nodeinfo, 'metadata.library.tracks.total', null),
|
||||
listenings: _.get(this.nodeinfo, 'metadata.usage.listenings.total', null)
|
||||
users: get(this.nodeinfo, 'usage.users.activeMonth', null),
|
||||
hours: get(this.nodeinfo, 'metadata.library.music.hours', null),
|
||||
artists: get(this.nodeinfo, 'metadata.library.artists.total', null),
|
||||
albums: get(this.nodeinfo, 'metadata.library.albums.total', null),
|
||||
tracks: get(this.nodeinfo, 'metadata.library.tracks.total', null),
|
||||
listenings: get(this.nodeinfo, 'metadata.usage.listenings.total', null)
|
||||
}
|
||||
if (data.users === null || data.artists === null) {
|
||||
return
|
||||
|
@ -487,28 +487,28 @@ export default {
|
|||
return data
|
||||
},
|
||||
contactEmail () {
|
||||
return _.get(this.nodeinfo, 'metadata.contactEmail')
|
||||
return get(this.nodeinfo, 'metadata.contactEmail')
|
||||
},
|
||||
anonymousCanListen () {
|
||||
return _.get(this.nodeinfo, 'metadata.library.anonymousCanListen')
|
||||
return get(this.nodeinfo, 'metadata.library.anonymousCanListen')
|
||||
},
|
||||
allowListEnabled () {
|
||||
return _.get(this.nodeinfo, 'metadata.allowList.enabled')
|
||||
return get(this.nodeinfo, 'metadata.allowList.enabled')
|
||||
},
|
||||
allowListDomains () {
|
||||
return _.get(this.nodeinfo, 'metadata.allowList.domains')
|
||||
return get(this.nodeinfo, 'metadata.allowList.domains')
|
||||
},
|
||||
version () {
|
||||
return _.get(this.nodeinfo, 'software.version')
|
||||
return get(this.nodeinfo, 'software.version')
|
||||
},
|
||||
openRegistrations () {
|
||||
return _.get(this.nodeinfo, 'openRegistrations')
|
||||
return get(this.nodeinfo, 'openRegistrations')
|
||||
},
|
||||
defaultUploadQuota () {
|
||||
return _.get(this.nodeinfo, 'metadata.defaultUploadQuota')
|
||||
return get(this.nodeinfo, 'metadata.defaultUploadQuota')
|
||||
},
|
||||
federationEnabled () {
|
||||
return _.get(this.nodeinfo, 'metadata.library.federationEnabled')
|
||||
return get(this.nodeinfo, 'metadata.library.federationEnabled')
|
||||
},
|
||||
headerStyle () {
|
||||
if (!this.banner) {
|
||||
|
|
|
@ -325,14 +325,14 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { get } from 'lodash-es'
|
||||
import { mapState } from 'vuex'
|
||||
import showdown from 'showdown'
|
||||
import AlbumWidget from '@/components/audio/album/Widget.vue'
|
||||
import ChannelsWidget from '@/components/audio/ChannelsWidget.vue'
|
||||
import LoginForm from '@/components/auth/LoginForm.vue'
|
||||
import SignupForm from '@/components/auth/SignupForm.vue'
|
||||
import { humanSize } from '@/filters'
|
||||
import { humanSize } from '@/modules/filters'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -358,19 +358,19 @@ export default {
|
|||
}
|
||||
},
|
||||
podName () {
|
||||
return _.get(this.nodeinfo, 'metadata.nodeName') || 'Funkwhale'
|
||||
return get(this.nodeinfo, 'metadata.nodeName') || 'Funkwhale'
|
||||
},
|
||||
banner () {
|
||||
return _.get(this.nodeinfo, 'metadata.banner')
|
||||
return get(this.nodeinfo, 'metadata.banner')
|
||||
},
|
||||
shortDescription () {
|
||||
return _.get(this.nodeinfo, 'metadata.shortDescription')
|
||||
return get(this.nodeinfo, 'metadata.shortDescription')
|
||||
},
|
||||
longDescription () {
|
||||
return _.get(this.nodeinfo, 'metadata.longDescription')
|
||||
return get(this.nodeinfo, 'metadata.longDescription')
|
||||
},
|
||||
rules () {
|
||||
return _.get(this.nodeinfo, 'metadata.rules')
|
||||
return get(this.nodeinfo, 'metadata.rules')
|
||||
},
|
||||
renderedDescription () {
|
||||
if (!this.longDescription) {
|
||||
|
@ -381,8 +381,8 @@ export default {
|
|||
},
|
||||
stats () {
|
||||
const data = {
|
||||
users: _.get(this.nodeinfo, 'usage.users.activeMonth', null),
|
||||
hours: _.get(this.nodeinfo, 'metadata.library.music.hours', null)
|
||||
users: get(this.nodeinfo, 'usage.users.activeMonth', null),
|
||||
hours: get(this.nodeinfo, 'metadata.library.music.hours', null)
|
||||
}
|
||||
if (data.users === null || data.artists === null) {
|
||||
return
|
||||
|
@ -390,16 +390,16 @@ export default {
|
|||
return data
|
||||
},
|
||||
contactEmail () {
|
||||
return _.get(this.nodeinfo, 'metadata.contactEmail')
|
||||
return get(this.nodeinfo, 'metadata.contactEmail')
|
||||
},
|
||||
defaultUploadQuota () {
|
||||
return _.get(this.nodeinfo, 'metadata.defaultUploadQuota')
|
||||
return get(this.nodeinfo, 'metadata.defaultUploadQuota')
|
||||
},
|
||||
anonymousCanListen () {
|
||||
return _.get(this.nodeinfo, 'metadata.library.anonymousCanListen')
|
||||
return get(this.nodeinfo, 'metadata.library.anonymousCanListen')
|
||||
},
|
||||
openRegistrations () {
|
||||
return _.get(this.nodeinfo, 'openRegistrations')
|
||||
return get(this.nodeinfo, 'openRegistrations')
|
||||
},
|
||||
headerStyle () {
|
||||
if (!this.banner) {
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { range as lodashRange, sortBy, uniq } from 'lodash-es'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
@ -57,15 +57,15 @@ export default {
|
|||
pages: function () {
|
||||
const range = 2
|
||||
const current = this.current
|
||||
const beginning = _.range(1, Math.min(this.maxPage, 1 + range))
|
||||
const middle = _.range(
|
||||
const beginning = lodashRange(1, Math.min(this.maxPage, 1 + range))
|
||||
const middle = lodashRange(
|
||||
Math.max(1, current - range + 1),
|
||||
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)
|
||||
allowed = _.uniq(allowed)
|
||||
allowed = _.sortBy(allowed, [
|
||||
allowed = uniq(allowed)
|
||||
allowed = sortBy(allowed, [
|
||||
e => {
|
||||
return e
|
||||
}
|
||||
|
|
|
@ -345,8 +345,8 @@
|
|||
import { mapState, mapGetters, mapActions } from 'vuex'
|
||||
import $ from 'jquery'
|
||||
import moment from 'moment'
|
||||
import lodash from 'lodash'
|
||||
import time from '@/utils/time.js'
|
||||
import { sum } from 'lodash-es'
|
||||
import time from '@/utils/time'
|
||||
import { createFocusTrap } from 'focus-trap'
|
||||
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon.vue'
|
||||
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon.vue'
|
||||
|
@ -405,7 +405,7 @@ export default {
|
|||
}
|
||||
},
|
||||
timeLeft () {
|
||||
const seconds = lodash.sum(
|
||||
const seconds = sum(
|
||||
this.queue.tracks.slice(this.queue.currentIndex).map((t) => {
|
||||
return (t.uploads || []).map((u) => {
|
||||
return u.duration || 0
|
||||
|
|
|
@ -107,7 +107,7 @@
|
|||
<script>
|
||||
import Modal from '@/components/semantic/Modal.vue'
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import { uniq } from 'lodash-es'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -135,7 +135,7 @@ export default {
|
|||
}
|
||||
const self = this
|
||||
instances.push(this.$store.getters['instance/defaultUrl'](), 'https://demo.funkwhale.audio/')
|
||||
return _.uniq(instances.filter((e) => { return e !== self.$store.state.instance.instanceUrl }))
|
||||
return uniq(instances.filter((e) => { return e !== self.$store.state.instance.instanceUrl }))
|
||||
},
|
||||
instanceHostname () {
|
||||
const url = this.$store.state.instance.instanceUrl
|
||||
|
|
|
@ -208,17 +208,17 @@
|
|||
</div>
|
||||
<div class="content">
|
||||
<fieldset
|
||||
v-for="theme in themes"
|
||||
:key="theme.key"
|
||||
v-for="t in themes"
|
||||
:key="t.key"
|
||||
>
|
||||
<input
|
||||
:id="theme.key"
|
||||
v-model="themeSelection"
|
||||
:id="t.key"
|
||||
v-model="theme"
|
||||
type="radio"
|
||||
name="theme"
|
||||
:value="theme.key"
|
||||
:value="t.key"
|
||||
>
|
||||
<label :for="theme.key">{{ theme.name }}</label>
|
||||
<label :for="t.key">{{ t.name }}</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
</modal>
|
||||
|
@ -475,11 +475,11 @@ import { mapState, mapActions, mapGetters } from 'vuex'
|
|||
import UserModal from '@/components/common/UserModal.vue'
|
||||
import Logo from '@/components/Logo.vue'
|
||||
import SearchBar from '@/components/audio/SearchBar.vue'
|
||||
import ThemesMixin from '@/components/mixins/Themes.vue'
|
||||
import UserMenu from '@/components/common/UserMenu.vue'
|
||||
import Modal from '@/components/semantic/Modal.vue'
|
||||
|
||||
import $ from 'jquery'
|
||||
import useThemeList from '@/composables/useThemeList'
|
||||
|
||||
export default {
|
||||
name: 'Sidebar',
|
||||
|
@ -490,10 +490,15 @@ export default {
|
|||
UserModal,
|
||||
Modal
|
||||
},
|
||||
mixins: [ThemesMixin],
|
||||
props: {
|
||||
width: { type: Number, required: true }
|
||||
},
|
||||
setup () {
|
||||
const theme = useTheme()
|
||||
const themes = useThemeList()
|
||||
|
||||
return { theme, themes }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
selectedTab: 'library',
|
||||
|
@ -504,8 +509,7 @@ export default {
|
|||
showUserModal: false,
|
||||
showLanguageModal: false,
|
||||
showThemeModal: false,
|
||||
languageSelection: this.$language.current,
|
||||
themeSelection: this.$store.state.ui.theme
|
||||
languageSelection: this.$language.current
|
||||
}
|
||||
},
|
||||
destroy () {
|
||||
|
@ -645,10 +649,6 @@ export default {
|
|||
languageSelection: function (v) {
|
||||
this.$store.dispatch('ui/currentLanguage', v)
|
||||
this.$refs.languageModal.closeModal()
|
||||
},
|
||||
themeSelection: function (v) {
|
||||
this.$store.dispatch('ui/theme', v)
|
||||
this.$refs.themeModal.closeModal()
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
|
@ -691,11 +691,14 @@ export default {
|
|||
// works as expected
|
||||
const link = $($el).closest('a')
|
||||
const url = link.attr('href')
|
||||
if (url.startsWith('http')) {
|
||||
window.open(url, '_blank').focus()
|
||||
} else {
|
||||
self.$router.push(url)
|
||||
if (url) {
|
||||
if (url.startsWith('http')) {
|
||||
window.open(url, '_blank').focus()
|
||||
} else {
|
||||
self.$router.push(url)
|
||||
}
|
||||
}
|
||||
|
||||
$(self.$el).find(selector).dropdown('hide')
|
||||
}
|
||||
})
|
||||
|
|
|
@ -157,7 +157,7 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import lodash from 'lodash'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import SignupFormBuilder from '@/components/admin/SignupFormBuilder.vue'
|
||||
|
||||
export default {
|
||||
|
@ -241,7 +241,7 @@ export default {
|
|||
},
|
||||
set (key, value) {
|
||||
// otherwise reactivity doesn't trigger :/
|
||||
this.values = lodash.cloneDeep(this.values)
|
||||
this.values = cloneDeep(this.values)
|
||||
this.$set(this.values, key, value)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -161,7 +161,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import lodash from 'lodash'
|
||||
import { cloneDeep, tap, set } from 'lodash-es'
|
||||
|
||||
import SignupForm from '@/components/auth/SignupForm.vue'
|
||||
|
||||
|
@ -209,7 +209,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
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),
|
||||
required: true,
|
||||
input_type: 'short_text'
|
||||
|
@ -217,7 +217,7 @@ export default {
|
|||
this.$emit('input', newValue)
|
||||
},
|
||||
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) {
|
||||
if (idx === 0 && incr < 0) {
|
||||
|
@ -226,7 +226,7 @@ export default {
|
|||
if (idx + incr >= this.local.fields.length) {
|
||||
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)
|
||||
},
|
||||
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 TagsList from '@/components/tags/List.vue'
|
||||
|
||||
import { momentFormat } from '@/filters'
|
||||
import { momentFormat } from '@/modules/filters'
|
||||
import moment from 'moment'
|
||||
|
||||
export default {
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { clone } from 'lodash-es'
|
||||
import axios from 'axios'
|
||||
import PodcastTable from '@/components/audio/podcast/Table.vue'
|
||||
import TrackTable from '@/components/audio/track/Table.vue'
|
||||
|
@ -96,7 +96,7 @@ export default {
|
|||
}
|
||||
this.isLoading = true
|
||||
const self = this
|
||||
const params = _.clone(this.filters)
|
||||
const params = clone(this.filters)
|
||||
params.page_size = this.limit
|
||||
params.page = this.page
|
||||
params.include_channels = true
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { clone } from 'lodash-es'
|
||||
import axios from 'axios'
|
||||
import ChannelSerieCard from '@/components/audio/ChannelSerieCard.vue'
|
||||
import AlbumCard from '@/components/audio/album/Card.vue'
|
||||
|
@ -87,7 +87,7 @@ export default {
|
|||
}
|
||||
this.isLoading = true
|
||||
const self = this
|
||||
const params = _.clone(this.filters)
|
||||
const params = clone(this.filters)
|
||||
params.page_size = this.limit
|
||||
params.include_channels = true
|
||||
axios.get(url, { params: params }).then((response) => {
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { clone } from 'lodash-es'
|
||||
import axios from 'axios'
|
||||
import ChannelCard from '@/components/audio/ChannelCard.vue'
|
||||
|
||||
|
@ -68,7 +68,7 @@ export default {
|
|||
}
|
||||
this.isLoading = true
|
||||
const self = this
|
||||
const params = _.clone(this.filters)
|
||||
const params = clone(this.filters)
|
||||
params.page_size = this.limit
|
||||
params.include_channels = true
|
||||
axios.get(url, { params: params }).then((response) => {
|
||||
|
|
|
@ -106,7 +106,7 @@
|
|||
<script>
|
||||
|
||||
import { mapState } from 'vuex'
|
||||
import _ from 'lodash'
|
||||
import { get } from 'lodash-es'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
@ -131,7 +131,7 @@ export default {
|
|||
nodeinfo: state => state.instance.nodeinfo
|
||||
}),
|
||||
anonymousCanListen () {
|
||||
return _.get(this.nodeinfo, 'metadata.library.anonymousCanListen', false)
|
||||
return get(this.nodeinfo, 'metadata.library.anonymousCanListen', false)
|
||||
},
|
||||
iframeSrc () {
|
||||
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 { toLinearVolumeScale } from '@/audio/volume.js'
|
||||
import { Howl, Howler } from 'howler'
|
||||
import _ from 'lodash'
|
||||
import url from '@/utils/url'
|
||||
import { throttle, reverse } from 'lodash-es'
|
||||
import axios from 'axios'
|
||||
import VolumeControl from './VolumeControl.vue'
|
||||
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon.vue'
|
||||
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon.vue'
|
||||
import updateQueryString from '@/composables/updateQueryString'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -399,7 +399,7 @@ export default {
|
|||
progress: 'player/progress'
|
||||
}),
|
||||
updateProgressThrottled () {
|
||||
return _.throttle(this.updateProgress, 50)
|
||||
return throttle(this.updateProgress, 50)
|
||||
},
|
||||
labels () {
|
||||
const audioPlayer = this.$pgettext('Sidebar/Player/Hidden text', 'Media player')
|
||||
|
@ -655,7 +655,7 @@ export default {
|
|||
// not support other codecs to be able to play it :)
|
||||
sources.push({
|
||||
type: 'mp3',
|
||||
url: url.updateQueryString(
|
||||
url: updateQueryString(
|
||||
this.$store.getters['instance/absoluteUrl'](trackData.listen_url),
|
||||
'to',
|
||||
'mp3'
|
||||
|
@ -675,7 +675,7 @@ export default {
|
|||
value = this.$store.state.auth.scopedTokens.listen
|
||||
}
|
||||
sources.forEach(e => {
|
||||
e.url = url.updateQueryString(e.url, param, value)
|
||||
e.url = updateQueryString(e.url, param, value)
|
||||
})
|
||||
}
|
||||
return sources
|
||||
|
@ -816,14 +816,14 @@ export default {
|
|||
checkCache () {
|
||||
const self = this
|
||||
const toKeep = []
|
||||
_.reverse(this.soundsCache).forEach((e) => {
|
||||
reverse(this.soundsCache).forEach((e) => {
|
||||
if (toKeep.length < self.maxPreloaded) {
|
||||
toKeep.push(e)
|
||||
} else {
|
||||
e.sound.unload()
|
||||
}
|
||||
})
|
||||
this.soundsCache = _.reverse(toKeep)
|
||||
this.soundsCache = reverse(toKeep)
|
||||
},
|
||||
removeFromCache (sound) {
|
||||
const toKeep = []
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { debounce } from 'lodash-es'
|
||||
import axios from 'axios'
|
||||
import logger from '@/logging'
|
||||
import AlbumCard from '@/components/audio/album/Card.vue'
|
||||
|
@ -112,7 +112,7 @@ export default {
|
|||
this.search()
|
||||
},
|
||||
methods: {
|
||||
search: _.debounce(function () {
|
||||
search: debounce(function () {
|
||||
if (this.query.length < 1) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<script>
|
||||
import jQuery from 'jquery'
|
||||
import router from '@/router'
|
||||
import lodash from 'lodash'
|
||||
import { trim } from 'lodash-es'
|
||||
import GlobalEvents from '@/components/utils/global-events.vue'
|
||||
|
||||
export default {
|
||||
|
@ -248,8 +248,8 @@ export default {
|
|||
this.$refs.search.focus()
|
||||
},
|
||||
extractObjId (query) {
|
||||
query = lodash.trim(query)
|
||||
query = lodash.trim(query, '@')
|
||||
query = trim(query)
|
||||
query = trim(query, '@')
|
||||
if (query.indexOf(' ') > -1) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -158,12 +158,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { clone } from 'lodash-es'
|
||||
import axios from 'axios'
|
||||
import TrackRow from '@/components/audio/track/Row.vue'
|
||||
import TrackMobileRow from '@/components/audio/track/MobileRow.vue'
|
||||
import Pagination from '@/components/Pagination.vue'
|
||||
import { unique } from '@/filters'
|
||||
import { unique } from '@/modules/filters'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -228,7 +228,7 @@ export default {
|
|||
}
|
||||
this.isLoading = true
|
||||
const self = this
|
||||
const params = _.clone(this.filters)
|
||||
const params = clone(this.filters)
|
||||
params.page_size = this.paginateBy
|
||||
params.page = this.currentPage
|
||||
params.include_channels = true
|
||||
|
|
|
@ -135,7 +135,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { clone } from 'lodash-es'
|
||||
import axios from 'axios'
|
||||
import PlayButton from '@/components/audio/PlayButton.vue'
|
||||
import TagsList from '@/components/tags/List.vue'
|
||||
|
@ -184,7 +184,7 @@ export default {
|
|||
}
|
||||
this.isLoading = true
|
||||
const self = this
|
||||
const params = _.clone(this.filters)
|
||||
const params = clone(this.filters)
|
||||
params.page_size = this.limit
|
||||
params.offset = this.offset
|
||||
axios.get(url, { params: params }).then((response) => {
|
||||
|
|
|
@ -119,7 +119,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { uniq } from 'lodash-es'
|
||||
import axios from 'axios'
|
||||
import TranslationsMixin from '@/components/mixins/Translations.vue'
|
||||
|
||||
|
@ -163,7 +163,7 @@ export default {
|
|||
return this.fields.scopes.split(' ')
|
||||
},
|
||||
set (v) {
|
||||
this.fields.scopes = _.uniq(v).join(' ')
|
||||
this.fields.scopes = uniq(v).join(' ')
|
||||
}
|
||||
},
|
||||
allScopes () {
|
||||
|
|
|
@ -229,8 +229,8 @@ export default {
|
|||
})
|
||||
}
|
||||
},
|
||||
created () {
|
||||
checkRedirectToLogin(this.$store, this.$router)
|
||||
async created () {
|
||||
await checkRedirectToLogin(this.$store, this.$router)
|
||||
if (this.clientId) {
|
||||
this.fetchApplication()
|
||||
}
|
||||
|
|
|
@ -166,7 +166,7 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import lodash from 'lodash'
|
||||
import { clone } from 'lodash-es'
|
||||
import showdown from 'showdown'
|
||||
export default {
|
||||
props: {
|
||||
|
@ -178,7 +178,7 @@ export default {
|
|||
markdown: new showdown.Converter(),
|
||||
isLoading: false,
|
||||
enabled: this.plugin.enabled,
|
||||
values: lodash.clone(this.plugin.values || {}),
|
||||
values: clone(this.plugin.values || {}),
|
||||
errors: []
|
||||
}
|
||||
},
|
||||
|
|
|
@ -144,7 +144,7 @@
|
|||
<script>
|
||||
import Modal from '@/components/semantic/Modal.vue'
|
||||
import ChannelUploadForm from '@/components/channels/UploadForm.vue'
|
||||
import { humanSize } from '@/filters'
|
||||
import { humanSize } from '@/modules/filters'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
</span>
|
||||
</template>
|
||||
<script>
|
||||
import { secondsToObject } from '@/filters'
|
||||
import { secondsToObject } from '@/modules/filters'
|
||||
|
||||
export default {
|
||||
props: { seconds: { type: Number, default: null } },
|
||||
|
|
|
@ -26,14 +26,14 @@
|
|||
class="menu"
|
||||
>
|
||||
<a
|
||||
v-for="theme in themes"
|
||||
:key="theme.key"
|
||||
:class="[{'active': $store.state.ui.theme === theme.key}, 'item']"
|
||||
:value="theme.key"
|
||||
@click="$store.dispatch('ui/theme', theme.key)"
|
||||
v-for="t in themes"
|
||||
:key="t.key"
|
||||
:class="[{'active': theme === t.key}, 'item']"
|
||||
:value="t.key"
|
||||
@click="theme = t.key"
|
||||
>
|
||||
<i :class="theme.icon" />
|
||||
{{ theme.name }}
|
||||
<i :class="t.icon" />
|
||||
{{ t.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -154,11 +154,16 @@
|
|||
<script>
|
||||
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import ThemesMixin from '@/components/mixins/Themes.vue'
|
||||
import useThemeList from '@/composables/useThemeList'
|
||||
import useTheme from '@/composables/useTheme'
|
||||
|
||||
export default {
|
||||
mixins: [ThemesMixin],
|
||||
setup () {
|
||||
return {
|
||||
theme: useTheme(),
|
||||
themes: useThemeList()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
return {
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
<i class="palette icon user-modal list-icon" />
|
||||
<span class="user-modal list-item">{{ labels.theme }}:</span>
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -175,17 +175,23 @@
|
|||
|
||||
<script>
|
||||
import Modal from '@/components/semantic/Modal.vue'
|
||||
import ThemesMixin from '@/components/mixins/Themes.vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
import useThemeList from '@/composables/useThemeList'
|
||||
import useTheme from '@/composables/useTheme'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal
|
||||
},
|
||||
mixins: [ThemesMixin],
|
||||
props: {
|
||||
show: { type: Boolean, required: true }
|
||||
},
|
||||
setup () {
|
||||
return {
|
||||
theme: useTheme(),
|
||||
themes: useThemeList()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
return {
|
||||
|
|
|
@ -191,8 +191,8 @@ export default {
|
|||
this.updateQueryString()
|
||||
}
|
||||
},
|
||||
created () {
|
||||
checkRedirectToLogin(this.$store, this.$router)
|
||||
async created () {
|
||||
await checkRedirectToLogin(this.$store, this.$router)
|
||||
this.fetchFavorites(FAVORITES_URL)
|
||||
},
|
||||
mounted () {
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { clone } from 'lodash-es'
|
||||
import axios from 'axios'
|
||||
import LibraryCard from '@/views/content/remote/Card.vue'
|
||||
|
||||
|
@ -86,7 +86,7 @@ export default {
|
|||
fetchData (url) {
|
||||
this.isLoading = true
|
||||
const self = this
|
||||
const params = _.clone({})
|
||||
const params = clone({})
|
||||
params.page_size = this.limit
|
||||
params.offset = this.offset
|
||||
axios.get(url, { params: params }).then((response) => {
|
||||
|
|
|
@ -249,7 +249,7 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import lodash from 'lodash'
|
||||
import { sum } from 'lodash-es'
|
||||
import PlayButton from '@/components/audio/PlayButton.vue'
|
||||
import TagsList from '@/components/tags/List.vue'
|
||||
import ArtistLabel from '@/components/audio/ArtistLabel.vue'
|
||||
|
@ -307,7 +307,7 @@ export default {
|
|||
durations.push(t.uploads[0].duration)
|
||||
}
|
||||
})
|
||||
return lodash.sum(durations)
|
||||
return sum(durations)
|
||||
},
|
||||
labels () {
|
||||
return {
|
||||
|
|
|
@ -92,7 +92,7 @@
|
|||
|
||||
<script>
|
||||
|
||||
import time from '@/utils/time.js'
|
||||
import time from '@/utils/time'
|
||||
import LibraryWidget from '@/components/federation/LibraryWidget.vue'
|
||||
import ChannelEntries from '@/components/audio/ChannelEntries.vue'
|
||||
import TrackTable from '@/components/audio/track/Table.vue'
|
||||
|
|
|
@ -215,7 +215,7 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import logger from '@/logging.js'
|
||||
import logger from '@/logging'
|
||||
import PlayButton from '@/components/audio/PlayButton.vue'
|
||||
import EmbedWizard from '@/components/audio/EmbedWizard.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 ReportMixin from '@/components/mixins/Report.vue'
|
||||
|
||||
import { getDomain } from '@/utils.js'
|
||||
import { getDomain } from '@/utils'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
|
@ -167,7 +167,7 @@ import qs from 'qs'
|
|||
import axios from 'axios'
|
||||
import $ from 'jquery'
|
||||
|
||||
import logger from '@/logging.js'
|
||||
import logger from '@/logging'
|
||||
|
||||
import OrderingMixin from '@/components/mixins/Ordering.vue'
|
||||
import PaginationMixin from '@/components/mixins/Pagination.vue'
|
||||
|
|
|
@ -240,7 +240,7 @@
|
|||
|
||||
<script>
|
||||
import $ from 'jquery'
|
||||
import _ from 'lodash'
|
||||
import { isEqual, clone } from 'lodash-es'
|
||||
import axios from 'axios'
|
||||
import AttachmentInput from '@/components/common/AttachmentInput.vue'
|
||||
import EditList from '@/components/library/EditList.vue'
|
||||
|
@ -296,7 +296,7 @@ export default {
|
|||
mutationPayload () {
|
||||
const self = this
|
||||
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) {
|
||||
return null
|
||||
|
@ -339,15 +339,15 @@ export default {
|
|||
setValues () {
|
||||
const self = this
|
||||
this.config.fields.forEach(f => {
|
||||
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.values, f.id, clone(f.getValue(self.object)))
|
||||
self.$set(self.initialValues, f.id, clone(self.values[f.id]))
|
||||
})
|
||||
},
|
||||
submit () {
|
||||
const self = this
|
||||
self.isLoading = true
|
||||
self.errors = []
|
||||
const payload = _.clone(this.mutationPayload || {})
|
||||
const payload = clone(this.mutationPayload || {})
|
||||
if (this.canEdit) {
|
||||
payload.is_approved = true
|
||||
}
|
||||
|
@ -363,10 +363,10 @@ export default {
|
|||
)
|
||||
},
|
||||
fieldValuesChanged (fieldId) {
|
||||
return !_.isEqual(this.values[fieldId], this.initialValues[fieldId])
|
||||
return !isEqual(this.values[fieldId], this.initialValues[fieldId])
|
||||
},
|
||||
resetField (fieldId) {
|
||||
this.values[fieldId] = _.clone(this.initialValues[fieldId])
|
||||
this.values[fieldId] = clone(this.initialValues[fieldId])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { clone } from 'lodash-es'
|
||||
import axios from 'axios'
|
||||
|
||||
import EditCard from '@/components/library/EditCard.vue'
|
||||
|
@ -84,7 +84,7 @@ export default {
|
|||
}
|
||||
this.isLoading = true
|
||||
const self = this
|
||||
const params = _.clone(this.filters)
|
||||
const params = clone(this.filters)
|
||||
params.page_size = this.limit
|
||||
axios.get(url, { params: params }).then((response) => {
|
||||
self.previousPage = response.data.previous
|
||||
|
|
|
@ -311,7 +311,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { sortBy, debounce } from 'lodash-es'
|
||||
import axios from 'axios'
|
||||
import FileUploadWidget from './FileUploadWidget.vue'
|
||||
import FsBrowser from './FsBrowser.vue'
|
||||
|
@ -435,7 +435,7 @@ export default {
|
|||
sortedFiles () {
|
||||
// return errored files on top
|
||||
|
||||
return _.sortBy(this.files.map(f => {
|
||||
return sortBy(this.files.map(f => {
|
||||
let statusIndex = 0
|
||||
if (f.errored) {
|
||||
statusIndex = -1
|
||||
|
@ -467,7 +467,7 @@ export default {
|
|||
}
|
||||
},
|
||||
watch: {
|
||||
importReference: _.debounce(function () {
|
||||
importReference: debounce(function () {
|
||||
this.$router.replace({ query: { import: this.importReference } })
|
||||
}, 500),
|
||||
remainingSpace (newValue) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import FileUpload from 'vue-upload-component'
|
||||
import { setCsrf } from '@/utils.js'
|
||||
import { setCsrf } from '@/utils'
|
||||
|
||||
export default {
|
||||
extends: FileUpload,
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import logger from '@/logging.js'
|
||||
import logger from '@/logging'
|
||||
import ChannelsWidget from '@/components/audio/ChannelsWidget.vue'
|
||||
import TrackWidget from '@/components/audio/track/Widget.vue'
|
||||
import AlbumWidget from '@/components/audio/album/Widget.vue'
|
||||
|
|
|
@ -199,7 +199,7 @@ import qs from 'qs'
|
|||
import axios from 'axios'
|
||||
import $ from 'jquery'
|
||||
|
||||
import logger from '@/logging.js'
|
||||
import logger from '@/logging'
|
||||
|
||||
import OrderingMixin from '@/components/mixins/Ordering.vue'
|
||||
import PaginationMixin from '@/components/mixins/Pagination.vue'
|
||||
|
|
|
@ -178,7 +178,7 @@
|
|||
import axios from 'axios'
|
||||
import $ from 'jquery'
|
||||
|
||||
import logger from '@/logging.js'
|
||||
import logger from '@/logging'
|
||||
|
||||
import OrderingMixin from '@/components/mixins/Ordering.vue'
|
||||
import PaginationMixin from '@/components/mixins/Pagination.vue'
|
||||
|
|
|
@ -20,14 +20,14 @@
|
|||
<script>
|
||||
import $ from 'jquery'
|
||||
|
||||
import lodash from 'lodash'
|
||||
import { isEqual } from 'lodash-es'
|
||||
export default {
|
||||
props: { value: { type: Array, required: true } },
|
||||
watch: {
|
||||
value: {
|
||||
handler (v) {
|
||||
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)
|
||||
}
|
||||
},
|
||||
|
|
|
@ -219,18 +219,18 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import time from '@/utils/time.js'
|
||||
import time from '@/utils/time'
|
||||
import axios from 'axios'
|
||||
import url from '@/utils/url.js'
|
||||
import { getDomain } from '@/utils.js'
|
||||
import logger from '@/logging.js'
|
||||
import { getDomain } from '@/utils'
|
||||
import logger from '@/logging'
|
||||
import PlayButton from '@/components/audio/PlayButton.vue'
|
||||
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon.vue'
|
||||
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon.vue'
|
||||
import Modal from '@/components/semantic/Modal.vue'
|
||||
import EmbedWizard from '@/components/audio/EmbedWizard.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/'
|
||||
|
||||
|
@ -322,7 +322,7 @@ export default {
|
|||
param = 'token'
|
||||
value = this.$store.state.auth.scopedTokens.listen
|
||||
}
|
||||
u = url.updateQueryString(
|
||||
u = updateQueryString(
|
||||
u,
|
||||
param,
|
||||
encodeURI(value)
|
||||
|
|
|
@ -186,7 +186,7 @@
|
|||
<script>
|
||||
import axios from 'axios'
|
||||
import $ from 'jquery'
|
||||
import _ from 'lodash'
|
||||
import { clone } from 'lodash-es'
|
||||
import BuilderFilter from './Filter.vue'
|
||||
import TrackTable from '@/components/audio/track/Table.vue'
|
||||
import RadioButton from '@/components/radios/Button.vue'
|
||||
|
@ -309,7 +309,7 @@ export default {
|
|||
const self = this
|
||||
const url = 'radios/radios/validate/'
|
||||
let final = this.filters.map(f => {
|
||||
const c = _.clone(f.config)
|
||||
const c = clone(f.config)
|
||||
c.type = f.filter.type
|
||||
return c
|
||||
})
|
||||
|
@ -326,7 +326,7 @@ export default {
|
|||
self.isLoading = true
|
||||
|
||||
let final = this.filters.map(f => {
|
||||
const c = _.clone(f.config)
|
||||
const c = clone(f.config)
|
||||
c.type = f.filter.type
|
||||
return c
|
||||
})
|
||||
|
|
|
@ -107,7 +107,7 @@
|
|||
<script>
|
||||
import axios from 'axios'
|
||||
import $ from 'jquery'
|
||||
import _ from 'lodash'
|
||||
import { clone } from 'lodash-es'
|
||||
|
||||
import Modal from '@/components/semantic/Modal.vue'
|
||||
import TrackTable from '@/components/audio/track/Table.vue'
|
||||
|
@ -181,7 +181,7 @@ export default {
|
|||
fetchCandidates: function () {
|
||||
const self = this
|
||||
const url = 'radios/radios/validate/'
|
||||
let final = _.clone(this.config)
|
||||
let final = clone(this.config)
|
||||
final.type = this.filter.type
|
||||
final = { filters: [final] }
|
||||
axios.post(url, final).then((response) => {
|
||||
|
|
|
@ -201,8 +201,8 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import time from '@/utils/time.js'
|
||||
import { merge } from 'lodash-es'
|
||||
import time from '@/utils/time'
|
||||
import { normalizeQuery, parseTokens } from '@/search'
|
||||
import Pagination from '@/components/Pagination.vue'
|
||||
import ActionTable from '@/components/common/ActionTable.vue'
|
||||
|
@ -247,7 +247,7 @@ export default {
|
|||
q: this.search.query
|
||||
}
|
||||
if (this.filters) {
|
||||
return _.merge(currentFilters, this.filters)
|
||||
return merge(currentFilters, this.filters)
|
||||
} else {
|
||||
return currentFilters
|
||||
}
|
||||
|
@ -287,7 +287,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
fetchData () {
|
||||
const params = _.merge({
|
||||
const params = merge({
|
||||
page: this.page,
|
||||
page_size: this.paginateBy,
|
||||
q: this.search.query,
|
||||
|
|
|
@ -186,8 +186,8 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import time from '@/utils/time.js'
|
||||
import { merge } from 'lodash-es'
|
||||
import time from '@/utils/time'
|
||||
import { normalizeQuery, parseTokens } from '@/search'
|
||||
import Pagination from '@/components/Pagination.vue'
|
||||
import ActionTable from '@/components/common/ActionTable.vue'
|
||||
|
@ -233,7 +233,7 @@ export default {
|
|||
q: this.search.query
|
||||
}
|
||||
if (this.filters) {
|
||||
return _.merge(currentFilters, this.filters)
|
||||
return merge(currentFilters, this.filters)
|
||||
} else {
|
||||
return currentFilters
|
||||
}
|
||||
|
@ -273,7 +273,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
fetchData () {
|
||||
const params = _.merge({
|
||||
const params = merge({
|
||||
page: this.page,
|
||||
page_size: this.paginateBy,
|
||||
q: this.search.query,
|
||||
|
|
|
@ -185,8 +185,8 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import time from '@/utils/time.js'
|
||||
import { merge } from 'lodash-es'
|
||||
import time from '@/utils/time'
|
||||
import { normalizeQuery, parseTokens } from '@/search'
|
||||
import Pagination from '@/components/Pagination.vue'
|
||||
import ActionTable from '@/components/common/ActionTable.vue'
|
||||
|
@ -230,7 +230,7 @@ export default {
|
|||
q: this.search.query
|
||||
}
|
||||
if (this.filters) {
|
||||
return _.merge(currentFilters, this.filters)
|
||||
return merge(currentFilters, this.filters)
|
||||
} else {
|
||||
return currentFilters
|
||||
}
|
||||
|
@ -276,7 +276,7 @@ export default {
|
|||
return { name: 'manage.library.artists.detail', params: { id: artist.id } }
|
||||
},
|
||||
fetchData () {
|
||||
const params = _.merge({
|
||||
const params = merge({
|
||||
page: this.page,
|
||||
page_size: this.paginateBy,
|
||||
q: this.search.query,
|
||||
|
|
|
@ -131,8 +131,8 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import time from '@/utils/time.js'
|
||||
import { uniq, merge } from 'lodash-es'
|
||||
import time from '@/utils/time'
|
||||
import Pagination from '@/components/Pagination.vue'
|
||||
import OrderingMixin from '@/components/mixins/Ordering.vue'
|
||||
import TranslationsMixin from '@/components/mixins/Translations.vue'
|
||||
|
@ -197,7 +197,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
fetchData () {
|
||||
const params = _.merge({
|
||||
const params = merge({
|
||||
page: this.page,
|
||||
page_size: this.paginateBy,
|
||||
q: this.search.query,
|
||||
|
@ -236,7 +236,7 @@ export default {
|
|||
if (config.ids.length === 0) {
|
||||
return
|
||||
}
|
||||
axios.get(config.url, { params: { id: _.uniq(config.ids), hidden: 'null' } }).then((response) => {
|
||||
axios.get(config.url, { params: { id: uniq(config.ids), hidden: 'null' } }).then((response) => {
|
||||
response.data.results.forEach((e) => {
|
||||
self.$set(self.targets[k], e.id, {
|
||||
payload: e,
|
||||
|
|
|
@ -216,8 +216,8 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import time from '@/utils/time.js'
|
||||
import { merge } from 'lodash-es'
|
||||
import time from '@/utils/time'
|
||||
import { normalizeQuery, parseTokens } from '@/search'
|
||||
import Pagination from '@/components/Pagination.vue'
|
||||
import ActionTable from '@/components/common/ActionTable.vue'
|
||||
|
@ -262,7 +262,7 @@ export default {
|
|||
q: this.search.query
|
||||
}
|
||||
if (this.filters) {
|
||||
return _.merge(currentFilters, this.filters)
|
||||
return merge(currentFilters, this.filters)
|
||||
} else {
|
||||
return currentFilters
|
||||
}
|
||||
|
@ -302,7 +302,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
fetchData () {
|
||||
const params = _.merge({
|
||||
const params = merge({
|
||||
page: this.page,
|
||||
page_size: this.paginateBy,
|
||||
q: this.search.query,
|
||||
|
|
|
@ -147,8 +147,8 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import time from '@/utils/time.js'
|
||||
import { merge } from 'lodash-es'
|
||||
import time from '@/utils/time'
|
||||
import { normalizeQuery, parseTokens } from '@/search'
|
||||
import Pagination from '@/components/Pagination.vue'
|
||||
import ActionTable from '@/components/common/ActionTable.vue'
|
||||
|
@ -198,7 +198,7 @@ export default {
|
|||
q: this.search.query
|
||||
}
|
||||
if (this.filters) {
|
||||
return _.merge(currentFilters, this.filters)
|
||||
return merge(currentFilters, this.filters)
|
||||
} else {
|
||||
return currentFilters
|
||||
}
|
||||
|
@ -238,7 +238,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
fetchData () {
|
||||
const params = _.merge({
|
||||
const params = merge({
|
||||
page: this.page,
|
||||
page_size: this.paginateBy,
|
||||
q: this.search.query,
|
||||
|
|
|
@ -199,8 +199,8 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import time from '@/utils/time.js'
|
||||
import { merge } from 'lodash-es'
|
||||
import time from '@/utils/time'
|
||||
import { normalizeQuery, parseTokens } from '@/search'
|
||||
import Pagination from '@/components/Pagination.vue'
|
||||
import ActionTable from '@/components/common/ActionTable.vue'
|
||||
|
@ -243,7 +243,7 @@ export default {
|
|||
q: this.search.query
|
||||
}
|
||||
if (this.filters) {
|
||||
return _.merge(currentFilters, this.filters)
|
||||
return merge(currentFilters, this.filters)
|
||||
} else {
|
||||
return currentFilters
|
||||
}
|
||||
|
@ -283,7 +283,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
fetchData () {
|
||||
const params = _.merge({
|
||||
const params = merge({
|
||||
page: this.page,
|
||||
page_size: this.paginateBy,
|
||||
q: this.search.query,
|
||||
|
|
|
@ -308,8 +308,8 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import time from '@/utils/time.js'
|
||||
import { merge } from 'lodash-es'
|
||||
import time from '@/utils/time'
|
||||
import { normalizeQuery, parseTokens } from '@/search'
|
||||
import Pagination from '@/components/Pagination.vue'
|
||||
import ActionTable from '@/components/common/ActionTable.vue'
|
||||
|
@ -361,7 +361,7 @@ export default {
|
|||
q: this.search.query
|
||||
}
|
||||
if (this.filters) {
|
||||
return _.merge(currentFilters, this.filters)
|
||||
return merge(currentFilters, this.filters)
|
||||
} else {
|
||||
return currentFilters
|
||||
}
|
||||
|
@ -401,7 +401,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
fetchData () {
|
||||
const params = _.merge({
|
||||
const params = merge({
|
||||
page: this.page,
|
||||
page_size: this.paginateBy,
|
||||
q: this.search.query,
|
||||
|
|
|
@ -172,8 +172,8 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import time from '@/utils/time.js'
|
||||
import { merge } from 'lodash-es'
|
||||
import time from '@/utils/time'
|
||||
import { normalizeQuery, parseTokens } from '@/search'
|
||||
import Pagination from '@/components/Pagination.vue'
|
||||
import ActionTable from '@/components/common/ActionTable.vue'
|
||||
|
@ -220,7 +220,7 @@ export default {
|
|||
q: this.search.query
|
||||
}
|
||||
if (this.filters) {
|
||||
return _.merge(currentFilters, this.filters)
|
||||
return merge(currentFilters, this.filters)
|
||||
} else {
|
||||
return currentFilters
|
||||
}
|
||||
|
@ -255,7 +255,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
fetchData () {
|
||||
const params = _.merge({
|
||||
const params = merge({
|
||||
page: this.page,
|
||||
page_size: this.paginateBy,
|
||||
q: this.search.query,
|
||||
|
|
|
@ -183,8 +183,8 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import time from '@/utils/time.js'
|
||||
import { merge } from 'lodash-es'
|
||||
import time from '@/utils/time'
|
||||
import Pagination from '@/components/Pagination.vue'
|
||||
import ActionTable from '@/components/common/ActionTable.vue'
|
||||
import OrderingMixin from '@/components/mixins/Ordering.vue'
|
||||
|
@ -229,7 +229,7 @@ export default {
|
|||
q: this.search
|
||||
}
|
||||
if (this.filters) {
|
||||
return _.merge(currentFilters, this.filters)
|
||||
return merge(currentFilters, this.filters)
|
||||
} else {
|
||||
return currentFilters
|
||||
}
|
||||
|
@ -290,7 +290,7 @@ export default {
|
|||
if (this.allowed !== null) {
|
||||
baseFilters.allowed = this.allowed
|
||||
}
|
||||
const params = _.merge(baseFilters, this.filters)
|
||||
const params = merge(baseFilters, this.filters)
|
||||
const self = this
|
||||
self.isLoading = true
|
||||
self.checked = []
|
||||
|
|
|
@ -169,7 +169,7 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import { get } from 'lodash-es'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
@ -183,12 +183,12 @@ export default {
|
|||
isLoading: false,
|
||||
errors: [],
|
||||
current: {
|
||||
summary: _.get(current, 'summary', ''),
|
||||
isActive: _.get(current, 'is_active', true),
|
||||
blockAll: _.get(current, 'block_all', true),
|
||||
silenceActivity: _.get(current, 'silence_activity', false),
|
||||
silenceNotifications: _.get(current, 'silence_notifications', false),
|
||||
rejectMedia: _.get(current, 'reject_media', false)
|
||||
summary: get(current, 'summary', ''),
|
||||
isActive: get(current, 'is_active', true),
|
||||
blockAll: get(current, 'block_all', true),
|
||||
silenceActivity: get(current, 'silence_activity', false),
|
||||
silenceNotifications: get(current, 'silence_notifications', false),
|
||||
rejectMedia: get(current, 'reject_media', false)
|
||||
},
|
||||
fieldConfig: [
|
||||
// we hide those until we actually have the related features implemented :)
|
||||
|
|
|
@ -158,7 +158,7 @@
|
|||
<script>
|
||||
import axios from 'axios'
|
||||
import moment from 'moment'
|
||||
import _ from 'lodash'
|
||||
import { merge } from 'lodash-es'
|
||||
import Pagination from '@/components/Pagination.vue'
|
||||
import ActionTable from '@/components/common/ActionTable.vue'
|
||||
import OrderingMixin from '@/components/mixins/Ordering.vue'
|
||||
|
@ -199,7 +199,7 @@ export default {
|
|||
q: this.search
|
||||
}
|
||||
if (this.filters) {
|
||||
return _.merge(currentFilters, this.filters)
|
||||
return merge(currentFilters, this.filters)
|
||||
} else {
|
||||
return currentFilters
|
||||
}
|
||||
|
@ -243,7 +243,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
fetchData () {
|
||||
const params = _.merge({
|
||||
const params = merge({
|
||||
page: this.page,
|
||||
page_size: this.paginateBy,
|
||||
q: this.search,
|
||||
|
|
|
@ -200,8 +200,8 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import time from '@/utils/time.js'
|
||||
import { merge } from 'lodash-es'
|
||||
import time from '@/utils/time'
|
||||
import Pagination from '@/components/Pagination.vue'
|
||||
import ActionTable from '@/components/common/ActionTable.vue'
|
||||
import OrderingMixin from '@/components/mixins/Ordering.vue'
|
||||
|
@ -261,7 +261,7 @@ export default {
|
|||
q: this.search
|
||||
}
|
||||
if (this.filters) {
|
||||
return _.merge(currentFilters, this.filters)
|
||||
return merge(currentFilters, this.filters)
|
||||
} else {
|
||||
return currentFilters
|
||||
}
|
||||
|
@ -296,7 +296,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
fetchData () {
|
||||
const params = _.merge({
|
||||
const params = merge({
|
||||
page: this.page,
|
||||
page_size: this.paginateBy,
|
||||
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>
|
||||
import TranslationsMixin from '@/components/mixins/Translations.vue'
|
||||
import lodash from 'lodash'
|
||||
export default {
|
||||
mixins: [TranslationsMixin],
|
||||
props: {
|
||||
|
@ -52,7 +51,7 @@ export default {
|
|||
if (this.restrictTo.length > 0) {
|
||||
choices = this.restrictTo
|
||||
} else {
|
||||
choices = lodash.keys(this.sharedLabels.fields.report_type.choices)
|
||||
choices = Object.keys(this.sharedLabels.fields.report_type.choices)
|
||||
}
|
||||
return c.concat(
|
||||
choices.sort().map((v) => {
|
||||
|
|
|
@ -192,9 +192,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import filter from 'lodash/fp/filter'
|
||||
import sortBy from 'lodash/fp/sortBy'
|
||||
import flow from 'lodash/fp/flow'
|
||||
import { filter, sortBy, flow } from 'lodash-es'
|
||||
|
||||
import axios from 'axios'
|
||||
import { mapState } from 'vuex'
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { clone } from 'lodash-es'
|
||||
import axios from 'axios'
|
||||
import PlaylistCard from '@/components/playlists/Card.vue'
|
||||
|
||||
|
@ -104,7 +104,7 @@ export default {
|
|||
}
|
||||
this.isLoading = true
|
||||
const self = this
|
||||
const params = _.clone(this.filters)
|
||||
const params = clone(this.filters)
|
||||
params.page_size = this.limit
|
||||
params.offset = this.offset
|
||||
axios.get(url, { params: params }).then((response) => {
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
<script>
|
||||
|
||||
import lodash from 'lodash'
|
||||
import { isEqual } from 'lodash-es'
|
||||
export default {
|
||||
props: {
|
||||
customRadioId: { type: Number, required: false, default: null },
|
||||
|
@ -37,7 +37,7 @@ export default {
|
|||
if (!state.running) {
|
||||
return false
|
||||
} 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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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 EmbedFrame from './EmbedFrame.vue'
|
||||
import VuePlyr from 'vue-plyr'
|
|
@ -247,7 +247,7 @@
|
|||
<script>
|
||||
import axios from 'axios'
|
||||
import Logo from '@/components/Logo.vue'
|
||||
import url from '@/utils/url'
|
||||
import updateQueryString from './composables/updateQueryString'
|
||||
import time from '@/utils/time'
|
||||
|
||||
function getURLParams () {
|
||||
|
@ -516,7 +516,7 @@ export default {
|
|||
// not support other codecs to be able to play it :)
|
||||
sources.push({
|
||||
type: 'audio/mpeg',
|
||||
src: url.updateQueryString(
|
||||
src: updateQueryString(
|
||||
self.fullUrl(sources[0].src),
|
||||
'to',
|
||||
'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 time from '@/utils/time'
|
||||
|
||||
import time from '~/utils/time'
|
||||
import moment from 'moment'
|
||||
|
||||
export function truncate (str, max, ellipsis, middle) {
|
||||
export function truncate (str: string, max = 100, ellipsis = '…', middle = false) {
|
||||
if (max === 0) {
|
||||
return
|
||||
return ''
|
||||
}
|
||||
max = max || 100
|
||||
ellipsis = ellipsis || '…'
|
||||
|
||||
if (str.length <= max) {
|
||||
return str
|
||||
}
|
||||
|
||||
if (middle) {
|
||||
const sepLen = 1
|
||||
const charsToShow = max - sepLen
|
||||
|
@ -27,9 +27,7 @@ export function truncate (str, max, ellipsis, middle) {
|
|||
}
|
||||
}
|
||||
|
||||
Vue.filter('truncate', truncate)
|
||||
|
||||
export function ago (date, locale) {
|
||||
export function ago (date: Date, locale: string) {
|
||||
locale = locale || 'en'
|
||||
const m = moment(date)
|
||||
m.locale(locale)
|
||||
|
@ -43,9 +41,7 @@ export function ago (date, locale) {
|
|||
})
|
||||
}
|
||||
|
||||
Vue.filter('ago', ago)
|
||||
|
||||
export function fromNow (date, locale) {
|
||||
export function fromNow (date: Date, locale: string) {
|
||||
locale = 'en'
|
||||
moment.locale('en', {
|
||||
relativeTime: {
|
||||
|
@ -70,9 +66,7 @@ export function fromNow (date, locale) {
|
|||
return m.fromNow(true)
|
||||
}
|
||||
|
||||
Vue.filter('fromNow', fromNow)
|
||||
|
||||
export function secondsToObject (seconds) {
|
||||
export function secondsToObject (seconds: number) {
|
||||
const m = moment.duration(seconds, 'seconds')
|
||||
return {
|
||||
seconds: m.seconds(),
|
||||
|
@ -81,42 +75,30 @@ export function secondsToObject (seconds) {
|
|||
}
|
||||
}
|
||||
|
||||
Vue.filter('secondsToObject', secondsToObject)
|
||||
|
||||
export function padDuration (duration) {
|
||||
export function padDuration (duration: string) {
|
||||
let s = String(duration)
|
||||
while (s.length < 2) { s = '0' + s }
|
||||
return s
|
||||
}
|
||||
|
||||
Vue.filter('padDuration', padDuration)
|
||||
|
||||
export function duration (seconds) {
|
||||
return time.parse(seconds)
|
||||
export function duration (seconds: string) {
|
||||
return time.parse(+seconds)
|
||||
}
|
||||
|
||||
Vue.filter('duration', duration)
|
||||
|
||||
export function momentFormat (date, format) {
|
||||
export function momentFormat (date: Date, format: string) {
|
||||
format = format || 'lll'
|
||||
return moment(date).format(format)
|
||||
}
|
||||
|
||||
Vue.filter('moment', momentFormat)
|
||||
|
||||
export function year (date) {
|
||||
export function year (date: Date) {
|
||||
return moment(date).year()
|
||||
}
|
||||
|
||||
Vue.filter('year', year)
|
||||
|
||||
export function capitalize (str) {
|
||||
export function capitalize (str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
|
||||
Vue.filter('capitalize', capitalize)
|
||||
|
||||
export function humanSize (bytes) {
|
||||
export function humanSize (bytes: number) {
|
||||
const si = true
|
||||
const thresh = si ? 1000 : 1024
|
||||
if (Math.abs(bytes) < thresh) {
|
||||
|
@ -133,15 +115,24 @@ export function humanSize (bytes) {
|
|||
return bytes.toFixed(1) + ' ' + units[u]
|
||||
}
|
||||
|
||||
Vue.filter('humanSize', humanSize)
|
||||
|
||||
// Removes duplicates from a list
|
||||
export function unique (list, property) {
|
||||
export function unique (list: { [key: string]: unknown }[], property: string) {
|
||||
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))
|
||||
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 HumanDuration from '@/components/common/HumanDuration.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 InlineSearchBar from '@/components/common/InlineSearchBar.vue'
|
||||
|
||||
Vue.component('HumanDate', HumanDate)
|
||||
Vue.component('HumanDuration', HumanDuration)
|
||||
Vue.component('Username', Username)
|
||||
Vue.component('UserLink', UserLink)
|
||||
Vue.component('ActorLink', ActorLink)
|
||||
Vue.component('ActorAvatar', ActorAvatar)
|
||||
Vue.component('Duration', Duration)
|
||||
Vue.component('DangerousButton', DangerousButton)
|
||||
Vue.component('Message', Message)
|
||||
Vue.component('CopyInput', CopyInput)
|
||||
Vue.component('AjaxButton', AjaxButton)
|
||||
Vue.component('Tooltip', Tooltip)
|
||||
Vue.component('EmptyState', EmptyState)
|
||||
Vue.component('ExpandableDiv', ExpandableDiv)
|
||||
Vue.component('CollapseLink', CollapseLink)
|
||||
Vue.component('ActionFeedback', ActionFeedback)
|
||||
Vue.component('RenderedDescription', RenderedDescription)
|
||||
Vue.component('ContentForm', ContentForm)
|
||||
Vue.component('InlineSearchBar', InlineSearchBar)
|
||||
|
||||
export default {}
|
||||
export const install: AppModule = ({ app }) => {
|
||||
app.component('HumanDate', HumanDate)
|
||||
app.component('HumanDuration', HumanDuration)
|
||||
app.component('Username', Username)
|
||||
app.component('UserLink', UserLink)
|
||||
app.component('ActorLink', ActorLink)
|
||||
app.component('ActorAvatar', ActorAvatar)
|
||||
app.component('Duration', Duration)
|
||||
app.component('DangerousButton', DangerousButton)
|
||||
app.component('Message', Message)
|
||||
app.component('CopyInput', CopyInput)
|
||||
app.component('AjaxButton', AjaxButton)
|
||||
app.component('Tooltip', Tooltip)
|
||||
app.component('EmptyState', EmptyState)
|
||||
app.component('ExpandableDiv', ExpandableDiv)
|
||||
app.component('CollapseLink', CollapseLink)
|
||||
app.component('ActionFeedback', ActionFeedback)
|
||||
app.component('RenderedDescription', RenderedDescription)
|
||||
app.component('ContentForm', ContentForm)
|
||||
app.component('InlineSearchBar', InlineSearchBar)
|
||||
}
|
|
@ -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 axios from 'axios'
|
||||
import logger from '@/logging'
|
||||
import lodash from 'lodash'
|
||||
|
||||
function getDefaultScopedTokens () {
|
||||
return {
|
||||
|
@ -110,7 +109,7 @@ export default {
|
|||
state.availablePermissions[key] = status
|
||||
},
|
||||
profilePartialUpdate: (state, payload) => {
|
||||
lodash.keys(payload).forEach((k) => {
|
||||
Object.keys(payload).forEach((k) => {
|
||||
Vue.set(state.profile, k, payload[k])
|
||||
})
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import Vuex, { Store } from 'vuex'
|
||||
import createPersistedState from 'vuex-persistedstate'
|
||||
|
||||
import favorites from './favorites'
|
||||
|
@ -16,7 +16,7 @@ import ui from './ui'
|
|||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store({
|
||||
export default <Store<any>> new Vuex.Store({
|
||||
modules: {
|
||||
ui,
|
||||
auth,
|
||||
|
@ -44,7 +44,7 @@ export default new Vuex.Store({
|
|||
}),
|
||||
createPersistedState({
|
||||
key: 'ui',
|
||||
paths: ['ui.currentLanguage', 'ui.selectedLanguage', 'ui.momentLocale', 'ui.theme', 'ui.routePreferences']
|
||||
paths: ['ui.currentLanguage', 'ui.selectedLanguage', 'ui.momentLocale', 'ui.routePreferences']
|
||||
}),
|
||||
createPersistedState({
|
||||
key: 'radios',
|
|
@ -1,6 +1,6 @@
|
|||
import axios from 'axios'
|
||||
import logger from '@/logging'
|
||||
import _ from 'lodash'
|
||||
import { merge } from 'lodash-es'
|
||||
|
||||
function getDefaultUrl () {
|
||||
return (
|
||||
|
@ -65,7 +65,7 @@ export default {
|
|||
},
|
||||
mutations: {
|
||||
settings: (state, value) => {
|
||||
_.merge(state.settings, value)
|
||||
merge(state.settings, value)
|
||||
},
|
||||
event: (state, value) => {
|
||||
state.events.unshift(value)
|
||||
|
@ -112,11 +112,11 @@ export default {
|
|||
if (relativeUrl.startsWith('http')) {
|
||||
return relativeUrl
|
||||
}
|
||||
if (state.instanceUrl.endsWith('/') && relativeUrl.startsWith('/')) {
|
||||
if (state.instanceUrl?.endsWith('/') && relativeUrl.startsWith('/')) {
|
||||
relativeUrl = relativeUrl.slice(1)
|
||||
}
|
||||
|
||||
const instanceUrl = state.instanceUrl || getDefaultUrl()
|
||||
const instanceUrl = state.instanceUrl ?? getDefaultUrl()
|
||||
return instanceUrl + relativeUrl
|
||||
},
|
||||
domain: (state) => {
|
||||
|
@ -149,17 +149,15 @@ export default {
|
|||
fetchSettings ({ commit }, payload) {
|
||||
return axios.get('instance/settings/').then(response => {
|
||||
logger.default.info('Successfully fetched instance settings')
|
||||
const sections = {}
|
||||
response.data.forEach(e => {
|
||||
sections[e.section] = {}
|
||||
})
|
||||
response.data.forEach(e => {
|
||||
sections[e.section][e.name] = e
|
||||
})
|
||||
|
||||
const sections = response.data.reduce((map, entry) => {
|
||||
map[entry.section] ??= {}
|
||||
map[entry.section][entry.name] = entry
|
||||
return map
|
||||
}, {})
|
||||
|
||||
commit('settings', sections)
|
||||
if (payload && payload.callback) {
|
||||
payload.callback()
|
||||
}
|
||||
payload?.callback?.()
|
||||
}, response => {
|
||||
logger.default.error('Error while fetching settings', response.data)
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import axios from 'axios'
|
||||
import logger from '@/logging'
|
||||
import _ from 'lodash'
|
||||
import {sortBy} from "lodash-es";
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
|
@ -70,7 +70,7 @@ export default {
|
|||
const f = state.filters.filter((f) => {
|
||||
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()
|
||||
return p
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue