feat: create router for UI v2

This commit is contained in:
Kasper Seweryn 2024-01-25 00:38:38 +01:00 committed by upsiflu
parent 1c10a5b257
commit 59cd41d331
27 changed files with 1198 additions and 147 deletions

View File

@ -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>

View File

@ -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 ?? '/'),

View File

@ -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',

View File

@ -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')
} }
] ]

View File

@ -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>

View File

@ -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;

View File

@ -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);

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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]}`
} }

View File

@ -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 }))
}) })
} }

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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[]

View File

@ -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[]

View File

@ -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[]

View File

@ -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[]

View File

@ -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[]

View File

@ -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[]

50
front/src/ui/routes/ui.ts Normal file
View File

@ -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[]

View File

@ -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[]

View File

@ -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

View File

@ -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...`)