310 lines
7.1 KiB
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>
|