funkwhale/front/src/ui/components/Sidebar.vue

310 lines
7.1 KiB
Vue

<script setup lang="ts">
import { ref, onMounted, watch, computed } from 'vue'
import { useUploadsStore } from '../stores/upload'
import { useI18n } from 'vue-i18n'
import { useStore } from '~/store'
import { color } from '~/composables/color'
import Logo from '~/components/Logo.vue'
import Input from '~/components/ui/Input.vue'
import Link from '~/components/ui/Link.vue'
import ActorAvatar from '~/components/common/ActorAvatar.vue'
import UserMenu from './UserMenu.vue'
import Button from '~/components/ui/Button.vue'
import Layout from '~/components/ui/Layout.vue'
import Spacer from '~/components/ui/layout/Spacer.vue'
import { useRoute } from 'vue-router'
const isCollapsed = ref(false)
const route = useRoute()
watch(() => route.path, () => ( isCollapsed.value = true ))
const searchQuery = ref('')
// Hide the fake app when the real one is loaded
onMounted(() => {
document.getElementById('fake-app')?.remove()
})
const { t } = useI18n()
const store = useStore()
const uploads = useUploadsStore()
const logoUrl = computed(() => store.state.auth.authenticated ? 'library.index' : 'index')
</script>
<template>
<Layout aside :class="[$style.sidebar, $style['sticky-content']]" v-bind="color({}, ['default', 'solid', 'raised'])">
<Layout flex no-gap header style="justify-content:space-between; padding-right:8px;">
<Link
:to="{name: logoUrl}"
:class="$style['logo']"
>
<i>
<Logo />
<span class="visually-hidden">{{ t('components.Sidebar.link.home') }}</span>
</i>
</Link>
<Layout nav no-gap flex style="align-items: center;">
<Link to="/manage/settings"
round
icon="bi-wrench"
ghost
>
</Link>
<Link to="/upload"
round icon="bi-upload"
class="is-icon-only"
ghost
>
<Transition>
<div
v-if="uploads.currentIndex < uploads.queue.length"
:class="$style['upload-progress']"
>
<div :class="[$style.progress, $style.fake]" />
<div
:class="$style.progress"
:style="{ maxWidth: `${uploads.progress}%` }"
/>
</div>
</Transition>
</Link>
<UserMenu/>
<Button round ghost icon="bi-list large" class="hide-on-desktop" @click="isCollapsed=!isCollapsed"/>
</Layout>
</Layout>
<Layout no-gap stack :class="[$style['menu-links'], isCollapsed && 'hide-on-mobile']">
<div :class="$style.search">
<Input
v-model="searchQuery"
type="search"
icon="bi-search"
:placeholder="t('components.audio.SearchBar.placeholder.search')"
/>
</div>
<!-- Sign up, Log in -->
<div style="display:contents;" v-if="!store.state.auth.authenticated">
<Layout flex grow no-gap>
<Link :to="{ name: 'login' }"
solid
icon="bi-box-arrow-in-right"
style="flex-grow:1"
class="active"
>
{{ t('components.common.UserMenu.link.login') }}
</Link>
<Link :to="{ name: 'signup' }"
default solid
icon="bi-person-square"
style="flex-grow:1"
>
{{ t('components.common.UserMenu.link.signup') }}
</Link>
</Layout>
<Spacer :size="32" />
</div>
<nav style="display:contents;">
<Link to="/library"
ghost
icon="bi-compass"
thickWhenActive
>
{{ t('components.Sidebar.header.explore') }}
</Link>
<Link to="/library/artists"
ghost
icon="bi-person"
thickWhenActive
>
{{ t('components.Sidebar.link.artists') }}
</Link>
<Link to="/library/albums"
ghost
icon="bi-disc"
thickWhenActive
>
{{ t('components.Sidebar.link.albums') }}
</Link>
<Link to="/library/playlists"
ghost
icon="bi-music-note-list"
thickWhenActive
>
{{ t('components.Sidebar.link.playlists') }}
</Link>
<Link to="/library/radios"
ghost
icon="bi-boombox-fill"
thickWhenActive
>
{{ t('components.Sidebar.link.radios') }}
</Link>
<Link to="/library/podcasts"
ghost
icon="bi-mic"
thickWhenActive
>
{{ t('components.Sidebar.link.podcasts') }}
</Link>
<Link to="/favorites"
ghost
icon="bi-heart"
thickWhenActive
>
{{ t('components.Sidebar.link.favorites') }}
</Link>
</nav>
<Spacer />
<h3>{{ t('components.Sidebar.link.channels') }}</h3>
<Spacer grow />
<Layout nav flex no-gap style="justify-content: center">
<Link thin to="/about">
{{ t('components.Sidebar.link.about') }}
</Link>
<Spacer shrink />
<Link thin to="/privacy">
Privacy
</Link>
<Spacer shrink />
<Link thin to="/legal">
Legal
</Link>
</Layout>
</Layout>
</Layout>
</template>
<style module lang="scss">
.sidebar {
.logo {
display: block;
width: 40px;
height: 40px;
margin: 16px;
}
&.sticky-content {
max-height: 100dvh;
overflow: auto;
top: 0;
.upload-progress {
background: var(--fw-blue-500);
position: absolute;
left: 0;
bottom: 2px;
width: 100%;
padding: 2px;
&.v-enter-active,
&.v-leave-active {
transition: transform 0.2s ease, opacity 0.2s ease;
}
&.v-leave-to,
&.v-enter-from {
transform: translateY(0.5rem);
opacity: 0;
}
> .progress {
height: 0.25rem;
width: 100%;
transition: max-width 0.1s ease;
background: var(--fw-gray-100);
border-radius: 100vh;
position: relative;
&.fake {
background: var(--fw-blue-700);
}
&:not(.fake) {
position: absolute;
inset: 2px;
}
}
}
.avatar {
aspect-ratio: 1;
background: var(--fw-beige-100);
border-radius: 100%;
text-decoration: none !important;
color: var(--fw-gray-700);
> img,
> i {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
margin: 0 !important;
border-radius: 100vh;
}
}
.search {
padding: 0 4px;
margin-bottom: 40px;
input {
height: 50px;
&:focus {
border-color: var(--fw-gray-100);
}
}
}
> h3 {
margin: 0;
padding: 0 32px 8px;
font-size: 14px;
line-height: 1.2;
}
.menu-links {
padding: 0 16px 32px;
flex-grow: 1
}
}
:global(.hide-on-mobile) {
display: none;
}
@media screen and (min-width: 1024px) {
height: 100%;
:global(.hide-on-desktop) {
display: none !important;
}
:global(.hide-on-mobile) {
display: inherit;
}
&.sticky-content {
position: sticky;
height: 100%;
}
}
}
</style>