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 ChannelUploadModal from '~/components/channels/UploadModal.vue'
|
||||
import PlaylistModal from '~/components/playlists/PlaylistModal.vue'
|
||||
import FilterModal from '~/components/moderation/FilterModal.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']
|
||||
})
|
||||
|
||||
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
|
||||
// TODO: rename to `process : 'creating' | 'editing'`
|
||||
const creating = computed(() => props.object === null)
|
||||
|
@ -257,7 +252,11 @@ defineExpose({
|
|||
</attachment-input>
|
||||
</div>
|
||||
<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')"
|
||||
/>
|
||||
<div
|
||||
|
|
|
@ -3,9 +3,8 @@ import type { BackendError, Application, PrivacyLevel } from '~/types'
|
|||
import type { $ElementType } from 'utility-types'
|
||||
|
||||
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 { useRouter } from 'vue-router'
|
||||
import { useStore } from '~/store'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import type { Actor } from '~/types'
|
||||
import type { components } from '~/generated/types'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
@ -15,7 +15,7 @@ interface Events {
|
|||
}
|
||||
|
||||
interface Props {
|
||||
actor: Actor
|
||||
actor: components['schemas']['FullActor']
|
||||
}
|
||||
|
||||
const emit = defineEmits<Events>()
|
||||
|
|
|
@ -64,6 +64,7 @@ onMounted(() => {
|
|||
for="dropdown"
|
||||
>
|
||||
<!-- Label -->
|
||||
|
||||
<span
|
||||
v-if="$slots['label']"
|
||||
:class="$style.label"
|
||||
|
|
|
@ -26,8 +26,9 @@ const currentIndex = computed(() =>
|
|||
tabs.findIndex(({ title }) => title === actualCurrentTitle.value)
|
||||
)
|
||||
|
||||
watch(() => tabs.length, (_, from) => {
|
||||
if (from === 0) {
|
||||
// select first tab
|
||||
watch(tabs, () => {
|
||||
if (tabs.length === 1) {
|
||||
currentTitle.value = tabs[0].title
|
||||
}
|
||||
})
|
||||
|
@ -43,7 +44,7 @@ watch(() => tabs.length, (_, from) => {
|
|||
ghost
|
||||
:class="{ 'is-active': actualCurrentTitle === tab.title }"
|
||||
v-bind="tab"
|
||||
:on-click="() => { currentTitle = tab.title }"
|
||||
:on-click="'to' in tab ? undefined : () => { currentTitle = tab.title }"
|
||||
class="tabs-item"
|
||||
@keydown.left="currentTitle = tabs[(currentIndex - 1 + tabs.length) % tabs.length].title"
|
||||
@keydown.right="currentTitle = tabs[(currentIndex + 1) % tabs.length].title"
|
||||
|
|
|
@ -10,7 +10,7 @@ const play = defineEmits(['play'])
|
|||
class="play-button"
|
||||
shadow
|
||||
round
|
||||
@click="play()"
|
||||
@click="$emit('play')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -8437,6 +8437,7 @@ export interface components {
|
|||
channel?: string;
|
||||
/** @default pending */
|
||||
import_status: components["schemas"]["ImportStatusEnum"];
|
||||
privacy_level?: components["schemas"]["LibraryPrivacyLevelEnum"];
|
||||
import_metadata?: unknown;
|
||||
import_reference?: string;
|
||||
source?: string | null;
|
||||
|
@ -8827,6 +8828,11 @@ export interface components {
|
|||
extension: string;
|
||||
readonly is_local: boolean;
|
||||
};
|
||||
UploadBulkUpdateRequest: {
|
||||
/** Format: uuid */
|
||||
uuid: string;
|
||||
privacy_level: components["schemas"]["LibraryPrivacyLevelEnum"];
|
||||
};
|
||||
UploadForOwner: {
|
||||
/** Format: uuid */
|
||||
readonly uuid: string;
|
||||
|
@ -8844,6 +8850,7 @@ export interface components {
|
|||
readonly import_date: string | null;
|
||||
/** @default pending */
|
||||
import_status: components["schemas"]["ImportStatusEnum"];
|
||||
privacy_level?: components["schemas"]["LibraryPrivacyLevelEnum"];
|
||||
readonly import_details: unknown;
|
||||
import_metadata?: unknown;
|
||||
import_reference?: string;
|
||||
|
@ -8857,6 +8864,7 @@ export interface components {
|
|||
channel?: string;
|
||||
/** @default pending */
|
||||
import_status: components["schemas"]["ImportStatusEnum"];
|
||||
privacy_level: components["schemas"]["LibraryPrivacyLevelEnum"];
|
||||
import_metadata?: unknown;
|
||||
import_reference?: string;
|
||||
source?: string | null;
|
||||
|
@ -15694,12 +15702,12 @@ export interface operations {
|
|||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: {
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["PatchedUploadForOwnerRequest"];
|
||||
"application/x-www-form-urlencoded": components["schemas"]["PatchedUploadForOwnerRequest"];
|
||||
"multipart/form-data": components["schemas"]["PatchedUploadForOwnerRequest"];
|
||||
"application/activity+json": components["schemas"]["PatchedUploadForOwnerRequest"];
|
||||
"application/json": components["schemas"]["UploadBulkUpdateRequest"][];
|
||||
"application/x-www-form-urlencoded": components["schemas"]["UploadBulkUpdateRequest"][];
|
||||
"multipart/form-data": components["schemas"]["UploadBulkUpdateRequest"][];
|
||||
"application/activity+json": components["schemas"]["UploadBulkUpdateRequest"][];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
|
@ -22863,12 +22871,12 @@ export interface operations {
|
|||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: {
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["PatchedUploadForOwnerRequest"];
|
||||
"application/x-www-form-urlencoded": components["schemas"]["PatchedUploadForOwnerRequest"];
|
||||
"multipart/form-data": components["schemas"]["PatchedUploadForOwnerRequest"];
|
||||
"application/activity+json": components["schemas"]["PatchedUploadForOwnerRequest"];
|
||||
"application/json": components["schemas"]["UploadBulkUpdateRequest"][];
|
||||
"application/x-www-form-urlencoded": components["schemas"]["UploadBulkUpdateRequest"][];
|
||||
"multipart/form-data": components["schemas"]["UploadBulkUpdateRequest"][];
|
||||
"application/activity+json": components["schemas"]["UploadBulkUpdateRequest"][];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import type { Actor } from '~/types'
|
||||
import type { components } from '~/generated/types'
|
||||
|
||||
import { onBeforeRouteUpdate } from 'vue-router'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useStore } from '~/store'
|
||||
import { hashCode, intToRGB } from '~/utils/color'
|
||||
|
@ -15,11 +16,13 @@ import useReport from '~/composables/moderation/useReport'
|
|||
import RenderedDescription from '~/components/common/RenderedDescription.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 Alert from '~/components/ui/Alert.vue'
|
||||
|
||||
interface Events {
|
||||
(e: 'updated', value: Actor): void
|
||||
(e: 'updated', value: components['schemas']['FullActor']): void
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
@ -35,8 +38,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
const { report, getReportableObjects } = useReport()
|
||||
const store = useStore()
|
||||
|
||||
// We are working either with an Actor or null
|
||||
const object = ref<Actor | null>(null)
|
||||
const object = ref<components['schemas']['FullActor'] | null>(null)
|
||||
|
||||
const actorColor = computed(() => intToRGB(hashCode(object.value?.full_username)))
|
||||
const defaultAvatarStyle = computed(() => ({ backgroundColor: `#${actorColor.value}` }))
|
||||
|
@ -79,6 +81,8 @@ const fetchData = async () => {
|
|||
watch(props, fetchData, { immediate: true })
|
||||
const recentActivity = ref(0)
|
||||
|
||||
const { copy, copied, isSupported } = useClipboard()
|
||||
|
||||
const tabs = ref([{
|
||||
title: t('views.auth.ProfileBase.link.overview') ,
|
||||
to: { name: 'profile.overview', params: routerParams }
|
||||
|
@ -86,7 +90,7 @@ const tabs = ref([{
|
|||
title: t('views.auth.ProfileBase.link.activity') ,
|
||||
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') ,
|
||||
to: { name: 'profile.manageUploads', params: routerParams }
|
||||
|
@ -101,11 +105,23 @@ const tabs = ref([{
|
|||
stack
|
||||
main
|
||||
>
|
||||
<Layout flex>
|
||||
<!-- Profile Picture -->
|
||||
<!-- TODO: Translate Edit Link -->
|
||||
<Header
|
||||
:h1="props.username"
|
||||
:action="{
|
||||
text:'Edit profile',
|
||||
to:'/settings',
|
||||
primary: true,
|
||||
solid: true,
|
||||
icon: 'bi-pencil-fill'
|
||||
}"
|
||||
style="margin-top: 58px;"
|
||||
page-heading
|
||||
>
|
||||
<template #image>
|
||||
<img
|
||||
v-if="object?.icon"
|
||||
v-lazy="store.getters['instance/absoluteUrl'](object.icon.urls.large_square_crop)"
|
||||
v-if="object?.user.avatar"
|
||||
v-lazy="store.getters['instance/absoluteUrl'](object.user.avatar.urls.large_square_crop)"
|
||||
alt=""
|
||||
class="avatar"
|
||||
>
|
||||
|
@ -116,39 +132,57 @@ const tabs = ref([{
|
|||
>
|
||||
{{ store.state.auth.profile?.full_username?.[0] || "" }}
|
||||
</span>
|
||||
<!-- TODO: Translate Edit Link -->
|
||||
<Section
|
||||
:h1="props.username"
|
||||
:action="{
|
||||
text:'Edit profile',
|
||||
to:'/settings',
|
||||
primary: true,
|
||||
solid: true,
|
||||
icon: 'bi-pencil-fill'
|
||||
}"
|
||||
style="margin-top: 58px;"
|
||||
>
|
||||
</template>
|
||||
<Layout flex>
|
||||
<span style="grid-column: 1 / -1">
|
||||
{{ object?.full_username }}
|
||||
<i
|
||||
class="bi bi-copy"
|
||||
<Button
|
||||
icon="bi-copy"
|
||||
style="margin-left: 8px;"
|
||||
:aria-label="t('views.auth.ProfileBase.copyUsername')"
|
||||
:title="t('components.common.CopyInput.button.copy')"
|
||||
ghost
|
||||
secondary
|
||||
@click="copy(fullUsername)"
|
||||
/>
|
||||
</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
|
||||
style="grid-column: 1 / -1"
|
||||
:content="object?.summary? {text: object.summary} : null"
|
||||
:content="{ html: object?.summary.html || '' }"
|
||||
:field-name="'summary'"
|
||||
:update-url="`users/${store.state.auth.username}/`"
|
||||
:can-update="store.state.auth.authenticated && object?.full_username === store.state.auth.fullUsername"
|
||||
:truncate-length="100"
|
||||
@updated="emit('updated', $event)"
|
||||
/>
|
||||
</Layout>
|
||||
<UserFollowButton
|
||||
v-if="store.state.auth.authenticated && object && object.full_username !== store.state.auth.fullUsername"
|
||||
:actor="object"
|
||||
/>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Header>
|
||||
<Nav v-model="tabs" />
|
||||
|
||||
<router-view
|
||||
|
|
Loading…
Reference in New Issue