forked from Mirrors/swingmusic-webclient
first draft
This commit is contained in:
4
src/assets/icons/pencil.svg
Normal file
4
src/assets/icons/pencil.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.42607 18.5857L16.593 5.42412L14.344 3.16546L1.1674 16.3366L0.0267015 19.0816C-0.10197 19.4303 0.258496 19.8049 0.592479 19.6708L3.42607 18.5857ZM17.715 4.32139L18.9829 3.07476C19.6122 2.44546 19.6378 1.7482 19.0703 1.16906L18.6128 0.709452C18.0454 0.139922 17.3439 0.200625 16.7125 0.808593L15.4467 2.06273L17.715 4.32139Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 439 B |
@@ -22,6 +22,12 @@
|
||||
@hideModal="hideModal"
|
||||
@setTitle="setTitle"
|
||||
/>
|
||||
<CrudPage
|
||||
v-if="modal.component == modal.options.page"
|
||||
@hideModal="hideModal"
|
||||
@setTitle="setTitle"
|
||||
v-bind="modal.props"
|
||||
/>
|
||||
<UpdatePlaylist
|
||||
v-if="modal.component == modal.options.updatePlaylist"
|
||||
v-bind="modal.props"
|
||||
@@ -49,6 +55,7 @@ import { useRouter } from 'vue-router'
|
||||
|
||||
import AuthLogin from './modals/AuthLogin.vue'
|
||||
import ConfirmModal from './modals/ConfirmModal.vue'
|
||||
import CrudPage from './modals/CrudPage.vue'
|
||||
import NewPlaylist from './modals/NewPlaylist.vue'
|
||||
import RootDirsPrompt from './modals/RootDirsPrompt.vue'
|
||||
import SetRootDirs from './modals/SetRootDirs.vue'
|
||||
|
||||
66
src/components/modals/CrudPage.vue
Normal file
66
src/components/modals/CrudPage.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<form class="playlist-modal" @submit.prevent="submit">
|
||||
<label for="name">Page name</label>
|
||||
<br />
|
||||
<input type="search" class="rounded-sm" id="name" :value="page?.name" />
|
||||
<br />
|
||||
<label for="description">Description</label>
|
||||
<br />
|
||||
<input type="search" class="rounded-sm" id="description" :value="page?.extra.description" />
|
||||
<br /><br />
|
||||
<button type="submit">{{ page ? 'Update' : 'Create' }}</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Page } from '@/interfaces'
|
||||
import { createNewPage, updatePage } from '@/requests/pages'
|
||||
import { NotifType, Notification } from '@/stores/notification'
|
||||
|
||||
const props = defineProps<{
|
||||
page?: Page
|
||||
hash?: string
|
||||
type?: string
|
||||
extra?: any
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'hideModal'): void
|
||||
(e: 'setTitle', title: string): void
|
||||
}>()
|
||||
|
||||
emit('setTitle', props.page ? 'Update Page' : 'New Page')
|
||||
|
||||
async function submit(e: Event) {
|
||||
e.preventDefault()
|
||||
const name = (e.target as any).elements['name'].value
|
||||
const description = (e.target as any).elements['description'].value
|
||||
|
||||
// If the page is null, we are creating a new page
|
||||
if (props.page == null) {
|
||||
const created = await createNewPage(name, description, [
|
||||
{
|
||||
hash: props.hash as string,
|
||||
type: props.type as string,
|
||||
extra: props.extra,
|
||||
},
|
||||
])
|
||||
|
||||
if (created) {
|
||||
new Notification('New page created', NotifType.Success)
|
||||
emit('hideModal')
|
||||
}
|
||||
} else {
|
||||
const updatedPage = await updatePage(props.page, name, description)
|
||||
|
||||
if (updatedPage) {
|
||||
props.page.name = updatedPage.name
|
||||
props.page.extra.description = updatedPage.extra.description
|
||||
new Notification('Page updated', NotifType.Success)
|
||||
emit('hideModal')
|
||||
}
|
||||
}
|
||||
|
||||
// If the page is not null, we are updating the page
|
||||
}
|
||||
</script>
|
||||
@@ -1,36 +1,66 @@
|
||||
<template>
|
||||
<div v-if="type == 'album'" class="cardlistrow">
|
||||
<AlbumCard v-for="item in items" :key="item.albumhash" class="hlistitem" :album="(item as Album)" />
|
||||
</div>
|
||||
<div v-else-if="type == 'artist'" class="cardlistrow">
|
||||
<ArtistCard v-for="item in items" :key="item.artisthash" class="hlistitem" :artist="(item as Artist)" />
|
||||
</div>
|
||||
<div v-else-if="type == 'mix'" class="cardlistrow">
|
||||
<MixCard v-for="item in items" :key="item.sourcehash" class="hlistitem" :mix="(item as Mix)" />
|
||||
</div>
|
||||
<div class="cardlistrow">
|
||||
<component v-for="item in items" :key="item.key" :is="item.component" v-bind="item.props" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Album, Artist, Mix } from "@/interfaces";
|
||||
import AlbumCard from "./AlbumCard.vue";
|
||||
import ArtistCard from "./ArtistCard.vue";
|
||||
import MixCard from "../Mixes/MixCard.vue";
|
||||
import { Album, Artist, Mix } from '@/interfaces'
|
||||
import AlbumCard from './AlbumCard.vue'
|
||||
import ArtistCard from './ArtistCard.vue'
|
||||
import MixCard from '../Mixes/MixCard.vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
type: string | "album" | "artist" | "mix";
|
||||
items: Album[] | Artist[] | Mix[];
|
||||
}>();
|
||||
const props = defineProps<{
|
||||
items: Album[] | Artist[] | Mix[]
|
||||
}>()
|
||||
|
||||
const items = computed(() => {
|
||||
return props.items.map((item: any) => {
|
||||
const i = {
|
||||
component: <any>null,
|
||||
props: {},
|
||||
key: '',
|
||||
}
|
||||
|
||||
switch (item['type']) {
|
||||
case 'album':
|
||||
i.component = AlbumCard
|
||||
i.key = item.albumhash
|
||||
i.props = {
|
||||
album: item,
|
||||
}
|
||||
break
|
||||
case 'artist':
|
||||
i.component = ArtistCard
|
||||
i.key = item.artisthash
|
||||
i.props = {
|
||||
artist: item,
|
||||
}
|
||||
break
|
||||
case 'mix':
|
||||
i.component = MixCard
|
||||
i.key = item.sourcehash
|
||||
i.props = {
|
||||
mix: item,
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return i
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.cardlistrow {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax($cardwidth, 1fr));
|
||||
padding-bottom: 2rem;
|
||||
z-index: -1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax($cardwidth, 1fr));
|
||||
padding-bottom: 2rem;
|
||||
z-index: -1;
|
||||
|
||||
@include mediumPhones {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
|
||||
}
|
||||
@include mediumPhones {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -30,6 +30,12 @@
|
||||
grid-template-columns: 1fr max-content;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.after {
|
||||
margin-top: 2rem;
|
||||
margin-left: -$medium;
|
||||
|
||||
@@ -85,6 +85,9 @@ export const paths = {
|
||||
return this.base + '/artists'
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
base: base_url + '/pages',
|
||||
},
|
||||
search: {
|
||||
base: base_url + '/search',
|
||||
get top() {
|
||||
|
||||
@@ -1,56 +1,66 @@
|
||||
import useModal from "@/stores/modal";
|
||||
import useAlbum from "@/stores/pages/album";
|
||||
import useTracklist from "@/stores/queue/tracklist";
|
||||
import useModal from '@/stores/modal'
|
||||
import useAlbum from '@/stores/pages/album'
|
||||
import useTracklist from '@/stores/queue/tracklist'
|
||||
|
||||
import { Option, Playlist } from "@/interfaces";
|
||||
import { addAlbumToPlaylist } from "@/requests/playlists";
|
||||
import { getAddToPlaylistOptions, get_find_on_social } from "./utils";
|
||||
import { AddToQueueIcon, PlayNextIcon, PlaylistIcon, PlusIcon } from "@/icons";
|
||||
import { Option, Page, Playlist } from '@/interfaces'
|
||||
import { addAlbumToPlaylist } from '@/requests/playlists'
|
||||
import { getAddToPageOptions, getAddToPlaylistOptions, get_find_on_social } from './utils'
|
||||
import { AddToQueueIcon, PlayNextIcon, PlaylistIcon, PlusIcon } from '@/icons'
|
||||
import { addItemToPage } from '@/requests/pages'
|
||||
|
||||
export default async () => {
|
||||
const album = useAlbum();
|
||||
const album = useAlbum()
|
||||
|
||||
const play_next = <Option>{
|
||||
label: "Play next",
|
||||
action: () => {
|
||||
const tracks = album.tracks.filter(
|
||||
(track) => !track.is_album_disc_number
|
||||
);
|
||||
useTracklist().insertAfterCurrent(tracks);
|
||||
},
|
||||
icon: PlayNextIcon,
|
||||
};
|
||||
const play_next = <Option>{
|
||||
label: 'Play next',
|
||||
action: () => {
|
||||
const tracks = album.tracks.filter(track => !track.is_album_disc_number)
|
||||
useTracklist().insertAfterCurrent(tracks)
|
||||
},
|
||||
icon: PlayNextIcon,
|
||||
}
|
||||
|
||||
const add_to_queue = <Option>{
|
||||
label: "Add to queue",
|
||||
action: () => {
|
||||
const tracks = album.tracks.filter(
|
||||
(track) => !track.is_album_disc_number
|
||||
);
|
||||
useTracklist().addTracks(tracks);
|
||||
},
|
||||
icon: AddToQueueIcon,
|
||||
};
|
||||
const add_to_queue = <Option>{
|
||||
label: 'Add to queue',
|
||||
action: () => {
|
||||
const tracks = album.tracks.filter(track => !track.is_album_disc_number)
|
||||
useTracklist().addTracks(tracks)
|
||||
},
|
||||
icon: AddToQueueIcon,
|
||||
}
|
||||
|
||||
// Action for each playlist option
|
||||
const AddToPlaylistAction = (playlist: Playlist) => {
|
||||
const store = album;
|
||||
addAlbumToPlaylist(playlist, store.info.albumhash);
|
||||
};
|
||||
// Action for each playlist option
|
||||
const AddToPlaylistAction = (playlist: Playlist) => {
|
||||
const store = album
|
||||
addAlbumToPlaylist(playlist, store.info.albumhash)
|
||||
}
|
||||
|
||||
const add_to_playlist: Option = {
|
||||
label: "Add to Playlist",
|
||||
children: () => getAddToPlaylistOptions(AddToPlaylistAction, {
|
||||
albumhash: album.info.albumhash,
|
||||
playlist_name: album.info.title,
|
||||
}),
|
||||
icon: PlusIcon,
|
||||
};
|
||||
const add_to_playlist: Option = {
|
||||
label: 'Add to Playlist',
|
||||
children: () =>
|
||||
getAddToPlaylistOptions(AddToPlaylistAction, {
|
||||
albumhash: album.info.albumhash,
|
||||
playlist_name: album.info.title,
|
||||
}),
|
||||
icon: PlusIcon,
|
||||
}
|
||||
|
||||
return [
|
||||
play_next,
|
||||
add_to_queue,
|
||||
add_to_playlist,
|
||||
get_find_on_social(),
|
||||
];
|
||||
};
|
||||
const addToPageAction = (page: Page) => {
|
||||
const store = album
|
||||
addItemToPage(page, store.info, 'album')
|
||||
}
|
||||
|
||||
const add_to_page: Option = {
|
||||
label: 'Add to Page',
|
||||
children: () =>
|
||||
getAddToPageOptions(addToPageAction, {
|
||||
page: null,
|
||||
hash: album.info.albumhash,
|
||||
type: 'album',
|
||||
extra: {},
|
||||
}),
|
||||
icon: PlusIcon,
|
||||
}
|
||||
|
||||
return [play_next, add_to_queue, add_to_playlist, add_to_page, get_find_on_social()]
|
||||
}
|
||||
|
||||
@@ -3,16 +3,15 @@ import useAlbum from '@/stores/pages/album'
|
||||
import useArtist from '@/stores/pages/artist'
|
||||
|
||||
import { SearchIcon } from '@/icons'
|
||||
import { Option, Playlist } from '@/interfaces'
|
||||
import { Option, Page, Playlist } from '@/interfaces'
|
||||
import { getAllPlaylists } from '@/requests/playlists'
|
||||
import { getAllPages } from '@/requests/pages'
|
||||
|
||||
export const separator: Option = {
|
||||
type: 'separator',
|
||||
}
|
||||
|
||||
export function get_new_playlist_option(
|
||||
new_playlist_modal_props: any = {}
|
||||
): Option {
|
||||
export function get_new_playlist_option(new_playlist_modal_props: any = {}): Option {
|
||||
return {
|
||||
label: 'New playlist',
|
||||
action: () => {
|
||||
@@ -21,6 +20,15 @@ export function get_new_playlist_option(
|
||||
}
|
||||
}
|
||||
|
||||
export function get_new_page_option(new_playlist_modal_props: any = {}): Option {
|
||||
return {
|
||||
label: 'New page',
|
||||
action: () => {
|
||||
modal().showNewPageModal(new_playlist_modal_props)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type action = (playlist: Playlist) => void
|
||||
|
||||
/**
|
||||
@@ -29,10 +37,7 @@ type action = (playlist: Playlist) => void
|
||||
* @param new_playlist_modal_props Props to be passed to the modal when creating a new playlist
|
||||
* @returns A list of options to be used in a context menu
|
||||
*/
|
||||
export async function getAddToPlaylistOptions(
|
||||
addToPlaylist: action,
|
||||
new_playlist_modal_props: any = {}
|
||||
) {
|
||||
export async function getAddToPlaylistOptions(addToPlaylist: action, new_playlist_modal_props: any = {}) {
|
||||
const new_playlist = get_new_playlist_option(new_playlist_modal_props)
|
||||
const p = await getAllPlaylists(true)
|
||||
|
||||
@@ -44,7 +49,7 @@ export async function getAddToPlaylistOptions(
|
||||
|
||||
let playlists = <Option[]>[]
|
||||
|
||||
playlists = p.map((playlist) => {
|
||||
playlists = p.map(playlist => {
|
||||
return <Option>{
|
||||
label: playlist.name,
|
||||
action: () => {
|
||||
@@ -56,20 +61,44 @@ export async function getAddToPlaylistOptions(
|
||||
return [...items, separator, ...playlists]
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param addToPlaylist Function to be called when a playlist is selected
|
||||
* @param new_playlist_modal_props Props to be passed to the modal when creating a new playlist
|
||||
* @returns A list of options to be used in a context menu
|
||||
*/
|
||||
export async function getAddToPageOptions(addToPage: (page: Page) => void, new_page_modal_props: any = {}) {
|
||||
const new_page = get_new_page_option(new_page_modal_props)
|
||||
const p = await getAllPages()
|
||||
|
||||
let items = [new_page]
|
||||
|
||||
if (p.length === 0) {
|
||||
return items
|
||||
}
|
||||
|
||||
let pages = <Option[]>[]
|
||||
|
||||
pages = p.map(playlist => {
|
||||
return <Option>{
|
||||
label: playlist.name,
|
||||
action: () => {
|
||||
addToPage(playlist)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
return [...items, separator, ...pages]
|
||||
}
|
||||
|
||||
export const get_find_on_social = (page = 'album', query = '') => {
|
||||
const is_album = page === 'album'
|
||||
const getAlbumSearchTerm = () => {
|
||||
const store = useAlbum()
|
||||
|
||||
return `${store.info.title} - ${store.info.albumartists
|
||||
.map((a) => a.name)
|
||||
.join(', ')}`
|
||||
return `${store.info.title} - ${store.info.albumartists.map(a => a.name).join(', ')}`
|
||||
}
|
||||
const search_term = query
|
||||
? query
|
||||
: is_album
|
||||
? getAlbumSearchTerm()
|
||||
: useArtist().info.name
|
||||
const search_term = query ? query : is_album ? getAlbumSearchTerm() : useArtist().info.name
|
||||
|
||||
return <Option>{
|
||||
label: 'Search on',
|
||||
@@ -77,67 +106,36 @@ export const get_find_on_social = (page = 'album', query = '') => {
|
||||
children: async () => [
|
||||
{
|
||||
label: 'Google',
|
||||
action: () =>
|
||||
window.open(
|
||||
`https://www.google.com/search?q=${search_term}`,
|
||||
'_blank'
|
||||
),
|
||||
action: () => window.open(`https://www.google.com/search?q=${search_term}`, '_blank'),
|
||||
},
|
||||
{
|
||||
label: 'YouTube',
|
||||
action: () =>
|
||||
window.open(
|
||||
`https://www.youtube.com/results?search_query=${search_term}`,
|
||||
'_blank'
|
||||
),
|
||||
action: () => window.open(`https://www.youtube.com/results?search_query=${search_term}`, '_blank'),
|
||||
},
|
||||
{
|
||||
label: 'Spotify',
|
||||
action: () =>
|
||||
window.open(
|
||||
`https://open.spotify.com/search/${search_term}/${page}s`,
|
||||
'_blank'
|
||||
),
|
||||
action: () => window.open(`https://open.spotify.com/search/${search_term}/${page}s`, '_blank'),
|
||||
},
|
||||
{
|
||||
label: 'Tidal',
|
||||
action: () =>
|
||||
window.open(
|
||||
`https://listen.tidal.com/search/${page}s?q=${search_term}`,
|
||||
'_blank'
|
||||
),
|
||||
action: () => window.open(`https://listen.tidal.com/search/${page}s?q=${search_term}`, '_blank'),
|
||||
},
|
||||
{
|
||||
label: 'Apple Music',
|
||||
action: () =>
|
||||
window.open(
|
||||
`https://music.apple.com/search?term=${search_term}`,
|
||||
'_blank'
|
||||
),
|
||||
action: () => window.open(`https://music.apple.com/search?term=${search_term}`, '_blank'),
|
||||
},
|
||||
{
|
||||
label: 'Deezer',
|
||||
action: () =>
|
||||
window.open(
|
||||
`https://www.deezer.com/search/${search_term}/${page}`,
|
||||
'_blank'
|
||||
),
|
||||
action: () => window.open(`https://www.deezer.com/search/${search_term}/${page}`, '_blank'),
|
||||
},
|
||||
{
|
||||
label: 'Wikipedia',
|
||||
action: () =>
|
||||
window.open(
|
||||
`https://en.wikipedia.org/wiki/Special:Search?search=${search_term}`,
|
||||
'_blank'
|
||||
),
|
||||
window.open(`https://en.wikipedia.org/wiki/Special:Search?search=${search_term}`, '_blank'),
|
||||
},
|
||||
{
|
||||
label: 'Last.fm',
|
||||
action: () =>
|
||||
window.open(
|
||||
`https://www.last.fm/search/${page}s?q=${search_term}`,
|
||||
'_blank'
|
||||
),
|
||||
action: () => window.open(`https://www.last.fm/search/${page}s?q=${search_term}`, '_blank'),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -179,6 +179,15 @@ export interface Playlist {
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface Page {
|
||||
id: number
|
||||
name: string
|
||||
items: (Album | Artist | Mix | Playlist)[]
|
||||
extra: {
|
||||
description: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface Radio {
|
||||
name: string
|
||||
image: string
|
||||
|
||||
107
src/requests/pages.ts
Normal file
107
src/requests/pages.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { Album, Artist, Mix, Page, Playlist } from '@/interfaces'
|
||||
import useAxios from './useAxios'
|
||||
import { paths } from '@/config'
|
||||
|
||||
const { base: basePageUrl } = paths.api.pages
|
||||
|
||||
export async function getAllPages() {
|
||||
const { data, status } = await useAxios({
|
||||
url: basePageUrl,
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (status == 200) {
|
||||
return data as Page[]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export async function getPage(page_id: string) {
|
||||
const { data, status } = await useAxios({
|
||||
url: basePageUrl + `/${page_id}`,
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
return data as Page
|
||||
}
|
||||
|
||||
export async function createNewPage(
|
||||
name: string,
|
||||
description: string,
|
||||
items?: { hash: string; type: string; extra: any }[]
|
||||
) {
|
||||
const { data, status } = await useAxios({
|
||||
url: basePageUrl,
|
||||
props: {
|
||||
name,
|
||||
description,
|
||||
items,
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (status == 201) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export async function updatePage(page: Page, name: string, description: string) {
|
||||
const { data, status } = await useAxios({
|
||||
url: basePageUrl + `/${page.id}`,
|
||||
props: {
|
||||
name,
|
||||
description,
|
||||
},
|
||||
method: 'PUT',
|
||||
})
|
||||
|
||||
if (status == 200) {
|
||||
return data.page as Page
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export async function addItemToPage(page: Page, item: Album | Artist | Mix | Playlist, type: string) {
|
||||
const payload = {
|
||||
type: type,
|
||||
hash: '',
|
||||
extra: {},
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'album':
|
||||
payload.hash = (item as Album).albumhash
|
||||
break
|
||||
case 'artist':
|
||||
payload.hash = (item as Artist).artisthash
|
||||
break
|
||||
case 'mix':
|
||||
payload.hash = (item as Mix).sourcehash
|
||||
break
|
||||
case 'playlist':
|
||||
payload.hash = (item as Playlist).id.toString()
|
||||
break
|
||||
}
|
||||
|
||||
if (payload.hash === '') {
|
||||
throw new Error('Invalid item type. Item not added to page.')
|
||||
}
|
||||
|
||||
const { data, status } = await useAxios({
|
||||
url: basePageUrl + `/${page.id}/items`,
|
||||
props: {
|
||||
items: [payload],
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (status == 200) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -28,6 +28,7 @@ const FavoriteCardScroller = () => import("@/views/FavoriteCardScroller.vue");
|
||||
const StatsView = () => import("@/views/Stats/main.vue");
|
||||
const MixView = () => import("@/views/MixView.vue");
|
||||
const MixListView = () => import("@/views/MixListView.vue");
|
||||
const Page = () => import("@/views/Pages/Page.vue");
|
||||
|
||||
const folder = {
|
||||
path: "/folder/:path",
|
||||
@@ -202,6 +203,12 @@ const MixList = {
|
||||
component: MixListView,
|
||||
};
|
||||
|
||||
const PageView = {
|
||||
path: "/pages/:page",
|
||||
name: "Page",
|
||||
component: Page,
|
||||
};
|
||||
|
||||
const routes = [
|
||||
folder,
|
||||
playlists,
|
||||
@@ -225,6 +232,7 @@ const routes = [
|
||||
Stats,
|
||||
Mix,
|
||||
MixList,
|
||||
PageView,
|
||||
];
|
||||
|
||||
const Routes = {
|
||||
@@ -250,6 +258,7 @@ const Routes = {
|
||||
Stats: Stats.name,
|
||||
Mix: Mix.name,
|
||||
MixList: MixList.name,
|
||||
Page: PageView.name,
|
||||
};
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -1,86 +1,90 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export enum ModalOptions {
|
||||
newPlaylist,
|
||||
updatePlaylist,
|
||||
deletePlaylist,
|
||||
SetIP,
|
||||
rootDirsPrompt,
|
||||
setRootDirs,
|
||||
saveFolderAsPlaylist,
|
||||
login,
|
||||
settings
|
||||
newPlaylist,
|
||||
page,
|
||||
updatePlaylist,
|
||||
deletePlaylist,
|
||||
SetIP,
|
||||
rootDirsPrompt,
|
||||
setRootDirs,
|
||||
saveFolderAsPlaylist,
|
||||
login,
|
||||
settings,
|
||||
}
|
||||
|
||||
export default defineStore("newModal", {
|
||||
state: () => ({
|
||||
title: "",
|
||||
options: ModalOptions,
|
||||
component: <any>null,
|
||||
props: <any>{},
|
||||
visible: false,
|
||||
}),
|
||||
actions: {
|
||||
showModal(modalOption: ModalOptions, props: any = {}) {
|
||||
this.component = modalOption;
|
||||
this.visible = true;
|
||||
this.props = props;
|
||||
export default defineStore('newModal', {
|
||||
state: () => ({
|
||||
title: '',
|
||||
options: ModalOptions,
|
||||
component: <any>null,
|
||||
props: <any>{},
|
||||
visible: false,
|
||||
}),
|
||||
actions: {
|
||||
showModal(modalOption: ModalOptions, props: any = {}) {
|
||||
this.component = modalOption
|
||||
this.visible = true
|
||||
this.props = props
|
||||
},
|
||||
showNewPlaylistModal(props: any = {}) {
|
||||
this.showModal(ModalOptions.newPlaylist, props)
|
||||
},
|
||||
showNewPageModal(props: any = {}) {
|
||||
this.showModal(ModalOptions.page, props)
|
||||
},
|
||||
showSaveFolderAsPlaylistModal(path: string) {
|
||||
const playlist_name = path.split('/').pop()
|
||||
const props = {
|
||||
playlist_name,
|
||||
path,
|
||||
}
|
||||
this.showModal(ModalOptions.newPlaylist, props)
|
||||
},
|
||||
showSaveArtistAsPlaylistModal(name: string, artisthash: string) {
|
||||
const props = {
|
||||
artisthash,
|
||||
playlist_name: `This is ${name}`,
|
||||
}
|
||||
this.showModal(ModalOptions.newPlaylist, props)
|
||||
},
|
||||
showSaveQueueAsPlaylistModal(name: string) {
|
||||
const props = {
|
||||
is_queue: true,
|
||||
playlist_name: name,
|
||||
}
|
||||
this.showModal(ModalOptions.newPlaylist, props)
|
||||
},
|
||||
showEditPlaylistModal() {
|
||||
this.showModal(ModalOptions.updatePlaylist)
|
||||
},
|
||||
showDeletePlaylistModal(pid: number) {
|
||||
const props = {
|
||||
pid: pid,
|
||||
}
|
||||
this.showModal(ModalOptions.deletePlaylist, props)
|
||||
},
|
||||
showSetIPModal() {
|
||||
this.showModal(ModalOptions.SetIP)
|
||||
},
|
||||
showRootDirsPromptModal() {
|
||||
this.showModal(ModalOptions.rootDirsPrompt)
|
||||
},
|
||||
showSetRootDirsModal() {
|
||||
this.showModal(ModalOptions.setRootDirs)
|
||||
},
|
||||
showLoginModal() {
|
||||
this.showModal(ModalOptions.login)
|
||||
},
|
||||
showSettingsModal() {
|
||||
this.showModal(ModalOptions.settings)
|
||||
},
|
||||
hideModal() {
|
||||
this.visible = false
|
||||
this.setTitle('')
|
||||
},
|
||||
setTitle(new_title: string) {
|
||||
this.title = new_title
|
||||
},
|
||||
},
|
||||
showNewPlaylistModal(props: any = {}) {
|
||||
this.showModal(ModalOptions.newPlaylist, props);
|
||||
},
|
||||
showSaveFolderAsPlaylistModal(path: string) {
|
||||
const playlist_name = path.split("/").pop();
|
||||
const props = {
|
||||
playlist_name,
|
||||
path,
|
||||
};
|
||||
this.showModal(ModalOptions.newPlaylist, props);
|
||||
},
|
||||
showSaveArtistAsPlaylistModal(name: string, artisthash: string) {
|
||||
const props = {
|
||||
artisthash,
|
||||
playlist_name: `This is ${name}`,
|
||||
};
|
||||
this.showModal(ModalOptions.newPlaylist, props);
|
||||
},
|
||||
showSaveQueueAsPlaylistModal(name: string) {
|
||||
const props = {
|
||||
is_queue: true,
|
||||
playlist_name: name,
|
||||
};
|
||||
this.showModal(ModalOptions.newPlaylist, props);
|
||||
},
|
||||
showEditPlaylistModal() {
|
||||
this.showModal(ModalOptions.updatePlaylist);
|
||||
},
|
||||
showDeletePlaylistModal(pid: number) {
|
||||
const props = {
|
||||
pid: pid,
|
||||
};
|
||||
this.showModal(ModalOptions.deletePlaylist, props);
|
||||
},
|
||||
showSetIPModal() {
|
||||
this.showModal(ModalOptions.SetIP);
|
||||
},
|
||||
showRootDirsPromptModal() {
|
||||
this.showModal(ModalOptions.rootDirsPrompt);
|
||||
},
|
||||
showSetRootDirsModal() {
|
||||
this.showModal(ModalOptions.setRootDirs);
|
||||
},
|
||||
showLoginModal(){
|
||||
this.showModal(ModalOptions.login);
|
||||
},
|
||||
showSettingsModal(){
|
||||
this.showModal(ModalOptions.settings);
|
||||
},
|
||||
hideModal() {
|
||||
this.visible = false;
|
||||
this.setTitle("");
|
||||
},
|
||||
setTitle(new_title: string) {
|
||||
this.title = new_title;
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
58
src/views/Pages/Page.vue
Normal file
58
src/views/Pages/Page.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<CardGridPage :items="page?.items || []">
|
||||
<template #header>
|
||||
<GenericHeader>
|
||||
<template #name>
|
||||
<span @click="updatePage">
|
||||
{{ page?.name }} <span><PencilSvg height="0.8rem" width="0.8rem" /></span
|
||||
></span>
|
||||
</template>
|
||||
<template #description v-if="page?.extra.description">
|
||||
<span @click="updatePage"> {{ page?.extra.description }} </span>
|
||||
</template>
|
||||
</GenericHeader>
|
||||
</template>
|
||||
</CardGridPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Page } from '@/interfaces'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { getPage } from '@/requests/pages'
|
||||
|
||||
import CardGridPage from '@/views/SearchView/CardGridPage.vue'
|
||||
import GenericHeader from '@/components/shared/GenericHeader.vue'
|
||||
import PencilSvg from '@/assets/icons/pencil.svg'
|
||||
|
||||
import useModal from '@/stores/modal'
|
||||
|
||||
const modal = useModal()
|
||||
|
||||
const page = ref<Page | null>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
const route = useRoute()
|
||||
const page_id = route.params.page as string
|
||||
page.value = await getPage(page_id)
|
||||
console.log(page.value)
|
||||
})
|
||||
|
||||
function updatePage() {
|
||||
console.log('update page')
|
||||
modal.showNewPageModal({
|
||||
page: page.value,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
span {
|
||||
cursor: text;
|
||||
margin-right: $smaller;
|
||||
}
|
||||
|
||||
.generichead {
|
||||
margin-top: 2rem
|
||||
}
|
||||
</style>
|
||||
@@ -1,100 +1,90 @@
|
||||
<template>
|
||||
<NoItems
|
||||
:title="`No ${page} results`"
|
||||
:description="desc"
|
||||
:icon="SearchSvg"
|
||||
:flag="!items.length"
|
||||
v-if="showNoItemsComponent"
|
||||
/>
|
||||
<div class="v-scroll-page" style="height: 100%">
|
||||
<DynamicScroller
|
||||
style="height: 100%"
|
||||
class="scroller"
|
||||
:min-item-size="64"
|
||||
:items="scrollerItems"
|
||||
>
|
||||
<template #before>
|
||||
<slot name="header"></slot>
|
||||
</template>
|
||||
<template #default="{ item, index, active }">
|
||||
<DynamicScrollerItem
|
||||
:item="item"
|
||||
:active="active"
|
||||
:size-dependencies="[item.props]"
|
||||
:data-index="index"
|
||||
>
|
||||
<component
|
||||
:is="item.component"
|
||||
:key="index"
|
||||
v-bind="item.props"
|
||||
></component>
|
||||
</DynamicScrollerItem>
|
||||
</template>
|
||||
</DynamicScroller>
|
||||
</div>
|
||||
<NoItems
|
||||
:title="`No ${page} results`"
|
||||
:description="desc"
|
||||
:icon="SearchSvg"
|
||||
:flag="!items.length"
|
||||
v-if="showNoItemsComponent"
|
||||
/>
|
||||
<div class="v-scroll-page" style="height: 100%">
|
||||
<DynamicScroller style="height: 100%" class="scroller" :min-item-size="64" :items="scrollerItems">
|
||||
<template #before>
|
||||
<slot name="header"></slot>
|
||||
</template>
|
||||
<template #default="{ item, index, active }">
|
||||
<DynamicScrollerItem
|
||||
:item="item"
|
||||
:active="active"
|
||||
:size-dependencies="[item.props]"
|
||||
:data-index="index"
|
||||
>
|
||||
<component :is="item.component" :key="index" v-bind="item.props"></component>
|
||||
</DynamicScrollerItem>
|
||||
</template>
|
||||
</DynamicScroller>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { computed } from 'vue'
|
||||
|
||||
import useSearchStore from "@/stores/search";
|
||||
import { maxAbumCards } from "@/stores/content-width";
|
||||
import useSearchStore from '@/stores/search'
|
||||
import { maxAbumCards } from '@/stores/content-width'
|
||||
|
||||
import SearchSvg from "@/assets/icons/search.svg";
|
||||
import NoItems from "@/components/shared/NoItems.vue";
|
||||
import CardRow from "@/components/shared/CardRow.vue";
|
||||
import AlbumsFetcher from "@/components/ArtistView/AlbumsFetcher.vue";
|
||||
import SearchSvg from '@/assets/icons/search.svg'
|
||||
import NoItems from '@/components/shared/NoItems.vue'
|
||||
import CardRow from '@/components/shared/CardRow.vue'
|
||||
import AlbumsFetcher from '@/components/ArtistView/AlbumsFetcher.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
page: "album" | "artist" | "mix";
|
||||
fetch_callback?: () => Promise<void>;
|
||||
items: any[];
|
||||
outside_route?: boolean;
|
||||
showNoItemsComponent?: boolean;
|
||||
}>();
|
||||
page?: 'album' | 'artist' | 'mix'
|
||||
fetch_callback?: () => Promise<void>
|
||||
items: any[]
|
||||
outside_route?: boolean
|
||||
showNoItemsComponent?: boolean
|
||||
}>()
|
||||
|
||||
const search = useSearchStore();
|
||||
const search = useSearchStore()
|
||||
|
||||
const desc = computed(() =>
|
||||
search.query === ""
|
||||
? `Start typing to search for ${props.page}s`
|
||||
: `Results for '${search.query}' should appear here`
|
||||
);
|
||||
search.query === ''
|
||||
? `Start typing to search for ${props.page}s`
|
||||
: `Results for '${search.query}' should appear here`
|
||||
)
|
||||
|
||||
const scrollerItems = computed(() => {
|
||||
let maxCards = maxAbumCards.value;
|
||||
let maxCards = maxAbumCards.value
|
||||
|
||||
if (props.outside_route) {
|
||||
maxCards = 6;
|
||||
}
|
||||
if (props.outside_route) {
|
||||
maxCards = 6
|
||||
}
|
||||
|
||||
const groups = Math.ceil(props.items.length / maxCards);
|
||||
const items = [];
|
||||
const groups = Math.ceil(props.items.length / maxCards)
|
||||
const items = []
|
||||
|
||||
for (let i = 0; i < groups; i++) {
|
||||
items.push({
|
||||
id: i,
|
||||
component: CardRow,
|
||||
props: {
|
||||
type: props.page,
|
||||
items: props.items.slice(i * maxCards, (i + 1) * maxCards),
|
||||
},
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < groups; i++) {
|
||||
items.push({
|
||||
id: i,
|
||||
component: CardRow,
|
||||
props: {
|
||||
items: props.items.slice(i * maxCards, (i + 1) * maxCards),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const moreItems = props.page === 'album' ? search.albums.more : search.artists.more
|
||||
const moreItems = props.page === 'album' ? search.albums.more : search.artists.more
|
||||
|
||||
if (props.fetch_callback && moreItems) {
|
||||
items.push({
|
||||
id: Math.random(),
|
||||
component: AlbumsFetcher,
|
||||
props: {
|
||||
fetch_callback: props.fetch_callback,
|
||||
outside_route: props.outside_route,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (props.fetch_callback && moreItems) {
|
||||
items.push({
|
||||
id: Math.random(),
|
||||
component: AlbumsFetcher,
|
||||
props: {
|
||||
fetch_callback: props.fetch_callback,
|
||||
outside_route: props.outside_route,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return items;
|
||||
});
|
||||
return items
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user