kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
Rename <modal> to <semantic-modal>
rodzic
937a44251e
commit
09c1aba30d
|
@ -1,5 +1,32 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { RouteLocationRaw } from 'vue-router'
|
||||||
|
import type { Cover } from '~/types'
|
||||||
|
|
||||||
|
import SemanticModal from '~/components/semantic/Modal.vue'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
nextRoute: RouteLocationRaw
|
||||||
|
message: string
|
||||||
|
cover: Cover
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const show = ref(false)
|
||||||
|
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
const labels = computed(() => ({
|
||||||
|
header: $pgettext('Popup/Title/Noun', 'Unauthenticated'),
|
||||||
|
login: $pgettext('*/*/Button.Label/Verb', 'Log in'),
|
||||||
|
signup: $pgettext('*/*/Button.Label/Verb', 'Sign up'),
|
||||||
|
description: $pgettext('Popup/*/Paragraph', "You don't have access!")
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<modal v-model:show="show">
|
<semantic-modal v-model:show="show">
|
||||||
<h4 class="header">
|
<h4 class="header">
|
||||||
{{ labels.header }}
|
{{ labels.header }}
|
||||||
</h4>
|
</h4>
|
||||||
|
@ -32,7 +59,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{path: '/login', query: { next: nextRoute }}"
|
:to="{path: '/login', query: { next: nextRoute as string }}"
|
||||||
class="ui labeled icon button"
|
class="ui labeled icon button"
|
||||||
>
|
>
|
||||||
<i class="key icon" />
|
<i class="key icon" />
|
||||||
|
@ -47,36 +74,5 @@
|
||||||
{{ labels.signup }}
|
{{ labels.signup }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</semantic-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import Modal from '~/components/semantic/Modal.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Modal
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
nextRoute: { type: String, required: true },
|
|
||||||
message: { type: String, required: true },
|
|
||||||
cover: { type: Object, required: true }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
show: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labels () {
|
|
||||||
return {
|
|
||||||
header: this.$pgettext('Popup/Title/Noun', 'Unauthenticated'),
|
|
||||||
login: this.$pgettext('*/*/Button.Label/Verb', 'Log in'),
|
|
||||||
signup: this.$pgettext('*/*/Button.Label/Verb', 'Sign up'),
|
|
||||||
description: this.$pgettext('Popup/*/Paragraph', "You don't have access!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,7 +1,50 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SemanticModal from '~/components/semantic/Modal.vue'
|
||||||
|
import useThemeList from '~/composables/useThemeList'
|
||||||
|
import useTheme from '~/composables/useTheme'
|
||||||
|
import { useVModel } from '@vueuse/core'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
show: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:show', 'showThemeModalEvent', 'showLanguageModalEvent'])
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const show = useVModel(props, 'show', emit)
|
||||||
|
|
||||||
|
const theme = useTheme()
|
||||||
|
const themes = useThemeList()
|
||||||
|
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
const labels = computed(() => ({
|
||||||
|
header: $pgettext('Popup/Title/Noun', 'Options'),
|
||||||
|
profile: $pgettext('*/*/*/Noun', 'Profile'),
|
||||||
|
settings: $pgettext('*/*/*/Noun', 'Settings'),
|
||||||
|
logout: $pgettext('Sidebar/Login/List item.Link/Verb', 'Log out'),
|
||||||
|
about: $pgettext('Sidebar/About/List item.Link', 'About'),
|
||||||
|
shortcuts: $pgettext('*/*/*/Noun', 'Keyboard shortcuts'),
|
||||||
|
support: $pgettext('Sidebar/*/Listitem.Link', 'Help'),
|
||||||
|
forum: $pgettext('Sidebar/*/Listitem.Link', 'Forum'),
|
||||||
|
docs: $pgettext('Sidebar/*/Listitem.Link', 'Documentation'),
|
||||||
|
help: $pgettext('Sidebar/*/Listitem.Link', 'Help'),
|
||||||
|
language: $pgettext('Sidebar/Settings/Dropdown.Label/Short, Verb', 'Language'),
|
||||||
|
theme: $pgettext('Sidebar/Settings/Dropdown.Label/Short, Verb', 'Theme'),
|
||||||
|
chat: $pgettext('Sidebar/*/Listitem.Link', 'Chat room'),
|
||||||
|
git: $pgettext('Sidebar/*/List item.Link', 'Issue tracker'),
|
||||||
|
login: $pgettext('*/*/Button.Label/Verb', 'Log in'),
|
||||||
|
signup: $pgettext('*/*/Button.Label/Verb', 'Sign up'),
|
||||||
|
notifications: $pgettext('*/Notifications/*', 'Notifications'),
|
||||||
|
useOtherInstance: $pgettext('Sidebar/*/List item.Link', 'Use another instance')
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- TODO make generic and move to semantic/modal? -->
|
<!-- TODO make generic and move to semantic/modal? -->
|
||||||
<modal
|
<semantic-modal
|
||||||
v-model:show="showRef"
|
v-model:show="show"
|
||||||
:scrolling="true"
|
:scrolling="true"
|
||||||
:fullscreen="false"
|
:fullscreen="false"
|
||||||
>
|
>
|
||||||
|
@ -84,7 +127,7 @@
|
||||||
class="column"
|
class="column"
|
||||||
role="button"
|
role="button"
|
||||||
@click="navigate"
|
@click="navigate"
|
||||||
@keypress.enter="navigate"
|
@keypress.enter="navigate()"
|
||||||
>
|
>
|
||||||
<i class="user-modal list-icon bell icon" />
|
<i class="user-modal list-icon bell icon" />
|
||||||
<span class="user-modal list-item">{{ labels.notifications }}</span>
|
<span class="user-modal list-item">{{ labels.notifications }}</span>
|
||||||
|
@ -101,7 +144,7 @@
|
||||||
class="column"
|
class="column"
|
||||||
role="button"
|
role="button"
|
||||||
@click="navigate"
|
@click="navigate"
|
||||||
@keypress.enter="navigate"
|
@keypress.enter="navigate()"
|
||||||
>
|
>
|
||||||
<i class="user-modal list-icon cog icon" />
|
<i class="user-modal list-icon cog icon" />
|
||||||
<span class="user-modal list-item">{{ labels.settings }}</span>
|
<span class="user-modal list-item">{{ labels.settings }}</span>
|
||||||
|
@ -140,7 +183,7 @@
|
||||||
class="column"
|
class="column"
|
||||||
role="button"
|
role="button"
|
||||||
@click="navigate"
|
@click="navigate"
|
||||||
@keypress.enter="navigate"
|
@keypress.enter="navigate()"
|
||||||
>
|
>
|
||||||
<i class="user-modal list-icon question circle outline icon" />
|
<i class="user-modal list-icon question circle outline icon" />
|
||||||
<span class="user-modal list-item">{{ labels.about }}</span>
|
<span class="user-modal list-item">{{ labels.about }}</span>
|
||||||
|
@ -159,7 +202,7 @@
|
||||||
class="column"
|
class="column"
|
||||||
role="button"
|
role="button"
|
||||||
@click="navigate"
|
@click="navigate"
|
||||||
@keypress.enter="navigate"
|
@keypress.enter="navigate()"
|
||||||
>
|
>
|
||||||
<i class="user-modal list-icon sign out alternate icon" />
|
<i class="user-modal list-icon sign out alternate icon" />
|
||||||
<span class="user-modal list-item">{{ labels.logout }}</span>
|
<span class="user-modal list-item">{{ labels.logout }}</span>
|
||||||
|
@ -175,7 +218,7 @@
|
||||||
class="column"
|
class="column"
|
||||||
role="button"
|
role="button"
|
||||||
@click="navigate"
|
@click="navigate"
|
||||||
@keypress.enter="navigate"
|
@keypress.enter="navigate()"
|
||||||
>
|
>
|
||||||
<i class="user-modal list-icon sign in alternate icon" />
|
<i class="user-modal list-icon sign in alternate icon" />
|
||||||
<span class="user-modal list-item">{{ labels.login }}</span>
|
<span class="user-modal list-item">{{ labels.login }}</span>
|
||||||
|
@ -191,7 +234,7 @@
|
||||||
class="column"
|
class="column"
|
||||||
role="button"
|
role="button"
|
||||||
@click="navigate"
|
@click="navigate"
|
||||||
@keypress.enter="navigate"
|
@keypress.enter="navigate()"
|
||||||
>
|
>
|
||||||
<i class="user-modal list-item user icon" />
|
<i class="user-modal list-item user icon" />
|
||||||
<span class="user-modal list-item">{{ labels.signup }}</span>
|
<span class="user-modal list-item">{{ labels.signup }}</span>
|
||||||
|
@ -199,71 +242,9 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</semantic-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import Modal from '~/components/semantic/Modal.vue'
|
|
||||||
import { mapGetters } from 'vuex'
|
|
||||||
import useThemeList from '~/composables/useThemeList'
|
|
||||||
import useTheme from '~/composables/useTheme'
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Modal
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
show: { type: Boolean, required: true }
|
|
||||||
},
|
|
||||||
setup (props) {
|
|
||||||
// TODO (wvffle): Add defineEmits when rewriting to <script setup>
|
|
||||||
const showRef = useVModel(props, 'show'/*, emit */)
|
|
||||||
return {
|
|
||||||
showRef,
|
|
||||||
theme: useTheme(),
|
|
||||||
themes: useThemeList()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labels () {
|
|
||||||
return {
|
|
||||||
header: this.$pgettext('Popup/Title/Noun', 'Options'),
|
|
||||||
profile: this.$pgettext('*/*/*/Noun', 'Profile'),
|
|
||||||
settings: this.$pgettext('*/*/*/Noun', 'Settings'),
|
|
||||||
logout: this.$pgettext('Sidebar/Login/List item.Link/Verb', 'Log out'),
|
|
||||||
about: this.$pgettext('Sidebar/About/List item.Link', 'About'),
|
|
||||||
shortcuts: this.$pgettext('*/*/*/Noun', 'Keyboard shortcuts'),
|
|
||||||
support: this.$pgettext('Sidebar/*/Listitem.Link', 'Help'),
|
|
||||||
forum: this.$pgettext('Sidebar/*/Listitem.Link', 'Forum'),
|
|
||||||
docs: this.$pgettext('Sidebar/*/Listitem.Link', 'Documentation'),
|
|
||||||
help: this.$pgettext('Sidebar/*/Listitem.Link', 'Help'),
|
|
||||||
language: this.$pgettext(
|
|
||||||
'Sidebar/Settings/Dropdown.Label/Short, Verb',
|
|
||||||
'Language'
|
|
||||||
),
|
|
||||||
theme: this.$pgettext(
|
|
||||||
'Sidebar/Settings/Dropdown.Label/Short, Verb',
|
|
||||||
'Theme'
|
|
||||||
),
|
|
||||||
chat: this.$pgettext('Sidebar/*/Listitem.Link', 'Chat room'),
|
|
||||||
git: this.$pgettext('Sidebar/*/List item.Link', 'Issue tracker'),
|
|
||||||
login: this.$pgettext('*/*/Button.Label/Verb', 'Log in'),
|
|
||||||
signup: this.$pgettext('*/*/Button.Label/Verb', 'Sign up'),
|
|
||||||
notifications: this.$pgettext('*/Notifications/*', 'Notifications'),
|
|
||||||
useOtherInstance: this.$pgettext(
|
|
||||||
'Sidebar/*/List item.Link',
|
|
||||||
'Use another instance'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
...mapGetters({
|
|
||||||
additionalNotifications: 'ui/additionalNotifications'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.action-hint {
|
.action-hint {
|
||||||
margin-left: 1rem !important;
|
margin-left: 1rem !important;
|
||||||
|
|
|
@ -1,12 +1,76 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { BackendError } from '~/types'
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
import SemanticModal from '~/components/semantic/Modal.vue'
|
||||||
|
import { useTimeoutFn } from '@vueuse/core'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const MAX_POLLS = 15
|
||||||
|
|
||||||
|
const pollsCount = ref(0)
|
||||||
|
const showModal = ref(false)
|
||||||
|
const data = ref()
|
||||||
|
const errors = ref([] as string[])
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const isPolling = ref(false)
|
||||||
|
|
||||||
|
const fetch = async () => {
|
||||||
|
showModal.value = true
|
||||||
|
isLoading.value = true
|
||||||
|
isPolling.value = false
|
||||||
|
errors.value = []
|
||||||
|
pollsCount.value = 0
|
||||||
|
data.value = undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(props.url)
|
||||||
|
data.value = response.data
|
||||||
|
startPolling()
|
||||||
|
} catch (error) {
|
||||||
|
errors.value = (error as BackendError).backendErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const poll = async () => {
|
||||||
|
isPolling.value = true
|
||||||
|
showModal.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`federation/fetches/${data.value?.id}/`)
|
||||||
|
data.value = response.data
|
||||||
|
|
||||||
|
if (response.data.status === 'pending' && pollsCount.value++ < MAX_POLLS) {
|
||||||
|
startPolling()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
errors.value = (error as BackendError).backendErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
isPolling.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const { start: startPolling } = useTimeoutFn(poll, 1000, { immediate: false })
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
@click="createFetch"
|
@click="fetch"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<modal
|
<semantic-modal
|
||||||
v-model:show="showModal"
|
v-model:show="showModal"
|
||||||
class="small"
|
class="small"
|
||||||
>
|
>
|
||||||
|
@ -16,9 +80,9 @@
|
||||||
</translate>
|
</translate>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="scrolling content">
|
<div class="scrolling content">
|
||||||
<template v-if="fetch && fetch.status != 'pending'">
|
<template v-if="data && data.status != 'pending'">
|
||||||
<div
|
<div
|
||||||
v-if="fetch.status === 'skipped'"
|
v-if="data.status === 'skipped'"
|
||||||
class="ui message"
|
class="ui message"
|
||||||
>
|
>
|
||||||
<h4 class="header">
|
<h4 class="header">
|
||||||
|
@ -33,7 +97,7 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="fetch.status === 'finished'"
|
v-else-if="data.status === 'finished'"
|
||||||
class="ui success message"
|
class="ui success message"
|
||||||
>
|
>
|
||||||
<h4 class="header">
|
<h4 class="header">
|
||||||
|
@ -48,7 +112,7 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="fetch.status === 'errored'"
|
v-else-if="data.status === 'errored'"
|
||||||
class="ui error message"
|
class="ui error message"
|
||||||
>
|
>
|
||||||
<h4 class="header">
|
<h4 class="header">
|
||||||
|
@ -70,7 +134,7 @@
|
||||||
</translate>
|
</translate>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ fetch.detail.error_code }}
|
{{ data.detail.error_code }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -81,44 +145,44 @@
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<translate
|
<translate
|
||||||
v-if="fetch.detail.error_code === 'http' && fetch.detail.status_code"
|
v-if="data.detail.error_code === 'http' && data.detail.status_code"
|
||||||
:translate-params="{status: fetch.detail.status_code}"
|
:translate-params="{status: data.detail.status_code}"
|
||||||
translate-context="*/*/Error"
|
translate-context="*/*/Error"
|
||||||
>
|
>
|
||||||
The remote server answered with HTTP %{ status }
|
The remote server answered with HTTP %{ status }
|
||||||
</translate>
|
</translate>
|
||||||
<translate
|
<translate
|
||||||
v-else-if="['http', 'request'].indexOf(fetch.detail.error_code) > -1"
|
v-else-if="['http', 'request'].indexOf(data.detail.error_code) > -1"
|
||||||
translate-context="*/*/Error"
|
translate-context="*/*/Error"
|
||||||
>
|
>
|
||||||
An HTTP error occurred while contacting the remote server
|
An HTTP error occurred while contacting the remote server
|
||||||
</translate>
|
</translate>
|
||||||
<translate
|
<translate
|
||||||
v-else-if="fetch.detail.error_code === 'timeout'"
|
v-else-if="data.detail.error_code === 'timeout'"
|
||||||
translate-context="*/*/Error"
|
translate-context="*/*/Error"
|
||||||
>
|
>
|
||||||
The remote server didn't respond quickly enough
|
The remote server didn't respond quickly enough
|
||||||
</translate>
|
</translate>
|
||||||
<translate
|
<translate
|
||||||
v-else-if="fetch.detail.error_code === 'connection'"
|
v-else-if="data.detail.error_code === 'connection'"
|
||||||
translate-context="*/*/Error"
|
translate-context="*/*/Error"
|
||||||
>
|
>
|
||||||
Impossible to connect to the remote server
|
Impossible to connect to the remote server
|
||||||
</translate>
|
</translate>
|
||||||
<translate
|
<translate
|
||||||
v-else-if="['invalid_json', 'invalid_jsonld', 'missing_jsonld_type'].indexOf(fetch.detail.error_code) > -1"
|
v-else-if="['invalid_json', 'invalid_jsonld', 'missing_jsonld_type'].indexOf(data.detail.error_code) > -1"
|
||||||
translate-context="*/*/Error"
|
translate-context="*/*/Error"
|
||||||
>
|
>
|
||||||
The remote server returned invalid JSON or JSON-LD data
|
The remote server returned invalid JSON or JSON-LD data
|
||||||
</translate>
|
</translate>
|
||||||
<translate
|
<translate
|
||||||
v-else-if="fetch.detail.error_code === 'validation'"
|
v-else-if="data.detail.error_code === 'validation'"
|
||||||
translate-context="*/*/Error"
|
translate-context="*/*/Error"
|
||||||
>
|
>
|
||||||
Data returned by the remote server had invalid or missing attributes
|
Data returned by the remote server had invalid or missing attributes
|
||||||
</translate>
|
</translate>
|
||||||
<translate
|
<translate
|
||||||
v-else-if="fetch.detail.error_code === 'unhandled'"
|
v-else-if="data.detail.error_code === 'unhandled'"
|
||||||
translate-context="*/*/Error"
|
translate-context="*/*/Error"
|
||||||
>
|
>
|
||||||
Unknown error
|
Unknown error
|
||||||
|
@ -136,7 +200,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div
|
||||||
v-else-if="isCreatingFetch"
|
v-else-if="isLoading"
|
||||||
class="ui active inverted dimmer"
|
class="ui active inverted dimmer"
|
||||||
>
|
>
|
||||||
<div class="ui text loader">
|
<div class="ui text loader">
|
||||||
|
@ -146,7 +210,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="isWaitingFetch"
|
v-else-if="isPolling"
|
||||||
class="ui active inverted dimmer"
|
class="ui active inverted dimmer"
|
||||||
>
|
>
|
||||||
<div class="ui text loader">
|
<div class="ui text loader">
|
||||||
|
@ -175,7 +239,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="fetch && fetch.status === 'pending' && pollsCount >= maxPolls"
|
v-else-if="data && data.status === 'pending' && pollsCount >= MAX_POLLS"
|
||||||
role="alert"
|
role="alert"
|
||||||
class="ui warning message"
|
class="ui warning message"
|
||||||
>
|
>
|
||||||
|
@ -198,7 +262,7 @@
|
||||||
</translate>
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="fetch && fetch.status === 'finished'"
|
v-if="data && data.status === 'finished'"
|
||||||
class="ui confirm success button"
|
class="ui confirm success button"
|
||||||
@click.prevent="showModal = false; $emit('refresh')"
|
@click.prevent="showModal = false; $emit('refresh')"
|
||||||
>
|
>
|
||||||
|
@ -207,69 +271,6 @@
|
||||||
</translate>
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</semantic-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from 'axios'
|
|
||||||
import Modal from '~/components/semantic/Modal.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Modal
|
|
||||||
},
|
|
||||||
props: { url: { type: String, required: true } },
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
fetch: null,
|
|
||||||
isCreatingFetch: false,
|
|
||||||
errors: [],
|
|
||||||
showModal: false,
|
|
||||||
isWaitingFetch: false,
|
|
||||||
maxPolls: 15,
|
|
||||||
pollsCount: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
createFetch () {
|
|
||||||
const self = this
|
|
||||||
this.fetch = null
|
|
||||||
this.pollsCount = 0
|
|
||||||
this.errors = []
|
|
||||||
this.isCreatingFetch = true
|
|
||||||
this.isWaitingFetch = false
|
|
||||||
self.showModal = true
|
|
||||||
axios.post(this.url).then((response) => {
|
|
||||||
self.isCreatingFetch = false
|
|
||||||
self.fetch = response.data
|
|
||||||
self.pollFetch()
|
|
||||||
}, (error) => {
|
|
||||||
self.isCreatingFetch = false
|
|
||||||
self.errors = error.backendErrors
|
|
||||||
})
|
|
||||||
},
|
|
||||||
pollFetch () {
|
|
||||||
this.isWaitingFetch = true
|
|
||||||
this.pollsCount += 1
|
|
||||||
const url = `federation/fetches/${this.fetch.id}/`
|
|
||||||
const self = this
|
|
||||||
self.showModal = true
|
|
||||||
axios.get(url).then((response) => {
|
|
||||||
self.isCreatingFetch = false
|
|
||||||
self.fetch = response.data
|
|
||||||
if (self.fetch.status === 'pending' && self.pollsCount < self.maxPolls) {
|
|
||||||
setTimeout(() => {
|
|
||||||
self.pollFetch()
|
|
||||||
}, 1000)
|
|
||||||
} else {
|
|
||||||
self.isWaitingFetch = false
|
|
||||||
}
|
|
||||||
}, (error) => {
|
|
||||||
self.errors = error.backendErrors
|
|
||||||
self.isWaitingFetch = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import type { Album, Artist, Library } from '~/types'
|
import type { Album, Artist, Library } from '~/types'
|
||||||
|
|
||||||
import EmbedWizard from '~/components/audio/EmbedWizard.vue'
|
import EmbedWizard from '~/components/audio/EmbedWizard.vue'
|
||||||
import Modal from '~/components/semantic/Modal.vue'
|
import SemanticModal from '~/components/semantic/Modal.vue'
|
||||||
import useReport from '~/composables/moderation/useReport'
|
import useReport from '~/composables/moderation/useReport'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useGettext } from 'vue3-gettext'
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
@ -42,7 +42,7 @@ const remove = () => emit('remove')
|
||||||
<template>
|
<template>
|
||||||
<span>
|
<span>
|
||||||
|
|
||||||
<modal
|
<semantic-modal
|
||||||
v-if="isEmbedable"
|
v-if="isEmbedable"
|
||||||
v-model:show="showEmbedModal"
|
v-model:show="showEmbedModal"
|
||||||
>
|
>
|
||||||
|
@ -63,7 +63,7 @@ const remove = () => emit('remove')
|
||||||
<translate translate-context="*/*/Button.Label/Verb">Cancel</translate>
|
<translate translate-context="*/*/Button.Label/Verb">Cancel</translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</semantic-modal>
|
||||||
<button
|
<button
|
||||||
v-dropdown="{direction: 'downward'}"
|
v-dropdown="{direction: 'downward'}"
|
||||||
class="ui floating dropdown circular icon basic button"
|
class="ui floating dropdown circular icon basic button"
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useGettext } from 'vue3-gettext'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import PlayButton from '~/components/audio/PlayButton.vue'
|
import PlayButton from '~/components/audio/PlayButton.vue'
|
||||||
import EmbedWizard from '~/components/audio/EmbedWizard.vue'
|
import EmbedWizard from '~/components/audio/EmbedWizard.vue'
|
||||||
import Modal from '~/components/semantic/Modal.vue'
|
import SemanticModal from '~/components/semantic/Modal.vue'
|
||||||
import RadioButton from '~/components/radios/Button.vue'
|
import RadioButton from '~/components/radios/Button.vue'
|
||||||
import TagsList from '~/components/tags/List.vue'
|
import TagsList from '~/components/tags/List.vue'
|
||||||
import useReport from '~/composables/moderation/useReport'
|
import useReport from '~/composables/moderation/useReport'
|
||||||
|
@ -136,7 +136,7 @@ watch(() => props.id, fetchData, { immediate: true })
|
||||||
<div class="ui buttons">
|
<div class="ui buttons">
|
||||||
<radio-button
|
<radio-button
|
||||||
type="artist"
|
type="artist"
|
||||||
:object-id="String(object.id)"
|
:object-id="object.id"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui buttons">
|
<div class="ui buttons">
|
||||||
|
@ -151,7 +151,7 @@ watch(() => props.id, fetchData, { immediate: true })
|
||||||
</play-button>
|
</play-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modal
|
<semantic-modal
|
||||||
v-if="publicLibraries.length > 0"
|
v-if="publicLibraries.length > 0"
|
||||||
v-model:show="showEmbedModal"
|
v-model:show="showEmbedModal"
|
||||||
>
|
>
|
||||||
|
@ -175,7 +175,7 @@ watch(() => props.id, fetchData, { immediate: true })
|
||||||
</translate>
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</semantic-modal>
|
||||||
<div class="ui buttons">
|
<div class="ui buttons">
|
||||||
<button
|
<button
|
||||||
class="ui button"
|
class="ui button"
|
||||||
|
|
|
@ -1,5 +1,77 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Upload } from '~/types'
|
||||||
|
|
||||||
|
import SemanticModal from '~/components/semantic/Modal.vue'
|
||||||
|
import { useVModel } from '@vueuse/core'
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
|
||||||
|
interface ErrorEntry {
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
upload: Upload
|
||||||
|
show: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:show'])
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const show = useVModel(props, 'show', emit)
|
||||||
|
|
||||||
|
const getErrors = (details: object): ErrorEntry[] => {
|
||||||
|
const errors = []
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(details)) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
errors.push({ key, value: value.join(', ') })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
errors.push(...getErrors(value).map(error => ({
|
||||||
|
...error,
|
||||||
|
key: `${key} / ${error.key}`
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
|
||||||
|
const getErrorData = (upload: Upload) => {
|
||||||
|
const payload = upload.import_details ?? { error_code: '', detail: {} }
|
||||||
|
|
||||||
|
const errorCode = payload.error_code
|
||||||
|
? payload.error_code
|
||||||
|
: 'unknown_error'
|
||||||
|
|
||||||
|
return {
|
||||||
|
errorCode,
|
||||||
|
supportUrl: 'https://forum.funkwhale.audio/t/support',
|
||||||
|
documentationUrl: `https://docs.funkwhale.audio/users/upload.html#${errorCode}`,
|
||||||
|
label: errorCode === 'invalid_metadata'
|
||||||
|
? $pgettext('Popup/Import/Error.Label', 'Invalid metadata')
|
||||||
|
: $pgettext('*/*/Error', 'Unknown error'),
|
||||||
|
detail: errorCode === 'invalid_metadata'
|
||||||
|
? $pgettext('Popup/Import/Error.Label', 'The metadata included in the file is invalid or some mandatory fields are missing.')
|
||||||
|
: $pgettext('Popup/Import/Error.Label', 'An unknown error occurred'),
|
||||||
|
errorRows: errorCode === 'invalid_metadata'
|
||||||
|
? getErrors(payload.detail ?? {})
|
||||||
|
: [],
|
||||||
|
debugInfo: {
|
||||||
|
source: upload.source,
|
||||||
|
...payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<modal v-model:show="showModal">
|
<semantic-modal v-model:show="show">
|
||||||
<h4 class="header">
|
<h4 class="header">
|
||||||
<translate translate-context="Popup/Import/Title">
|
<translate translate-context="Popup/Import/Title">
|
||||||
Import detail
|
Import detail
|
||||||
|
@ -112,7 +184,7 @@
|
||||||
<textarea
|
<textarea
|
||||||
class="ui textarea"
|
class="ui textarea"
|
||||||
rows="10"
|
rows="10"
|
||||||
:value="getErrorData(upload).debugInfo"
|
:value="JSON.stringify(getErrorData(upload).debugInfo)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -129,87 +201,5 @@
|
||||||
</translate>
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</semantic-modal>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
|
||||||
import Modal from '~/components/semantic/Modal.vue'
|
|
||||||
|
|
||||||
function getErrors (payload) {
|
|
||||||
const errors = []
|
|
||||||
for (const k in payload) {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(payload, k)) {
|
|
||||||
const value = payload[k]
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
errors.push({
|
|
||||||
key: k,
|
|
||||||
value: value.join(', ')
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// possibly artists, so nested errors
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
getErrors(value).forEach((e) => {
|
|
||||||
errors.push({
|
|
||||||
key: `${k} / ${e.key}`,
|
|
||||||
value: e.value
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Modal
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
upload: { type: Object, required: true },
|
|
||||||
show: { type: Boolean }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
showModal: this.show
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
showModal (v) {
|
|
||||||
this.$emit('update:show', v)
|
|
||||||
},
|
|
||||||
show (v) {
|
|
||||||
this.showModal = v
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getErrorData (upload) {
|
|
||||||
const payload = upload.import_details || {}
|
|
||||||
const d = {
|
|
||||||
supportUrl: 'https://forum.funkwhale.audio/t/support',
|
|
||||||
errorRows: []
|
|
||||||
}
|
|
||||||
if (!payload.error_code) {
|
|
||||||
d.errorCode = 'unknown_error'
|
|
||||||
} else {
|
|
||||||
d.errorCode = payload.error_code
|
|
||||||
}
|
|
||||||
d.documentationUrl = `https://docs.funkwhale.audio/users/upload.html#${d.errorCode}`
|
|
||||||
if (d.errorCode === 'invalid_metadata') {
|
|
||||||
d.label = this.$pgettext('Popup/Import/Error.Label', 'Invalid metadata')
|
|
||||||
d.detail = this.$pgettext('Popup/Import/Error.Label', 'The metadata included in the file is invalid or some mandatory fields are missing.')
|
|
||||||
const detail = payload.detail || {}
|
|
||||||
d.errorRows = getErrors(detail)
|
|
||||||
} else {
|
|
||||||
d.label = this.$pgettext('*/*/Error', 'Unknown error')
|
|
||||||
d.detail = this.$pgettext('Popup/Import/Error.Label', 'An unknown error occurred')
|
|
||||||
}
|
|
||||||
const debugInfo = {
|
|
||||||
source: upload.source,
|
|
||||||
...payload
|
|
||||||
}
|
|
||||||
d.debugInfo = JSON.stringify(debugInfo, null, 4)
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import $ from 'jquery'
|
||||||
import ArtistCard from '~/components/audio/artist/Card.vue'
|
import ArtistCard from '~/components/audio/artist/Card.vue'
|
||||||
import Pagination from '~/components/vui/Pagination.vue'
|
import Pagination from '~/components/vui/Pagination.vue'
|
||||||
import TagsSelector from '~/components/library/TagsSelector.vue'
|
import TagsSelector from '~/components/library/TagsSelector.vue'
|
||||||
import Modal from '~/components/semantic/Modal.vue'
|
import SemanticModal from '~/components/semantic/Modal.vue'
|
||||||
import RemoteSearchForm from '~/components/RemoteSearchForm.vue'
|
import RemoteSearchForm from '~/components/RemoteSearchForm.vue'
|
||||||
import useLogger from '~/composables/useLogger'
|
import useLogger from '~/composables/useLogger'
|
||||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||||
|
@ -271,7 +271,7 @@ const labels = computed(() => ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<modal
|
<semantic-modal
|
||||||
v-model:show="showSubscribeModal"
|
v-model:show="showSubscribeModal"
|
||||||
class="tiny"
|
class="tiny"
|
||||||
:fullscreen="false"
|
:fullscreen="false"
|
||||||
|
@ -310,6 +310,6 @@ const labels = computed(() => ({
|
||||||
</translate>
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</semantic-modal>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import axios from 'axios'
|
||||||
import PlayButton from '~/components/audio/PlayButton.vue'
|
import PlayButton from '~/components/audio/PlayButton.vue'
|
||||||
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
||||||
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
|
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
|
||||||
import Modal from '~/components/semantic/Modal.vue'
|
import SemanticModal from '~/components/semantic/Modal.vue'
|
||||||
import EmbedWizard from '~/components/audio/EmbedWizard.vue'
|
import EmbedWizard from '~/components/audio/EmbedWizard.vue'
|
||||||
import { momentFormat } from '~/utils/filters'
|
import { momentFormat } from '~/utils/filters'
|
||||||
import updateQueryString from '~/composables/updateQueryString'
|
import updateQueryString from '~/composables/updateQueryString'
|
||||||
|
@ -176,7 +176,7 @@ const remove = async () => {
|
||||||
>
|
>
|
||||||
<i class="download icon" />
|
<i class="download icon" />
|
||||||
</a>
|
</a>
|
||||||
<modal
|
<semantic-modal
|
||||||
v-if="isEmbedable"
|
v-if="isEmbedable"
|
||||||
v-model:show="showEmbedModal"
|
v-model:show="showEmbedModal"
|
||||||
>
|
>
|
||||||
|
@ -200,7 +200,7 @@ const remove = async () => {
|
||||||
</translate>
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</semantic-modal>
|
||||||
<button
|
<button
|
||||||
v-dropdown="{direction: 'downward'}"
|
v-dropdown="{direction: 'downward'}"
|
||||||
class="ui floating dropdown circular icon basic button"
|
class="ui floating dropdown circular icon basic button"
|
||||||
|
|
|
@ -1,3 +1,123 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
// TODO (wvffle): SORT IMPORTS LIKE SO EVERYWHERE
|
||||||
|
import type { Track } from '~/types'
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
import $ from 'jquery'
|
||||||
|
|
||||||
|
import { clone } from 'lodash-es'
|
||||||
|
import { ref, onMounted, watch } from 'vue'
|
||||||
|
import { useCurrentElement } from '@vueuse/core'
|
||||||
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
|
import SemanticModal from '~/components/semantic/Modal.vue'
|
||||||
|
import TrackTable from '~/components/audio/track/Table.vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
index: number
|
||||||
|
|
||||||
|
filter: {
|
||||||
|
type: string
|
||||||
|
label: string
|
||||||
|
fields: {
|
||||||
|
name: string
|
||||||
|
placeholder: string
|
||||||
|
type: 'list'
|
||||||
|
subtype: 'number'
|
||||||
|
autocomplete?: string
|
||||||
|
autocomplete_qs: string
|
||||||
|
autocomplete_fields: {
|
||||||
|
remoteValues?: unknown
|
||||||
|
}
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
config: {
|
||||||
|
not: boolean
|
||||||
|
names: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Filter = { candidates: { count: number, sample: Track[] } }
|
||||||
|
type ResponseType = { filters: Array<Filter> }
|
||||||
|
|
||||||
|
const emit = defineEmits(['update-config', 'delete'])
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const checkResult = ref<Filter | null>(null)
|
||||||
|
const showCandidadesModal = ref(false)
|
||||||
|
const exclude = ref(props.config.not)
|
||||||
|
|
||||||
|
const el = useCurrentElement()
|
||||||
|
onMounted(() => {
|
||||||
|
for (const field of props.filter.fields) {
|
||||||
|
const selector = ['.dropdown']
|
||||||
|
|
||||||
|
if (field.type === 'list') {
|
||||||
|
selector.push('.multiple')
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings: SemanticUI.DropdownSettings = {
|
||||||
|
onChange (value) {
|
||||||
|
value = $(this).dropdown('get value').split(',')
|
||||||
|
|
||||||
|
if (field.type === 'list' && field.subtype === 'number') {
|
||||||
|
value = value.map((number: string) => parseInt(number))
|
||||||
|
}
|
||||||
|
|
||||||
|
value.value = value
|
||||||
|
emit('update-config', props.index, field.name, value)
|
||||||
|
fetchCandidates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.autocomplete) {
|
||||||
|
selector.push('.autocomplete')
|
||||||
|
// @ts-expect-error custom field?
|
||||||
|
settings.fields = field.autocomplete_fields
|
||||||
|
settings.minCharacters = 1
|
||||||
|
settings.apiSettings = {
|
||||||
|
url: store.getters['instance/absoluteUrl'](`${field.autocomplete}?${field.autocomplete_qs}`),
|
||||||
|
beforeXHR (xhrObject) {
|
||||||
|
if (store.state.auth.oauth.accessToken) {
|
||||||
|
xhrObject.setRequestHeader('Authorization', store.getters['auth/header'])
|
||||||
|
}
|
||||||
|
|
||||||
|
return xhrObject
|
||||||
|
},
|
||||||
|
onResponse (initialResponse) {
|
||||||
|
return !settings.fields?.remoteValues
|
||||||
|
? { results: initialResponse.results }
|
||||||
|
: initialResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$(el.value).find(selector.join('')).dropdown(settings)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchCandidates = async () => {
|
||||||
|
const params = {
|
||||||
|
filters: [{
|
||||||
|
...clone(props.config),
|
||||||
|
type: props.filter.type,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post('radios/radios/validate/', params)
|
||||||
|
checkResult.value = (response.data as ResponseType).filters[0]
|
||||||
|
} catch (error) {
|
||||||
|
// TODO (wvffle): Handle error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(exclude, fetchCandidates)
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ filter.label }}</td>
|
<td>{{ filter.label }}</td>
|
||||||
|
@ -66,7 +186,7 @@
|
||||||
>
|
>
|
||||||
{{ checkResult.candidates.count }} tracks matching filter
|
{{ checkResult.candidates.count }} tracks matching filter
|
||||||
</a>
|
</a>
|
||||||
<modal
|
<semantic-modal
|
||||||
v-if="checkResult"
|
v-if="checkResult"
|
||||||
v-model:show="showCandidadesModal"
|
v-model:show="showCandidadesModal"
|
||||||
>
|
>
|
||||||
|
@ -90,7 +210,7 @@
|
||||||
</translate>
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</semantic-modal>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
|
@ -104,90 +224,3 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
|
||||||
import axios from 'axios'
|
|
||||||
import $ from 'jquery'
|
|
||||||
import { clone } from 'lodash-es'
|
|
||||||
|
|
||||||
import Modal from '~/components/semantic/Modal.vue'
|
|
||||||
import TrackTable from '~/components/audio/track/Table.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
TrackTable,
|
|
||||||
Modal
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
filter: { type: Object, required: true },
|
|
||||||
config: { type: Object, required: true },
|
|
||||||
index: { type: Number, required: true }
|
|
||||||
},
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
checkResult: null,
|
|
||||||
showCandidadesModal: false,
|
|
||||||
exclude: this.config.not
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
exclude: function () {
|
|
||||||
this.fetchCandidates()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted: function () {
|
|
||||||
const self = this
|
|
||||||
this.filter.fields.forEach(f => {
|
|
||||||
const selector = ['.dropdown']
|
|
||||||
const settings = {
|
|
||||||
onChange: function (value, text, $choice) {
|
|
||||||
value = $(this).dropdown('get value').split(',')
|
|
||||||
if (f.type === 'list' && f.subtype === 'number') {
|
|
||||||
value = value.map(e => {
|
|
||||||
return parseInt(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
self.value = value
|
|
||||||
self.$emit('update-config', self.index, f.name, value)
|
|
||||||
self.fetchCandidates()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (f.type === 'list') {
|
|
||||||
selector.push('.multiple')
|
|
||||||
}
|
|
||||||
if (f.autocomplete) {
|
|
||||||
selector.push('.autocomplete')
|
|
||||||
settings.fields = f.autocomplete_fields
|
|
||||||
settings.minCharacters = 1
|
|
||||||
settings.apiSettings = {
|
|
||||||
url: self.$store.getters['instance/absoluteUrl'](f.autocomplete + '?' + f.autocomplete_qs),
|
|
||||||
beforeXHR: function (xhrObject) {
|
|
||||||
if (self.$store.state.auth.oauth.accessToken) {
|
|
||||||
xhrObject.setRequestHeader('Authorization', self.$store.getters['auth/header'])
|
|
||||||
}
|
|
||||||
return xhrObject
|
|
||||||
},
|
|
||||||
onResponse: function (initialResponse) {
|
|
||||||
if (settings.fields.remoteValues) {
|
|
||||||
return initialResponse
|
|
||||||
}
|
|
||||||
return { results: initialResponse.results }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$(self.$el).find(selector.join('')).dropdown(settings)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetchCandidates: function () {
|
|
||||||
const self = this
|
|
||||||
const url = 'radios/radios/validate/'
|
|
||||||
let final = clone(this.config)
|
|
||||||
final.type = this.filter.type
|
|
||||||
final = { filters: [final] }
|
|
||||||
axios.post(url, final).then((response) => {
|
|
||||||
self.checkResult = response.data.filters[0]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,3 +1,60 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { BackendError } from '~/types'
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
import InstancePolicyForm from '~/components/manage/moderation/InstancePolicyForm.vue'
|
||||||
|
import InstancePolicyCard from '~/components/manage/moderation/InstancePolicyCard.vue'
|
||||||
|
import SemanticModal from '~/components/semantic/Modal.vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
target: string
|
||||||
|
type: 'domain' | 'actor'
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const show = ref(false)
|
||||||
|
const showForm = ref(false)
|
||||||
|
|
||||||
|
const errors = ref([] as string[])
|
||||||
|
const result = ref()
|
||||||
|
|
||||||
|
const obj = computed(() => result.value?.results[0] ?? null)
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const fetchData = async () => {
|
||||||
|
const [username, domain] = props.target.split('@')
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
target_domain: props.type === 'domain'
|
||||||
|
? props.target
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
target_account_username: props.type === 'actor'
|
||||||
|
? username
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
target_account_domain: props.type === 'actor'
|
||||||
|
? domain
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/manage/moderation/instance-policies/', { params })
|
||||||
|
result.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
errors.value = (error as BackendError).backendErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
class="ui button"
|
class="ui button"
|
||||||
|
@ -9,7 +66,7 @@
|
||||||
Moderation rules…
|
Moderation rules…
|
||||||
</translate>
|
</translate>
|
||||||
</slot>
|
</slot>
|
||||||
<modal
|
<semantic-modal
|
||||||
v-model:show="show"
|
v-model:show="show"
|
||||||
@show="fetchData"
|
@show="fetchData"
|
||||||
>
|
>
|
||||||
|
@ -60,64 +117,6 @@
|
||||||
</translate>
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</semantic-modal>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from 'axios'
|
|
||||||
import InstancePolicyForm from '~/components/manage/moderation/InstancePolicyForm.vue'
|
|
||||||
import InstancePolicyCard from '~/components/manage/moderation/InstancePolicyCard.vue'
|
|
||||||
import Modal from '~/components/semantic/Modal.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
InstancePolicyForm,
|
|
||||||
InstancePolicyCard,
|
|
||||||
Modal
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
target: { type: String, required: true },
|
|
||||||
type: { type: String, required: true }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
show: false,
|
|
||||||
isLoading: false,
|
|
||||||
errors: [],
|
|
||||||
showForm: false,
|
|
||||||
result: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
obj () {
|
|
||||||
if (!this.result) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return this.result.results[0]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetchData () {
|
|
||||||
const params = {}
|
|
||||||
if (this.type === 'domain') {
|
|
||||||
params.target_domain = this.target
|
|
||||||
}
|
|
||||||
if (this.type === 'actor') {
|
|
||||||
const parts = this.target.split('@')
|
|
||||||
params.target_account_username = parts[0]
|
|
||||||
params.target_account_domain = parts[1]
|
|
||||||
}
|
|
||||||
const self = this
|
|
||||||
self.isLoading = true
|
|
||||||
axios.get('/manage/moderation/instance-policies/', { params: params }).then((response) => {
|
|
||||||
self.result = response.data
|
|
||||||
self.isLoading = false
|
|
||||||
}, error => {
|
|
||||||
self.isLoading = false
|
|
||||||
self.errors = error.backendErrors
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,13 +1,70 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { BackendError } from '~/types'
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
|
import SemanticModal from '~/components/semantic/Modal.vue'
|
||||||
|
import useLogger from '~/composables/useLogger'
|
||||||
|
|
||||||
|
|
||||||
|
const logger = useLogger()
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const show = computed({
|
||||||
|
get: () => store.state.moderation.showFilterModal,
|
||||||
|
set: (value) => {
|
||||||
|
store.commit('moderation/showFilterModal', value)
|
||||||
|
errors.value = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const type = computed(() => store.state.moderation.filterModalTarget.type)
|
||||||
|
const target = computed(() => store.state.moderation.filterModalTarget.target)
|
||||||
|
|
||||||
|
const errors = ref([] as string[])
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
const hide = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
target: {
|
||||||
|
type: type.value,
|
||||||
|
id: target.value?.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post('moderation/content-filters/', payload)
|
||||||
|
logger.info(`Successfully hidden ${type.value} ${target.value?.id}`)
|
||||||
|
show.value = false
|
||||||
|
store.state.moderation.lastUpdate = new Date()
|
||||||
|
store.commit('moderation/contentFilter', response.data)
|
||||||
|
store.commit('ui/addMessage', {
|
||||||
|
content: $pgettext('*/Moderation/Message', 'Content filter successfully added'),
|
||||||
|
date: new Date()
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error while hiding ${type.value} ${target.value?.id}`)
|
||||||
|
errors.value = (error as BackendError).backendErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<modal
|
<semantic-modal v-model:show="show">
|
||||||
v-model:show="showRef"
|
|
||||||
@update:show="update"
|
|
||||||
>
|
|
||||||
<h4 class="header">
|
<h4 class="header">
|
||||||
<translate
|
<translate
|
||||||
v-if="type === 'artist'"
|
v-if="type === 'artist'"
|
||||||
translate-context="Popup/Moderation/Title/Verb"
|
translate-context="Popup/Moderation/Title/Verb"
|
||||||
:translate-params="{name: target.name}"
|
:translate-params="{name: target?.name}"
|
||||||
>
|
>
|
||||||
Do you want to hide content from artist "%{ name }"?
|
Do you want to hide content from artist "%{ name }"?
|
||||||
</translate>
|
</translate>
|
||||||
|
@ -84,73 +141,5 @@
|
||||||
</translate>
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</semantic-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from 'axios'
|
|
||||||
import { mapState } from 'vuex'
|
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
import Modal from '~/components/semantic/Modal.vue'
|
|
||||||
import useLogger from '~/composables/useLogger'
|
|
||||||
import { useStore } from '~/store'
|
|
||||||
|
|
||||||
const logger = useLogger()
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Modal
|
|
||||||
},
|
|
||||||
setup () {
|
|
||||||
const store = useStore()
|
|
||||||
const showRef = computed(() => store.state.moderation.showFilterModal)
|
|
||||||
return { showRef }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
formKey: String(new Date()),
|
|
||||||
errors: [],
|
|
||||||
isLoading: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState({
|
|
||||||
type: state => state.moderation.filterModalTarget.type,
|
|
||||||
target: state => state.moderation.filterModalTarget.target
|
|
||||||
})
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
update (v) {
|
|
||||||
this.$store.commit('moderation/showFilterModal', v)
|
|
||||||
this.errors.length = 0
|
|
||||||
},
|
|
||||||
hide () {
|
|
||||||
const self = this
|
|
||||||
self.isLoading = true
|
|
||||||
const payload = {
|
|
||||||
target: {
|
|
||||||
type: this.type,
|
|
||||||
id: this.target.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return axios.post('moderation/content-filters/', payload).then(response => {
|
|
||||||
logger.info('Successfully added track to playlist')
|
|
||||||
self.update(false)
|
|
||||||
self.$store.moderation.state.lastUpdate = new Date()
|
|
||||||
self.isLoading = false
|
|
||||||
const msg = this.$pgettext('*/Moderation/Message', 'Content filter successfully added')
|
|
||||||
self.$store.commit('moderation/contentFilter', response.data)
|
|
||||||
self.$store.commit('ui/addMessage', {
|
|
||||||
content: msg,
|
|
||||||
date: new Date()
|
|
||||||
})
|
|
||||||
}, error => {
|
|
||||||
logger.error(`Error while hiding ${self.type} ${self.target.id}`)
|
|
||||||
self.errors = error.backendErrors
|
|
||||||
self.isLoading = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,8 +1,126 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { BackendError } from '~/types'
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
import ReportCategoryDropdown from '~/components/moderation/ReportCategoryDropdown.vue'
|
||||||
|
import SemanticModal from '~/components/semantic/Modal.vue'
|
||||||
|
import { computed, ref, watchEffect } from 'vue'
|
||||||
|
import { useStore } from '~/store'
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
|
||||||
|
interface ReportType {
|
||||||
|
anonymous: boolean
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const target = computed(() => store.state.moderation.reportModalTarget)
|
||||||
|
|
||||||
|
const forward = ref(false)
|
||||||
|
const summary = ref('')
|
||||||
|
const category = ref('')
|
||||||
|
const submitterEmail = ref('')
|
||||||
|
|
||||||
|
const reportTypes = ref([] as ReportType[])
|
||||||
|
const allowedCategories = computed(() => {
|
||||||
|
if (store.state.auth.authenticated) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return reportTypes.value
|
||||||
|
.filter((type) => type.anonymous === true)
|
||||||
|
.map((type) => type.type)
|
||||||
|
})
|
||||||
|
|
||||||
|
const canSubmit = computed(() => store.state.auth.authenticated || allowedCategories.value.length > 0)
|
||||||
|
|
||||||
|
const targetDomain = computed(() => {
|
||||||
|
if (!target.value._obj) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fid = target.value.type === 'channel' && target.value._obj.actor
|
||||||
|
? target.value._obj.actor.fid
|
||||||
|
: target.value._obj.fid
|
||||||
|
|
||||||
|
return !fid
|
||||||
|
? store.getters['instance/domain']
|
||||||
|
: new URL(fid).hostname
|
||||||
|
})
|
||||||
|
|
||||||
|
const isLocal = computed(() => store.getters['instance/domain'] === targetDomain.value)
|
||||||
|
|
||||||
|
const errors = ref([] as string[])
|
||||||
|
|
||||||
|
const show = computed({
|
||||||
|
get: () => store.state.moderation.showReportModal,
|
||||||
|
set: (value: boolean) => {
|
||||||
|
store.commit('moderation/showReportModal', value)
|
||||||
|
errors.value = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
// TODO (wvffle): MOVE ALL use*() METHODS SOMEWHERE TO THE TOP
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
target: { ...target.value, _obj: undefined },
|
||||||
|
summary: summary.value,
|
||||||
|
type: category.value,
|
||||||
|
forward: forward.value,
|
||||||
|
submitter_email: !store.state.auth.authenticated
|
||||||
|
? submitterEmail.value
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post('moderation/reports/', payload)
|
||||||
|
show.value = false
|
||||||
|
|
||||||
|
store.commit('moderation/contentFilter', response.data)
|
||||||
|
store.commit('ui/addMessage', {
|
||||||
|
content: $pgettext('*/Moderation/Message', 'Report successfully submitted, thank you'),
|
||||||
|
date: new Date()
|
||||||
|
})
|
||||||
|
|
||||||
|
summary.value = ''
|
||||||
|
category.value = ''
|
||||||
|
} catch (error) {
|
||||||
|
errors.value = (error as BackendError).backendErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLoadingReportTypes = ref(false)
|
||||||
|
watchEffect(async () => {
|
||||||
|
if (!store.state.moderation.showReportModal || store.state.auth.authenticated) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadingReportTypes.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get('instance/nodeinfo/2.0/')
|
||||||
|
reportTypes.value = response.data.metadata.reportTypes ?? []
|
||||||
|
} catch (error) {
|
||||||
|
store.commit('ui/addMessage', {
|
||||||
|
content: $pgettext('*/Moderation/Message', 'Cannot fetch Node Info: %{ error }', { error: `${error}` }),
|
||||||
|
date: new Date()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadingReportTypes.value = false
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<modal
|
<semantic-modal v-model:show="show">
|
||||||
v-model:show="showRef"
|
|
||||||
@update:show="update"
|
|
||||||
>
|
|
||||||
<h2
|
<h2
|
||||||
v-if="target"
|
v-if="target"
|
||||||
class="ui header"
|
class="ui header"
|
||||||
|
@ -150,137 +268,5 @@
|
||||||
</translate>
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</semantic-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from 'axios'
|
|
||||||
import { mapState } from 'vuex'
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import ReportCategoryDropdown from '~/components/moderation/ReportCategoryDropdown.vue'
|
|
||||||
import Modal from '~/components/semantic/Modal.vue'
|
|
||||||
import { useStore } from '~/store'
|
|
||||||
|
|
||||||
function urlDomain (data) {
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.href = data
|
|
||||||
return a.hostname
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
ReportCategoryDropdown,
|
|
||||||
Modal
|
|
||||||
},
|
|
||||||
setup () {
|
|
||||||
const store = useStore()
|
|
||||||
const showRef = computed(() => store.state.moderation.showReportModal)
|
|
||||||
return { showRef }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
formKey: String(new Date()),
|
|
||||||
errors: [],
|
|
||||||
isLoading: false,
|
|
||||||
isLoadingReportTypes: false,
|
|
||||||
summary: '',
|
|
||||||
submitterEmail: '',
|
|
||||||
category: null,
|
|
||||||
reportTypes: [],
|
|
||||||
forward: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState({
|
|
||||||
target: state => state.moderation.reportModalTarget
|
|
||||||
}),
|
|
||||||
allowedCategories () {
|
|
||||||
if (this.$store.state.auth.authenticated) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return this.reportTypes.filter((t) => {
|
|
||||||
return t.anonymous === true
|
|
||||||
}).map((c) => {
|
|
||||||
return c.type
|
|
||||||
})
|
|
||||||
},
|
|
||||||
canSubmit () {
|
|
||||||
if (this.$store.state.auth.authenticated) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.allowedCategories.length > 0
|
|
||||||
},
|
|
||||||
targetDomain () {
|
|
||||||
if (!this.target._obj) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let fid = this.target._obj.fid
|
|
||||||
if (this.target.type === 'channel' && this.target._obj.actor) {
|
|
||||||
fid = this.target._obj.actor.fid
|
|
||||||
}
|
|
||||||
if (!fid) {
|
|
||||||
return this.$store.getters['instance/domain']
|
|
||||||
}
|
|
||||||
return urlDomain(fid)
|
|
||||||
},
|
|
||||||
isLocal () {
|
|
||||||
return this.$store.getters['instance/domain'] === this.targetDomain
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
'$store.state.moderation.showReportModal': function (v) {
|
|
||||||
if (!v || this.$store.state.auth.authenticated) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const self = this
|
|
||||||
self.isLoadingReportTypes = true
|
|
||||||
axios.get('instance/nodeinfo/2.0/').then(response => {
|
|
||||||
self.isLoadingReportTypes = false
|
|
||||||
self.reportTypes = response.data.metadata.reportTypes || []
|
|
||||||
}, error => {
|
|
||||||
self.isLoadingReportTypes = false
|
|
||||||
self.$store.commit('ui/addMessage', {
|
|
||||||
content: 'Cannot fetch Node Info: ' + error,
|
|
||||||
date: new Date()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
update (v) {
|
|
||||||
this.$store.commit('moderation/showReportModal', v)
|
|
||||||
this.errors = []
|
|
||||||
},
|
|
||||||
submit () {
|
|
||||||
const self = this
|
|
||||||
self.isLoading = true
|
|
||||||
const payload = {
|
|
||||||
target: { ...this.target, _obj: null },
|
|
||||||
summary: this.summary,
|
|
||||||
type: this.category,
|
|
||||||
forward: this.forward
|
|
||||||
}
|
|
||||||
if (!this.$store.state.auth.authenticated) {
|
|
||||||
payload.submitter_email = this.submitterEmail
|
|
||||||
}
|
|
||||||
return axios.post('moderation/reports/', payload).then(response => {
|
|
||||||
self.update(false)
|
|
||||||
self.isLoading = false
|
|
||||||
const msg = this.$pgettext('*/Moderation/Message', 'Report successfully submitted, thank you')
|
|
||||||
self.$store.commit('moderation/contentFilter', response.data)
|
|
||||||
self.$store.commit('ui/addMessage', {
|
|
||||||
content: msg,
|
|
||||||
date: new Date()
|
|
||||||
})
|
|
||||||
self.summary = ''
|
|
||||||
self.category = ''
|
|
||||||
}, error => {
|
|
||||||
self.errors = error.backendErrors
|
|
||||||
self.isLoading = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ interface Props {
|
||||||
customRadioId?: number | null
|
customRadioId?: number | null
|
||||||
type?: string
|
type?: string
|
||||||
clientOnly?: boolean
|
clientOnly?: boolean
|
||||||
objectId?: ObjectId | null
|
objectId?: ObjectId | string | null
|
||||||
radioConfig?: RadioConfig | null
|
radioConfig?: RadioConfig | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,10 @@ const running = computed(() => {
|
||||||
|
|
||||||
return store.state.radios.current?.type === props.type
|
return store.state.radios.current?.type === props.type
|
||||||
&& store.state.radios.current?.customRadioId === props.customRadioId
|
&& store.state.radios.current?.customRadioId === props.customRadioId
|
||||||
&& store.state.radios.current?.objectId.fullUsername === props.objectId?.fullUsername
|
&& (
|
||||||
|
typeof props.objectId === 'string'
|
||||||
|
|| store.state.radios.current?.objectId.fullUsername === props.objectId?.fullUsername
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const { $pgettext } = useGettext()
|
const { $pgettext } = useGettext()
|
||||||
|
|
|
@ -6,17 +6,23 @@ import { sortBy } from 'lodash-es'
|
||||||
import useLogger from '~/composables/useLogger'
|
import useLogger from '~/composables/useLogger'
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
filters: ContentFilter[],
|
filters: ContentFilter[]
|
||||||
showFilterModal: boolean,
|
showFilterModal: boolean
|
||||||
showReportModal: boolean,
|
showReportModal: boolean
|
||||||
lastUpdate: Date,
|
lastUpdate: Date,
|
||||||
filterModalTarget: {
|
filterModalTarget: {
|
||||||
type: null,
|
type: null
|
||||||
target: null
|
target: null | { id: string, name: string }
|
||||||
},
|
}
|
||||||
reportModalTarget: {
|
reportModalTarget: {
|
||||||
type: null,
|
type: null | 'channel'
|
||||||
target: null
|
target: null
|
||||||
|
typeLabel: string
|
||||||
|
label: string
|
||||||
|
_obj?: {
|
||||||
|
fid?: string
|
||||||
|
actor?: { fid: string }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +50,9 @@ const store: Module<State, RootState> = {
|
||||||
},
|
},
|
||||||
reportModalTarget: {
|
reportModalTarget: {
|
||||||
type: null,
|
type: null,
|
||||||
target: null
|
target: null,
|
||||||
|
typeLabel: '',
|
||||||
|
label: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
|
@ -74,7 +82,9 @@ const store: Module<State, RootState> = {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
state.reportModalTarget = {
|
state.reportModalTarget = {
|
||||||
type: null,
|
type: null,
|
||||||
target: null
|
target: null,
|
||||||
|
typeLabel: '',
|
||||||
|
label: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -88,7 +98,9 @@ const store: Module<State, RootState> = {
|
||||||
state.showReportModal = false
|
state.showReportModal = false
|
||||||
state.reportModalTarget = {
|
state.reportModalTarget = {
|
||||||
type: null,
|
type: null,
|
||||||
target: null
|
target: null,
|
||||||
|
typeLabel: '',
|
||||||
|
label: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deleteContentFilter (state, uuid) {
|
deleteContentFilter (state, uuid) {
|
||||||
|
|
|
@ -16,7 +16,7 @@ export interface State {
|
||||||
currentTime: number
|
currentTime: number
|
||||||
errored: boolean
|
errored: boolean
|
||||||
bufferProgress: number
|
bufferProgress: number
|
||||||
looping: 0 | 1 | 2
|
looping: 0 | 1 | 2 // 0 -> no, 1 -> on track, 2 -> on queue
|
||||||
}
|
}
|
||||||
|
|
||||||
const logger = useLogger()
|
const logger = useLogger()
|
||||||
|
@ -34,7 +34,7 @@ const store: Module<State, RootState> = {
|
||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
errored: false,
|
errored: false,
|
||||||
bufferProgress: 0,
|
bufferProgress: 0,
|
||||||
looping: 0 // 0 -> no, 1 -> on track, 2 -> on queue
|
looping: 0
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
reset (state) {
|
reset (state) {
|
||||||
|
@ -123,14 +123,13 @@ const store: Module<State, RootState> = {
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resumePlayback ({ commit, state, dispatch }) {
|
async resumePlayback ({ commit, state, dispatch }) {
|
||||||
commit('playing', true)
|
commit('playing', true)
|
||||||
if (state.errored && state.errorCount < state.maxConsecutiveErrors) {
|
if (state.errored && state.errorCount < state.maxConsecutiveErrors) {
|
||||||
setTimeout(() => {
|
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||||
if (state.playing) {
|
if (state.playing) {
|
||||||
dispatch('queue/next', null, { root: true })
|
return dispatch('queue/next', null, { root: true })
|
||||||
}
|
}
|
||||||
}, 3000)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pausePlayback ({ commit }) {
|
pausePlayback ({ commit }) {
|
||||||
|
|
|
@ -157,14 +157,15 @@ const store: Module<State, RootState> = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
last ({ state, dispatch }) {
|
last ({ state, dispatch }) {
|
||||||
dispatch('currentIndex', state.tracks.length - 1)
|
return dispatch('currentIndex', state.tracks.length - 1)
|
||||||
},
|
},
|
||||||
currentIndex ({ commit, state, rootState, dispatch }, index) {
|
currentIndex ({ commit, state, rootState, dispatch }, index) {
|
||||||
commit('ended', false)
|
commit('ended', false)
|
||||||
commit('player/currentTime', 0, { root: true })
|
commit('player/currentTime', 0, { root: true })
|
||||||
commit('currentIndex', index)
|
commit('currentIndex', index)
|
||||||
|
|
||||||
if (state.tracks.length - index <= 2 && rootState.radios.running) {
|
if (state.tracks.length - index <= 2 && rootState.radios.running) {
|
||||||
dispatch('radios/populateQueue', null, { root: true })
|
return dispatch('radios/populateQueue', null, { root: true })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clean ({ dispatch, commit, state }) {
|
clean ({ dispatch, commit, state }) {
|
||||||
|
|
|
@ -11,9 +11,9 @@ export interface State {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ObjectId {
|
export interface ObjectId {
|
||||||
username: string
|
username: string
|
||||||
fullUsername: string
|
fullUsername: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CurrentRadio {
|
export interface CurrentRadio {
|
||||||
clientOnly: boolean
|
clientOnly: boolean
|
||||||
|
@ -112,7 +112,7 @@ const store: Module<State, RootState> = {
|
||||||
commit('current', null)
|
commit('current', null)
|
||||||
commit('running', false)
|
commit('running', false)
|
||||||
},
|
},
|
||||||
populateQueue ({ commit, rootState, state, dispatch }, playNow) {
|
async populateQueue ({ commit, rootState, state, dispatch }, playNow) {
|
||||||
if (!state.running) {
|
if (!state.running) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -127,21 +127,22 @@ const store: Module<State, RootState> = {
|
||||||
return CLIENT_RADIOS[state.current.type].populateQueue({ current: state.current, dispatch, playNow })
|
return CLIENT_RADIOS[state.current.type].populateQueue({ current: state.current, dispatch, playNow })
|
||||||
}
|
}
|
||||||
|
|
||||||
return axios.post('radios/tracks/', params).then((response) => {
|
try {
|
||||||
|
const response = await axios.post('radios/tracks/', params)
|
||||||
|
|
||||||
logger.info('Adding track to queue from radio')
|
logger.info('Adding track to queue from radio')
|
||||||
const append = dispatch('queue/append', { track: response.data.track }, { root: true })
|
await dispatch('queue/append', { track: response.data.track }, { root: true })
|
||||||
|
|
||||||
if (playNow) {
|
if (playNow) {
|
||||||
append.then(() => {
|
await dispatch('queue/last', null, { root: true })
|
||||||
dispatch('queue/last', null, { root: true })
|
await dispatch('player/resumePlayback', null, { root: true })
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}, () => {
|
} catch (error) {
|
||||||
logger.error('Error while adding track to queue from radio')
|
logger.error('Error while adding track to queue from radio', error)
|
||||||
commit('reset')
|
commit('reset')
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default store
|
export default store
|
||||||
|
|
|
@ -279,6 +279,12 @@ export interface Upload {
|
||||||
mimetype: string
|
mimetype: string
|
||||||
extension: string
|
extension: string
|
||||||
listen_url: string
|
listen_url: string
|
||||||
|
|
||||||
|
import_status: ImportStatus
|
||||||
|
import_details?: {
|
||||||
|
detail: object
|
||||||
|
error_code: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileSystem Logs
|
// FileSystem Logs
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { RouteLocationRaw } from 'vue-router'
|
||||||
|
|
||||||
import LoginForm from '~/components/auth/LoginForm.vue'
|
import LoginForm from '~/components/auth/LoginForm.vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
@ -6,7 +8,7 @@ import { useGettext } from 'vue3-gettext'
|
||||||
import { useStore } from '~/store'
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
next?: string
|
next?: RouteLocationRaw
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
|
@ -1,3 +1,25 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Actor } from '~/types'
|
||||||
|
|
||||||
|
import SemanticModal from '~/components/semantic/Modal.vue'
|
||||||
|
import LibraryWidget from '~/components/federation/LibraryWidget.vue'
|
||||||
|
import ChannelsWidget from '~/components/audio/ChannelsWidget.vue'
|
||||||
|
import ChannelForm from '~/components/audio/ChannelForm.vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
object: Actor
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const step = ref(1)
|
||||||
|
const showCreateModal = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const submittable = ref(false)
|
||||||
|
const category = ref('podcast')
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section>
|
<section>
|
||||||
<div v-if="$store.getters['ui/layoutVersion'] === 'small'">
|
<div v-if="$store.getters['ui/layoutVersion'] === 'small'">
|
||||||
|
@ -54,7 +76,7 @@
|
||||||
</library-widget>
|
</library-widget>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modal v-model:show="showCreateModal">
|
<semantic-modal v-model:show="showCreateModal">
|
||||||
<h4 class="header">
|
<h4 class="header">
|
||||||
<translate
|
<translate
|
||||||
v-if="step === 1"
|
v-if="step === 1"
|
||||||
|
@ -83,7 +105,7 @@
|
||||||
ref="createForm"
|
ref="createForm"
|
||||||
:object="null"
|
:object="null"
|
||||||
:step="step"
|
:step="step"
|
||||||
@loading="isLoading = $event"
|
@loading="loading = $event"
|
||||||
@submittable="submittable = $event"
|
@submittable="submittable = $event"
|
||||||
@category="category = $event"
|
@category="category = $event"
|
||||||
@errored="$refs.modalContent.scrollTop = 0"
|
@errored="$refs.modalContent.scrollTop = 0"
|
||||||
|
@ -120,9 +142,9 @@
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="step === 2"
|
v-if="step === 2"
|
||||||
:class="['ui', 'primary button', {loading: isLoading}]"
|
:class="['ui', 'primary button', { loading }]"
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="!submittable && !isLoading || null"
|
:disabled="!submittable && !loading"
|
||||||
@click.prevent.stop="$refs.createForm.submit"
|
@click.prevent.stop="$refs.createForm.submit"
|
||||||
>
|
>
|
||||||
<translate translate-context="*/Channels/Button.Label">
|
<translate translate-context="*/Channels/Button.Label">
|
||||||
|
@ -130,27 +152,6 @@
|
||||||
</translate>
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</semantic-modal>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import Modal from '~/components/semantic/Modal.vue'
|
|
||||||
import LibraryWidget from '~/components/federation/LibraryWidget.vue'
|
|
||||||
import ChannelsWidget from '~/components/audio/ChannelsWidget.vue'
|
|
||||||
import ChannelForm from '~/components/audio/ChannelForm.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: { ChannelsWidget, LibraryWidget, ChannelForm, Modal },
|
|
||||||
props: { object: { type: Object, required: true } },
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
showCreateModal: false,
|
|
||||||
isLoading: false,
|
|
||||||
submittable: false,
|
|
||||||
step: 1,
|
|
||||||
category: 'podcast'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import type { Channel } from '~/types'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import PlayButton from '~/components/audio/PlayButton.vue'
|
import PlayButton from '~/components/audio/PlayButton.vue'
|
||||||
import EmbedWizard from '~/components/audio/EmbedWizard.vue'
|
import EmbedWizard from '~/components/audio/EmbedWizard.vue'
|
||||||
import Modal from '~/components/semantic/Modal.vue'
|
import SemanticModal from '~/components/semantic/Modal.vue'
|
||||||
import TagsList from '~/components/tags/List.vue'
|
import TagsList from '~/components/tags/List.vue'
|
||||||
import SubscribeButton from '~/components/channels/SubscribeButton.vue'
|
import SubscribeButton from '~/components/channels/SubscribeButton.vue'
|
||||||
import useReport from '~/composables/moderation/useReport'
|
import useReport from '~/composables/moderation/useReport'
|
||||||
|
@ -204,7 +204,7 @@ const updateSubscriptionCount = (delta: number) => {
|
||||||
>
|
>
|
||||||
<i class="feed icon" />
|
<i class="feed icon" />
|
||||||
</a>
|
</a>
|
||||||
<modal
|
<semantic-modal
|
||||||
v-model:show="showSubscribeModal"
|
v-model:show="showSubscribeModal"
|
||||||
class="tiny"
|
class="tiny"
|
||||||
>
|
>
|
||||||
|
@ -268,7 +268,7 @@ const updateSubscriptionCount = (delta: number) => {
|
||||||
</translate>
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</semantic-modal>
|
||||||
<button
|
<button
|
||||||
ref="dropdown"
|
ref="dropdown"
|
||||||
v-dropdown="{direction: 'downward'}"
|
v-dropdown="{direction: 'downward'}"
|
||||||
|
@ -434,7 +434,7 @@ const updateSubscriptionCount = (delta: number) => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modal
|
<semantic-modal
|
||||||
v-if="totalTracks > 0"
|
v-if="totalTracks > 0"
|
||||||
v-model:show="showEmbedModal"
|
v-model:show="showEmbedModal"
|
||||||
>
|
>
|
||||||
|
@ -458,8 +458,8 @@ const updateSubscriptionCount = (delta: number) => {
|
||||||
</translate>
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</semantic-modal>
|
||||||
<modal
|
<semantic-modal
|
||||||
v-if="isOwner"
|
v-if="isOwner"
|
||||||
v-model:show="showEditModal"
|
v-model:show="showEditModal"
|
||||||
>
|
>
|
||||||
|
@ -503,7 +503,7 @@ const updateSubscriptionCount = (delta: number) => {
|
||||||
</translate>
|
</translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</semantic-modal>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="$store.getters['ui/layoutVersion'] === 'large'">
|
<div v-if="$store.getters['ui/layoutVersion'] === 'large'">
|
||||||
<rendered-description
|
<rendered-description
|
||||||
|
|
Ładowanie…
Reference in New Issue