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,54 +105,84 @@ const tabs = ref([{ | |||
|     stack | ||||
|     main | ||||
|   > | ||||
|     <Layout flex> | ||||
|       <!-- Profile Picture --> | ||||
|       <img | ||||
|         v-if="object?.icon" | ||||
|         v-lazy="store.getters['instance/absoluteUrl'](object.icon.urls.large_square_crop)" | ||||
|         alt="" | ||||
|         class="avatar" | ||||
|       > | ||||
|       <span | ||||
|         v-else | ||||
|         :style="defaultAvatarStyle" | ||||
|         class="ui avatar circular label" | ||||
|       > | ||||
|         {{ 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;" | ||||
|       > | ||||
|     <!-- 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?.user.avatar" | ||||
|           v-lazy="store.getters['instance/absoluteUrl'](object.user.avatar.urls.large_square_crop)" | ||||
|           alt="" | ||||
|           class="avatar" | ||||
|         > | ||||
|         <span | ||||
|           v-else | ||||
|           :style="defaultAvatarStyle" | ||||
|           class="ui avatar circular label" | ||||
|         > | ||||
|           {{ store.state.auth.profile?.full_username?.[0] || "" }} | ||||
|         </span> | ||||
|       </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)" | ||||
|         /> | ||||
|         <UserFollowButton | ||||
|           v-if="store.state.auth.authenticated && object && object.full_username !== store.state.auth.fullUsername" | ||||
|           :actor="object" | ||||
|         /> | ||||
|       </Section> | ||||
|     </Layout> | ||||
|       </Layout> | ||||
|       <UserFollowButton | ||||
|         v-if="store.state.auth.authenticated && object && object.full_username !== store.state.auth.fullUsername" | ||||
|         :actor="object" | ||||
|       /> | ||||
|     </Header> | ||||
|     <Nav v-model="tabs" /> | ||||
| 
 | ||||
|     <router-view | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 upsiflu
						upsiflu