Rename <modal> to <semantic-modal>

environments/review-front-deve-otr6gc/deployments/13419
wvffle 2022-07-20 22:44:19 +00:00 zatwierdzone przez Georg Krause
rodzic 937a44251e
commit 09c1aba30d
21 zmienionych plików z 715 dodań i 715 usunięć

Wyświetl plik

@ -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>

Wyświetl plik

@ -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;

Wyświetl plik

@ -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>

Wyświetl plik

@ -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"

Wyświetl plik

@ -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"

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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"

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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()

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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 }) {

Wyświetl plik

@ -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 }) {

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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>(), {

Wyświetl plik

@ -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>

Wyświetl plik

@ -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