chore(front): correct types
This commit is contained in:
parent
e1bc6f524e
commit
8ec00c26f8
|
@ -9,7 +9,6 @@ import { color } from '~/composables/color'
|
||||||
|
|
||||||
import { generateTrackCreditStringFromQueue } from '~/utils/utils'
|
import { generateTrackCreditStringFromQueue } from '~/utils/utils'
|
||||||
|
|
||||||
// import ChannelUploadModal from '~/components/channels/UploadModal.vue'
|
|
||||||
import PlaylistModal from '~/components/playlists/PlaylistModal.vue'
|
import PlaylistModal from '~/components/playlists/PlaylistModal.vue'
|
||||||
import FilterModal from '~/components/moderation/FilterModal.vue'
|
import FilterModal from '~/components/moderation/FilterModal.vue'
|
||||||
import ReportModal from '~/components/moderation/ReportModal.vue'
|
import ReportModal from '~/components/moderation/ReportModal.vue'
|
||||||
|
|
|
@ -1,592 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import type { RouteRecordName } from 'vue-router'
|
|
||||||
|
|
||||||
import { computed, ref, watch, watchEffect, onMounted } from 'vue'
|
|
||||||
import { setI18nLanguage, SUPPORTED_LOCALES } from '~/init/locale'
|
|
||||||
// import { useCurrentElement } from '@vueuse/core'
|
|
||||||
// import { setupDropdown } from '~/utils/fomantic'
|
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
import { useStore } from '~/store'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
import Modal from '~/components/ui/Modal.vue'
|
|
||||||
import UserModal from '~/components/common/UserModal.vue'
|
|
||||||
import SearchBar from '~/components/audio/SearchBar.vue'
|
|
||||||
import UserMenu from '~/components/common/UserMenu.vue'
|
|
||||||
import Logo from '~/components/Logo.vue'
|
|
||||||
|
|
||||||
import useThemeList from '~/composables/useThemeList'
|
|
||||||
import useTheme from '~/composables/useTheme'
|
|
||||||
import { isTauri as checkTauri } from '~/composables/tauri'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
width: number
|
|
||||||
}
|
|
||||||
|
|
||||||
defineProps<Props>()
|
|
||||||
|
|
||||||
const store = useStore()
|
|
||||||
const { theme } = useTheme()
|
|
||||||
const themes = useThemeList()
|
|
||||||
const { t, locale: i18nLocale } = useI18n()
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const isCollapsed = ref(true)
|
|
||||||
watch(() => route.path, () => (isCollapsed.value = true))
|
|
||||||
|
|
||||||
const additionalNotifications = computed(() => store.getters['ui/additionalNotifications'])
|
|
||||||
const logoUrl = computed(() => store.state.auth.authenticated ? 'library.index' : 'index')
|
|
||||||
|
|
||||||
const labels = computed(() => ({
|
|
||||||
mainMenu: t('components.Sidebar.label.main'),
|
|
||||||
selectTrack: t('components.Sidebar.label.play'),
|
|
||||||
pendingFollows: t('components.Sidebar.label.follows'),
|
|
||||||
pendingReviewEdits: t('components.Sidebar.label.edits'),
|
|
||||||
pendingReviewReports: t('components.Sidebar.label.reports'),
|
|
||||||
language: t('components.Sidebar.label.language'),
|
|
||||||
theme: t('components.Sidebar.label.theme'),
|
|
||||||
addContent: t('components.Sidebar.label.add'),
|
|
||||||
administration: t('components.Sidebar.label.administration')
|
|
||||||
}))
|
|
||||||
|
|
||||||
type SidebarMenuTabs = 'explore' | 'myLibrary'
|
|
||||||
const expanded = ref<SidebarMenuTabs>('explore')
|
|
||||||
|
|
||||||
const ROUTE_MAPPINGS: Record<SidebarMenuTabs, RouteRecordName[]> = {
|
|
||||||
explore: [
|
|
||||||
'search',
|
|
||||||
'library.index',
|
|
||||||
'library.podcasts.browse',
|
|
||||||
'library.albums.browse',
|
|
||||||
'library.albums.detail',
|
|
||||||
'library.artists.browse',
|
|
||||||
'library.artists.detail',
|
|
||||||
'library.tracks.detail',
|
|
||||||
'library.playlists.browse',
|
|
||||||
'library.playlists.detail',
|
|
||||||
'library.radios.browse',
|
|
||||||
'library.radios.detail'
|
|
||||||
],
|
|
||||||
myLibrary: [
|
|
||||||
'library.me',
|
|
||||||
'library.albums.me',
|
|
||||||
'library.artists.me',
|
|
||||||
'library.playlists.me',
|
|
||||||
'library.radios.me',
|
|
||||||
'favorites'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
if (ROUTE_MAPPINGS.explore.includes(route.name as RouteRecordName)) {
|
|
||||||
expanded.value = 'explore'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ROUTE_MAPPINGS.myLibrary.includes(route.name as RouteRecordName)) {
|
|
||||||
expanded.value = 'myLibrary'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
expanded.value = store.state.auth.authenticated ? 'myLibrary' : 'explore'
|
|
||||||
})
|
|
||||||
|
|
||||||
const moderationNotifications = computed(() =>
|
|
||||||
store.state.ui.notifications.pendingReviewEdits
|
|
||||||
+ store.state.ui.notifications.pendingReviewReports
|
|
||||||
+ store.state.ui.notifications.pendingReviewRequests
|
|
||||||
)
|
|
||||||
|
|
||||||
const isLanguageModalOpen = ref(false)
|
|
||||||
const locale = ref(i18nLocale.value)
|
|
||||||
watch(locale, (locale) => {
|
|
||||||
setI18nLanguage(locale)
|
|
||||||
})
|
|
||||||
|
|
||||||
const isProduction = import.meta.env.PROD
|
|
||||||
const isTauri = checkTauri()
|
|
||||||
|
|
||||||
const isUserModalOpen = ref(false)
|
|
||||||
const isThemeModalOpen = ref(false)
|
|
||||||
|
|
||||||
// const el = useCurrentElement()
|
|
||||||
// watchEffect(() => {
|
|
||||||
// if (store.state.auth.authenticated) {
|
|
||||||
// setupDropdown('.admin-dropdown', el.value)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// setupDropdown('.user-dropdown', el.value)
|
|
||||||
// })
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
document.getElementById('fake-sidebar')?.classList.add('loaded')
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<aside :class="['ui', 'vertical', 'left', 'visible', 'wide', {'collapsed': isCollapsed}, 'sidebar', 'component-sidebar']">
|
|
||||||
<header class="ui basic segment header-wrapper">
|
|
||||||
<router-link
|
|
||||||
:title="'Funkwhale'"
|
|
||||||
:to="{name: logoUrl}"
|
|
||||||
>
|
|
||||||
<i class="logo bordered inverted vibrant big icon">
|
|
||||||
<logo class="logo" />
|
|
||||||
<span class="visually-hidden">{{ t('components.Sidebar.link.home') }}</span>
|
|
||||||
</i>
|
|
||||||
</router-link>
|
|
||||||
<nav class="top ui compact right aligned inverted text menu">
|
|
||||||
<div class="right menu">
|
|
||||||
<div
|
|
||||||
v-if="store.state.auth.availablePermissions['settings'] || store.state.auth.availablePermissions['moderation']"
|
|
||||||
class="item"
|
|
||||||
:title="labels.administration"
|
|
||||||
>
|
|
||||||
<div class="item ui inline admin-dropdown dropdown">
|
|
||||||
<i class="wrench icon" />
|
|
||||||
<div
|
|
||||||
v-if="moderationNotifications > 0"
|
|
||||||
:class="['ui', 'accent', 'mini', 'bottom floating', 'circular', 'label']"
|
|
||||||
>
|
|
||||||
{{ moderationNotifications }}
|
|
||||||
</div>
|
|
||||||
<div class="menu">
|
|
||||||
<h3 class="header">
|
|
||||||
{{ t('components.Sidebar.header.administration') }}
|
|
||||||
</h3>
|
|
||||||
<div class="divider" />
|
|
||||||
<router-link
|
|
||||||
v-if="store.state.auth.availablePermissions['library']"
|
|
||||||
class="item"
|
|
||||||
:to="{name: 'manage.library.edits', query: {q: 'is_approved:null'}}"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="store.state.ui.notifications.pendingReviewEdits > 0"
|
|
||||||
:title="labels.pendingReviewEdits"
|
|
||||||
:class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']"
|
|
||||||
>
|
|
||||||
{{ store.state.ui.notifications.pendingReviewEdits }}
|
|
||||||
</div>
|
|
||||||
{{ t('components.Sidebar.link.library') }}
|
|
||||||
</router-link>
|
|
||||||
<router-link
|
|
||||||
v-if="store.state.auth.availablePermissions['moderation']"
|
|
||||||
class="item"
|
|
||||||
:to="{name: 'manage.moderation.reports.list', query: {q: 'resolved:no'}}"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="store.state.ui.notifications.pendingReviewReports + store.state.ui.notifications.pendingReviewRequests > 0"
|
|
||||||
:title="labels.pendingReviewReports"
|
|
||||||
:class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']"
|
|
||||||
>
|
|
||||||
{{ store.state.ui.notifications.pendingReviewReports + store.state.ui.notifications.pendingReviewRequests }}
|
|
||||||
</div>
|
|
||||||
{{ t('components.Sidebar.link.moderation') }}
|
|
||||||
</router-link>
|
|
||||||
<router-link
|
|
||||||
v-if="store.state.auth.availablePermissions['settings']"
|
|
||||||
class="item"
|
|
||||||
:to="{name: 'manage.users.users.list'}"
|
|
||||||
>
|
|
||||||
{{ t('components.Sidebar.link.users') }}
|
|
||||||
</router-link>
|
|
||||||
<router-link
|
|
||||||
v-if="store.state.auth.availablePermissions['settings']"
|
|
||||||
class="item"
|
|
||||||
:to="{path: '/manage/settings'}"
|
|
||||||
>
|
|
||||||
{{ t('components.Sidebar.link.settings') }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<router-link
|
|
||||||
v-if="store.state.auth.authenticated"
|
|
||||||
class="item"
|
|
||||||
:to="{name: 'content.index'}"
|
|
||||||
>
|
|
||||||
<i class="upload icon" />
|
|
||||||
<span class="visually-hidden">{{ labels.addContent }}</span>
|
|
||||||
</router-link>
|
|
||||||
<template v-if="width > 768">
|
|
||||||
<div class="item">
|
|
||||||
<div class="ui user-dropdown dropdown">
|
|
||||||
<img
|
|
||||||
v-if="store.state.auth.authenticated && store.state.auth.profile?.avatar && store.state.auth.profile?.avatar.urls.small_square_crop"
|
|
||||||
class="ui avatar image"
|
|
||||||
alt=""
|
|
||||||
:src="store.getters['instance/absoluteUrl'](store.state.auth.profile?.avatar.urls.small_square_crop)"
|
|
||||||
>
|
|
||||||
<actor-avatar
|
|
||||||
v-else-if="store.state.auth.authenticated"
|
|
||||||
:actor="{preferred_username: store.state.auth.username, full_username: store.state.auth.username,}"
|
|
||||||
/>
|
|
||||||
<i
|
|
||||||
v-else
|
|
||||||
class="cog icon"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="store.state.ui.notifications.inbox + additionalNotifications > 0"
|
|
||||||
:class="['ui', 'accent', 'mini', 'bottom floating', 'circular', 'label']"
|
|
||||||
>
|
|
||||||
{{ store.state.ui.notifications.inbox + additionalNotifications }}
|
|
||||||
</div>
|
|
||||||
<user-menu
|
|
||||||
v-bind="$attrs"
|
|
||||||
:width="width"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<a
|
|
||||||
href=""
|
|
||||||
class="item"
|
|
||||||
@click.prevent.exact="isUserModalOpen = !isUserModalOpen"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
v-if="store.state.auth.authenticated && store.state.auth.profile?.avatar?.urls.small_square_crop"
|
|
||||||
class="ui avatar image"
|
|
||||||
alt=""
|
|
||||||
:src="store.getters['instance/absoluteUrl'](store.state.auth.profile?.avatar.urls.small_square_crop)"
|
|
||||||
>
|
|
||||||
<actor-avatar
|
|
||||||
v-else-if="store.state.auth.authenticated"
|
|
||||||
:actor="{preferred_username: store.state.auth.username, full_username: store.state.auth.username,}"
|
|
||||||
/>
|
|
||||||
<i
|
|
||||||
v-else
|
|
||||||
class="cog icon"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="store.state.ui.notifications.inbox + additionalNotifications > 0"
|
|
||||||
:class="['ui', 'accent', 'mini', 'bottom floating', 'circular', 'label']"
|
|
||||||
>
|
|
||||||
{{ store.state.ui.notifications.inbox + additionalNotifications }}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
<user-modal
|
|
||||||
v-model:show="isUserModalOpen"
|
|
||||||
@show-theme-modal-event="isThemeModalOpen=true"
|
|
||||||
@show-language-modal-event="isLanguageModalOpen=true"
|
|
||||||
/>
|
|
||||||
<Modal
|
|
||||||
ref="languageModal"
|
|
||||||
v-model="isLanguageModalOpen"
|
|
||||||
:title="labels.language"
|
|
||||||
:fullscreen="false"
|
|
||||||
>
|
|
||||||
<!-- TODO: Is this actually a popover menu, not a modal? -->
|
|
||||||
<i
|
|
||||||
role="button"
|
|
||||||
class="left chevron back inside icon"
|
|
||||||
@click.prevent.exact="isUserModalOpen = !isUserModalOpen"
|
|
||||||
/>
|
|
||||||
<div class="header">
|
|
||||||
<h3 class="title">
|
|
||||||
{{ labels.language }}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<fieldset
|
|
||||||
v-for="(language, key) in SUPPORTED_LOCALES"
|
|
||||||
:key="key"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
:id="`${key}`"
|
|
||||||
v-model="locale"
|
|
||||||
type="radio"
|
|
||||||
name="language"
|
|
||||||
:value="key"
|
|
||||||
>
|
|
||||||
<label :for="`${key}`">{{ language }}</label>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
<Modal
|
|
||||||
ref="themeModal"
|
|
||||||
v-model:show="isThemeModalOpen"
|
|
||||||
:title="labels.theme"
|
|
||||||
:fullscreen="false"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
role="button"
|
|
||||||
class="left chevron back inside icon"
|
|
||||||
@click.prevent.exact="isUserModalOpen = !isUserModalOpen"
|
|
||||||
/>
|
|
||||||
<div class="header">
|
|
||||||
<h3 class="title">
|
|
||||||
{{ labels.theme }}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<fieldset
|
|
||||||
v-for="th in themes"
|
|
||||||
:key="th.key"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
:id="th.key"
|
|
||||||
v-model="theme"
|
|
||||||
type="radio"
|
|
||||||
name="theme"
|
|
||||||
:value="th.key"
|
|
||||||
>
|
|
||||||
<label :for="th.key">{{ th.name }}</label>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
<div class="item collapse-button-wrapper">
|
|
||||||
<button
|
|
||||||
:class="['ui', 'basic', 'big', {'vibrant': !isCollapsed}, 'inverted icon', 'collapse', 'button']"
|
|
||||||
@click="isCollapsed = !isCollapsed"
|
|
||||||
>
|
|
||||||
<i class="sidebar icon" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<div class="ui basic search-wrapper segment">
|
|
||||||
<search-bar @search="isCollapsed = false" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="!store.state.auth.authenticated"
|
|
||||||
class="ui basic signup segment"
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
class="ui fluid tiny primary button"
|
|
||||||
:to="{name: 'login'}"
|
|
||||||
>
|
|
||||||
{{ t('components.Sidebar.link.login') }}
|
|
||||||
</router-link>
|
|
||||||
<div class="ui small hidden divider" />
|
|
||||||
<router-link
|
|
||||||
class="ui fluid tiny button"
|
|
||||||
:to="{path: '/signup'}"
|
|
||||||
>
|
|
||||||
{{ t('components.Sidebar.link.createAccount') }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
<nav
|
|
||||||
class="secondary"
|
|
||||||
role="navigation"
|
|
||||||
aria-labelledby="navigation-label"
|
|
||||||
>
|
|
||||||
<h1
|
|
||||||
id="navigation-label"
|
|
||||||
class="visually-hidden"
|
|
||||||
>
|
|
||||||
{{ t('components.Sidebar.header.main') }}
|
|
||||||
</h1>
|
|
||||||
<div class="ui small hidden divider" />
|
|
||||||
<section
|
|
||||||
:aria-label="labels.mainMenu"
|
|
||||||
class="ui bottom attached active tab"
|
|
||||||
>
|
|
||||||
<nav
|
|
||||||
class="ui vertical large fluid inverted menu"
|
|
||||||
role="navigation"
|
|
||||||
:aria-label="labels.mainMenu"
|
|
||||||
>
|
|
||||||
<div :class="[{ collapsed: expanded !== 'explore' }, 'collapsible item']">
|
|
||||||
<h2
|
|
||||||
class="header"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
@click="expanded = 'explore'"
|
|
||||||
@focus="expanded = 'explore'"
|
|
||||||
>
|
|
||||||
{{ t('components.Sidebar.header.explore') }}
|
|
||||||
<i
|
|
||||||
v-if="expanded !== 'explore'"
|
|
||||||
class="angle right icon"
|
|
||||||
/>
|
|
||||||
</h2>
|
|
||||||
<div class="menu">
|
|
||||||
<router-link
|
|
||||||
class="item"
|
|
||||||
:to="{name: 'search'}"
|
|
||||||
>
|
|
||||||
<i class="search icon" />
|
|
||||||
{{ t('components.Sidebar.link.search') }}
|
|
||||||
</router-link>
|
|
||||||
<router-link
|
|
||||||
class="item"
|
|
||||||
:to="{name: 'library.index'}"
|
|
||||||
active-class="_active"
|
|
||||||
>
|
|
||||||
<i class="music icon" />
|
|
||||||
{{ t('components.Sidebar.link.browse') }}
|
|
||||||
</router-link>
|
|
||||||
<router-link
|
|
||||||
class="item"
|
|
||||||
:to="{name: 'library.podcasts.browse'}"
|
|
||||||
>
|
|
||||||
<i class="podcast icon" />
|
|
||||||
{{ t('components.Sidebar.link.podcasts') }}
|
|
||||||
</router-link>
|
|
||||||
<router-link
|
|
||||||
class="item"
|
|
||||||
:to="{name: 'library.albums.browse'}"
|
|
||||||
>
|
|
||||||
<i class="compact disc icon" />
|
|
||||||
{{ t('components.Sidebar.link.albums') }}
|
|
||||||
</router-link>
|
|
||||||
<router-link
|
|
||||||
class="item"
|
|
||||||
:to="{name: 'library.artists.browse'}"
|
|
||||||
>
|
|
||||||
<i class="user icon" />
|
|
||||||
{{ t('components.Sidebar.link.artists') }}
|
|
||||||
</router-link>
|
|
||||||
<router-link
|
|
||||||
class="item"
|
|
||||||
:to="{name: 'library.playlists.browse'}"
|
|
||||||
>
|
|
||||||
<i class="list icon" />
|
|
||||||
{{ t('components.Sidebar.link.playlists') }}
|
|
||||||
</router-link>
|
|
||||||
<router-link
|
|
||||||
class="item"
|
|
||||||
:to="{name: 'library.radios.browse'}"
|
|
||||||
>
|
|
||||||
<i class="feed icon" />
|
|
||||||
{{ t('components.Sidebar.link.radios') }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="store.state.auth.authenticated"
|
|
||||||
:class="[{ collapsed: expanded !== 'myLibrary' }, 'collapsible item']"
|
|
||||||
>
|
|
||||||
<h3
|
|
||||||
class="header"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
@click="expanded = 'myLibrary'"
|
|
||||||
@focus="expanded = 'myLibrary'"
|
|
||||||
>
|
|
||||||
{{ t('components.Sidebar.header.library') }}
|
|
||||||
<i
|
|
||||||
v-if="expanded !== 'myLibrary'"
|
|
||||||
class="angle right icon"
|
|
||||||
/>
|
|
||||||
</h3>
|
|
||||||
<div class="menu">
|
|
||||||
<router-link
|
|
||||||
class="item"
|
|
||||||
:to="{name: 'library.me'}"
|
|
||||||
>
|
|
||||||
<i class="music icon" />
|
|
||||||
{{ t('components.Sidebar.link.browse') }}
|
|
||||||
</router-link>
|
|
||||||
<router-link
|
|
||||||
class="item"
|
|
||||||
:to="{name: 'library.albums.me'}"
|
|
||||||
>
|
|
||||||
<i class="compact disc icon" />
|
|
||||||
{{ t('components.Sidebar.link.albums') }}
|
|
||||||
</router-link>
|
|
||||||
<router-link
|
|
||||||
class="item"
|
|
||||||
:to="{name: 'library.artists.me'}"
|
|
||||||
>
|
|
||||||
<i class="user icon" />
|
|
||||||
{{ t('components.Sidebar.link.artists') }}
|
|
||||||
</router-link>
|
|
||||||
<router-link
|
|
||||||
class="item"
|
|
||||||
:to="{name: 'library.playlists.me'}"
|
|
||||||
>
|
|
||||||
<i class="list icon" />
|
|
||||||
{{ t('components.Sidebar.link.playlists') }}
|
|
||||||
</router-link>
|
|
||||||
<router-link
|
|
||||||
class="item"
|
|
||||||
:to="{name: 'library.radios.me'}"
|
|
||||||
>
|
|
||||||
<i class="feed icon" />
|
|
||||||
{{ t('components.Sidebar.link.radios') }}
|
|
||||||
</router-link>
|
|
||||||
<router-link
|
|
||||||
class="item"
|
|
||||||
:to="{name: 'favorites'}"
|
|
||||||
>
|
|
||||||
<i class="heart icon" />
|
|
||||||
{{ t('components.Sidebar.link.favorites') }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<router-link
|
|
||||||
v-if="store.state.auth.authenticated"
|
|
||||||
class="header item"
|
|
||||||
:to="{name: 'subscriptions'}"
|
|
||||||
>
|
|
||||||
{{ t('components.Sidebar.link.channels') }}
|
|
||||||
</router-link>
|
|
||||||
<div class="item">
|
|
||||||
<h3 class="header">
|
|
||||||
{{ t('components.Sidebar.header.more') }}
|
|
||||||
</h3>
|
|
||||||
<div class="menu">
|
|
||||||
<router-link
|
|
||||||
class="item"
|
|
||||||
to="/about"
|
|
||||||
active-class="router-link-exact-active active"
|
|
||||||
>
|
|
||||||
<i class="info icon" />
|
|
||||||
{{ t('components.Sidebar.link.about') }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="!isProduction || isTauri"
|
|
||||||
class="item"
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
to="/instance-chooser"
|
|
||||||
class="link item"
|
|
||||||
>
|
|
||||||
{{ t('components.Sidebar.link.switchInstance') }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</section>
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
[type="radio"] {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
height: 0;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
[type="radio"] + label::after {
|
|
||||||
content: "";
|
|
||||||
font-size: 1.4em;
|
|
||||||
}
|
|
||||||
[type="radio"]:checked + label::after {
|
|
||||||
margin-left: 10px;
|
|
||||||
content: "\2713"; /* Checkmark */
|
|
||||||
font-size: 1.4em;
|
|
||||||
}
|
|
||||||
[type="radio"]:checked + label {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
fieldset {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
.back {
|
|
||||||
font-size: 1.25em !important;
|
|
||||||
position: absolute;
|
|
||||||
top: 0.5rem;
|
|
||||||
left: 0.5rem;
|
|
||||||
width: 2.25rem !important;
|
|
||||||
height: 2.25rem !important;
|
|
||||||
padding: 0.625rem 0 0 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -46,11 +46,6 @@ const newValues = reactive({
|
||||||
metadata: { ...(props.object?.metadata ?? {}) } as Channel['metadata']
|
metadata: { ...(props.object?.metadata ?? {}) } as Channel['metadata']
|
||||||
})
|
})
|
||||||
|
|
||||||
const tagList = computed(() => ({
|
|
||||||
currents: newValues.tags.map(tag => ({ type: 'custom' as const, label: tag })),
|
|
||||||
others: [].map(tag => ({ type: 'custom' as const, label: tag }))
|
|
||||||
}))
|
|
||||||
|
|
||||||
// If props has an object, then this form edits, else it creates
|
// If props has an object, then this form edits, else it creates
|
||||||
// TODO: rename to `process : 'creating' | 'editing'`
|
// TODO: rename to `process : 'creating' | 'editing'`
|
||||||
const creating = computed(() => props.object === null)
|
const creating = computed(() => props.object === null)
|
||||||
|
@ -257,7 +252,11 @@ defineExpose({
|
||||||
</attachment-input>
|
</attachment-input>
|
||||||
</div>
|
</div>
|
||||||
<Pills
|
<Pills
|
||||||
v-model="tagList"
|
:get="model => { newValues.tags = model.currents.map(({ label }) => label) }"
|
||||||
|
:set="model => ({
|
||||||
|
currents: newValues.tags.map(tag => ({ type: 'custom' as const, label: tag })),
|
||||||
|
others: [].map(tag => ({ type: 'custom' as const, label: tag }))
|
||||||
|
})"
|
||||||
:label="t('components.audio.ChannelForm.label.tags')"
|
:label="t('components.audio.ChannelForm.label.tags')"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -3,9 +3,8 @@ import type { BackendError, Application, PrivacyLevel } from '~/types'
|
||||||
import type { $ElementType } from 'utility-types'
|
import type { $ElementType } from 'utility-types'
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import $ from 'jquery'
|
|
||||||
|
|
||||||
import { computed, reactive, ref, onMounted } from 'vue'
|
import { computed, reactive, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useStore } from '~/store'
|
import { useStore } from '~/store'
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Actor } from '~/types'
|
import type { components } from '~/generated/types'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
@ -15,7 +15,7 @@ interface Events {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
actor: Actor
|
actor: components['schemas']['FullActor']
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits<Events>()
|
const emit = defineEmits<Events>()
|
||||||
|
|
|
@ -64,6 +64,7 @@ onMounted(() => {
|
||||||
for="dropdown"
|
for="dropdown"
|
||||||
>
|
>
|
||||||
<!-- Label -->
|
<!-- Label -->
|
||||||
|
|
||||||
<span
|
<span
|
||||||
v-if="$slots['label']"
|
v-if="$slots['label']"
|
||||||
:class="$style.label"
|
:class="$style.label"
|
||||||
|
|
|
@ -26,8 +26,9 @@ const currentIndex = computed(() =>
|
||||||
tabs.findIndex(({ title }) => title === actualCurrentTitle.value)
|
tabs.findIndex(({ title }) => title === actualCurrentTitle.value)
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(() => tabs.length, (_, from) => {
|
// select first tab
|
||||||
if (from === 0) {
|
watch(tabs, () => {
|
||||||
|
if (tabs.length === 1) {
|
||||||
currentTitle.value = tabs[0].title
|
currentTitle.value = tabs[0].title
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -43,7 +44,7 @@ watch(() => tabs.length, (_, from) => {
|
||||||
ghost
|
ghost
|
||||||
:class="{ 'is-active': actualCurrentTitle === tab.title }"
|
:class="{ 'is-active': actualCurrentTitle === tab.title }"
|
||||||
v-bind="tab"
|
v-bind="tab"
|
||||||
:on-click="() => { currentTitle = tab.title }"
|
:on-click="'to' in tab ? undefined : () => { currentTitle = tab.title }"
|
||||||
class="tabs-item"
|
class="tabs-item"
|
||||||
@keydown.left="currentTitle = tabs[(currentIndex - 1 + tabs.length) % tabs.length].title"
|
@keydown.left="currentTitle = tabs[(currentIndex - 1 + tabs.length) % tabs.length].title"
|
||||||
@keydown.right="currentTitle = tabs[(currentIndex + 1) % tabs.length].title"
|
@keydown.right="currentTitle = tabs[(currentIndex + 1) % tabs.length].title"
|
||||||
|
|
|
@ -10,7 +10,7 @@ const play = defineEmits(['play'])
|
||||||
class="play-button"
|
class="play-button"
|
||||||
shadow
|
shadow
|
||||||
round
|
round
|
||||||
@click="play()"
|
@click="$emit('play')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -8437,6 +8437,7 @@ export interface components {
|
||||||
channel?: string;
|
channel?: string;
|
||||||
/** @default pending */
|
/** @default pending */
|
||||||
import_status: components["schemas"]["ImportStatusEnum"];
|
import_status: components["schemas"]["ImportStatusEnum"];
|
||||||
|
privacy_level?: components["schemas"]["LibraryPrivacyLevelEnum"];
|
||||||
import_metadata?: unknown;
|
import_metadata?: unknown;
|
||||||
import_reference?: string;
|
import_reference?: string;
|
||||||
source?: string | null;
|
source?: string | null;
|
||||||
|
@ -8827,6 +8828,11 @@ export interface components {
|
||||||
extension: string;
|
extension: string;
|
||||||
readonly is_local: boolean;
|
readonly is_local: boolean;
|
||||||
};
|
};
|
||||||
|
UploadBulkUpdateRequest: {
|
||||||
|
/** Format: uuid */
|
||||||
|
uuid: string;
|
||||||
|
privacy_level: components["schemas"]["LibraryPrivacyLevelEnum"];
|
||||||
|
};
|
||||||
UploadForOwner: {
|
UploadForOwner: {
|
||||||
/** Format: uuid */
|
/** Format: uuid */
|
||||||
readonly uuid: string;
|
readonly uuid: string;
|
||||||
|
@ -8844,6 +8850,7 @@ export interface components {
|
||||||
readonly import_date: string | null;
|
readonly import_date: string | null;
|
||||||
/** @default pending */
|
/** @default pending */
|
||||||
import_status: components["schemas"]["ImportStatusEnum"];
|
import_status: components["schemas"]["ImportStatusEnum"];
|
||||||
|
privacy_level?: components["schemas"]["LibraryPrivacyLevelEnum"];
|
||||||
readonly import_details: unknown;
|
readonly import_details: unknown;
|
||||||
import_metadata?: unknown;
|
import_metadata?: unknown;
|
||||||
import_reference?: string;
|
import_reference?: string;
|
||||||
|
@ -8857,6 +8864,7 @@ export interface components {
|
||||||
channel?: string;
|
channel?: string;
|
||||||
/** @default pending */
|
/** @default pending */
|
||||||
import_status: components["schemas"]["ImportStatusEnum"];
|
import_status: components["schemas"]["ImportStatusEnum"];
|
||||||
|
privacy_level: components["schemas"]["LibraryPrivacyLevelEnum"];
|
||||||
import_metadata?: unknown;
|
import_metadata?: unknown;
|
||||||
import_reference?: string;
|
import_reference?: string;
|
||||||
source?: string | null;
|
source?: string | null;
|
||||||
|
@ -15694,12 +15702,12 @@ export interface operations {
|
||||||
path?: never;
|
path?: never;
|
||||||
cookie?: never;
|
cookie?: never;
|
||||||
};
|
};
|
||||||
requestBody?: {
|
requestBody: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["PatchedUploadForOwnerRequest"];
|
"application/json": components["schemas"]["UploadBulkUpdateRequest"][];
|
||||||
"application/x-www-form-urlencoded": components["schemas"]["PatchedUploadForOwnerRequest"];
|
"application/x-www-form-urlencoded": components["schemas"]["UploadBulkUpdateRequest"][];
|
||||||
"multipart/form-data": components["schemas"]["PatchedUploadForOwnerRequest"];
|
"multipart/form-data": components["schemas"]["UploadBulkUpdateRequest"][];
|
||||||
"application/activity+json": components["schemas"]["PatchedUploadForOwnerRequest"];
|
"application/activity+json": components["schemas"]["UploadBulkUpdateRequest"][];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
|
@ -22863,12 +22871,12 @@ export interface operations {
|
||||||
path?: never;
|
path?: never;
|
||||||
cookie?: never;
|
cookie?: never;
|
||||||
};
|
};
|
||||||
requestBody?: {
|
requestBody: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["PatchedUploadForOwnerRequest"];
|
"application/json": components["schemas"]["UploadBulkUpdateRequest"][];
|
||||||
"application/x-www-form-urlencoded": components["schemas"]["PatchedUploadForOwnerRequest"];
|
"application/x-www-form-urlencoded": components["schemas"]["UploadBulkUpdateRequest"][];
|
||||||
"multipart/form-data": components["schemas"]["PatchedUploadForOwnerRequest"];
|
"multipart/form-data": components["schemas"]["UploadBulkUpdateRequest"][];
|
||||||
"application/activity+json": components["schemas"]["PatchedUploadForOwnerRequest"];
|
"application/activity+json": components["schemas"]["UploadBulkUpdateRequest"][];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Actor } from '~/types'
|
import type { components } from '~/generated/types'
|
||||||
|
|
||||||
import { onBeforeRouteUpdate } from 'vue-router'
|
import { onBeforeRouteUpdate } from 'vue-router'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { useClipboard } from '@vueuse/core'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useStore } from '~/store'
|
import { useStore } from '~/store'
|
||||||
import { hashCode, intToRGB } from '~/utils/color'
|
import { hashCode, intToRGB } from '~/utils/color'
|
||||||
|
@ -15,11 +16,13 @@ import useReport from '~/composables/moderation/useReport'
|
||||||
import RenderedDescription from '~/components/common/RenderedDescription.vue'
|
import RenderedDescription from '~/components/common/RenderedDescription.vue'
|
||||||
|
|
||||||
import Layout from '~/components/ui/Layout.vue'
|
import Layout from '~/components/ui/Layout.vue'
|
||||||
import Section from '~/components/ui/Section.vue'
|
import Header from '~/components/ui/Header.vue'
|
||||||
|
import Button from '~/components/ui/Button.vue'
|
||||||
import Nav from '~/components/ui/Nav.vue'
|
import Nav from '~/components/ui/Nav.vue'
|
||||||
|
import Alert from '~/components/ui/Alert.vue'
|
||||||
|
|
||||||
interface Events {
|
interface Events {
|
||||||
(e: 'updated', value: Actor): void
|
(e: 'updated', value: components['schemas']['FullActor']): void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -35,8 +38,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
const { report, getReportableObjects } = useReport()
|
const { report, getReportableObjects } = useReport()
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
// We are working either with an Actor or null
|
const object = ref<components['schemas']['FullActor'] | null>(null)
|
||||||
const object = ref<Actor | null>(null)
|
|
||||||
|
|
||||||
const actorColor = computed(() => intToRGB(hashCode(object.value?.full_username)))
|
const actorColor = computed(() => intToRGB(hashCode(object.value?.full_username)))
|
||||||
const defaultAvatarStyle = computed(() => ({ backgroundColor: `#${actorColor.value}` }))
|
const defaultAvatarStyle = computed(() => ({ backgroundColor: `#${actorColor.value}` }))
|
||||||
|
@ -79,6 +81,8 @@ const fetchData = async () => {
|
||||||
watch(props, fetchData, { immediate: true })
|
watch(props, fetchData, { immediate: true })
|
||||||
const recentActivity = ref(0)
|
const recentActivity = ref(0)
|
||||||
|
|
||||||
|
const { copy, copied, isSupported } = useClipboard()
|
||||||
|
|
||||||
const tabs = ref([{
|
const tabs = ref([{
|
||||||
title: t('views.auth.ProfileBase.link.overview') ,
|
title: t('views.auth.ProfileBase.link.overview') ,
|
||||||
to: { name: 'profile.overview', params: routerParams }
|
to: { name: 'profile.overview', params: routerParams }
|
||||||
|
@ -86,7 +90,7 @@ const tabs = ref([{
|
||||||
title: t('views.auth.ProfileBase.link.activity') ,
|
title: t('views.auth.ProfileBase.link.activity') ,
|
||||||
to: { name: 'profile.activity', params: routerParams }
|
to: { name: 'profile.activity', params: routerParams }
|
||||||
}, ...(
|
}, ...(
|
||||||
store.state.auth.authenticated && object.value && object.value.full_username === store.state.auth.fullUsername
|
store.state.auth.authenticated && object.value?.full_username === store.state.auth.fullUsername
|
||||||
? [{
|
? [{
|
||||||
title: t('views.auth.ProfileBase.link.manageUploads') ,
|
title: t('views.auth.ProfileBase.link.manageUploads') ,
|
||||||
to: { name: 'profile.manageUploads', params: routerParams }
|
to: { name: 'profile.manageUploads', params: routerParams }
|
||||||
|
@ -101,54 +105,84 @@ const tabs = ref([{
|
||||||
stack
|
stack
|
||||||
main
|
main
|
||||||
>
|
>
|
||||||
<Layout flex>
|
<!-- TODO: Translate Edit Link -->
|
||||||
<!-- Profile Picture -->
|
<Header
|
||||||
<img
|
:h1="props.username"
|
||||||
v-if="object?.icon"
|
:action="{
|
||||||
v-lazy="store.getters['instance/absoluteUrl'](object.icon.urls.large_square_crop)"
|
text:'Edit profile',
|
||||||
alt=""
|
to:'/settings',
|
||||||
class="avatar"
|
primary: true,
|
||||||
>
|
solid: true,
|
||||||
<span
|
icon: 'bi-pencil-fill'
|
||||||
v-else
|
}"
|
||||||
:style="defaultAvatarStyle"
|
style="margin-top: 58px;"
|
||||||
class="ui avatar circular label"
|
page-heading
|
||||||
>
|
>
|
||||||
{{ store.state.auth.profile?.full_username?.[0] || "" }}
|
<template #image>
|
||||||
</span>
|
<img
|
||||||
<!-- TODO: Translate Edit Link -->
|
v-if="object?.user.avatar"
|
||||||
<Section
|
v-lazy="store.getters['instance/absoluteUrl'](object.user.avatar.urls.large_square_crop)"
|
||||||
:h1="props.username"
|
alt=""
|
||||||
:action="{
|
class="avatar"
|
||||||
text:'Edit profile',
|
>
|
||||||
to:'/settings',
|
<span
|
||||||
primary: true,
|
v-else
|
||||||
solid: true,
|
:style="defaultAvatarStyle"
|
||||||
icon: 'bi-pencil-fill'
|
class="ui avatar circular label"
|
||||||
}"
|
>
|
||||||
style="margin-top: 58px;"
|
{{ store.state.auth.profile?.full_username?.[0] || "" }}
|
||||||
>
|
</span>
|
||||||
|
</template>
|
||||||
|
<Layout flex>
|
||||||
<span style="grid-column: 1 / -1">
|
<span style="grid-column: 1 / -1">
|
||||||
{{ object?.full_username }}
|
{{ object?.full_username }}
|
||||||
<i
|
<Button
|
||||||
class="bi bi-copy"
|
icon="bi-copy"
|
||||||
style="margin-left: 8px;"
|
style="margin-left: 8px;"
|
||||||
|
:aria-label="t('views.auth.ProfileBase.copyUsername')"
|
||||||
|
:title="t('components.common.CopyInput.button.copy')"
|
||||||
|
ghost
|
||||||
|
secondary
|
||||||
|
@click="copy(fullUsername)"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
<Alert
|
||||||
|
v-if="isSupported && copied"
|
||||||
|
green
|
||||||
|
>
|
||||||
|
<Layout flex>
|
||||||
|
<i class="bi bi-check" />
|
||||||
|
{{ t('components.common.CopyInput.message.success') }}
|
||||||
|
</Layout>
|
||||||
|
</Alert>
|
||||||
|
<Alert
|
||||||
|
v-else-if="!isSupported && copied"
|
||||||
|
red
|
||||||
|
>
|
||||||
|
<Layout flex>
|
||||||
|
<i class="bi bi-x" />
|
||||||
|
{{ t('components.common.CopyInput.message.fail') }}
|
||||||
|
</Layout>
|
||||||
|
</Alert>
|
||||||
|
</Layout>
|
||||||
|
<Layout
|
||||||
|
flex
|
||||||
|
no-gap
|
||||||
|
>
|
||||||
<RenderedDescription
|
<RenderedDescription
|
||||||
style="grid-column: 1 / -1"
|
:content="{ html: object?.summary.html || '' }"
|
||||||
:content="object?.summary? {text: object.summary} : null"
|
|
||||||
:field-name="'summary'"
|
:field-name="'summary'"
|
||||||
:update-url="`users/${store.state.auth.username}/`"
|
:update-url="`users/${store.state.auth.username}/`"
|
||||||
:can-update="store.state.auth.authenticated && object?.full_username === store.state.auth.fullUsername"
|
:can-update="store.state.auth.authenticated && object?.full_username === store.state.auth.fullUsername"
|
||||||
|
:truncate-length="100"
|
||||||
@updated="emit('updated', $event)"
|
@updated="emit('updated', $event)"
|
||||||
/>
|
/>
|
||||||
<UserFollowButton
|
</Layout>
|
||||||
v-if="store.state.auth.authenticated && object && object.full_username !== store.state.auth.fullUsername"
|
<UserFollowButton
|
||||||
:actor="object"
|
v-if="store.state.auth.authenticated && object && object.full_username !== store.state.auth.fullUsername"
|
||||||
/>
|
:actor="object"
|
||||||
</Section>
|
/>
|
||||||
</Layout>
|
</Header>
|
||||||
<Nav v-model="tabs" />
|
<Nav v-model="tabs" />
|
||||||
|
|
||||||
<router-view
|
<router-view
|
||||||
|
|
Loading…
Reference in New Issue