feat: create router for UI v2
This commit is contained in:
parent
1c10a5b257
commit
59cd41d331
|
@ -11,9 +11,7 @@ import { useStore } from '~/store'
|
||||||
|
|
||||||
import useLogger from '~/composables/useLogger'
|
import useLogger from '~/composables/useLogger'
|
||||||
|
|
||||||
import { useRoute } from 'vue-router'
|
import { useLocalStorage } from '@vueuse/core'
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
const logger = useLogger()
|
const logger = useLogger()
|
||||||
logger.debug('App setup()')
|
logger.debug('App setup()')
|
||||||
|
@ -71,10 +69,12 @@ const { width } = useWindowSize()
|
||||||
// NOTE: We're not checking if we're authenticated in the store,
|
// NOTE: We're not checking if we're authenticated in the store,
|
||||||
// because we want to learn if we are authenticated at all
|
// because we want to learn if we are authenticated at all
|
||||||
store.dispatch('auth/fetchUser')
|
store.dispatch('auth/fetchUser')
|
||||||
|
|
||||||
|
const isUIv2 = useLocalStorage('ui-v2', false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UiApp v-if="route.fullPath.startsWith('/ui')" />
|
<UiApp v-if="isUIv2" />
|
||||||
<LegacyLayout v-else />
|
<LegacyLayout v-else />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
|
import { useLocalStorage } from '@vueuse/core'
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import { forceInstanceChooser } from './guards'
|
import { forceInstanceChooser } from './guards'
|
||||||
import routes from './routes'
|
|
||||||
|
import routesV1 from './routes'
|
||||||
|
import routesV2 from '~/ui/routes'
|
||||||
|
|
||||||
|
const isUIv2 = useLocalStorage('ui-v2', false)
|
||||||
|
const routes = isUIv2.value ? routesV2 : routesV1
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.VUE_APP_ROUTER_BASE_URL as string ?? '/'),
|
history: createWebHistory(import.meta.env.VUE_APP_ROUTER_BASE_URL as string ?? '/'),
|
||||||
|
|
|
@ -7,11 +7,9 @@ import manage from './manage'
|
||||||
import store from '~/store'
|
import store from '~/store'
|
||||||
import auth from './auth'
|
import auth from './auth'
|
||||||
import user from './user'
|
import user from './user'
|
||||||
import ui from './ui'
|
|
||||||
import { requireLoggedIn } from '~/router/guards'
|
import { requireLoggedIn } from '~/router/guards'
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
...ui,
|
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'index',
|
name: 'index',
|
||||||
|
|
|
@ -9,18 +9,18 @@ export default [
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'upload',
|
path: 'upload',
|
||||||
name: 'ui.upload',
|
name: 'upload',
|
||||||
component: () => import('~/ui/pages/upload.vue'),
|
component: () => import('~/ui/pages/upload.vue'),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
name: 'ui.upload.index',
|
name: 'upload.index',
|
||||||
component: () => import('~/ui/pages/upload/index.vue')
|
component: () => import('~/ui/pages/upload/index.vue')
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: 'running',
|
path: 'running',
|
||||||
name: 'ui.upload.running',
|
name: 'upload.running',
|
||||||
component: () => import('~/ui/pages/upload/running.vue'),
|
component: () => import('~/ui/pages/upload/running.vue'),
|
||||||
beforeEnter: (_to, _from, next) => {
|
beforeEnter: (_to, _from, next) => {
|
||||||
const uploads = useUploadsStore()
|
const uploads = useUploadsStore()
|
||||||
|
@ -34,13 +34,13 @@ export default [
|
||||||
|
|
||||||
{
|
{
|
||||||
path: 'history',
|
path: 'history',
|
||||||
name: 'ui.upload.history',
|
name: 'upload.history',
|
||||||
component: () => import('~/ui/pages/upload/history.vue')
|
component: () => import('~/ui/pages/upload/history.vue')
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: 'all',
|
path: 'all',
|
||||||
name: 'ui.upload.all',
|
name: 'upload.all',
|
||||||
component: () => import('~/ui/pages/upload/all.vue')
|
component: () => import('~/ui/pages/upload/all.vue')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -15,8 +15,14 @@ const coverUrl = computed(() => {
|
||||||
<template>
|
<template>
|
||||||
<div class="cover-art">
|
<div class="cover-art">
|
||||||
<Transition mode="out-in">
|
<Transition mode="out-in">
|
||||||
<img v-if="coverUrl" :src="coverUrl" />
|
<img
|
||||||
<Icon v-else icon="bi:disc" />
|
v-if="coverUrl"
|
||||||
|
:src="coverUrl"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
v-else
|
||||||
|
icon="bi:disc"
|
||||||
|
/>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useUploadsStore } from '../stores/upload';
|
import { useUploadsStore } from '../stores/upload'
|
||||||
|
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
|
|
||||||
|
@ -19,26 +19,51 @@ const uploads = useUploadsStore()
|
||||||
<div class="sticky-content">
|
<div class="sticky-content">
|
||||||
<nav class="quick-actions">
|
<nav class="quick-actions">
|
||||||
<RouterLink to="/">
|
<RouterLink to="/">
|
||||||
<img src="../../assets/logo/logo.svg" alt="Logo" class="logo" />
|
<img
|
||||||
|
src="../../assets/logo/logo.svg"
|
||||||
|
alt="Logo"
|
||||||
|
class="logo"
|
||||||
|
>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
<FwButton icon="bi:wrench" color="secondary" variant="ghost" />
|
<FwButton
|
||||||
|
icon="bi:wrench"
|
||||||
|
color="secondary"
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
|
||||||
<FwButton icon="bi:upload" color="secondary" variant="ghost" :class="[{ active: route.name === 'ui.upload' }, 'icon-only']">
|
<RouterLink to="/upload">
|
||||||
|
<FwButton
|
||||||
|
icon="bi:upload"
|
||||||
|
color="secondary"
|
||||||
|
variant="ghost"
|
||||||
|
:class="[{ active: route.name === 'ui.upload' }, 'icon-only']"
|
||||||
|
>
|
||||||
<Transition>
|
<Transition>
|
||||||
<div v-if="uploads.currentIndex < uploads.queue.length" class="upload-progress">
|
<div
|
||||||
|
v-if="uploads.currentIndex < uploads.queue.length"
|
||||||
|
class="upload-progress"
|
||||||
|
>
|
||||||
<div class="progress fake" />
|
<div class="progress fake" />
|
||||||
<div class="progress" :style="{ maxWidth: `${uploads.progress}%` }" />
|
<div
|
||||||
|
class="progress"
|
||||||
|
:style="{ maxWidth: `${uploads.progress}%` }"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</FwButton>
|
</FwButton>
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
<FwButton icon="bi:inbox" color="secondary" variant="ghost" />
|
<FwButton
|
||||||
|
icon="bi:inbox"
|
||||||
|
color="secondary"
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
@click.prevent
|
|
||||||
href=""
|
href=""
|
||||||
class="avatar"
|
class="avatar"
|
||||||
|
@click.prevent
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="$store.state.auth.authenticated && $store.state.auth.profile?.avatar?.urls.medium_square_crop"
|
v-if="$store.state.auth.authenticated && $store.state.auth.profile?.avatar?.urls.medium_square_crop"
|
||||||
|
@ -66,24 +91,82 @@ const uploads = useUploadsStore()
|
||||||
|
|
||||||
<h3>Explore</h3>
|
<h3>Explore</h3>
|
||||||
<nav class="button-list">
|
<nav class="button-list">
|
||||||
<FwButton color="secondary" variant="ghost" icon="bi-compass">All Funkwhale</FwButton>
|
<FwButton
|
||||||
<FwButton color="secondary" variant="ghost" icon="bi-music-note-beamed">Music</FwButton>
|
color="secondary"
|
||||||
<FwButton color="secondary" variant="ghost" icon="bi-mic">Podcasts</FwButton>
|
variant="ghost"
|
||||||
|
icon="bi-compass"
|
||||||
|
>
|
||||||
|
All Funkwhale
|
||||||
|
</FwButton>
|
||||||
|
<FwButton
|
||||||
|
color="secondary"
|
||||||
|
variant="ghost"
|
||||||
|
icon="bi-music-note-beamed"
|
||||||
|
>
|
||||||
|
Music
|
||||||
|
</FwButton>
|
||||||
|
<FwButton
|
||||||
|
color="secondary"
|
||||||
|
variant="ghost"
|
||||||
|
icon="bi-mic"
|
||||||
|
>
|
||||||
|
Podcasts
|
||||||
|
</FwButton>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<h3>Library</h3>
|
<h3>Library</h3>
|
||||||
<div class="pill-list">
|
<div class="pill-list">
|
||||||
<FwPill>Music</FwPill>
|
<FwPill>Music</FwPill>
|
||||||
<FwPill outline>Podcasts</FwPill>
|
<FwPill outline>
|
||||||
<FwPill outline>Sharing</FwPill>
|
Podcasts
|
||||||
|
</FwPill>
|
||||||
|
<FwPill outline>
|
||||||
|
Sharing
|
||||||
|
</FwPill>
|
||||||
</div>
|
</div>
|
||||||
<nav class="button-list">
|
<nav class="button-list">
|
||||||
<FwButton color="secondary" variant="ghost" icon="bi-collection">Collections</FwButton>
|
<FwButton
|
||||||
<FwButton color="secondary" variant="ghost" icon="bi-person">Artists</FwButton>
|
color="secondary"
|
||||||
<FwButton color="secondary" variant="ghost" icon="bi-disc">Albums</FwButton>
|
variant="ghost"
|
||||||
<FwButton color="secondary" variant="ghost" icon="bi-music-note-list">Playlists</FwButton>
|
icon="bi-collection"
|
||||||
<FwButton color="secondary" variant="ghost" icon="bi-question-diamond">Radios</FwButton>
|
>
|
||||||
<FwButton color="secondary" variant="ghost" icon="bi-heart">Favorites</FwButton>
|
Collections
|
||||||
|
</FwButton>
|
||||||
|
<FwButton
|
||||||
|
color="secondary"
|
||||||
|
variant="ghost"
|
||||||
|
icon="bi-person"
|
||||||
|
>
|
||||||
|
Artists
|
||||||
|
</FwButton>
|
||||||
|
<FwButton
|
||||||
|
color="secondary"
|
||||||
|
variant="ghost"
|
||||||
|
icon="bi-disc"
|
||||||
|
>
|
||||||
|
Albums
|
||||||
|
</FwButton>
|
||||||
|
<FwButton
|
||||||
|
color="secondary"
|
||||||
|
variant="ghost"
|
||||||
|
icon="bi-music-note-list"
|
||||||
|
>
|
||||||
|
Playlists
|
||||||
|
</FwButton>
|
||||||
|
<FwButton
|
||||||
|
color="secondary"
|
||||||
|
variant="ghost"
|
||||||
|
icon="bi-question-diamond"
|
||||||
|
>
|
||||||
|
Radios
|
||||||
|
</FwButton>
|
||||||
|
<FwButton
|
||||||
|
color="secondary"
|
||||||
|
variant="ghost"
|
||||||
|
icon="bi-heart"
|
||||||
|
>
|
||||||
|
Favorites
|
||||||
|
</FwButton>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
@ -155,7 +238,6 @@ aside {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
> :first-child {
|
> :first-child {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue'
|
||||||
import { UploadGroup } from '~/ui/stores/upload'
|
import { UploadGroup } from '~/ui/stores/upload'
|
||||||
import VerticalCollapse from '~/ui/components/VerticalCollapse.vue'
|
import VerticalCollapse from '~/ui/components/VerticalCollapse.vue'
|
||||||
import UploadList from '~/ui/components/UploadList.vue'
|
import UploadList from '~/ui/components/UploadList.vue'
|
||||||
import { UseTimeAgo } from '@vueuse/components'
|
import { UseTimeAgo } from '@vueuse/components'
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
|
|
||||||
|
|
||||||
defineProps<{ groups: UploadGroup[], isUploading?: boolean }>()
|
defineProps<{ groups: UploadGroup[], isUploading?: boolean }>()
|
||||||
|
|
||||||
const openUploadGroup = ref<UploadGroup>()
|
const openUploadGroup = ref<UploadGroup>()
|
||||||
|
@ -19,7 +18,7 @@ const toggle = (group: UploadGroup) => {
|
||||||
const labels = {
|
const labels = {
|
||||||
'music-library': 'Music library',
|
'music-library': 'Music library',
|
||||||
'music-channel': 'Music channel',
|
'music-channel': 'Music channel',
|
||||||
'podcast-channel': 'Podcast channel',
|
'podcast-channel': 'Podcast channel'
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDescription = (group: UploadGroup) => {
|
const getDescription = (group: UploadGroup) => {
|
||||||
|
@ -48,61 +47,104 @@ const getDescription = (group: UploadGroup) => {
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="upload-group"
|
|
||||||
v-for="group of groups"
|
v-for="group of groups"
|
||||||
:key="group.guid"
|
:key="group.guid"
|
||||||
|
class="upload-group"
|
||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="upload-group-header">
|
<div class="upload-group-header">
|
||||||
<div class="upload-group-title">{{ labels[group.type] }}</div>
|
<div class="upload-group-title">
|
||||||
<div class="upload-group-albums">{{ getDescription(group) }}</div>
|
{{ labels[group.type] }}
|
||||||
|
</div>
|
||||||
|
<div class="upload-group-albums">
|
||||||
|
{{ getDescription(group) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="timeago">
|
<div class="timeago">
|
||||||
<UseTimeAgo :time="group.createdAt" v-slot="{ timeAgo }">{{ timeAgo }}</UseTimeAgo>
|
<UseTimeAgo
|
||||||
|
v-slot="{ timeAgo }"
|
||||||
|
:time="group.createdAt"
|
||||||
|
>
|
||||||
|
{{ timeAgo }}
|
||||||
|
</UseTimeAgo>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<FwPill
|
||||||
<FwPill v-if="group.failedCount > 0" color="red">
|
v-if="group.failedCount > 0"
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
<template #image>
|
<template #image>
|
||||||
<div class="flex items-center justify-center">{{ group.failedCount }}</div>
|
<div class="flex items-center justify-center">
|
||||||
|
{{ group.failedCount }}
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
failed
|
failed
|
||||||
</FwPill>
|
</FwPill>
|
||||||
|
|
||||||
<FwPill v-if="group.importedCount > 0" color="blue">
|
<FwPill
|
||||||
|
v-if="group.importedCount > 0"
|
||||||
|
color="blue"
|
||||||
|
>
|
||||||
<template #image>
|
<template #image>
|
||||||
<div class="flex items-center justify-center">{{ group.importedCount }}</div>
|
<div class="flex items-center justify-center">
|
||||||
|
{{ group.importedCount }}
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
imported
|
imported
|
||||||
</FwPill>
|
</FwPill>
|
||||||
|
|
||||||
<FwPill v-if="group.processingCount > 0" color="secondary">
|
<FwPill
|
||||||
|
v-if="group.processingCount > 0"
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
<template #image>
|
<template #image>
|
||||||
<div class="flex items-center justify-center">{{ group.processingCount }}</div>
|
<div class="flex items-center justify-center">
|
||||||
|
{{ group.processingCount }}
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
processing
|
processing
|
||||||
</FwPill>
|
</FwPill>
|
||||||
|
|
||||||
|
|
||||||
<FwButton
|
<FwButton
|
||||||
@click="toggle(group)"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
class="icon-only"
|
class="icon-only"
|
||||||
|
@click="toggle(group)"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<Icon icon="bi:chevron-right" :rotate="group === openUploadGroup ? 1 : 0" />
|
<Icon
|
||||||
|
icon="bi:chevron-right"
|
||||||
|
:rotate="group === openUploadGroup ? 1 : 0"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</FwButton>
|
</FwButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isUploading" class="flex items-center upload-progress">
|
<div
|
||||||
<FwButton v-if="group.processingCount === 0 && group.failedCount > 0" @click="group.retry()" color="secondary">Retry</FwButton>
|
v-if="isUploading"
|
||||||
<FwButton v-else-if="group.queue.length !== group.importedCount" @click="group.cancel()" color="secondary">Interrupt</FwButton>
|
class="flex items-center upload-progress"
|
||||||
|
>
|
||||||
|
<FwButton
|
||||||
|
v-if="group.processingCount === 0 && group.failedCount > 0"
|
||||||
|
color="secondary"
|
||||||
|
@click="group.retry()"
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</FwButton>
|
||||||
|
<FwButton
|
||||||
|
v-else-if="group.queue.length !== group.importedCount"
|
||||||
|
color="secondary"
|
||||||
|
@click="group.cancel()"
|
||||||
|
>
|
||||||
|
Interrupt
|
||||||
|
</FwButton>
|
||||||
|
|
||||||
<div class="progress">
|
<div class="progress">
|
||||||
<div class="progress-bar" :style="{ width: `${group.progress}%` }" />
|
<div
|
||||||
|
class="progress-bar"
|
||||||
|
:style="{ width: `${group.progress}%` }"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="shrink-0">
|
<div class="shrink-0">
|
||||||
|
@ -110,7 +152,11 @@ const getDescription = (group: UploadGroup) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VerticalCollapse @click.stop :open="openUploadGroup === group" class="collapse">
|
<VerticalCollapse
|
||||||
|
:open="openUploadGroup === group"
|
||||||
|
class="collapse"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
<UploadList :uploads="group.queue" />
|
<UploadList :uploads="group.queue" />
|
||||||
</VerticalCollapse>
|
</VerticalCollapse>
|
||||||
</div>
|
</div>
|
||||||
|
@ -144,7 +190,6 @@ const getDescription = (group: UploadGroup) => {
|
||||||
color: var(--fw-gray-600);
|
color: var(--fw-gray-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.upload-progress {
|
.upload-progress {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: var(--fw-gray-600);
|
color: var(--fw-gray-600);
|
||||||
|
|
|
@ -14,28 +14,53 @@ defineProps<{
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="file-list">
|
<div class="file-list">
|
||||||
<div v-for="track in uploads" :key="track.id" class="list-track" :class="{ wide }">
|
<div
|
||||||
<CoverArt :src="track.metadata" class="track-cover" />
|
v-for="track in uploads"
|
||||||
|
:key="track.id"
|
||||||
|
class="list-track"
|
||||||
|
:class="{ wide }"
|
||||||
|
>
|
||||||
|
<CoverArt
|
||||||
|
:src="track.metadata"
|
||||||
|
class="track-cover"
|
||||||
|
/>
|
||||||
<Transition mode="out-in">
|
<Transition mode="out-in">
|
||||||
<div v-if="track.metadata?.tags" class="track-data">
|
<div
|
||||||
<div class="track-title">{{ track.metadata.tags.title }}</div>
|
v-if="track.metadata?.tags"
|
||||||
|
class="track-data"
|
||||||
|
>
|
||||||
|
<div class="track-title">
|
||||||
|
{{ track.metadata.tags.title }}
|
||||||
|
</div>
|
||||||
{{ track.metadata.tags.artist }} / {{ track.metadata.tags.album }}
|
{{ track.metadata.tags.artist }} / {{ track.metadata.tags.album }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="track-title">
|
<div
|
||||||
|
v-else
|
||||||
|
class="track-title"
|
||||||
|
>
|
||||||
{{ track.file.name }}
|
{{ track.file.name }}
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
<div class="upload-state">
|
<div class="upload-state">
|
||||||
<FwTooltip v-if="track.failReason" :tooltip="track.failReason">
|
<FwTooltip
|
||||||
|
v-if="track.failReason"
|
||||||
|
:tooltip="track.failReason"
|
||||||
|
>
|
||||||
<FwPill color="red">
|
<FwPill color="red">
|
||||||
<template #image>
|
<template #image>
|
||||||
<Icon icon="bi:question" class="h-4 w-4" />
|
<Icon
|
||||||
|
icon="bi:question"
|
||||||
|
class="h-4 w-4"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
failed
|
failed
|
||||||
</FwPill>
|
</FwPill>
|
||||||
</FwTooltip>
|
</FwTooltip>
|
||||||
<FwPill v-else :color="track.importedAt ? 'blue' : 'secondary'">
|
<FwPill
|
||||||
|
v-else
|
||||||
|
:color="track.importedAt ? 'blue' : 'secondary'"
|
||||||
|
>
|
||||||
{{
|
{{
|
||||||
track.importedAt
|
track.importedAt
|
||||||
? 'imported'
|
? 'imported'
|
||||||
|
@ -44,10 +69,21 @@ defineProps<{
|
||||||
: 'uploading'
|
: 'uploading'
|
||||||
}}
|
}}
|
||||||
</FwPill>
|
</FwPill>
|
||||||
<div v-if="track.importedAt" class="track-timeago">
|
<div
|
||||||
<UseTimeAgo :time="track.importedAt" v-slot="{ timeAgo }">{{ timeAgo }}</UseTimeAgo>
|
v-if="track.importedAt"
|
||||||
|
class="track-timeago"
|
||||||
|
>
|
||||||
|
<UseTimeAgo
|
||||||
|
v-slot="{ timeAgo }"
|
||||||
|
:time="track.importedAt"
|
||||||
|
>
|
||||||
|
{{ timeAgo }}
|
||||||
|
</UseTimeAgo>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="track-progress">
|
<div
|
||||||
|
v-else
|
||||||
|
class="track-progress"
|
||||||
|
>
|
||||||
{{ bytesToHumanSize(track.file.size / 100 * track.progress) }}
|
{{ bytesToHumanSize(track.file.size / 100 * track.progress) }}
|
||||||
/ {{ bytesToHumanSize(track.file.size) }}
|
/ {{ bytesToHumanSize(track.file.size) }}
|
||||||
⋅ {{ track.progress }}%
|
⋅ {{ track.progress }}%
|
||||||
|
@ -55,10 +91,10 @@ defineProps<{
|
||||||
</div>
|
</div>
|
||||||
<FwButton
|
<FwButton
|
||||||
v-if="track.failReason"
|
v-if="track.failReason"
|
||||||
@click="track.retry()"
|
|
||||||
icon="bi:arrow-repeat"
|
icon="bi:arrow-repeat"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
|
@click="track.retry()"
|
||||||
/>
|
/>
|
||||||
<FwButton
|
<FwButton
|
||||||
v-else
|
v-else
|
||||||
|
|
|
@ -71,14 +71,19 @@ const currentFilter = ref(filterItems[0])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FwModal v-model="libraryOpen" title="Upload music to library">
|
<FwModal
|
||||||
|
v-model="libraryOpen"
|
||||||
|
title="Upload music to library"
|
||||||
|
>
|
||||||
<template #alert="{ closeAlert }">
|
<template #alert="{ closeAlert }">
|
||||||
<FwAlert>
|
<FwAlert>
|
||||||
Before uploading, please ensure your files are tagged properly.
|
Before uploading, please ensure your files are tagged properly.
|
||||||
We recommend using Picard for that purpose.
|
We recommend using Picard for that purpose.
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<FwButton @click="closeAlert">Got it</FwButton>
|
<FwButton @click="closeAlert">
|
||||||
|
Got it
|
||||||
|
</FwButton>
|
||||||
</template>
|
</template>
|
||||||
</FwAlert>
|
</FwAlert>
|
||||||
</template>
|
</template>
|
||||||
|
@ -97,12 +102,19 @@ const currentFilter = ref(filterItems[0])
|
||||||
{{ queue.length }} files, {{ combinedFileSize }}
|
{{ queue.length }} files, {{ combinedFileSize }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FwSelect icon="bi:filter" v-model="currentFilter" :items="filterItems" />
|
<FwSelect
|
||||||
<FwSelect icon="bi:sort-down" v-model="currentSort" :items="sortItems" />
|
v-model="currentFilter"
|
||||||
|
icon="bi:filter"
|
||||||
|
:items="filterItems"
|
||||||
|
/>
|
||||||
|
<FwSelect
|
||||||
|
v-model="currentSort"
|
||||||
|
icon="bi:sort-down"
|
||||||
|
:items="sortItems"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UploadList :uploads="queue" />
|
<UploadList :uploads="queue" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Import path -->
|
<!-- Import path -->
|
||||||
|
@ -120,7 +132,12 @@ const currentFilter = ref(filterItems[0])
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<FwButton @click="cancel" color="secondary">Cancel</FwButton>
|
<FwButton
|
||||||
|
color="secondary"
|
||||||
|
@click="cancel"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</FwButton>
|
||||||
<FwButton @click="continueInBackground">
|
<FwButton @click="continueInBackground">
|
||||||
{{ uploads.queue.length ? 'Continue in background' : 'Save and close' }}
|
{{ uploads.queue.length ? 'Continue in background' : 'Save and close' }}
|
||||||
</FwButton>
|
</FwButton>
|
||||||
|
|
|
@ -3,7 +3,10 @@ defineProps<{ open: boolean }>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="v-collapse" :class="{ open }">
|
<div
|
||||||
|
class="v-collapse"
|
||||||
|
:class="{ open }"
|
||||||
|
>
|
||||||
<div class="v-collapse-body">
|
<div class="v-collapse-body">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,4 +5,3 @@ export const bytesToHumanSize = (bytes: number) => {
|
||||||
if (i === 0) return `${bytes} ${sizes[i]}`
|
if (i === 0) return `${bytes} ${sizes[i]}`
|
||||||
return `${(bytes / 1024 ** i).toFixed(1)} ${sizes[i]}`
|
return `${(bytes / 1024 ** i).toFixed(1)} ${sizes[i]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ export const getCoverUrl = async (tags: Tags): Promise<string | undefined> => {
|
||||||
onerror: () => reject(reader.error)
|
onerror: () => reject(reader.error)
|
||||||
})
|
})
|
||||||
|
|
||||||
reader.readAsDataURL(new File([picture.data], "", { type: picture.type }))
|
reader.readAsDataURL(new File([picture.data], '', { type: picture.type }))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref, computed } from 'vue'
|
import { reactive, ref, computed } from 'vue'
|
||||||
import { UseTimeAgo } from '@vueuse/components'
|
import { UseTimeAgo } from '@vueuse/components'
|
||||||
import { Icon } from '@iconify/vue';
|
import { Icon } from '@iconify/vue'
|
||||||
import { useUploadsStore } from '~/ui/stores/upload'
|
import { useUploadsStore } from '~/ui/stores/upload'
|
||||||
import { bytesToHumanSize } from '~/ui/composables/bytes'
|
import { bytesToHumanSize } from '~/ui/composables/bytes'
|
||||||
|
|
||||||
const filesystemStats = reactive({
|
const filesystemStats = reactive({
|
||||||
total: 10737418240,
|
total: 10737418240,
|
||||||
used: 3e9,
|
used: 3e9
|
||||||
})
|
})
|
||||||
|
|
||||||
const filesystemProgress = computed(() => {
|
const filesystemProgress = computed(() => {
|
||||||
|
@ -19,7 +19,7 @@ const tabs = [
|
||||||
{
|
{
|
||||||
label: 'Music library',
|
label: 'Music library',
|
||||||
icon: 'headphones',
|
icon: 'headphones',
|
||||||
description: 'Host music you listen to.',
|
description: 'Host music you listen to.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Music channel',
|
label: 'Music channel',
|
||||||
|
@ -29,13 +29,12 @@ const tabs = [
|
||||||
{
|
{
|
||||||
label: 'Podcast channel',
|
label: 'Podcast channel',
|
||||||
icon: 'mic',
|
icon: 'mic',
|
||||||
description: 'Publish podcast you make.',
|
description: 'Publish podcast you make.'
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const currentTab = ref(tabs[0].label)
|
const currentTab = ref(tabs[0].label)
|
||||||
|
|
||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
const libraryOpen = ref(false)
|
const libraryOpen = ref(false)
|
||||||
|
|
||||||
|
@ -53,7 +52,6 @@ const processFiles = (fileList: FileList) => {
|
||||||
for (const file of fileList) {
|
for (const file of fileList) {
|
||||||
uploads.queueUpload(file)
|
uploads.queueUpload(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
|
@ -78,10 +76,15 @@ const currentFilter = ref(filterItems[0])
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<h1 class="mr-auto">Upload</h1>
|
<h1 class="mr-auto">
|
||||||
|
Upload
|
||||||
|
</h1>
|
||||||
|
|
||||||
<div class="filesystem-stats">
|
<div class="filesystem-stats">
|
||||||
<div class="filesystem-stats--progress" :style="`--progress: ${filesystemProgress}%`" />
|
<div
|
||||||
|
class="filesystem-stats--progress"
|
||||||
|
:style="`--progress: ${filesystemProgress}%`"
|
||||||
|
/>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
{{ bytesToHumanSize(filesystemStats.total) }} total
|
{{ bytesToHumanSize(filesystemStats.total) }} total
|
||||||
|
|
||||||
|
@ -92,14 +95,14 @@ const currentFilter = ref(filterItems[0])
|
||||||
{{ bytesToHumanSize(filesystemStats.total - filesystemStats.used) }} available
|
{{ bytesToHumanSize(filesystemStats.total - filesystemStats.used) }} available
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p> Select a destination for your audio files: </p>
|
<p> Select a destination for your audio files: </p>
|
||||||
|
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<FwCard
|
<FwCard
|
||||||
v-for="tab in tabs" :key="tab.label"
|
v-for="tab in tabs"
|
||||||
|
:key="tab.label"
|
||||||
:title="tab.label"
|
:title="tab.label"
|
||||||
:class="currentTab === tab.label && 'active'"
|
:class="currentTab === tab.label && 'active'"
|
||||||
@click="currentTab = tab.label"
|
@click="currentTab = tab.label"
|
||||||
|
@ -115,15 +118,22 @@ const currentFilter = ref(filterItems[0])
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<FwButton @click="libraryOpen = true">Open library</FwButton>
|
<FwButton @click="libraryOpen = true">
|
||||||
<FwModal v-model="libraryOpen" title="Upload music to library">
|
Open library
|
||||||
|
</FwButton>
|
||||||
|
<FwModal
|
||||||
|
v-model="libraryOpen"
|
||||||
|
title="Upload music to library"
|
||||||
|
>
|
||||||
<template #alert="{ closeAlert }">
|
<template #alert="{ closeAlert }">
|
||||||
<FwAlert>
|
<FwAlert>
|
||||||
Before uploading, please ensure your files are tagged properly.
|
Before uploading, please ensure your files are tagged properly.
|
||||||
We recommend using Picard for that purpose.
|
We recommend using Picard for that purpose.
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<FwButton @click="closeAlert">Got it</FwButton>
|
<FwButton @click="closeAlert">
|
||||||
|
Got it
|
||||||
|
</FwButton>
|
||||||
</template>
|
</template>
|
||||||
</FwAlert>
|
</FwAlert>
|
||||||
</template>
|
</template>
|
||||||
|
@ -142,18 +152,38 @@ const currentFilter = ref(filterItems[0])
|
||||||
{{ uploads.queue.length }} files, {{ combinedFileSize }}
|
{{ uploads.queue.length }} files, {{ combinedFileSize }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FwSelect icon="bi:filter" v-model="currentFilter" :items="filterItems" />
|
<FwSelect
|
||||||
<FwSelect icon="bi:sort-down" v-model="currentSort" :items="sortItems" />
|
v-model="currentFilter"
|
||||||
|
icon="bi:filter"
|
||||||
|
:items="filterItems"
|
||||||
|
/>
|
||||||
|
<FwSelect
|
||||||
|
v-model="currentSort"
|
||||||
|
icon="bi:sort-down"
|
||||||
|
:items="sortItems"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="file-list">
|
<div class="file-list">
|
||||||
<div v-for="track in uploads.queue" :key="track.id" class="list-track">
|
<div
|
||||||
|
v-for="track in uploads.queue"
|
||||||
|
:key="track.id"
|
||||||
|
class="list-track"
|
||||||
|
>
|
||||||
<Transition mode="out-in">
|
<Transition mode="out-in">
|
||||||
<div v-if="track.tags" class="track-data">
|
<div
|
||||||
<div class="track-title">{{ track.tags.title }}</div>
|
v-if="track.tags"
|
||||||
|
class="track-data"
|
||||||
|
>
|
||||||
|
<div class="track-title">
|
||||||
|
{{ track.tags.title }}
|
||||||
|
</div>
|
||||||
{{ track.tags.artist }} / {{ track.tags.album }}
|
{{ track.tags.artist }} / {{ track.tags.album }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="track-title">
|
<div
|
||||||
|
v-else
|
||||||
|
class="track-title"
|
||||||
|
>
|
||||||
{{ track.file.name }}
|
{{ track.file.name }}
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
@ -169,10 +199,21 @@ const currentFilter = ref(filterItems[0])
|
||||||
: 'uploading'
|
: 'uploading'
|
||||||
}}
|
}}
|
||||||
</FwPill>
|
</FwPill>
|
||||||
<div v-if="track.importedAt" class="track-progress">
|
<div
|
||||||
<UseTimeAgo :time="track.importedAt" v-slot="{ timeAgo }">{{ timeAgo }}</UseTimeAgo>
|
v-if="track.importedAt"
|
||||||
|
class="track-progress"
|
||||||
|
>
|
||||||
|
<UseTimeAgo
|
||||||
|
v-slot="{ timeAgo }"
|
||||||
|
:time="track.importedAt"
|
||||||
|
>
|
||||||
|
{{ timeAgo }}
|
||||||
|
</UseTimeAgo>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="track-progress">
|
<div
|
||||||
|
v-else
|
||||||
|
class="track-progress"
|
||||||
|
>
|
||||||
{{ bytesToHumanSize(track.file.size / 100 * track.progress) }}
|
{{ bytesToHumanSize(track.file.size / 100 * track.progress) }}
|
||||||
/ {{ bytesToHumanSize(track.file.size) }}
|
/ {{ bytesToHumanSize(track.file.size) }}
|
||||||
⋅ {{ track.progress }}%
|
⋅ {{ track.progress }}%
|
||||||
|
@ -187,7 +228,6 @@ const currentFilter = ref(filterItems[0])
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Import path -->
|
<!-- Import path -->
|
||||||
|
@ -205,7 +245,12 @@ const currentFilter = ref(filterItems[0])
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<FwButton @click="cancel" color="secondary">Cancel</FwButton>
|
<FwButton
|
||||||
|
color="secondary"
|
||||||
|
@click="cancel"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</FwButton>
|
||||||
<FwButton @click="libraryOpen = false">
|
<FwButton @click="libraryOpen = false">
|
||||||
{{ uploads.queue.length ? 'Continue in background' : 'Save and close' }}
|
{{ uploads.queue.length ? 'Continue in background' : 'Save and close' }}
|
||||||
</FwButton>
|
</FwButton>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import UploadModal from '~/ui/components/UploadModal.vue'
|
||||||
|
|
||||||
const filesystemStats = reactive({
|
const filesystemStats = reactive({
|
||||||
total: 10737418240,
|
total: 10737418240,
|
||||||
used: 3e9,
|
used: 3e9
|
||||||
})
|
})
|
||||||
|
|
||||||
const filesystemProgress = computed(() => {
|
const filesystemProgress = computed(() => {
|
||||||
|
@ -35,16 +35,21 @@ const tabs = computed(() => [
|
||||||
label: 'All files',
|
label: 'All files',
|
||||||
key: 'all',
|
key: 'all',
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
}
|
||||||
].filter(tab => tab.enabled))
|
].filter(tab => tab.enabled))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<h1 class="mr-auto">Upload</h1>
|
<h1 class="mr-auto">
|
||||||
|
Upload
|
||||||
|
</h1>
|
||||||
|
|
||||||
<div class="filesystem-stats">
|
<div class="filesystem-stats">
|
||||||
<div class="filesystem-stats--progress" :style="`--progress: ${filesystemProgress}%`" />
|
<div
|
||||||
|
class="filesystem-stats--progress"
|
||||||
|
:style="`--progress: ${filesystemProgress}%`"
|
||||||
|
/>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
{{ bytesToHumanSize(filesystemStats.total) }} total
|
{{ bytesToHumanSize(filesystemStats.total) }} total
|
||||||
|
|
||||||
|
@ -55,12 +60,20 @@ const tabs = computed(() => [
|
||||||
{{ bytesToHumanSize(filesystemStats.total - filesystemStats.used) }} available
|
{{ bytesToHumanSize(filesystemStats.total - filesystemStats.used) }} available
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4 -ml-2">
|
<div class="mb-4 -ml-2">
|
||||||
<RouterLink v-for="tab in tabs" :key="tab.key" :to="`/ui/upload/${tab.key}`" custom #="{ navigate, isExactActive }">
|
<RouterLink
|
||||||
<FwPill @click="navigate" :color="isExactActive ? 'primary' : 'secondary'">
|
v-for="tab in tabs"
|
||||||
|
:key="tab.key"
|
||||||
|
:to="`/ui/upload/${tab.key}`"
|
||||||
|
custom
|
||||||
|
#="{ navigate, isExactActive }"
|
||||||
|
>
|
||||||
|
<FwPill
|
||||||
|
:color="isExactActive ? 'primary' : 'secondary'"
|
||||||
|
@click="navigate"
|
||||||
|
>
|
||||||
{{ tab.label }}
|
{{ tab.label }}
|
||||||
</FwPill>
|
</FwPill>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue'
|
||||||
import { bytesToHumanSize } from '~/ui/composables/bytes';
|
import { bytesToHumanSize } from '~/ui/composables/bytes'
|
||||||
import { useUploadsStore, type UploadGroupEntry } from '~/ui/stores/upload';
|
import { useUploadsStore, type UploadGroupEntry } from '~/ui/stores/upload'
|
||||||
import CoverArt from '~/ui/components/CoverArt.vue'
|
import CoverArt from '~/ui/components/CoverArt.vue'
|
||||||
|
|
||||||
interface Recording {
|
interface Recording {
|
||||||
|
@ -49,20 +49,30 @@ const columns = [
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="allTracks.length === 0" class="flex flex-col items-center py-32">
|
<div
|
||||||
<Icon icon="bi:file-earmark-music" class="h-16 w-16" />
|
v-if="allTracks.length === 0"
|
||||||
|
class="flex flex-col items-center py-32"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon="bi:file-earmark-music"
|
||||||
|
class="h-16 w-16"
|
||||||
|
/>
|
||||||
|
|
||||||
<h3>There is no file in your library</h3>
|
<h3>There is no file in your library</h3>
|
||||||
<p>Try uploading some before coming back here!</p>
|
<p>Try uploading some before coming back here!</p>
|
||||||
</div>
|
</div>
|
||||||
<FwTable v-else
|
<FwTable
|
||||||
|
v-else
|
||||||
id-key="guid"
|
id-key="guid"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:rows="allTracks"
|
:rows="allTracks"
|
||||||
>
|
>
|
||||||
<template #col-title="{ row, value }">
|
<template #col-title="{ row, value }">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<CoverArt :src="row.metadata" class="mr-2" />
|
<CoverArt
|
||||||
|
:src="row.metadata"
|
||||||
|
class="mr-2"
|
||||||
|
/>
|
||||||
{{ value }}
|
{{ value }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from '@iconify/vue';
|
import { Icon } from '@iconify/vue'
|
||||||
import { useUploadsStore, type UploadGroupType } from '~/ui/stores/upload'
|
import { useUploadsStore, type UploadGroupType } from '~/ui/stores/upload'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
@ -15,23 +15,22 @@ const tabs: Tab[] = [
|
||||||
label: 'Music library',
|
label: 'Music library',
|
||||||
icon: 'headphones',
|
icon: 'headphones',
|
||||||
description: 'Host music you listen to.',
|
description: 'Host music you listen to.',
|
||||||
key: 'music-library',
|
key: 'music-library'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Music channel',
|
label: 'Music channel',
|
||||||
icon: 'music-note-beamed',
|
icon: 'music-note-beamed',
|
||||||
description: 'Publish music you make.',
|
description: 'Publish music you make.',
|
||||||
key: 'music-channel',
|
key: 'music-channel'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Podcast channel',
|
label: 'Podcast channel',
|
||||||
icon: 'mic',
|
icon: 'mic',
|
||||||
description: 'Publish podcast you make.',
|
description: 'Publish podcast you make.',
|
||||||
key: 'podcast-channel',
|
key: 'podcast-channel'
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
const currentTab = ref(tabs[0])
|
const currentTab = ref(tabs[0])
|
||||||
|
|
||||||
const uploads = useUploadsStore()
|
const uploads = useUploadsStore()
|
||||||
|
@ -46,7 +45,8 @@ const openLibrary = () => {
|
||||||
|
|
||||||
<div class="flex gap-8">
|
<div class="flex gap-8">
|
||||||
<FwCard
|
<FwCard
|
||||||
v-for="tab in tabs" :key="tab.key"
|
v-for="tab in tabs"
|
||||||
|
:key="tab.key"
|
||||||
:title="tab.label"
|
:title="tab.label"
|
||||||
:class="currentTab.key === tab.key && 'active'"
|
:class="currentTab.key === tab.key && 'active'"
|
||||||
@click="currentTab = tab"
|
@click="currentTab = tab"
|
||||||
|
@ -61,7 +61,9 @@ const openLibrary = () => {
|
||||||
</FwCard>
|
</FwCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FwButton @click="openLibrary">Open library</FwButton>
|
<FwButton @click="openLibrary">
|
||||||
|
Open library
|
||||||
|
</FwButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -5,5 +5,8 @@ const uploads = useUploadsStore()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UploadGroupList :groups="uploads.uploadGroups" :is-uploading="true" />
|
<UploadGroupList
|
||||||
|
:groups="uploads.uploadGroups"
|
||||||
|
:is-uploading="true"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
import { requireLoggedOut, requireLoggedIn } from '~/router/guards'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
path: 'login',
|
||||||
|
name: 'login',
|
||||||
|
component: () => import('~/views/auth/Login.vue'),
|
||||||
|
props: route => ({ next: route.query.next || '/library' }),
|
||||||
|
beforeEnter: requireLoggedOut({ name: 'library.index' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'auth/password/reset',
|
||||||
|
name: 'auth.password-reset',
|
||||||
|
component: () => import('~/views/auth/PasswordReset.vue'),
|
||||||
|
props: route => ({ defaultEmail: route.query.email })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'auth/callback',
|
||||||
|
name: 'auth.callback',
|
||||||
|
component: () => import('~/views/auth/Callback.vue'),
|
||||||
|
props: route => ({
|
||||||
|
code: route.query.code,
|
||||||
|
state: route.query.state
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'auth/email/confirm',
|
||||||
|
name: 'auth.email-confirm',
|
||||||
|
component: () => import('~/views/auth/EmailConfirm.vue'),
|
||||||
|
props: route => ({ defaultKey: route.query.key })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'auth/password/reset/confirm',
|
||||||
|
name: 'auth.password-reset-confirm',
|
||||||
|
component: () => import('~/views/auth/PasswordResetConfirm.vue'),
|
||||||
|
props: route => ({
|
||||||
|
defaultUid: route.query.uid,
|
||||||
|
defaultToken: route.query.token
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'authorize',
|
||||||
|
name: 'authorize',
|
||||||
|
component: () => import('~/components/auth/Authorize.vue'),
|
||||||
|
props: route => ({
|
||||||
|
clientId: route.query.client_id,
|
||||||
|
redirectUri: route.query.redirect_uri,
|
||||||
|
scope: route.query.scope,
|
||||||
|
responseType: route.query.response_type,
|
||||||
|
nonce: route.query.nonce,
|
||||||
|
state: route.query.state
|
||||||
|
}),
|
||||||
|
beforeEnter: requireLoggedIn()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'signup',
|
||||||
|
name: 'signup',
|
||||||
|
component: () => import('~/views/auth/Signup.vue'),
|
||||||
|
props: route => ({ defaultInvitation: route.query.invitation })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'logout',
|
||||||
|
name: 'logout',
|
||||||
|
component: () => import('~/components/auth/Logout.vue')
|
||||||
|
}
|
||||||
|
] as RouteRecordRaw[]
|
|
@ -0,0 +1,41 @@
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
path: 'content',
|
||||||
|
component: () => import('~/views/content/Base.vue'),
|
||||||
|
children: [{
|
||||||
|
path: '',
|
||||||
|
name: 'content.index',
|
||||||
|
component: () => import('~/views/content/Home.vue')
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'content/libraries/tracks',
|
||||||
|
component: () => import('~/views/content/Base.vue'),
|
||||||
|
children: [{
|
||||||
|
path: '',
|
||||||
|
name: 'content.libraries.files',
|
||||||
|
component: () => import('~/views/content/libraries/Files.vue'),
|
||||||
|
props: route => ({ query: route.query.q })
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'content/libraries',
|
||||||
|
component: () => import('~/views/content/Base.vue'),
|
||||||
|
children: [{
|
||||||
|
path: '',
|
||||||
|
name: 'content.libraries.index',
|
||||||
|
component: () => import('~/views/content/libraries/Home.vue')
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'content/remote',
|
||||||
|
component: () => import('~/views/content/Base.vue'),
|
||||||
|
children: [{
|
||||||
|
path: '',
|
||||||
|
name: 'content.remote.index',
|
||||||
|
component: () => import('~/views/content/remote/Home.vue')
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
] as RouteRecordRaw[]
|
|
@ -0,0 +1,139 @@
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
import settings from './settings'
|
||||||
|
import library from './library'
|
||||||
|
import content from './content'
|
||||||
|
import manage from './manage'
|
||||||
|
import auth from './auth'
|
||||||
|
import user from './user'
|
||||||
|
import store from '~/store'
|
||||||
|
import { requireLoggedIn } from '~/router/guards'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'root',
|
||||||
|
component: () => import('~/ui/layouts/constrained.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'index',
|
||||||
|
component: () => import('~/components/Home.vue'),
|
||||||
|
beforeEnter (to, from, next) {
|
||||||
|
if (store.state.auth.authenticated) return next('/library')
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/index.html',
|
||||||
|
redirect: to => {
|
||||||
|
const { hash, query } = to
|
||||||
|
return { name: 'index', hash, query }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'upload',
|
||||||
|
name: 'upload',
|
||||||
|
component: () => import('~/ui/pages/upload.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'upload.index',
|
||||||
|
component: () => import('~/ui/pages/upload/index.vue')
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'running',
|
||||||
|
name: 'upload.running',
|
||||||
|
component: () => import('~/ui/pages/upload/running.vue'),
|
||||||
|
beforeEnter: (_to, _from, next) => {
|
||||||
|
const uploads = useUploadsStore()
|
||||||
|
if (uploads.uploadGroups.length === 0) {
|
||||||
|
next('/ui/upload')
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'history',
|
||||||
|
name: 'upload.history',
|
||||||
|
component: () => import('~/ui/pages/upload/history.vue')
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'all',
|
||||||
|
name: 'upload.all',
|
||||||
|
component: () => import('~/ui/pages/upload/all.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'about',
|
||||||
|
name: 'about',
|
||||||
|
component: () => import('~/components/About.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// TODO (wvffle): Make it a child of /about to have the active style on the sidebar link
|
||||||
|
path: 'about/pod',
|
||||||
|
name: 'about-pod',
|
||||||
|
component: () => import('~/components/AboutPod.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'notifications',
|
||||||
|
name: 'notifications',
|
||||||
|
component: () => import('~/views/Notifications.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'search',
|
||||||
|
name: 'search',
|
||||||
|
component: () => import('~/views/Search.vue')
|
||||||
|
},
|
||||||
|
...auth,
|
||||||
|
...settings,
|
||||||
|
...user,
|
||||||
|
{
|
||||||
|
path: 'favorites',
|
||||||
|
name: 'favorites',
|
||||||
|
component: () => import('~/components/favorites/List.vue'),
|
||||||
|
props: route => ({
|
||||||
|
defaultOrdering: route.query.ordering,
|
||||||
|
defaultPage: route.query.page ? +route.query.page : undefined
|
||||||
|
}),
|
||||||
|
beforeEnter: requireLoggedIn()
|
||||||
|
},
|
||||||
|
...content,
|
||||||
|
...manage,
|
||||||
|
...library,
|
||||||
|
{
|
||||||
|
path: 'channels/:id',
|
||||||
|
props: true,
|
||||||
|
component: () => import('~/views/channels/DetailBase.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'channels.detail',
|
||||||
|
component: () => import('~/views/channels/DetailOverview.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'episodes',
|
||||||
|
name: 'channels.detail.episodes',
|
||||||
|
component: () => import('~/views/channels/DetailEpisodes.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'subscriptions',
|
||||||
|
name: 'subscriptions',
|
||||||
|
component: () => import('~/views/channels/SubscriptionsList.vue'),
|
||||||
|
props: route => ({ defaultQuery: route.query.q })
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
name: '404',
|
||||||
|
component: () => import('~/components/PageNotFound.vue')
|
||||||
|
}
|
||||||
|
] as RouteRecordRaw[]
|
|
@ -0,0 +1,238 @@
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
path: 'library',
|
||||||
|
component: () => import('~/components/library/Library.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: () => import('~/components/library/Home.vue'),
|
||||||
|
name: 'library.index'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'me',
|
||||||
|
component: () => import('~/components/library/Home.vue'),
|
||||||
|
name: 'library.me',
|
||||||
|
props: () => ({ scope: 'me' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'artists/',
|
||||||
|
name: 'library.artists.browse',
|
||||||
|
component: () => import('~/components/library/Artists.vue'),
|
||||||
|
meta: {
|
||||||
|
paginateBy: 30
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'me/artists',
|
||||||
|
name: 'library.artists.me',
|
||||||
|
component: () => import('~/components/library/Artists.vue'),
|
||||||
|
props: { scope: 'me' },
|
||||||
|
meta: {
|
||||||
|
paginateBy: 30
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'albums/',
|
||||||
|
name: 'library.albums.browse',
|
||||||
|
component: () => import('~/components/library/Albums.vue'),
|
||||||
|
meta: {
|
||||||
|
paginateBy: 25
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'me/albums',
|
||||||
|
name: 'library.albums.me',
|
||||||
|
component: () => import('~/components/library/Albums.vue'),
|
||||||
|
props: { scope: 'me' },
|
||||||
|
meta: {
|
||||||
|
paginateBy: 25
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'podcasts/',
|
||||||
|
name: 'library.podcasts.browse',
|
||||||
|
component: () => import('~/components/library/Podcasts.vue'),
|
||||||
|
meta: {
|
||||||
|
paginateBy: 30
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'radios/',
|
||||||
|
name: 'library.radios.browse',
|
||||||
|
component: () => import('~/components/library/Radios.vue'),
|
||||||
|
meta: {
|
||||||
|
paginateBy: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'me/radios/',
|
||||||
|
name: 'library.radios.me',
|
||||||
|
component: () => import('~/components/library/Radios.vue'),
|
||||||
|
props: { scope: 'me' },
|
||||||
|
meta: {
|
||||||
|
paginateBy: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'radios/build',
|
||||||
|
name: 'library.radios.build',
|
||||||
|
component: () => import('~/components/library/radios/Builder.vue'),
|
||||||
|
props: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'radios/build/:id',
|
||||||
|
name: 'library.radios.edit',
|
||||||
|
component: () => import('~/components/library/radios/Builder.vue'),
|
||||||
|
props: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'radios/:id',
|
||||||
|
name: 'library.radios.detail',
|
||||||
|
component: () => import('~/views/radios/Detail.vue'),
|
||||||
|
props: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'playlists/',
|
||||||
|
name: 'library.playlists.browse',
|
||||||
|
component: () => import('~/views/playlists/List.vue'),
|
||||||
|
meta: {
|
||||||
|
paginateBy: 25
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'me/playlists/',
|
||||||
|
name: 'library.playlists.me',
|
||||||
|
component: () => import('~/views/playlists/List.vue'),
|
||||||
|
props: { scope: 'me' },
|
||||||
|
meta: {
|
||||||
|
paginateBy: 25
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'playlists/:id',
|
||||||
|
name: 'library.playlists.detail',
|
||||||
|
component: () => import('~/views/playlists/Detail.vue'),
|
||||||
|
props: route => ({
|
||||||
|
id: route.params.id,
|
||||||
|
defaultEdit: route.query.mode === 'edit'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tags/:id',
|
||||||
|
name: 'library.tags.detail',
|
||||||
|
component: () => import('~/components/library/TagDetail.vue'),
|
||||||
|
props: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'artists/:id',
|
||||||
|
component: () => import('~/components/library/ArtistBase.vue'),
|
||||||
|
props: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'library.artists.detail',
|
||||||
|
component: () => import('~/components/library/ArtistDetail.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit',
|
||||||
|
name: 'library.artists.edit',
|
||||||
|
component: () => import('~/components/library/ArtistEdit.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit/:editId',
|
||||||
|
name: 'library.artists.edit.detail',
|
||||||
|
component: () => import('~/components/library/EditDetail.vue'),
|
||||||
|
props: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'albums/:id',
|
||||||
|
component: () => import('~/components/library/AlbumBase.vue'),
|
||||||
|
props: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'library.albums.detail',
|
||||||
|
component: () => import('~/components/library/AlbumDetail.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit',
|
||||||
|
name: 'library.albums.edit',
|
||||||
|
component: () => import('~/components/library/AlbumEdit.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit/:editId',
|
||||||
|
name: 'library.albums.edit.detail',
|
||||||
|
component: () => import('~/components/library/EditDetail.vue'),
|
||||||
|
props: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tracks/:id',
|
||||||
|
component: () => import('~/components/library/TrackBase.vue'),
|
||||||
|
props: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'library.tracks.detail',
|
||||||
|
component: () => import('~/components/library/TrackDetail.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit',
|
||||||
|
name: 'library.tracks.edit',
|
||||||
|
component: () => import('~/components/library/TrackEdit.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit/:editId',
|
||||||
|
name: 'library.tracks.edit.detail',
|
||||||
|
component: () => import('~/components/library/EditDetail.vue'),
|
||||||
|
props: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'uploads/:id',
|
||||||
|
name: 'library.uploads.detail',
|
||||||
|
props: true,
|
||||||
|
component: () => import('~/components/library/UploadDetail.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// browse a single library via it's uuid
|
||||||
|
path: ':id([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})',
|
||||||
|
props: true,
|
||||||
|
component: () => import('~/views/library/LibraryBase.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'library.detail',
|
||||||
|
component: () => import('~/views/library/DetailOverview.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'albums',
|
||||||
|
name: 'library.detail.albums',
|
||||||
|
component: () => import('~/views/library/DetailAlbums.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tracks',
|
||||||
|
name: 'library.detail.tracks',
|
||||||
|
component: () => import('~/views/library/DetailTracks.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit',
|
||||||
|
name: 'library.detail.edit',
|
||||||
|
component: () => import('~/views/library/Edit.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'upload',
|
||||||
|
name: 'library.detail.upload',
|
||||||
|
redirect: () => '/upload'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
] as RouteRecordRaw[]
|
|
@ -0,0 +1,188 @@
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
import { hasPermissions } from '~/router/guards'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
path: 'manage/settings',
|
||||||
|
name: 'manage.settings',
|
||||||
|
beforeEnter: hasPermissions('settings'),
|
||||||
|
component: () => import('~/views/admin/Settings.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'manage/library',
|
||||||
|
beforeEnter: hasPermissions('library'),
|
||||||
|
component: () => import('~/views/admin/library/Base.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'edits',
|
||||||
|
name: 'manage.library.edits',
|
||||||
|
component: () => import('~/views/admin/library/EditsList.vue'),
|
||||||
|
props: route => ({ defaultQuery: route.query.q })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'artists',
|
||||||
|
name: 'manage.library.artists',
|
||||||
|
component: () => import('~/views/admin/CommonList.vue'),
|
||||||
|
props: route => ({ defaultQuery: route.query.q, type: 'artists' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'artists/:id',
|
||||||
|
name: 'manage.library.artists.detail',
|
||||||
|
component: () => import('~/views/admin/library/ArtistDetail.vue'),
|
||||||
|
props: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'channels',
|
||||||
|
name: 'manage.channels',
|
||||||
|
component: () => import('~/views/admin/CommonList.vue'),
|
||||||
|
props: route => ({ defaultQuery: route.query.q, type: 'channels' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'channels/:id',
|
||||||
|
name: 'manage.channels.detail',
|
||||||
|
component: () => import('~/views/admin/ChannelDetail.vue'),
|
||||||
|
props: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'albums',
|
||||||
|
name: 'manage.library.albums',
|
||||||
|
component: () => import('~/views/admin/CommonList.vue'),
|
||||||
|
props: route => ({ defaultQuery: route.query.q, type: 'albums' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'albums/:id',
|
||||||
|
name: 'manage.library.albums.detail',
|
||||||
|
component: () => import('~/views/admin/library/AlbumDetail.vue'),
|
||||||
|
props: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tracks',
|
||||||
|
name: 'manage.library.tracks',
|
||||||
|
component: () => import('~/views/admin/CommonList.vue'),
|
||||||
|
props: route => ({ defaultQuery: route.query.q, type: 'tracks' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tracks/:id',
|
||||||
|
name: 'manage.library.tracks.detail',
|
||||||
|
component: () => import('~/views/admin/library/TrackDetail.vue'),
|
||||||
|
props: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'libraries',
|
||||||
|
name: 'manage.library.libraries',
|
||||||
|
component: () => import('~/views/admin/CommonList.vue'),
|
||||||
|
props: route => ({ defaultQuery: route.query.q, type: 'libraries' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'libraries/:id',
|
||||||
|
name: 'manage.library.libraries.detail',
|
||||||
|
component: () => import('~/views/admin/library/LibraryDetail.vue'),
|
||||||
|
props: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'uploads',
|
||||||
|
name: 'manage.library.uploads',
|
||||||
|
component: () => import('~/views/admin/CommonList.vue'),
|
||||||
|
props: route => ({ defaultQuery: route.query.q, type: 'uploads' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'uploads/:id',
|
||||||
|
name: 'manage.library.uploads.detail',
|
||||||
|
component: () => import('~/views/admin/library/UploadDetail.vue'),
|
||||||
|
props: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tags',
|
||||||
|
name: 'manage.library.tags',
|
||||||
|
component: () => import('~/views/admin/CommonList.vue'),
|
||||||
|
props: route => ({ defaultQuery: route.query.q, type: 'tags' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tags/:id',
|
||||||
|
name: 'manage.library.tags.detail',
|
||||||
|
component: () => import('~/views/admin/library/TagDetail.vue'),
|
||||||
|
props: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'manage/users',
|
||||||
|
beforeEnter: hasPermissions('settings'),
|
||||||
|
component: () => import('~/views/admin/users/Base.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'users',
|
||||||
|
name: 'manage.users.users.list',
|
||||||
|
component: () => import('~/views/admin/CommonList.vue'),
|
||||||
|
props: route => ({ type: 'users' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'invitations',
|
||||||
|
name: 'manage.users.invitations.list',
|
||||||
|
component: () => import('~/views/admin/CommonList.vue'),
|
||||||
|
props: route => ({ type: 'invitations' })
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'manage/moderation',
|
||||||
|
beforeEnter: hasPermissions('moderation'),
|
||||||
|
component: () => import('~/views/admin/moderation/Base.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'domains',
|
||||||
|
name: 'manage.moderation.domains.list',
|
||||||
|
component: () => import('~/views/admin/moderation/DomainsList.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'domains/:id',
|
||||||
|
name: 'manage.moderation.domains.detail',
|
||||||
|
component: () => import('~/views/admin/moderation/DomainsDetail.vue'),
|
||||||
|
props: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'accounts',
|
||||||
|
name: 'manage.moderation.accounts.list',
|
||||||
|
component: () => import('~/views/admin/CommonList.vue'),
|
||||||
|
props: route => ({ defaultQuery: route.query.q, type: 'accounts' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'accounts/:id',
|
||||||
|
name: 'manage.moderation.accounts.detail',
|
||||||
|
component: () => import('~/views/admin/moderation/AccountsDetail.vue'),
|
||||||
|
props: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'reports',
|
||||||
|
name: 'manage.moderation.reports.list',
|
||||||
|
component: () => import('~/views/admin/moderation/ReportsList.vue'),
|
||||||
|
props: route => ({ defaultQuery: route.query.q }),
|
||||||
|
meta: {
|
||||||
|
paginateBy: 25
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'reports/:id',
|
||||||
|
name: 'manage.moderation.reports.detail',
|
||||||
|
component: () => import('~/views/admin/moderation/ReportDetail.vue'),
|
||||||
|
props: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'requests',
|
||||||
|
name: 'manage.moderation.requests.list',
|
||||||
|
component: () => import('~/views/admin/moderation/RequestsList.vue'),
|
||||||
|
props: route => ({ defaultQuery: route.query.q }),
|
||||||
|
meta: {
|
||||||
|
paginateBy: 25
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'requests/:id',
|
||||||
|
name: 'manage.moderation.requests.detail',
|
||||||
|
component: () => import('~/views/admin/moderation/RequestDetail.vue'),
|
||||||
|
props: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
] as RouteRecordRaw[]
|
|
@ -0,0 +1,30 @@
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
path: 'settings',
|
||||||
|
name: 'settings',
|
||||||
|
component: () => import('~/components/auth/Settings.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'settings/applications/new',
|
||||||
|
name: 'settings.applications.new',
|
||||||
|
props: route => ({
|
||||||
|
scopes: route.query.scopes,
|
||||||
|
name: route.query.name,
|
||||||
|
redirect_uris: route.query.redirect_uris
|
||||||
|
}),
|
||||||
|
component: () => import('~/components/auth/ApplicationNew.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'settings/plugins',
|
||||||
|
name: 'settings.plugins',
|
||||||
|
component: () => import('~/views/auth/Plugins.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'settings/applications/:id/edit',
|
||||||
|
name: 'settings.applications.edit',
|
||||||
|
component: () => import('~/components/auth/ApplicationEdit.vue'),
|
||||||
|
props: true
|
||||||
|
}
|
||||||
|
] as RouteRecordRaw[]
|
|
@ -0,0 +1,50 @@
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
import { useUploadsStore } from '~/ui/stores/upload'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
path: '/ui',
|
||||||
|
name: 'ui',
|
||||||
|
component: () => import('~/ui/layouts/constrained.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'upload',
|
||||||
|
name: 'upload',
|
||||||
|
component: () => import('~/ui/pages/upload.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'upload.index',
|
||||||
|
component: () => import('~/ui/pages/upload/index.vue')
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'running',
|
||||||
|
name: 'upload.running',
|
||||||
|
component: () => import('~/ui/pages/upload/running.vue'),
|
||||||
|
beforeEnter: (_to, _from, next) => {
|
||||||
|
const uploads = useUploadsStore()
|
||||||
|
if (uploads.uploadGroups.length === 0) {
|
||||||
|
next('/ui/upload')
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'history',
|
||||||
|
name: 'upload.history',
|
||||||
|
component: () => import('~/ui/pages/upload/history.vue')
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'all',
|
||||||
|
name: 'upload.all',
|
||||||
|
component: () => import('~/ui/pages/upload/all.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
] as RouteRecordRaw[]
|
|
@ -0,0 +1,34 @@
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
import store from '~/store'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{ suffix: '.full', path: '@:username@:domain' },
|
||||||
|
{ suffix: '', path: '@:username' }
|
||||||
|
].map((route) => {
|
||||||
|
return {
|
||||||
|
path: route.path,
|
||||||
|
name: `profile${route.suffix}`,
|
||||||
|
component: () => import('~/views/auth/ProfileBase.vue'),
|
||||||
|
beforeEnter (to, from, next) {
|
||||||
|
if (!store.state.auth.authenticated && to.query.domain && store.getters['instance/domain'] !== to.query.domain) {
|
||||||
|
return next({ name: 'login', query: { next: to.fullPath } })
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
},
|
||||||
|
props: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: `profile${route.suffix}.overview`,
|
||||||
|
component: () => import('~/views/auth/ProfileOverview.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'activity',
|
||||||
|
name: `profile${route.suffix}.activity`,
|
||||||
|
component: () => import('~/views/auth/ProfileActivity.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}) as RouteRecordRaw[]
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
import { defineStore, acceptHMRUpdate } from 'pinia'
|
import { defineStore, acceptHMRUpdate } from 'pinia'
|
||||||
import { computed, reactive, readonly, ref, markRaw, toRaw, unref, watch } from 'vue'
|
import { computed, reactive, readonly, ref, markRaw, toRaw, unref, watch } from 'vue'
|
||||||
import { whenever, useWebWorker } from '@vueuse/core'
|
import { whenever, useWebWorker } from '@vueuse/core'
|
||||||
|
@ -198,7 +197,7 @@ export const useUploadsStore = defineStore('uploads', () => {
|
||||||
window.addEventListener('beforeunload', (event) => {
|
window.addEventListener('beforeunload', (event) => {
|
||||||
if (isUploading.value) {
|
if (isUploading.value) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
return event.returnValue = 'The upload is still in progress. Are you sure you want to leave?'
|
return (event.returnValue = 'The upload is still in progress. Are you sure you want to leave?')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -212,7 +211,7 @@ export const useUploadsStore = defineStore('uploads', () => {
|
||||||
currentIndex: readonly(currentIndex),
|
currentIndex: readonly(currentIndex),
|
||||||
currentUpload,
|
currentUpload,
|
||||||
queue: readonly(uploadQueue),
|
queue: readonly(uploadQueue),
|
||||||
uploadGroups: uploadGroups,
|
uploadGroups,
|
||||||
createUploadGroup,
|
createUploadGroup,
|
||||||
currentUploadGroup,
|
currentUploadGroup,
|
||||||
progress
|
progress
|
||||||
|
|
|
@ -17,7 +17,6 @@ export interface MetadataParsingFailure {
|
||||||
|
|
||||||
export type MetadataParsingResult = MetadataParsingSuccess | MetadataParsingFailure
|
export type MetadataParsingResult = MetadataParsingSuccess | MetadataParsingFailure
|
||||||
|
|
||||||
|
|
||||||
const parse = async (id: string, file: File) => {
|
const parse = async (id: string, file: File) => {
|
||||||
try {
|
try {
|
||||||
console.log(`[${id}] parsing...`)
|
console.log(`[${id}] parsing...`)
|
||||||
|
|
Loading…
Reference in New Issue