mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
feat: cache routes and deprecate hashbang support (#1521)
This commit is contained in:
parent
5474655e90
commit
63a66bc511
18 changed files with 68 additions and 51 deletions
|
@ -14,8 +14,8 @@
|
|||
<AlbumThumbnail :entity="album"/>
|
||||
|
||||
<footer>
|
||||
<a :href="`#!/album/${album.id}`" class="name" data-testid="name">{{ album.name }}</a>
|
||||
<a v-if="isStandardArtist" :href="`#!/artist/${album.artist_id}`" class="artist">{{ album.artist_name }}</a>
|
||||
<a :href="`#/album/${album.id}`" class="name" data-testid="name">{{ album.name }}</a>
|
||||
<a v-if="isStandardArtist" :href="`#/artist/${album.artist_id}`" class="artist">{{ album.artist_name }}</a>
|
||||
<span v-else class="text-secondary">{{ album.artist_name }}</span>
|
||||
<p class="meta">
|
||||
<span class="left">
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
<footer>
|
||||
<div class="info">
|
||||
<a :href="`#!/artist/${artist.id}`" class="name" data-testid="name">{{ artist.name }}</a>
|
||||
<a :href="`#/artist/${artist.id}`" class="name" data-testid="name">{{ artist.name }}</a>
|
||||
</div>
|
||||
<p class="meta">
|
||||
<span class="left">
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
<icon :icon="faSliders"/>
|
||||
</button>
|
||||
|
||||
<a v-else :class="{ active: viewingQueue }" class="queue control" href="#!/queue">
|
||||
<a v-else :class="{ active: viewingQueue }" class="queue control" href="#/queue">
|
||||
<icon :icon="faListOl"/>
|
||||
</a>
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
<template v-if="song">
|
||||
<h3 class="title">{{ song.title }}</h3>
|
||||
<p class="meta">
|
||||
<a :href="`/#!/artist/${song.artist_id}`" class="artist">{{ song.artist_name }}</a> –
|
||||
<a :href="`/#!/album/${song.album_id}`" class="album">{{ song.album_name }}</a>
|
||||
<a :href="`/#/artist/${song.artist_id}`" class="artist">{{ song.artist_name }}</a> –
|
||||
<a :href="`/#/album/${song.album_id}`" class="album">{{ song.album_name }}</a>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
exports[`renders 1`] = `
|
||||
<div class="other-controls" data-testid="other-controls" data-v-add48cbe="">
|
||||
<div class="wrapper" data-v-add48cbe="">
|
||||
<!--v-if--><button class="control" data-testid="toggle-visualizer-btn" title="Show/hide the visualizer" type="button" data-v-add48cbe=""><br data-testid="icon" icon="[object Object]" data-v-add48cbe=""></button><button title="Like Fahrstuhl to Heaven by Led Zeppelin" class="text-secondary like" data-testid="like-btn" data-v-5d366bb1="" data-v-add48cbe=""><br data-testid="btn-like-unliked" icon="[object Object]" data-v-5d366bb1=""></button><button class="active control text-uppercase" data-testid="toggle-extra-panel-btn" title="View song information" type="button" data-v-add48cbe=""> Info </button><a class="queue control" href="#!/queue" data-v-add48cbe=""><br data-testid="icon" icon="[object Object]" data-v-add48cbe=""></a><br data-testid="RepeatModeSwitch" data-v-add48cbe=""><br data-testid="Volume" data-v-add48cbe="">
|
||||
<!--v-if--><button class="control" data-testid="toggle-visualizer-btn" title="Show/hide the visualizer" type="button" data-v-add48cbe=""><br data-testid="icon" icon="[object Object]" data-v-add48cbe=""></button><button title="Like Fahrstuhl to Heaven by Led Zeppelin" class="text-secondary like" data-testid="like-btn" data-v-5d366bb1="" data-v-add48cbe=""><br data-testid="btn-like-unliked" icon="[object Object]" data-v-5d366bb1=""></button><button class="active control text-uppercase" data-testid="toggle-extra-panel-btn" title="View song information" type="button" data-v-add48cbe=""> Info </button><a class="queue control" href="#/queue" data-v-add48cbe=""><br data-testid="icon" icon="[object Object]" data-v-add48cbe=""></a><br data-testid="RepeatModeSwitch" data-v-add48cbe=""><br data-testid="Volume" data-v-add48cbe="">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -4,7 +4,7 @@ exports[`renders with a song 1`] = `
|
|||
<div class="middle-pane" data-testid="footer-middle-pane" data-v-2ff4ca72="">
|
||||
<div id="progressPane" class="progress" data-v-2ff4ca72="">
|
||||
<h3 class="title" data-v-2ff4ca72="">Fahrstuhl to Heaven</h3>
|
||||
<p class="meta" data-v-2ff4ca72=""><a href="/#!/artist/3" class="artist" data-v-2ff4ca72="">Led Zeppelin</a> – <a href="/#!/album/4" class="album" data-v-2ff4ca72="">Led Zeppelin IV</a></p>
|
||||
<p class="meta" data-v-2ff4ca72=""><a href="/#/artist/3" class="artist" data-v-2ff4ca72="">Led Zeppelin</a> – <a href="/#/album/4" class="album" data-v-2ff4ca72="">Led Zeppelin IV</a></p>
|
||||
<div class="plyr" data-v-2ff4ca72=""><audio controls="" crossorigin="anonymous" data-v-2ff4ca72=""></audio></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
<ul class="menu">
|
||||
<li>
|
||||
<a :class="['home', activeScreen === 'Home' ? 'active' : '']" href="#!/home">
|
||||
<a :class="['home', activeScreen === 'Home' ? 'active' : '']" href="#/home">
|
||||
<icon :icon="faHome" fixed-width/>
|
||||
Home
|
||||
</a>
|
||||
|
@ -16,31 +16,31 @@
|
|||
@dragover="onQueueDragOver"
|
||||
@drop="onQueueDrop"
|
||||
>
|
||||
<a :class="['queue', activeScreen === 'Queue' ? 'active' : '']" href="#!/queue">
|
||||
<a :class="['queue', activeScreen === 'Queue' ? 'active' : '']" href="#/queue">
|
||||
<icon :icon="faListOl" fixed-width/>
|
||||
Current Queue
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['songs', activeScreen === 'Songs' ? 'active' : '']" href="#!/songs">
|
||||
<a :class="['songs', activeScreen === 'Songs' ? 'active' : '']" href="#/songs">
|
||||
<icon :icon="faMusic" fixed-width/>
|
||||
All Songs
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['albums', activeScreen === 'Albums' ? 'active' : '']" href="#!/albums">
|
||||
<a :class="['albums', activeScreen === 'Albums' ? 'active' : '']" href="#/albums">
|
||||
<icon :icon="faCompactDisc" fixed-width/>
|
||||
Albums
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['artists', activeScreen === 'Artists' ? 'active' : '']" href="#!/artists">
|
||||
<a :class="['artists', activeScreen === 'Artists' ? 'active' : '']" href="#/artists">
|
||||
<icon :icon="faMicrophone" fixed-width/>
|
||||
Artists
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="useYouTube">
|
||||
<a :class="['youtube', activeScreen === 'YouTube' ? 'active' : '']" href="#!/youtube">
|
||||
<a :class="['youtube', activeScreen === 'YouTube' ? 'active' : '']" href="#/youtube">
|
||||
<icon :icon="faYoutube" fixed-width/>
|
||||
YouTube Video
|
||||
</a>
|
||||
|
@ -55,19 +55,19 @@
|
|||
|
||||
<ul class="menu">
|
||||
<li>
|
||||
<a :class="['settings', activeScreen === 'Settings' ? 'active' : '']" href="#!/settings">
|
||||
<a :class="['settings', activeScreen === 'Settings' ? 'active' : '']" href="#/settings">
|
||||
<icon :icon="faTools" fixed-width/>
|
||||
Settings
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['upload', activeScreen === 'Upload' ? 'active' : '']" href="#!/upload">
|
||||
<a :class="['upload', activeScreen === 'Upload' ? 'active' : '']" href="#/upload">
|
||||
<icon :icon="faUpload" fixed-width/>
|
||||
Upload
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['users', activeScreen === 'Users' ? 'active' : '']" href="#!/users">
|
||||
<a :class="['users', activeScreen === 'Users' ? 'active' : '']" href="#/users">
|
||||
<icon :icon="faUsers" fixed-width/>
|
||||
Users
|
||||
</a>
|
||||
|
|
|
@ -52,9 +52,9 @@ const isRecentlyPlayedList = (list: PlaylistLike): list is RecentlyPlayedList =>
|
|||
const active = ref(false)
|
||||
|
||||
const url = computed(() => {
|
||||
if (isPlaylist(list.value)) return `#!/playlist/${list.value.id}`
|
||||
if (isFavoriteList(list.value)) return '#!/favorites'
|
||||
if (isRecentlyPlayedList(list.value)) return '#!/recently-played'
|
||||
if (isPlaylist(list.value)) return `#/playlist/${list.value.id}`
|
||||
if (isFavoriteList(list.value)) return '#/favorites'
|
||||
if (isRecentlyPlayedList(list.value)) return '#/recently-played'
|
||||
|
||||
throw new Error('Invalid playlist-like type.')
|
||||
})
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
</template>
|
||||
|
||||
<template v-slot:meta>
|
||||
<a v-if="isNormalArtist" :href="`#!/artist/${album.artist_id}`" class="artist">{{ album.artist_name }}</a>
|
||||
<a v-if="isNormalArtist" :href="`#/artist/${album.artist_id}`" class="artist">{{ album.artist_name }}</a>
|
||||
<span v-else class="nope">{{ album.artist_name }}</span>
|
||||
<span>{{ pluralize(songs, 'song') }}</span>
|
||||
<span>{{ duration }}</span>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<div class="details">
|
||||
<h3>{{ song.title }}</h3>
|
||||
<p class="by text-secondary">
|
||||
<a :href="`#!/artist/${song.artist_id}`">{{ song.artist_name }}</a>
|
||||
<a :href="`#/artist/${song.artist_id}`">{{ song.artist_name }}</a>
|
||||
- {{ pluralize(song.play_count, 'play') }}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
exports[`renders 1`] = `
|
||||
<div class="all-songs song-list-wrap main-scroll-wrap" data-testid="song-list" tabindex="0">
|
||||
<div class="song-list-wrap main-scroll-wrap" data-testid="song-list" tabindex="0">
|
||||
<div class="sortable song-list-header"><span class="track-number" data-testid="header-track-number" role="button" title="Sort by track number"> # <!--v-if--><!--v-if--></span><span class="title" data-testid="header-title" role="button" title="Sort by title"> Title <br data-testid="icon" icon="[object Object]" class="text-highlight"><!--v-if--></span><span class="artist" data-testid="header-artist" role="button" title="Sort by artist"> Artist <!--v-if--><!--v-if--></span><span class="album" data-testid="header-album" role="button" title="Sort by album"> Album <!--v-if--><!--v-if--></span><span class="time" data-testid="header-length" role="button" title="Sort by song duration"><!--v-if--><!--v-if--> <br data-testid="icon" icon="[object Object]" class="duration-header"></span><span class="favorite"></span><span class="play"></span></div><br data-testid="virtual-scroller" item-height="35" items="">
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
exports[`renders 1`] = `<transition-stub data-v-e7b6c7f6=""><button title="Scroll to top" type="button" data-v-e7b6c7f6="" style="display: none;"><br data-testid="icon" icon="[object Object]" data-v-e7b6c7f6=""> Top </button></transition-stub>`;
|
||||
exports[`renders 1`] = `<transition-stub name="fade" appear="false" persisted="true" css="true" data-v-e7b6c7f6=""><button title="Scroll to top" type="button" data-v-e7b6c7f6="" style="display: none;"><br data-testid="icon" icon="[object Object]" data-v-e7b6c7f6=""> Top </button></transition-stub>`;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<span class="profile" id="userBadge" v-if="currentUser">
|
||||
<a class="view-profile" data-testid="view-profile-link" href="/#!/profile" title="View/edit user profile">
|
||||
<a class="view-profile" data-testid="view-profile-link" href="/#/profile" title="View/edit user profile">
|
||||
<img :alt="`Avatar of ${currentUser.name}`" :src="currentUser.avatar" class="avatar"/>
|
||||
<span class="name">{{ currentUser.name }}</span>
|
||||
</a>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
exports[`renders 1`] = `<span class="profile" id="userBadge"><a class="view-profile" data-testid="view-profile-link" href="/#!/profile" title="View/edit user profile"><img alt="Avatar of John Doe" src="https://gravatar.com/foo" class="avatar"><span class="name">John Doe</span></a><a title="Log out" class="logout control" data-testid="btn-logout" href="" role="button"><br data-testid="icon" icon="[object Object]"></a></span>`;
|
||||
exports[`renders 1`] = `<span class="profile" id="userBadge"><a class="view-profile" data-testid="view-profile-link" href="/#/profile" title="View/edit user profile"><img alt="Avatar of John Doe" src="https://gravatar.com/foo" class="avatar"><span class="name">John Doe</span></a><a title="Log out" class="logout control" data-testid="btn-logout" href="" role="button"><br data-testid="icon" icon="[object Object]"></a></span>`;
|
||||
|
|
|
@ -40,9 +40,7 @@ eventBus.on({
|
|||
await userStore.logout()
|
||||
authService.destroy()
|
||||
forceReloadWindow()
|
||||
},
|
||||
|
||||
KOEL_READY: () => router.resolve()
|
||||
}
|
||||
})
|
||||
|
||||
router.onRouteChanged(() => {
|
||||
|
|
|
@ -16,6 +16,7 @@ export type Route = {
|
|||
|
||||
type RouteChangedHandler = (newRoute: Route, oldRoute: Route | undefined) => any
|
||||
|
||||
// @TODO: Remove support for hashbang (#!) and only support hash (#)
|
||||
export default class Router {
|
||||
public $currentRoute: Ref<Route>
|
||||
|
||||
|
@ -23,12 +24,13 @@ export default class Router {
|
|||
private readonly homeRoute: Route
|
||||
private readonly notFoundRoute: Route
|
||||
private routeChangedHandlers: RouteChangedHandler[] = []
|
||||
private cache: Map<string, { route: Route, params: RouteParams }> = new Map()
|
||||
|
||||
constructor (routes: Route[]) {
|
||||
this.routes = routes
|
||||
this.homeRoute = routes.find(route => route.screen === 'Home')!
|
||||
this.notFoundRoute = routes.find(route => route.screen === '404')!
|
||||
this.$currentRoute = ref<Route>(this.homeRoute)
|
||||
this.$currentRoute = ref(this.homeRoute)
|
||||
|
||||
watch(
|
||||
this.$currentRoute,
|
||||
|
@ -43,32 +45,49 @@ export default class Router {
|
|||
}
|
||||
|
||||
public async resolve () {
|
||||
if (!location.hash || location.hash === '#!/') {
|
||||
if (!location.hash || location.hash === '#/' || location.hash === '#!/') {
|
||||
return this.activateRoute(this.homeRoute)
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.routes.length; i++) {
|
||||
const route = this.routes[i]
|
||||
const matches = location.hash.match(new RegExp(`^#!${route.path}/?(?:\\?(.*))?$`))
|
||||
const matched = this.tryMatchRoute(location.hash)
|
||||
const [route, params] = matched ? [matched.route, matched.params] : [null, null]
|
||||
|
||||
if (matches) {
|
||||
const searchParams = new URLSearchParams(new URL(location.href.replace('#!/', '')).search)
|
||||
const routeParams = Object.assign(Object.fromEntries(searchParams.entries()), matches.groups || {})
|
||||
if (!route) {
|
||||
return this.triggerNotFound()
|
||||
}
|
||||
|
||||
if (route.onBeforeEnter && route.onBeforeEnter(routeParams) === false) {
|
||||
return this.triggerNotFound()
|
||||
if (route.onBeforeEnter && route.onBeforeEnter(params) === false) {
|
||||
return this.triggerNotFound()
|
||||
}
|
||||
|
||||
if (route.redirect) {
|
||||
const to = route.redirect(params)
|
||||
return typeof to === 'string' ? this.go(to) : this.activateRoute(to, params)
|
||||
}
|
||||
|
||||
return this.activateRoute(route, params)
|
||||
}
|
||||
|
||||
private tryMatchRoute (hash: string) {
|
||||
if (!this.cache.has(hash)) {
|
||||
for (let i = 0; i < this.routes.length; i++) {
|
||||
const route = this.routes[i]
|
||||
const matches = hash.match(new RegExp(`^#!?${route.path}/?(?:\\?(.*))?$`))
|
||||
|
||||
if (matches) {
|
||||
const searchParams = new URLSearchParams(new URL(location.href.replace(/#!?\?/, '')).search)
|
||||
|
||||
this.cache.set(hash, {
|
||||
route,
|
||||
params: Object.assign(Object.fromEntries(searchParams.entries()), matches.groups || {})
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if (route.redirect) {
|
||||
const to = route.redirect(routeParams)
|
||||
return typeof to === 'string' ? this.go(to) : this.activateRoute(to, routeParams)
|
||||
}
|
||||
|
||||
return this.activateRoute(route, routeParams)
|
||||
}
|
||||
}
|
||||
|
||||
await this.triggerNotFound()
|
||||
return this.cache.get(hash)
|
||||
}
|
||||
|
||||
public async triggerNotFound () {
|
||||
|
@ -93,8 +112,8 @@ export default class Router {
|
|||
path = `/${path}`
|
||||
}
|
||||
|
||||
if (!path.startsWith('/#!')) {
|
||||
path = `/#!${path}`
|
||||
if (!path.startsWith('/#')) {
|
||||
path = `/#${path}`
|
||||
}
|
||||
|
||||
path = path.substring(1, path.length)
|
||||
|
|
|
@ -158,7 +158,7 @@ new class extends UnitTestCase {
|
|||
|
||||
it('gets shareable URL', () => {
|
||||
const song = factory<Song>('song', { id: 'foo' })
|
||||
expect(songStore.getShareableUrl(song)).toBe('http://localhost/#!/song/foo')
|
||||
expect(songStore.getShareableUrl(song)).toBe('http://localhost/#/song/foo')
|
||||
})
|
||||
|
||||
it('syncs with the vault', () => {
|
||||
|
|
|
@ -117,7 +117,7 @@ export const songStore = {
|
|||
: `${commonStore.state.cdn_url}play/${song.id}?api_token=${authService.getToken()}`
|
||||
},
|
||||
|
||||
getShareableUrl: (song: Song) => `${window.BASE_URL}#!/song/${song.id}`,
|
||||
getShareableUrl: (song: Song) => `${window.BASE_URL}#/song/${song.id}`,
|
||||
|
||||
syncWithVault (songs: Song | Song[]) {
|
||||
return arrayify(songs).map(song => {
|
||||
|
|
Loading…
Reference in a new issue