fix(front):only request playlist.libfollow if not my playlist NOCHANGELOG
This commit is contained in:
parent
c9600fd0ac
commit
81b40be7f5
|
@ -189,4 +189,4 @@ def filter_files(files, allowed_extensions):
|
||||||
def get_search_url(query, page_size, page):
|
def get_search_url(query, page_size, page):
|
||||||
q = urllib.parse.urlencode({"q": query})
|
q = urllib.parse.urlencode({"q": query})
|
||||||
return f"https://archive.org/advancedsearch.php?{q}&sort[]=addeddate+desc&rows={page_size}\
|
return f"https://archive.org/advancedsearch.php?{q}&sort[]=addeddate+desc&rows={page_size}\
|
||||||
&page={page}&output=json&mediatype=audio"
|
&page={page}&output=json"
|
||||||
|
|
|
@ -122,7 +122,7 @@ store.dispatch('auth/fetchUser')
|
||||||
.responsive {
|
.responsive {
|
||||||
display: grid !important;
|
display: grid !important;
|
||||||
grid-template-rows: min-content;
|
grid-template-rows: min-content;
|
||||||
min-height: calc(100vh - 64px);
|
min-height: 100vh;
|
||||||
|
|
||||||
@media screen and (min-width: 1024px) {
|
@media screen and (min-width: 1024px) {
|
||||||
grid-template-columns: 300px 1fr;
|
grid-template-columns: 300px 1fr;
|
||||||
|
|
|
@ -108,7 +108,7 @@ const labels = computed(() => ({
|
||||||
|
|
||||||
const isOpen = ref(false)
|
const isOpen = ref(false)
|
||||||
|
|
||||||
const playlistFollowInfo = computed(() => {
|
const playlistLibraryFollowInfo = computed(() => {
|
||||||
const playlist = props.playlist;
|
const playlist = props.playlist;
|
||||||
if (!playlist) return null;
|
if (!playlist) return null;
|
||||||
|
|
||||||
|
@ -268,13 +268,13 @@ const playlistFollowInfo = computed(() => {
|
||||||
{{ obj.label }}
|
{{ obj.label }}
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
<PopoverItem
|
<PopoverItem
|
||||||
v-if="playlist && playlistFollowInfo"
|
v-if="playlist && playlistLibraryFollowInfo && store.state.auth.profile && playlist.actor.full_username != store.state.auth.fullUsername"
|
||||||
:title="playlistFollowInfo.tooltip"
|
:title="playlistLibraryFollowInfo.tooltip"
|
||||||
:icon="playlistFollowInfo.icon"
|
:icon="playlistLibraryFollowInfo.icon"
|
||||||
:disabled="playlistFollowInfo.disabled"
|
:disabled="playlistLibraryFollowInfo.disabled"
|
||||||
@click.stop.prevent="requestPlaylistUploadsAccess(playlist)"
|
@click.stop.prevent="requestPlaylistUploadsAccess(playlist)"
|
||||||
>
|
>
|
||||||
{{ playlistFollowInfo.label }}
|
{{ playlistLibraryFollowInfo.label }}
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
</template>
|
</template>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
|
@ -7,7 +7,7 @@ import useReport from '~/composables/moderation/useReport'
|
||||||
import { useStore } from '~/store'
|
import { useStore } from '~/store'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { computed, ref } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
import { generateTrackCreditString, getArtistCoverUrl } from '~/utils/utils'
|
import { generateTrackCreditString, getArtistCoverUrl } from '~/utils/utils'
|
||||||
|
|
||||||
|
@ -50,7 +50,6 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
account: null
|
account: null
|
||||||
})
|
})
|
||||||
|
|
||||||
const modal = ref()
|
|
||||||
|
|
||||||
const show = useVModel(props, 'show', emit)
|
const show = useVModel(props, 'show', emit)
|
||||||
|
|
||||||
|
@ -94,11 +93,8 @@ const labels = computed(() => ({
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal
|
||||||
ref="modal"
|
|
||||||
v-model="show"
|
v-model="show"
|
||||||
:title="track.title"
|
:title="track.title"
|
||||||
:scrolling="true"
|
|
||||||
class="scrolling-track-options"
|
|
||||||
>
|
>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="ui large centered rounded image">
|
<div class="ui large centered rounded image">
|
||||||
|
|
|
@ -67,20 +67,30 @@ const submitAndScan = async () => {
|
||||||
:class="['ui form', {loading: isLoading}]"
|
:class="['ui form', {loading: isLoading}]"
|
||||||
@submit.prevent="submit"
|
@submit.prevent="submit"
|
||||||
>
|
>
|
||||||
<h3>{{ plugin.label }}</h3>
|
<h2>{{ plugin.label }}</h2>
|
||||||
<sanitized-html
|
<Alert blue>
|
||||||
v-if="plugin.description"
|
<Layout flex>
|
||||||
:html="description"
|
<p><i class="bi bi-info-circle-fill" /></p>
|
||||||
/>
|
<Layout
|
||||||
<template v-if="plugin.homepage">
|
stack
|
||||||
<a
|
no-gap
|
||||||
:href="plugin.homepage"
|
>
|
||||||
target="_blank"
|
<sanitized-html
|
||||||
>
|
v-if="plugin.description"
|
||||||
<i class="external icon" />
|
:html="description"
|
||||||
{{ t('components.auth.Plugin.link.documentation') }}
|
/>
|
||||||
</a>
|
<template v-if="plugin.homepage">
|
||||||
</template>
|
<a
|
||||||
|
:href="plugin.homepage"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<i class="bi bi-box-arrow-up-right" />
|
||||||
|
{{ t('components.auth.Plugin.link.documentation') }}
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
</Alert>
|
||||||
<Alert
|
<Alert
|
||||||
v-if="errors.length > 0"
|
v-if="errors.length > 0"
|
||||||
red
|
red
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import type { OrderingProps } from '~/composables/navigation/useOrdering'
|
import type { OrderingProps } from '~/composables/navigation/useOrdering'
|
||||||
import type { RouteRecordName } from 'vue-router'
|
import type { RouteRecordName } from 'vue-router'
|
||||||
import type { OrderingField } from '~/store/ui'
|
import type { OrderingField } from '~/store/ui'
|
||||||
import type { Track } from '~/types'
|
import type { UserTrackFavorite } from '~/types'
|
||||||
|
|
||||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
@ -53,7 +53,7 @@ const sharedLabels = useSharedLabels()
|
||||||
|
|
||||||
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props)
|
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props)
|
||||||
|
|
||||||
const results = reactive<Track[]>([])
|
const results = reactive<UserTrackFavorite[]>([])
|
||||||
const nextLink = ref()
|
const nextLink = ref()
|
||||||
const previousLink = ref()
|
const previousLink = ref()
|
||||||
const count = ref(0)
|
const count = ref(0)
|
||||||
|
@ -66,7 +66,7 @@ const fetchFavorites = async () => {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
page_size: paginateBy.value,
|
page_size: paginateBy.value,
|
||||||
ordering: orderingString.value,
|
ordering: orderingString.value,
|
||||||
scope: store.state.auth.fullUsername
|
scope: "me"
|
||||||
}
|
}
|
||||||
|
|
||||||
const measureLoading = logger.time('Loading user favorites')
|
const measureLoading = logger.time('Loading user favorites')
|
||||||
|
@ -76,8 +76,8 @@ const fetchFavorites = async () => {
|
||||||
results.length = 0
|
results.length = 0
|
||||||
results.push(...response.data.results)
|
results.push(...response.data.results)
|
||||||
|
|
||||||
for (const track of results) {
|
for (const trackfavorite of results) {
|
||||||
store.commit('favorites/track', { id: track.id, value: true })
|
store.commit('favorites/track', { id: trackfavorite.track.id, value: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
count.value = response.data.count
|
count.value = response.data.count
|
||||||
|
@ -225,7 +225,7 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
|
||||||
:search="true"
|
:search="true"
|
||||||
:show-artist="true"
|
:show-artist="true"
|
||||||
:show-album="true"
|
:show-album="true"
|
||||||
:tracks="results"
|
:tracks="results.map(r => r.track)"
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Alert
|
<Alert
|
||||||
|
|
|
@ -2,19 +2,19 @@
|
||||||
import type { Library } from '~/types'
|
import type { Library } from '~/types'
|
||||||
|
|
||||||
import { ref, reactive, onMounted, watch } from 'vue'
|
import { ref, reactive, onMounted, watch } from 'vue'
|
||||||
import { useStore } from '~/store'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import LibraryCard from '~/views/content/remote/Card.vue'
|
|
||||||
import Button from '~/components/ui/Button.vue'
|
import Button from '~/components/ui/Button.vue'
|
||||||
import Section from '~/components/ui/Section.vue'
|
import Section from '~/components/ui/Section.vue'
|
||||||
import Loader from '~/components/ui/Loader.vue'
|
import Loader from '~/components/ui/Loader.vue'
|
||||||
import Alert from '~/components/ui/Alert.vue'
|
import Alert from '~/components/ui/Alert.vue'
|
||||||
import Spacer from '~/components/ui/Spacer.vue'
|
import Spacer from '~/components/ui/Spacer.vue'
|
||||||
|
import ActorLink from '~/components/common/ActorLink.vue'
|
||||||
|
|
||||||
import useErrorHandler from '~/composables/useErrorHandler'
|
import useErrorHandler from '~/composables/useErrorHandler'
|
||||||
|
import Layout from '../ui/Layout.vue'
|
||||||
|
|
||||||
interface Events {
|
interface Events {
|
||||||
(e: 'loaded', libraries: Library[]): void
|
(e: 'loaded', libraries: Library[]): void
|
||||||
|
@ -26,7 +26,6 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const store = useStore()
|
|
||||||
|
|
||||||
const emit = defineEmits<Events>()
|
const emit = defineEmits<Events>()
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
@ -67,7 +66,6 @@ watch(() => props.url, () => {
|
||||||
<Section
|
<Section
|
||||||
align-left
|
align-left
|
||||||
:h2="title"
|
:h2="title"
|
||||||
:columns-per-item="3"
|
|
||||||
>
|
>
|
||||||
<Loader
|
<Loader
|
||||||
v-if="isLoading"
|
v-if="isLoading"
|
||||||
|
@ -80,14 +78,21 @@ watch(() => props.url, () => {
|
||||||
>
|
>
|
||||||
{{ t('components.federation.LibraryWidget.empty.noMatch') }}
|
{{ t('components.federation.LibraryWidget.empty.noMatch') }}
|
||||||
</Alert>
|
</Alert>
|
||||||
<library-card
|
<Layout
|
||||||
v-for="library in libraries"
|
v-if="!isLoading && libraries.length > 0"
|
||||||
:key="library.uuid"
|
flex
|
||||||
:display-scan="false"
|
>
|
||||||
:display-follow="store.state.auth.authenticated && library.actor.full_username != store.state.auth.fullUsername"
|
{{ t('components.federation.LibraryWidget.main') }}
|
||||||
:initial-library="library"
|
<template
|
||||||
:display-copy-fid="true"
|
v-for="library in libraries"
|
||||||
/>
|
:key="library.uuid"
|
||||||
|
>
|
||||||
|
<ActorLink
|
||||||
|
:actor="library.actor"
|
||||||
|
discrete
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Layout>
|
||||||
<template v-if="nextPage">
|
<template v-if="nextPage">
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -1414,7 +1414,8 @@
|
||||||
},
|
},
|
||||||
"empty": {
|
"empty": {
|
||||||
"noMatch": "No matching library."
|
"noMatch": "No matching library."
|
||||||
}
|
},
|
||||||
|
"main": "This audio object is present in the audio collection of"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"forms": {
|
"forms": {
|
||||||
|
@ -1445,7 +1446,7 @@
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"episodes": "Episodes",
|
"episodes": "Episodes",
|
||||||
"libraries": "User libraries",
|
"libraries": "On the network",
|
||||||
"tracks": "Tracks"
|
"tracks": "Tracks"
|
||||||
},
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
|
|
|
@ -68,6 +68,7 @@ export type Cover = components['schemas']['CoverField']
|
||||||
export type RateLimitStatus = components['schemas']['RateLimit']['scopes'][number]
|
export type RateLimitStatus = components['schemas']['RateLimit']['scopes'][number]
|
||||||
export type PaginatedAlbumList = components['schemas']['PaginatedAlbumList']
|
export type PaginatedAlbumList = components['schemas']['PaginatedAlbumList']
|
||||||
export type PaginatedChannelList = components['schemas']['PaginatedChannelList']
|
export type PaginatedChannelList = components['schemas']['PaginatedChannelList']
|
||||||
|
export type UserTrackFavorite = components['schemas']['UserTrackFavorite']
|
||||||
|
|
||||||
export type Artist = components['schemas']['Artist']
|
export type Artist = components['schemas']['Artist']
|
||||||
|
|
||||||
|
|
|
@ -155,7 +155,7 @@ const categories = computed(() => [
|
||||||
type: 'playlists',
|
type: 'playlists',
|
||||||
label: t('views.Search.label.playlists'),
|
label: t('views.Search.label.playlists'),
|
||||||
more: '/library/playlists/',
|
more: '/library/playlists/',
|
||||||
endpoint: '/TODO'
|
endpoint: '/playlists'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'radios',
|
type: 'radios',
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import type { Track, Album, ArtistCredit, QueueItemSource } from '~/types'
|
import type { Track, Album, ArtistCredit, QueueItemSource } from '~/types'
|
||||||
import type { components } from '~/generated/types'
|
import type { components } from '~/generated/types'
|
||||||
import { useStore } from '~/store'
|
|
||||||
import type { QueueTrack } from '~/composables/audio/queue'
|
import type { QueueTrack } from '~/composables/audio/queue'
|
||||||
|
import store from '~/store'
|
||||||
|
|
||||||
const store = useStore()
|
|
||||||
|
|
||||||
export function generateTrackCreditString (track: Track | Album | null): string | null {
|
export function generateTrackCreditString (track: Track | Album | null): string | null {
|
||||||
if (!track || !track.artist_credit || track.artist_credit.length === 0) {
|
if (!track || !track.artist_credit || track.artist_credit.length === 0) {
|
||||||
|
@ -51,5 +50,15 @@ const getSimpleArtistCover = (artist: components['schemas']['SimpleChannelArtist
|
||||||
* @param artist: a simple artist
|
* @param artist: a simple artist
|
||||||
* @param field: the size you want
|
* @param field: the size you want
|
||||||
*/
|
*/
|
||||||
export const getSimpleArtistCoverUrl = (artist: components['schemas']['SimpleChannelArtist'] | components['schemas']['Artist'] | components['schemas']['ArtistWithAlbums'], field: 'original' | 'small_square_crop' | 'medium_square_crop' | 'large_square_crop') =>
|
export const getSimpleArtistCoverUrl = (
|
||||||
store.getters['instance/absoluteUrl'](getSimpleArtistCover(artist)(field))
|
artist: components['schemas']['SimpleChannelArtist'] | components['schemas']['Artist'] | components['schemas']['ArtistWithAlbums'],
|
||||||
|
field: 'original' | 'small_square_crop' | 'medium_square_crop' | 'large_square_crop'
|
||||||
|
): string | null => {
|
||||||
|
const coverGetter = getSimpleArtistCover(artist);
|
||||||
|
if (!coverGetter) return null;
|
||||||
|
|
||||||
|
const cover = coverGetter(field);
|
||||||
|
if (!cover) return null;
|
||||||
|
|
||||||
|
return store.getters['instance/absoluteUrl'](cover);
|
||||||
|
};
|
||||||
|
|
|
@ -6,7 +6,10 @@ import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import Layout from '~/components/ui/Layout.vue'
|
||||||
import Alert from '~/components/ui/Alert.vue'
|
import Alert from '~/components/ui/Alert.vue'
|
||||||
|
import Input from '~/components/ui/Input.vue'
|
||||||
|
import Link from '~/components/ui/Link.vue'
|
||||||
import Button from '~/components/ui/Button.vue'
|
import Button from '~/components/ui/Button.vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -56,15 +59,14 @@ const submit = async () => {
|
||||||
class="main"
|
class="main"
|
||||||
>
|
>
|
||||||
<h2>{{ labels.changePassword }}</h2>
|
<h2>{{ labels.changePassword }}</h2>
|
||||||
<form
|
<Layout
|
||||||
v-if="!success"
|
v-if="!success"
|
||||||
class="ui form"
|
form
|
||||||
@submit.prevent="submit()"
|
@submit.prevent="submit()"
|
||||||
>
|
>
|
||||||
<Alert
|
<Alert
|
||||||
v-if="errors.length > 0"
|
v-if="errors.length > 0"
|
||||||
role="alert"
|
red
|
||||||
class="ui negative message"
|
|
||||||
>
|
>
|
||||||
<h4 class="header">
|
<h4 class="header">
|
||||||
{{ t('views.auth.PasswordResetConfirm.header.failure') }}
|
{{ t('views.auth.PasswordResetConfirm.header.failure') }}
|
||||||
|
@ -83,26 +85,33 @@ const submit = async () => {
|
||||||
<Input
|
<Input
|
||||||
v-model="newPassword"
|
v-model="newPassword"
|
||||||
password
|
password
|
||||||
label="t('views.auth.PasswordResetConfirm.label.newPassword')"
|
:label="t('views.auth.PasswordResetConfirm.label.newPassword')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<router-link :to="{path: '/login'}">
|
<Layout flex>
|
||||||
{{ t('views.auth.PasswordResetConfirm.link.back') }}
|
<Link
|
||||||
</router-link>
|
solid
|
||||||
<Button
|
secondary
|
||||||
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']"
|
:to="{path: '/login'}"
|
||||||
type="submit"
|
>
|
||||||
auto
|
{{ t('views.auth.PasswordResetConfirm.link.back') }}
|
||||||
>
|
</Link>
|
||||||
{{ t('views.auth.PasswordResetConfirm.button.update') }}
|
<Button
|
||||||
</Button>
|
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']"
|
||||||
|
type="submit"
|
||||||
|
auto
|
||||||
|
primary
|
||||||
|
>
|
||||||
|
{{ t('views.auth.PasswordResetConfirm.button.update') }}
|
||||||
|
</Button>
|
||||||
|
</Layout>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<p>
|
<p>
|
||||||
{{ t('views.auth.PasswordResetConfirm.message.requestSent') }}
|
{{ t('views.auth.PasswordResetConfirm.message.requestSent') }}
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</form>
|
</Layout>
|
||||||
<Alert
|
<Alert
|
||||||
v-else
|
v-else
|
||||||
green
|
green
|
||||||
|
|
|
@ -7,6 +7,7 @@ import axios from 'axios'
|
||||||
import PluginForm from '~/components/auth/Plugin.vue'
|
import PluginForm from '~/components/auth/Plugin.vue'
|
||||||
|
|
||||||
import Layout from '~/components/ui/Layout.vue'
|
import Layout from '~/components/ui/Layout.vue'
|
||||||
|
import Loader from '~/components/ui/Loader.vue'
|
||||||
|
|
||||||
import useErrorHandler from '~/composables/useErrorHandler'
|
import useErrorHandler from '~/composables/useErrorHandler'
|
||||||
|
|
||||||
|
@ -46,13 +47,10 @@ fetchData()
|
||||||
main
|
main
|
||||||
stack
|
stack
|
||||||
>
|
>
|
||||||
<h2>{{ labels.title }}</h2>
|
<h1>{{ labels.title }}</h1>
|
||||||
<div
|
<Loader
|
||||||
v-if="isLoading"
|
v-if="isLoading"
|
||||||
class="ui inverted active dimmer"
|
/>
|
||||||
>
|
|
||||||
<div class="ui loader" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-if="plugins && plugins.length > 0">
|
<template v-if="plugins && plugins.length > 0">
|
||||||
<plugin-form
|
<plugin-form
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Actor } from '~/types'
|
import type { Actor } from '~/types'
|
||||||
|
|
||||||
import LibraryWidget from '~/components/federation/LibraryWidget.vue'
|
|
||||||
import ChannelsWidget from '~/components/audio/ChannelsWidget.vue'
|
import ChannelsWidget from '~/components/audio/ChannelsWidget.vue'
|
||||||
import ChannelForm from '~/components/audio/ChannelForm.vue'
|
import ChannelForm from '~/components/audio/ChannelForm.vue'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
@ -59,14 +58,6 @@ const createForm = ref()
|
||||||
</div>
|
</div>
|
||||||
</h2>
|
</h2>
|
||||||
<channels-widget :filters="{scope: `actor:${object?.full_username}`}" />
|
<channels-widget :filters="{scope: `actor:${object?.full_username}`}" />
|
||||||
<h2 class="ui with-actions header">
|
|
||||||
{{ t('views.auth.ProfileOverview.header.libraries') }}
|
|
||||||
</h2>
|
|
||||||
<library-widget :url="`federation/actors/${object?.full_username}/libraries/`">
|
|
||||||
<template #title>
|
|
||||||
{{ t('views.auth.ProfileOverview.header.sharedLibraries') }}
|
|
||||||
</template>
|
|
||||||
</library-widget>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|
Loading…
Reference in New Issue