Rename <modal> to <semantic-modal>
This commit is contained in:
parent
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>
|
||||
<modal v-model:show="show">
|
||||
<semantic-modal v-model:show="show">
|
||||
<h4 class="header">
|
||||
{{ labels.header }}
|
||||
</h4>
|
||||
|
@ -32,7 +59,7 @@
|
|||
</div>
|
||||
<div class="actions">
|
||||
<router-link
|
||||
:to="{path: '/login', query: { next: nextRoute }}"
|
||||
:to="{path: '/login', query: { next: nextRoute as string }}"
|
||||
class="ui labeled icon button"
|
||||
>
|
||||
<i class="key icon" />
|
||||
|
@ -47,36 +74,5 @@
|
|||
{{ labels.signup }}
|
||||
</router-link>
|
||||
</div>
|
||||
</modal>
|
||||
</semantic-modal>
|
||||
</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>
|
||||
<!-- TODO make generic and move to semantic/modal? -->
|
||||
<modal
|
||||
v-model:show="showRef"
|
||||
<semantic-modal
|
||||
v-model:show="show"
|
||||
:scrolling="true"
|
||||
:fullscreen="false"
|
||||
>
|
||||
|
@ -84,7 +127,7 @@
|
|||
class="column"
|
||||
role="button"
|
||||
@click="navigate"
|
||||
@keypress.enter="navigate"
|
||||
@keypress.enter="navigate()"
|
||||
>
|
||||
<i class="user-modal list-icon bell icon" />
|
||||
<span class="user-modal list-item">{{ labels.notifications }}</span>
|
||||
|
@ -101,7 +144,7 @@
|
|||
class="column"
|
||||
role="button"
|
||||
@click="navigate"
|
||||
@keypress.enter="navigate"
|
||||
@keypress.enter="navigate()"
|
||||
>
|
||||
<i class="user-modal list-icon cog icon" />
|
||||
<span class="user-modal list-item">{{ labels.settings }}</span>
|
||||
|
@ -140,7 +183,7 @@
|
|||
class="column"
|
||||
role="button"
|
||||
@click="navigate"
|
||||
@keypress.enter="navigate"
|
||||
@keypress.enter="navigate()"
|
||||
>
|
||||
<i class="user-modal list-icon question circle outline icon" />
|
||||
<span class="user-modal list-item">{{ labels.about }}</span>
|
||||
|
@ -159,7 +202,7 @@
|
|||
class="column"
|
||||
role="button"
|
||||
@click="navigate"
|
||||
@keypress.enter="navigate"
|
||||
@keypress.enter="navigate()"
|
||||
>
|
||||
<i class="user-modal list-icon sign out alternate icon" />
|
||||
<span class="user-modal list-item">{{ labels.logout }}</span>
|
||||
|
@ -175,7 +218,7 @@
|
|||
class="column"
|
||||
role="button"
|
||||
@click="navigate"
|
||||
@keypress.enter="navigate"
|
||||
@keypress.enter="navigate()"
|
||||
>
|
||||
<i class="user-modal list-icon sign in alternate icon" />
|
||||
<span class="user-modal list-item">{{ labels.login }}</span>
|
||||
|
@ -191,7 +234,7 @@
|
|||
class="column"
|
||||
role="button"
|
||||
@click="navigate"
|
||||
@keypress.enter="navigate"
|
||||
@keypress.enter="navigate()"
|
||||
>
|
||||
<i class="user-modal list-item user icon" />
|
||||
<span class="user-modal list-item">{{ labels.signup }}</span>
|
||||
|
@ -199,71 +242,9 @@
|
|||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</semantic-modal>
|
||||
</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>
|
||||
.action-hint {
|
||||
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>
|
||||
<div
|
||||
role="button"
|
||||
@click="createFetch"
|
||||
@click="fetch"
|
||||
>
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
<modal
|
||||
<semantic-modal
|
||||
v-model:show="showModal"
|
||||
class="small"
|
||||
>
|
||||
|
@ -16,9 +80,9 @@
|
|||
</translate>
|
||||
</h3>
|
||||
<div class="scrolling content">
|
||||
<template v-if="fetch && fetch.status != 'pending'">
|
||||
<template v-if="data && data.status != 'pending'">
|
||||
<div
|
||||
v-if="fetch.status === 'skipped'"
|
||||
v-if="data.status === 'skipped'"
|
||||
class="ui message"
|
||||
>
|
||||
<h4 class="header">
|
||||
|
@ -33,7 +97,7 @@
|
|||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="fetch.status === 'finished'"
|
||||
v-else-if="data.status === 'finished'"
|
||||
class="ui success message"
|
||||
>
|
||||
<h4 class="header">
|
||||
|
@ -48,7 +112,7 @@
|
|||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="fetch.status === 'errored'"
|
||||
v-else-if="data.status === 'errored'"
|
||||
class="ui error message"
|
||||
>
|
||||
<h4 class="header">
|
||||
|
@ -70,7 +134,7 @@
|
|||
</translate>
|
||||
</td>
|
||||
<td>
|
||||
{{ fetch.detail.error_code }}
|
||||
{{ data.detail.error_code }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -81,44 +145,44 @@
|
|||
</td>
|
||||
<td>
|
||||
<translate
|
||||
v-if="fetch.detail.error_code === 'http' && fetch.detail.status_code"
|
||||
:translate-params="{status: fetch.detail.status_code}"
|
||||
v-if="data.detail.error_code === 'http' && data.detail.status_code"
|
||||
:translate-params="{status: data.detail.status_code}"
|
||||
translate-context="*/*/Error"
|
||||
>
|
||||
The remote server answered with HTTP %{ status }
|
||||
</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"
|
||||
>
|
||||
An HTTP error occurred while contacting the remote server
|
||||
</translate>
|
||||
<translate
|
||||
v-else-if="fetch.detail.error_code === 'timeout'"
|
||||
v-else-if="data.detail.error_code === 'timeout'"
|
||||
translate-context="*/*/Error"
|
||||
>
|
||||
The remote server didn't respond quickly enough
|
||||
</translate>
|
||||
<translate
|
||||
v-else-if="fetch.detail.error_code === 'connection'"
|
||||
v-else-if="data.detail.error_code === 'connection'"
|
||||
translate-context="*/*/Error"
|
||||
>
|
||||
Impossible to connect to the remote server
|
||||
</translate>
|
||||
<translate
|
||||
v-else-if="['invalid_json', 'invalid_jsonld', 'missing_jsonld_type'].indexOf(fetch.detail.error_code) > -1"
|
||||
v-else-if="['invalid_json', 'invalid_jsonld', 'missing_jsonld_type'].indexOf(data.detail.error_code) > -1"
|
||||
translate-context="*/*/Error"
|
||||
>
|
||||
The remote server returned invalid JSON or JSON-LD data
|
||||
</translate>
|
||||
<translate
|
||||
v-else-if="fetch.detail.error_code === 'validation'"
|
||||
v-else-if="data.detail.error_code === 'validation'"
|
||||
translate-context="*/*/Error"
|
||||
>
|
||||
Data returned by the remote server had invalid or missing attributes
|
||||
</translate>
|
||||
<translate
|
||||
v-else-if="fetch.detail.error_code === 'unhandled'"
|
||||
v-else-if="data.detail.error_code === 'unhandled'"
|
||||
translate-context="*/*/Error"
|
||||
>
|
||||
Unknown error
|
||||
|
@ -136,7 +200,7 @@
|
|||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-else-if="isCreatingFetch"
|
||||
v-else-if="isLoading"
|
||||
class="ui active inverted dimmer"
|
||||
>
|
||||
<div class="ui text loader">
|
||||
|
@ -146,7 +210,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="isWaitingFetch"
|
||||
v-else-if="isPolling"
|
||||
class="ui active inverted dimmer"
|
||||
>
|
||||
<div class="ui text loader">
|
||||
|
@ -175,7 +239,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="fetch && fetch.status === 'pending' && pollsCount >= maxPolls"
|
||||
v-else-if="data && data.status === 'pending' && pollsCount >= MAX_POLLS"
|
||||
role="alert"
|
||||
class="ui warning message"
|
||||
>
|
||||
|
@ -198,7 +262,7 @@
|
|||
</translate>
|
||||
</button>
|
||||
<button
|
||||
v-if="fetch && fetch.status === 'finished'"
|
||||
v-if="data && data.status === 'finished'"
|
||||
class="ui confirm success button"
|
||||
@click.prevent="showModal = false; $emit('refresh')"
|
||||
>
|
||||
|
@ -207,69 +271,6 @@
|
|||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</semantic-modal>
|
||||
</div>
|
||||
</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 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 { computed, ref } from 'vue'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
@ -42,7 +42,7 @@ const remove = () => emit('remove')
|
|||
<template>
|
||||
<span>
|
||||
|
||||
<modal
|
||||
<semantic-modal
|
||||
v-if="isEmbedable"
|
||||
v-model:show="showEmbedModal"
|
||||
>
|
||||
|
@ -63,7 +63,7 @@ const remove = () => emit('remove')
|
|||
<translate translate-context="*/*/Button.Label/Verb">Cancel</translate>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</semantic-modal>
|
||||
<button
|
||||
v-dropdown="{direction: 'downward'}"
|
||||
class="ui floating dropdown circular icon basic button"
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useGettext } from 'vue3-gettext'
|
|||
import axios from 'axios'
|
||||
import PlayButton from '~/components/audio/PlayButton.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 TagsList from '~/components/tags/List.vue'
|
||||
import useReport from '~/composables/moderation/useReport'
|
||||
|
@ -136,7 +136,7 @@ watch(() => props.id, fetchData, { immediate: true })
|
|||
<div class="ui buttons">
|
||||
<radio-button
|
||||
type="artist"
|
||||
:object-id="String(object.id)"
|
||||
:object-id="object.id"
|
||||
/>
|
||||
</div>
|
||||
<div class="ui buttons">
|
||||
|
@ -151,7 +151,7 @@ watch(() => props.id, fetchData, { immediate: true })
|
|||
</play-button>
|
||||
</div>
|
||||
|
||||
<modal
|
||||
<semantic-modal
|
||||
v-if="publicLibraries.length > 0"
|
||||
v-model:show="showEmbedModal"
|
||||
>
|
||||
|
@ -175,7 +175,7 @@ watch(() => props.id, fetchData, { immediate: true })
|
|||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</semantic-modal>
|
||||
<div class="ui buttons">
|
||||
<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>
|
||||
<modal v-model:show="showModal">
|
||||
<semantic-modal v-model:show="show">
|
||||
<h4 class="header">
|
||||
<translate translate-context="Popup/Import/Title">
|
||||
Import detail
|
||||
|
@ -112,7 +184,7 @@
|
|||
<textarea
|
||||
class="ui textarea"
|
||||
rows="10"
|
||||
:value="getErrorData(upload).debugInfo"
|
||||
:value="JSON.stringify(getErrorData(upload).debugInfo)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
|
@ -129,87 +201,5 @@
|
|||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</semantic-modal>
|
||||
</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 Pagination from '~/components/vui/Pagination.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 useLogger from '~/composables/useLogger'
|
||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||
|
@ -271,7 +271,7 @@ const labels = computed(() => ({
|
|||
/>
|
||||
</div>
|
||||
</section>
|
||||
<modal
|
||||
<semantic-modal
|
||||
v-model:show="showSubscribeModal"
|
||||
class="tiny"
|
||||
:fullscreen="false"
|
||||
|
@ -310,6 +310,6 @@ const labels = computed(() => ({
|
|||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</semantic-modal>
|
||||
</main>
|
||||
</template>
|
||||
|
|
|
@ -6,7 +6,7 @@ import axios from 'axios'
|
|||
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 SemanticModal from '~/components/semantic/Modal.vue'
|
||||
import EmbedWizard from '~/components/audio/EmbedWizard.vue'
|
||||
import { momentFormat } from '~/utils/filters'
|
||||
import updateQueryString from '~/composables/updateQueryString'
|
||||
|
@ -176,7 +176,7 @@ const remove = async () => {
|
|||
>
|
||||
<i class="download icon" />
|
||||
</a>
|
||||
<modal
|
||||
<semantic-modal
|
||||
v-if="isEmbedable"
|
||||
v-model:show="showEmbedModal"
|
||||
>
|
||||
|
@ -200,7 +200,7 @@ const remove = async () => {
|
|||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</semantic-modal>
|
||||
<button
|
||||
v-dropdown="{direction: 'downward'}"
|
||||
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>
|
||||
<tr>
|
||||
<td>{{ filter.label }}</td>
|
||||
|
@ -66,7 +186,7 @@
|
|||
>
|
||||
{{ checkResult.candidates.count }} tracks matching filter
|
||||
</a>
|
||||
<modal
|
||||
<semantic-modal
|
||||
v-if="checkResult"
|
||||
v-model:show="showCandidadesModal"
|
||||
>
|
||||
|
@ -90,7 +210,7 @@
|
|||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</semantic-modal>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
|
@ -104,90 +224,3 @@
|
|||
</td>
|
||||
</tr>
|
||||
</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>
|
||||
<button
|
||||
class="ui button"
|
||||
|
@ -9,7 +66,7 @@
|
|||
Moderation rules…
|
||||
</translate>
|
||||
</slot>
|
||||
<modal
|
||||
<semantic-modal
|
||||
v-model:show="show"
|
||||
@show="fetchData"
|
||||
>
|
||||
|
@ -60,64 +117,6 @@
|
|||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</semantic-modal>
|
||||
</button>
|
||||
</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>
|
||||
<modal
|
||||
v-model:show="showRef"
|
||||
@update:show="update"
|
||||
>
|
||||
<semantic-modal v-model:show="show">
|
||||
<h4 class="header">
|
||||
<translate
|
||||
v-if="type === 'artist'"
|
||||
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 }"?
|
||||
</translate>
|
||||
|
@ -84,73 +141,5 @@
|
|||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</semantic-modal>
|
||||
</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>
|
||||
<modal
|
||||
v-model:show="showRef"
|
||||
@update:show="update"
|
||||
>
|
||||
<semantic-modal v-model:show="show">
|
||||
<h2
|
||||
v-if="target"
|
||||
class="ui header"
|
||||
|
@ -150,137 +268,5 @@
|
|||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</semantic-modal>
|
||||
</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
|
||||
type?: string
|
||||
clientOnly?: boolean
|
||||
objectId?: ObjectId | null
|
||||
objectId?: ObjectId | string | null
|
||||
radioConfig?: RadioConfig | null
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,10 @@ const running = computed(() => {
|
|||
|
||||
return store.state.radios.current?.type === props.type
|
||||
&& 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()
|
||||
|
|
|
@ -6,17 +6,23 @@ import { sortBy } from 'lodash-es'
|
|||
import useLogger from '~/composables/useLogger'
|
||||
|
||||
export interface State {
|
||||
filters: ContentFilter[],
|
||||
showFilterModal: boolean,
|
||||
showReportModal: boolean,
|
||||
filters: ContentFilter[]
|
||||
showFilterModal: boolean
|
||||
showReportModal: boolean
|
||||
lastUpdate: Date,
|
||||
filterModalTarget: {
|
||||
type: null,
|
||||
target: null
|
||||
},
|
||||
type: null
|
||||
target: null | { id: string, name: string }
|
||||
}
|
||||
reportModalTarget: {
|
||||
type: null,
|
||||
type: null | 'channel'
|
||||
target: null
|
||||
typeLabel: string
|
||||
label: string
|
||||
_obj?: {
|
||||
fid?: string
|
||||
actor?: { fid: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,7 +50,9 @@ const store: Module<State, RootState> = {
|
|||
},
|
||||
reportModalTarget: {
|
||||
type: null,
|
||||
target: null
|
||||
target: null,
|
||||
typeLabel: '',
|
||||
label: ''
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
|
@ -74,7 +82,9 @@ const store: Module<State, RootState> = {
|
|||
if (!value) {
|
||||
state.reportModalTarget = {
|
||||
type: null,
|
||||
target: null
|
||||
target: null,
|
||||
typeLabel: '',
|
||||
label: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -88,7 +98,9 @@ const store: Module<State, RootState> = {
|
|||
state.showReportModal = false
|
||||
state.reportModalTarget = {
|
||||
type: null,
|
||||
target: null
|
||||
target: null,
|
||||
typeLabel: '',
|
||||
label: ''
|
||||
}
|
||||
},
|
||||
deleteContentFilter (state, uuid) {
|
||||
|
|
|
@ -16,7 +16,7 @@ export interface State {
|
|||
currentTime: number
|
||||
errored: boolean
|
||||
bufferProgress: number
|
||||
looping: 0 | 1 | 2
|
||||
looping: 0 | 1 | 2 // 0 -> no, 1 -> on track, 2 -> on queue
|
||||
}
|
||||
|
||||
const logger = useLogger()
|
||||
|
@ -34,7 +34,7 @@ const store: Module<State, RootState> = {
|
|||
currentTime: 0,
|
||||
errored: false,
|
||||
bufferProgress: 0,
|
||||
looping: 0 // 0 -> no, 1 -> on track, 2 -> on queue
|
||||
looping: 0
|
||||
},
|
||||
mutations: {
|
||||
reset (state) {
|
||||
|
@ -123,14 +123,13 @@ const store: Module<State, RootState> = {
|
|||
}, 3000)
|
||||
}
|
||||
},
|
||||
resumePlayback ({ commit, state, dispatch }) {
|
||||
async resumePlayback ({ commit, state, dispatch }) {
|
||||
commit('playing', true)
|
||||
if (state.errored && state.errorCount < state.maxConsecutiveErrors) {
|
||||
setTimeout(() => {
|
||||
if (state.playing) {
|
||||
dispatch('queue/next', null, { root: true })
|
||||
}
|
||||
}, 3000)
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
if (state.playing) {
|
||||
return dispatch('queue/next', null, { root: true })
|
||||
}
|
||||
}
|
||||
},
|
||||
pausePlayback ({ commit }) {
|
||||
|
|
|
@ -157,14 +157,15 @@ const store: Module<State, RootState> = {
|
|||
}
|
||||
},
|
||||
last ({ state, dispatch }) {
|
||||
dispatch('currentIndex', state.tracks.length - 1)
|
||||
return dispatch('currentIndex', state.tracks.length - 1)
|
||||
},
|
||||
currentIndex ({ commit, state, rootState, dispatch }, index) {
|
||||
commit('ended', false)
|
||||
commit('player/currentTime', 0, { root: true })
|
||||
commit('currentIndex', index)
|
||||
|
||||
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 }) {
|
||||
|
|
|
@ -11,9 +11,9 @@ export interface State {
|
|||
}
|
||||
|
||||
export interface ObjectId {
|
||||
username: string
|
||||
fullUsername: string
|
||||
}
|
||||
username: string
|
||||
fullUsername: string
|
||||
}
|
||||
|
||||
export interface CurrentRadio {
|
||||
clientOnly: boolean
|
||||
|
@ -112,7 +112,7 @@ const store: Module<State, RootState> = {
|
|||
commit('current', null)
|
||||
commit('running', false)
|
||||
},
|
||||
populateQueue ({ commit, rootState, state, dispatch }, playNow) {
|
||||
async populateQueue ({ commit, rootState, state, dispatch }, playNow) {
|
||||
if (!state.running) {
|
||||
return
|
||||
}
|
||||
|
@ -127,21 +127,22 @@ const store: Module<State, RootState> = {
|
|||
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')
|
||||
const append = dispatch('queue/append', { track: response.data.track }, { root: true })
|
||||
await dispatch('queue/append', { track: response.data.track }, { root: true })
|
||||
|
||||
if (playNow) {
|
||||
append.then(() => {
|
||||
dispatch('queue/last', null, { root: true })
|
||||
})
|
||||
await dispatch('queue/last', null, { root: true })
|
||||
await dispatch('player/resumePlayback', null, { root: true })
|
||||
}
|
||||
}, () => {
|
||||
logger.error('Error while adding track to queue from radio')
|
||||
} catch (error) {
|
||||
logger.error('Error while adding track to queue from radio', error)
|
||||
commit('reset')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default store
|
||||
|
|
|
@ -279,6 +279,12 @@ export interface Upload {
|
|||
mimetype: string
|
||||
extension: string
|
||||
listen_url: string
|
||||
|
||||
import_status: ImportStatus
|
||||
import_details?: {
|
||||
detail: object
|
||||
error_code: string
|
||||
}
|
||||
}
|
||||
|
||||
// FileSystem Logs
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
import LoginForm from '~/components/auth/LoginForm.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed } from 'vue'
|
||||
|
@ -6,7 +8,7 @@ import { useGettext } from 'vue3-gettext'
|
|||
import { useStore } from '~/store'
|
||||
|
||||
interface Props {
|
||||
next?: string
|
||||
next?: RouteLocationRaw
|
||||
}
|
||||
|
||||
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>
|
||||
<section>
|
||||
<div v-if="$store.getters['ui/layoutVersion'] === 'small'">
|
||||
|
@ -54,7 +76,7 @@
|
|||
</library-widget>
|
||||
</div>
|
||||
|
||||
<modal v-model:show="showCreateModal">
|
||||
<semantic-modal v-model:show="showCreateModal">
|
||||
<h4 class="header">
|
||||
<translate
|
||||
v-if="step === 1"
|
||||
|
@ -83,7 +105,7 @@
|
|||
ref="createForm"
|
||||
:object="null"
|
||||
:step="step"
|
||||
@loading="isLoading = $event"
|
||||
@loading="loading = $event"
|
||||
@submittable="submittable = $event"
|
||||
@category="category = $event"
|
||||
@errored="$refs.modalContent.scrollTop = 0"
|
||||
|
@ -120,9 +142,9 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="step === 2"
|
||||
:class="['ui', 'primary button', {loading: isLoading}]"
|
||||
:class="['ui', 'primary button', { loading }]"
|
||||
type="submit"
|
||||
:disabled="!submittable && !isLoading || null"
|
||||
:disabled="!submittable && !loading"
|
||||
@click.prevent.stop="$refs.createForm.submit"
|
||||
>
|
||||
<translate translate-context="*/Channels/Button.Label">
|
||||
|
@ -130,27 +152,6 @@
|
|||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</semantic-modal>
|
||||
</section>
|
||||
</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 PlayButton from '~/components/audio/PlayButton.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 SubscribeButton from '~/components/channels/SubscribeButton.vue'
|
||||
import useReport from '~/composables/moderation/useReport'
|
||||
|
@ -204,7 +204,7 @@ const updateSubscriptionCount = (delta: number) => {
|
|||
>
|
||||
<i class="feed icon" />
|
||||
</a>
|
||||
<modal
|
||||
<semantic-modal
|
||||
v-model:show="showSubscribeModal"
|
||||
class="tiny"
|
||||
>
|
||||
|
@ -268,7 +268,7 @@ const updateSubscriptionCount = (delta: number) => {
|
|||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</semantic-modal>
|
||||
<button
|
||||
ref="dropdown"
|
||||
v-dropdown="{direction: 'downward'}"
|
||||
|
@ -434,7 +434,7 @@ const updateSubscriptionCount = (delta: number) => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<modal
|
||||
<semantic-modal
|
||||
v-if="totalTracks > 0"
|
||||
v-model:show="showEmbedModal"
|
||||
>
|
||||
|
@ -458,8 +458,8 @@ const updateSubscriptionCount = (delta: number) => {
|
|||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
<modal
|
||||
</semantic-modal>
|
||||
<semantic-modal
|
||||
v-if="isOwner"
|
||||
v-model:show="showEditModal"
|
||||
>
|
||||
|
@ -503,7 +503,7 @@ const updateSubscriptionCount = (delta: number) => {
|
|||
</translate>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</semantic-modal>
|
||||
</div>
|
||||
<div v-if="$store.getters['ui/layoutVersion'] === 'large'">
|
||||
<rendered-description
|
||||
|
|
Loading…
Reference in New Issue